package packets import ( "encoding/binary" "fmt" "time" "git.sharkk.net/EQ2/Protocol/crypto" "git.sharkk.net/EQ2/Protocol/opcodes" ) // ProtoPacket handles low-level protocol features (matches EQProtocolPacket/EQ2Packet) type ProtoPacket struct { *Packet // Protocol state flags eq2Compressed bool packetPrepared bool packetEncrypted bool acked bool // EQ2-specific LoginOp opcodes.EmuOpcode // Reliability and sequencing Sequence int32 SentTime int32 AttemptCount int8 // Opcode manager for translation manager opcodes.Manager // Compression/encoding settings CompressThreshold int EncodeKey int // Client version for protocol compatibility clientVersion int16 } const DefaultCompressThreshold = 100 // NewProtoPacket creates a protocol packet with opcode and buffer func NewProtoPacket(op uint16, buf []byte, manager opcodes.Manager) *ProtoPacket { return &ProtoPacket{ Packet: NewPacket(op, buf), manager: manager, CompressThreshold: DefaultCompressThreshold, } } // NewEQ2Packet creates an EQ2 protocol packet (matches EQ2Packet constructor) func NewEQ2Packet(loginOp opcodes.EmuOpcode, buf []byte, manager opcodes.Manager) *ProtoPacket { return &ProtoPacket{ Packet: NewPacket(opcodes.OP_Packet, buf), LoginOp: loginOp, manager: manager, CompressThreshold: DefaultCompressThreshold, } } // NewProtoPacketFromRaw creates from raw buffer (matches EQProtocolPacket constructor) func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manager) *ProtoPacket { var offset uint32 var opcode uint16 if opcodeOverride >= 0 { opcode = uint16(opcodeOverride) } else if len(buf) >= 2 { opcode = binary.BigEndian.Uint16(buf[:2]) offset = 2 } var data []byte if uint32(len(buf)) > offset { data = make([]byte, len(buf)-int(offset)) copy(data, buf[offset:]) } pp := &ProtoPacket{ Packet: &Packet{ Opcode: opcode, Buffer: data, Timestamp: time.Now(), }, manager: manager, CompressThreshold: DefaultCompressThreshold, } if pp.manager != nil { pp.LoginOp = pp.manager.EQToEmu(opcode) } return pp } // SetVersion sets the client version func (p *ProtoPacket) SetVersion(version int16) { p.clientVersion = version p.Version = version } // SetSequence sets the packet sequence number func (p *ProtoPacket) SetSequence(seq int32) { p.Sequence = seq } // IsPrepared returns whether packet has been prepared func (p *ProtoPacket) IsPrepared() bool { return p.packetPrepared } // IsEncrypted returns whether packet is encrypted func (p *ProtoPacket) IsEncrypted() bool { return p.packetEncrypted } // PreparePacket prepares an EQ2 packet for transmission (matches EQ2Packet::PreparePacket) func (p *ProtoPacket) PreparePacket(maxLen uint16) int8 { if p.packetPrepared { return 0 } if p.manager == nil { return -1 } p.packetPrepared = true // Convert emulator opcode to network opcode loginOpcode := p.manager.EmuToEQ(p.LoginOp) if loginOpcode == 0xcdcd { // Invalid opcode return -1 } // NOTE: Compression is NOT done here in C++, it's done separately // via EQ2_Compress after PreparePacket // Calculate new size: sequence(2) + compressed_flag(1) + opcode + data var offset int8 newSize := len(p.Buffer) + 2 + 1 // seq(2) + compress_flag(1) oversized := false // Handle different opcode sizes and formats if loginOpcode != 2 { // Not OP_SessionResponse newSize++ // for opcode type byte if loginOpcode >= 255 { newSize += 2 // oversized opcode needs extra 2 bytes oversized = true } else { // Swap bytes for network order loginOpcode = (loginOpcode>>8)&0xFF | (loginOpcode<<8)&0xFF00 } } // Build new buffer newBuffer := make([]byte, newSize) // Leave first 2 bytes for sequence (filled by SequencedPush) ptr := 2 // Space for compress flag (will be 0 for uncompressed) ptr++ // This is at position 2, will be overwritten if compressed if loginOpcode != 2 { if oversized { newBuffer[ptr] = 0xff // Oversized marker ptr++ } // Write opcode as 2 bytes binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode) ptr += 2 } else { // OP_SessionResponse: just 1 byte newBuffer[ptr] = byte(loginOpcode) ptr++ } // Copy original packet data copy(newBuffer[ptr:], p.Buffer) // Replace buffer p.Buffer = newBuffer offset = int8(newSize - len(p.Buffer) - 1) return offset } // IsCompressed returns whether the packet is compressed func (p *ProtoPacket) IsCompressed() bool { return p.eq2Compressed } // 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 } // Compress data from offset onwards dataToCompress := p.Buffer[offset:] 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 } } // EncryptPacket encrypts packet data (matches EQStream::EncryptPacket) func (p *ProtoPacket) EncryptPacket(cipher *crypto.Ciphers, compressOffset int8, offset int8) { if cipher == nil || !cipher.IsEncrypted() || len(p.Buffer) <= 2 { return } p.packetEncrypted = true if p.eq2Compressed && compressOffset > 0 { // Encrypted compressed data from compress offset if int(compressOffset) < len(p.Buffer) { cipher.Encrypt(p.Buffer[compressOffset:]) } } else { // Encrypt uncompressed data from 2 + offset startPos := 2 + int(offset) if startPos < len(p.Buffer) { cipher.Encrypt(p.Buffer[startPos:]) } } } // Serialize writes the protocol packet to a destination buffer func (p *ProtoPacket) Serialize(dest []byte, offset int8) uint32 { pos := 0 // Write compression flag if compressed if p.eq2Compressed { dest[pos] = 0x5a pos++ } // Write opcode (2 bytes) binary.BigEndian.PutUint16(dest[pos:], p.Opcode) pos += 2 // Copy packet data after opcode if offset < int8(len(p.Buffer)) { copy(dest[pos:], p.Buffer[offset:]) return uint32(len(p.Buffer)-int(offset)) + uint32(pos) } return uint32(pos) } // Combine combines this protocol packet with another (matches EQProtocolPacket::combine) func (p *ProtoPacket) Combine(rhs *ProtoPacket) bool { const opCombined = 0x03 // OP_Combined // Case 1: This packet is already combined - append to it if p.Opcode == opCombined && p.Size()+rhs.Size()+3 < 256 { newSize := len(p.Buffer) + int(rhs.Size()) + 1 newBuffer := make([]byte, newSize) // Copy existing combined data copy(newBuffer, p.Buffer) offset := len(p.Buffer) // Add size prefix for new packet newBuffer[offset] = byte(rhs.Size()) offset++ // Serialize and append new packet tmpBuf := make([]byte, rhs.Size()) rhs.Serialize(tmpBuf, 0) copy(newBuffer[offset:], tmpBuf) p.Buffer = newBuffer return true } // Case 2: Neither packet is combined - create new combined packet if p.Size()+rhs.Size()+4 < 256 { totalSize := int(p.Size()) + int(rhs.Size()) + 2 newBuffer := make([]byte, totalSize) offset := 0 // Add first packet with size prefix newBuffer[offset] = byte(p.Size()) offset++ tmpBuf := make([]byte, p.Size()) p.Serialize(tmpBuf, 0) copy(newBuffer[offset:], tmpBuf) offset += int(p.Size()) // Add second packet with size prefix newBuffer[offset] = byte(rhs.Size()) offset++ tmpBuf = make([]byte, rhs.Size()) rhs.Serialize(tmpBuf, 0) copy(newBuffer[offset:], tmpBuf) // Update buffer and mark as combined p.Buffer = newBuffer p.Opcode = opCombined return true } return false } // AppCombine combines app-level packets (matches EQ2Packet::AppCombine) func (p *ProtoPacket) AppCombine(rhs *ProtoPacket) bool { const opAppCombined = 0x19 // OP_AppCombined lhsSize := p.Size() rhsSize := rhs.Size() // Check max combined size if lhsSize+rhsSize > MaxCombinedSize { return false } // If already combined, add to it if p.Opcode == opAppCombined { tmpSize := rhsSize - 2 // Subtract opcode bytes var newSize int if tmpSize >= 255 { newSize = len(p.Buffer) + int(tmpSize) + 3 // oversized header } else { newSize = len(p.Buffer) + int(tmpSize) + 1 // normal header } newBuffer := make([]byte, newSize) copy(newBuffer, p.Buffer) pos := len(p.Buffer) // Add size header if tmpSize >= 255 { newBuffer[pos] = 255 // Oversized marker pos++ binary.BigEndian.PutUint16(newBuffer[pos:], uint16(tmpSize)) pos += 2 } else { newBuffer[pos] = byte(tmpSize) pos++ } // Serialize rhs packet (skip first 2 bytes - opcode) if len(rhs.Buffer) > 2 { copy(newBuffer[pos:], rhs.Buffer[2:]) } p.Buffer = newBuffer return true } // Create new combined packet lSize := lhsSize - 2 rSize := rhsSize - 2 totalSize := 0 if lSize >= 255 { totalSize += 3 + int(lSize) } else { totalSize += 1 + int(lSize) } if rSize >= 255 { totalSize += 3 + int(rSize) } else { totalSize += 1 + int(rSize) } if totalSize > MaxCombinedSize { return false } newBuffer := make([]byte, totalSize) pos := 0 // Add first packet if lSize >= 255 { newBuffer[pos] = 255 pos++ binary.BigEndian.PutUint16(newBuffer[pos:], uint16(lSize)) pos += 2 } else { newBuffer[pos] = byte(lSize) pos++ } if len(p.Buffer) > 2 { copy(newBuffer[pos:], p.Buffer[2:]) pos += int(lSize) } // Add second packet if rSize >= 255 { newBuffer[pos] = 255 pos++ binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rSize)) pos += 2 } else { newBuffer[pos] = byte(rSize) pos++ } if len(rhs.Buffer) > 2 { copy(newBuffer[pos:], rhs.Buffer[2:]) } p.Opcode = opAppCombined p.Buffer = newBuffer p.packetPrepared = false return true } // MakeApplicationPacket converts to app packet (matches EQProtocolPacket::MakeApplicationPacket) func (p *ProtoPacket) MakeApplicationPacket(opcodeSize uint8) *AppPacket { // 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 } } } // Decode chat if needed if p.packetEncrypted && p.EncodeKey != 0 { ChatDecode(p.Buffer, p.EncodeKey) p.packetEncrypted = false } return NewAppPacketFromRaw(p.Buffer, opcodeSize, p.manager) } // Copy creates a deep copy func (p *ProtoPacket) Copy() *ProtoPacket { newPacket := &ProtoPacket{ Packet: NewPacket(p.Opcode, p.Buffer), eq2Compressed: p.eq2Compressed, packetPrepared: p.packetPrepared, packetEncrypted: p.packetEncrypted, acked: p.acked, LoginOp: p.LoginOp, Sequence: p.Sequence, SentTime: p.SentTime, AttemptCount: p.AttemptCount, manager: p.manager, CompressThreshold: p.CompressThreshold, EncodeKey: p.EncodeKey, } newPacket.Packet.CopyInfo(p.Packet) return newPacket } // GetOpcodeName returns human-readable name for the opcode (matches C++ EQ2Packet::GetOpcodeName) func (p *ProtoPacket) GetOpcodeName() string { // Use manager to get opcode name if available if p.manager != nil { if name := p.manager.EmuToName(p.LoginOp); name != "" { return name } } // Fall back to protocol opcode names switch p.Opcode { case opcodes.OP_SessionRequest: return "OP_SessionRequest" case opcodes.OP_SessionResponse: return "OP_SessionResponse" case opcodes.OP_Combined: return "OP_Combined" case opcodes.OP_SessionDisconnect: return "OP_SessionDisconnect" case opcodes.OP_KeepAlive: return "OP_KeepAlive" case opcodes.OP_SessionStatRequest: return "OP_SessionStatRequest" case opcodes.OP_SessionStatResponse: return "OP_SessionStatResponse" case opcodes.OP_Packet: return "OP_Packet" case opcodes.OP_Fragment: return "OP_Fragment" case opcodes.OP_Ack: return "OP_Ack" case opcodes.OP_AppCombined: return "OP_AppCombined" case opcodes.OP_OutOfOrderAck: return "OP_OutOfOrderAck" case opcodes.OP_OutOfSession: return "OP_OutOfSession" default: return fmt.Sprintf("Unknown(0x%04X)", p.Opcode) } }