package packets import ( "bytes" "compress/zlib" "encoding/binary" "fmt" "io" "git.sharkk.net/EQ2/Protocol/crypto" "git.sharkk.net/EQ2/Protocol/opcodes" ) // ValidateCRC validates packet CRC using EQ2's custom CRC16 (matches C++ EQProtocolPacket::ValidateCRC) func ValidateCRC(buffer []byte, key uint32) bool { if len(buffer) < 2 { return false } // Session packets are not CRC protected if len(buffer) >= 2 && buffer[0] == 0x00 && (buffer[1] == byte(opcodes.OP_SessionRequest) || buffer[1] == byte(opcodes.OP_SessionResponse) || buffer[1] == byte(opcodes.OP_OutOfSession)) { return true } // Combined application packets are also exempt (OP_AppCombined = 0x0019) if len(buffer) >= 4 && buffer[2] == 0x00 && buffer[3] == 0x19 { return true } // All other packets must have valid CRC // Extract CRC from last 2 bytes (network byte order) packetCRC := binary.BigEndian.Uint16(buffer[len(buffer)-2:]) // Calculate CRC on data portion (excluding CRC bytes) data := buffer[:len(buffer)-2] calculatedCRC := crypto.CalculateCRC(data, key) // Valid if no CRC present (packetCRC == 0) or CRCs match return packetCRC == 0 || calculatedCRC == packetCRC } // AppendCRC appends CRC16 to packet buffer using EQ2's custom CRC func AppendCRC(buffer []byte, key uint32) []byte { crc := crypto.CalculateCRC(buffer, key) result := make([]byte, len(buffer)+2) copy(result, buffer) binary.BigEndian.PutUint16(result[len(buffer):], crc) return result } // StripCRC removes CRC16 from packet buffer func StripCRC(buffer []byte) []byte { if len(buffer) < 2 { return buffer } return buffer[:len(buffer)-2] } // Compress compresses packet data using zlib or simple encoding (matches C++ EQProtocolPacket::Compress) // Uses zlib for packets > 30 bytes, simple encoding for smaller packets func Compress(src []byte) ([]byte, error) { if len(src) == 0 { return src, nil } // C++ uses 30 bytes as threshold between simple encoding and zlib if len(src) > 30 { // Use zlib compression for larger packets var buf bytes.Buffer // Write uncompressed length first (4 bytes) - EQ protocol requirement if err := binary.Write(&buf, binary.BigEndian, uint32(len(src))); err != nil { return nil, err } // Compress the data w := zlib.NewWriter(&buf) if _, err := w.Write(src); err != nil { w.Close() return nil, err } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil } // Use simple encoding for smaller packets (just return data as-is) // The 0xa5 flag is added by the caller, not here return src, nil } // Decompress decompresses packet data using zlib // This is called after the compression flag has already been checked and removed func Decompress(src []byte) ([]byte, error) { if len(src) < 4 { return nil, fmt.Errorf("compressed data too small") } // Read uncompressed length (first 4 bytes) uncompressedLen := binary.BigEndian.Uint32(src[:4]) // Sanity check if uncompressedLen > MaxPacketSize { return nil, fmt.Errorf("uncompressed size %d exceeds max packet size", uncompressedLen) } // Create reader for compressed data (skip length prefix) r, err := zlib.NewReader(bytes.NewReader(src[4:])) if err != nil { return nil, err } defer r.Close() // Read decompressed data decompressed := make([]byte, uncompressedLen) if _, err := io.ReadFull(r, decompressed); err != nil { return nil, err } return decompressed, nil } // CompressPacket adds compression with proper flag handling (matches C++ EQProtocolPacket::Compress) // Uses zlib (0x5a) for packets > 30 bytes, simple encoding (0xa5) for smaller packets func CompressPacket(buffer []byte) ([]byte, error) { if len(buffer) < 2 { return buffer, nil } // Determine flag offset based on opcode size flagOffset := 1 if buffer[0] == 0x00 { flagOffset = 2 // Two-byte opcode } if len(buffer) <= flagOffset { return buffer, nil // Too small to compress } // Get data to compress (after opcode) dataToCompress := buffer[flagOffset:] // Choose compression method based on size (C++ uses 30 byte threshold) if len(dataToCompress) > 30 { // Use zlib compression compressed, err := Compress(dataToCompress) if err != nil { return nil, err } // Only use if compression actually reduced size if len(compressed) < len(dataToCompress) { result := make([]byte, flagOffset+1+len(compressed)) copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode result[flagOffset] = 0x5a // Zlib flag copy(result[flagOffset+1:], compressed) // Compressed data return result, nil } } else { // Use simple encoding - just add flag result := make([]byte, len(buffer)+1) copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode result[flagOffset] = 0xa5 // Simple encoding flag copy(result[flagOffset+1:], dataToCompress) // Original data return result, nil } // No compression if it doesn't help return buffer, nil } // DecompressPacket handles decompression with flag checking (matches C++ EQProtocolPacket::Decompress) // Supports both zlib compression (0x5a) and simple encoding (0xa5) func DecompressPacket(buffer []byte) ([]byte, error) { if len(buffer) < 3 { return buffer, nil // Too small to be compressed } // Determine flag offset based on opcode size flagOffset := uint32(1) if buffer[0] == 0x00 { flagOffset = 2 // Two-byte opcode } if uint32(len(buffer)) <= flagOffset { return buffer, nil // No room for compression flag } compressionFlag := buffer[flagOffset] // Check compression type if compressionFlag == 0x5a { // Zlib compression // Decompress data after flag, excluding last 2 CRC bytes dataStart := flagOffset + 1 dataEnd := uint32(len(buffer)) - 2 if dataEnd <= dataStart { return nil, fmt.Errorf("invalid compressed packet size") } decompressed, err := Decompress(buffer[dataStart:dataEnd]) if err != nil { return nil, fmt.Errorf("zlib decompression failed: %w", err) } // Rebuild packet: opcode + decompressed data + CRC result := make([]byte, flagOffset+uint32(len(decompressed))+2) copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode copy(result[flagOffset:], decompressed) // Copy decompressed data // Copy CRC bytes result[len(result)-2] = buffer[len(buffer)-2] result[len(result)-1] = buffer[len(buffer)-1] return result, nil } else if compressionFlag == 0xa5 { // Simple encoding - just remove the encoding flag result := make([]byte, len(buffer)-1) copy(result[:flagOffset], buffer[:flagOffset]) // Copy opcode copy(result[flagOffset:], buffer[flagOffset+1:]) // Skip flag, copy rest return result, nil } // No compression return buffer, nil } // ChatEncode encodes chat data using EQ's XOR-based encoding (matches C++ EQProtocolPacket::ChatEncode) // Uses 4-byte block XOR with rolling key that updates from encrypted data func ChatEncode(buffer []byte, encodeKey int) { if len(buffer) <= 2 || encodeKey == 0 { return } // Skip encoding for certain packet types (matches C++ conditions) if buffer[1] == 0x01 || buffer[0] == 0x02 || buffer[0] == 0x1d { return } // Skip first 2 bytes (opcode) data := buffer[2:] size := len(data) key := int32(encodeKey) // Encode 4-byte blocks with rolling key i := 0 for ; i+4 <= size; i += 4 { // Read 4 bytes as int32 (little-endian like C++) pt := binary.LittleEndian.Uint32(data[i:i+4]) encrypted := pt ^ uint32(key) key = int32(encrypted) // Update key with encrypted data binary.LittleEndian.PutUint32(data[i:i+4], encrypted) } // Encode remaining bytes with last key byte keyByte := byte(key & 0xFF) for ; i < size; i++ { data[i] ^= keyByte } } // ChatDecode decodes chat data using EQ's XOR-based encoding (matches C++ EQProtocolPacket::ChatDecode) // Uses 4-byte block XOR with rolling key that updates from encrypted data func ChatDecode(buffer []byte, decodeKey int) { if len(buffer) <= 2 || decodeKey == 0 { return } // Skip decoding for certain packet types (matches C++ conditions) if buffer[1] == 0x01 || buffer[0] == 0x02 || buffer[0] == 0x1d { return } // Skip first 2 bytes (opcode) data := buffer[2:] size := len(data) key := int32(decodeKey) // Decode 4-byte blocks with rolling key i := 0 for ; i+4 <= size; i += 4 { // Read 4 bytes as int32 (little-endian like C++) encrypted := binary.LittleEndian.Uint32(data[i:i+4]) decrypted := encrypted ^ uint32(key) key = int32(encrypted) // Update key with encrypted data (before decryption) binary.LittleEndian.PutUint32(data[i:i+4], decrypted) } // Decode remaining bytes with last key byte keyByte := byte(key & 0xFF) for ; i < size; i++ { data[i] ^= keyByte } } // IsChatPacket checks if opcode is a chat-related packet func IsChatPacket(opcode uint16) bool { chatOpcodes := map[uint16]bool{ 0x0300: true, // OP_ChatMsg 0x0302: true, // OP_TellMsg 0x0307: true, // OP_ChatLeaveChannelMsg 0x0308: true, // OP_ChatTellChannelMsg 0x0309: true, // OP_ChatTellUserMsg 0x0e07: true, // OP_GuildsayMsg } return chatOpcodes[opcode] } // longToIP converts uint32 IP to string func longToIP(ip uint32) string { return fmt.Sprintf("%d.%d.%d.%d", byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip)) } // IsProtocolPacket checks if buffer contains a valid protocol packet (matches C++ EQProtocolPacket::IsProtocolPacket) func IsProtocolPacket(buffer []byte) bool { if len(buffer) < 2 { return false } opcode := binary.BigEndian.Uint16(buffer[:2]) // Check against known protocol opcodes switch opcode { case opcodes.OP_SessionRequest, opcodes.OP_SessionResponse, opcodes.OP_Combined, opcodes.OP_SessionDisconnect, opcodes.OP_KeepAlive, opcodes.OP_SessionStatRequest, opcodes.OP_SessionStatResponse, opcodes.OP_Packet, opcodes.OP_Fragment, opcodes.OP_Ack, opcodes.OP_AppCombined, opcodes.OP_OutOfOrderAck, opcodes.OP_OutOfSession: return true default: return false } }