diff --git a/packets/helpers.go b/packets/helpers.go index dce1803..c8985e5 100644 --- a/packets/helpers.go +++ b/packets/helpers.go @@ -43,34 +43,44 @@ func StripCRC(buffer []byte) []byte { return buffer[:len(buffer)-2] } -// Compress compresses packet data using zlib (matches EQ compression) +// 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 } - var buf bytes.Buffer + // 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 + // 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 } - // 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") @@ -100,6 +110,110 @@ func Decompress(src []byte) ([]byte, error) { 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 func ChatEncode(buffer []byte, encodeKey int) { if len(buffer) == 0 || encodeKey == 0 { diff --git a/packets/protopacket.go b/packets/protopacket.go index 2ff80d2..103b746 100644 --- a/packets/protopacket.go +++ b/packets/protopacket.go @@ -189,7 +189,7 @@ func (p *ProtoPacket) IsCompressed() bool { return p.eq2Compressed } -// EQ2Compress compresses packet data (matches EQStream::EQ2_Compress) +// EQ2Compress compresses packet data (matches EQStream::EQ2_Compress and C++ EQProtocolPacket::Compress) func (p *ProtoPacket) EQ2Compress(offset int8) int8 { if offset <= 0 || int(offset) >= len(p.Buffer) { return 0 @@ -197,28 +197,53 @@ func (p *ProtoPacket) EQ2Compress(offset int8) int8 { // Compress data from offset onwards dataToCompress := p.Buffer[offset:] - compressed, err := Compress(dataToCompress) - if err != nil || len(compressed) >= len(dataToCompress) { - return 0 // Compression failed or didn't reduce size + dataLen := len(dataToCompress) + + // C++ uses 30 bytes as threshold - match this exactly + if dataLen > 30 { + // Use zlib compression for larger packets + compressed, err := Compress(dataToCompress) + if err != nil || len(compressed) >= dataLen { + return 0 // Compression failed or didn't reduce size + } + + // Rebuild buffer with zlib compression flag (0x5a) + newSize := int(offset) + len(compressed) + newBuffer := make([]byte, newSize) + + // Copy header bytes before offset + copy(newBuffer[:offset], p.Buffer[:offset]) + + // Set zlib compression flag at offset-1 + newBuffer[offset-1] = 0x5a + + // Copy compressed data + copy(newBuffer[offset:], compressed) + + p.Buffer = newBuffer + p.eq2Compressed = true + + return offset - 1 // Return compression flag position + } else { + // Use simple encoding for smaller packets (0xa5) + // Simple encoding just adds a flag, doesn't change data + newSize := len(p.Buffer) + 1 + newBuffer := make([]byte, newSize) + + // Copy header bytes before offset + copy(newBuffer[:offset], p.Buffer[:offset]) + + // Set simple encoding flag at offset-1 + newBuffer[offset-1] = 0xa5 + + // Copy data after flag (shift by 1 byte) + copy(newBuffer[offset:], p.Buffer[offset-1:]) + + p.Buffer = newBuffer + p.eq2Compressed = true + + return offset - 1 // Return compression flag position } - - // Rebuild buffer with compression flag - newSize := int(offset) + len(compressed) - newBuffer := make([]byte, newSize) - - // Copy header bytes before offset - copy(newBuffer[:offset], p.Buffer[:offset]) - - // Set compression flag at offset-1 - newBuffer[offset-1] = 1 // Compression flag - - // Copy compressed data - copy(newBuffer[offset:], compressed) - - p.Buffer = newBuffer - p.eq2Compressed = true - - return offset - 1 // Return compression flag position } // EncryptPacket encrypts packet data (matches EQStream::EncryptPacket) @@ -431,11 +456,35 @@ func (p *ProtoPacket) AppCombine(rhs *ProtoPacket) bool { // MakeApplicationPacket converts to app packet (matches EQProtocolPacket::MakeApplicationPacket) func (p *ProtoPacket) MakeApplicationPacket(opcodeSize uint8) *AppPacket { - // Decompress if needed - if p.eq2Compressed { - if decompressed, err := Decompress(p.Buffer); err == nil { - p.Buffer = decompressed - p.eq2Compressed = false + // Decompress if needed - handle both zlib and simple encoding + if p.eq2Compressed && len(p.Buffer) > 2 { + // Check compression type at position 2 (after sequence bytes) + compressionFlag := byte(0) + if len(p.Buffer) > 2 { + compressionFlag = p.Buffer[2] + } + + if compressionFlag == 0x5a { + // Zlib compression - decompress data after flag + if len(p.Buffer) > 3 { + if decompressed, err := Decompress(p.Buffer[3:]); err == nil { + // Rebuild buffer without compression + newBuffer := make([]byte, 2+len(decompressed)) + copy(newBuffer[:2], p.Buffer[:2]) // Copy sequence + copy(newBuffer[2:], decompressed) + p.Buffer = newBuffer + p.eq2Compressed = false + } + } + } else if compressionFlag == 0xa5 { + // Simple encoding - just remove the flag + if len(p.Buffer) > 3 { + newBuffer := make([]byte, len(p.Buffer)-1) + copy(newBuffer[:2], p.Buffer[:2]) // Copy sequence + copy(newBuffer[2:], p.Buffer[3:]) // Skip flag + p.Buffer = newBuffer + p.eq2Compressed = false + } } }