package udp import ( "bytes" "encoding/binary" "eq2emu/internal/common/opcodes" "errors" "fmt" ) // Common protocol errors var ( ErrPacketTooSmall = errors.New("packet too small") ErrInvalidCRC = errors.New("invalid CRC") ErrInvalidOpcode = errors.New("invalid opcode") ) // ProtocolPacket represents a low-level UDP protocol packet with opcode and payload type ProtocolPacket struct { Opcode uint8 // Protocol operation code (1-2 bytes when serialized) Data []byte // Packet payload data Raw []byte // Original raw packet data for debugging } // ApplicationPacket represents a higher-level game application packet type ApplicationPacket struct { Opcode uint16 // Application-level operation code Data []byte // Application payload data } // ParseProtocolPacket parses raw UDP data into a ProtocolPacket // Handles variable opcode sizing and CRC validation based on EQ2 protocol func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) { if len(data) < 2 { return nil, ErrPacketTooSmall } var opcode uint8 var dataStart int // EQ2 protocol uses 1-byte opcodes normally, 2-byte for opcodes >= 0xFF // When opcode >= 0xFF, it's prefixed with 0x00 if data[0] == 0x00 && len(data) > 2 { opcode = data[1] dataStart = 2 } else { opcode = data[0] dataStart = 1 } // Extract payload, handling CRC for non-session packets var payload []byte if requiresCRC(opcode) { if len(data) < dataStart+2 { return nil, ErrPacketTooSmall } // Payload excludes the 2-byte CRC suffix payload = data[dataStart : len(data)-2] // Validate CRC on the entire packet from beginning if !ValidateCRC(data) { return nil, fmt.Errorf("%w for opcode 0x%02X", ErrInvalidCRC, opcode) } } else { payload = data[dataStart:] } return &ProtocolPacket{ Opcode: opcode, Data: payload, Raw: data, }, nil } // Serialize converts ProtocolPacket back to wire format with proper opcode encoding and CRC func (p *ProtocolPacket) Serialize() []byte { var result []byte // Handle variable opcode encoding if p.Opcode == 0xFF { // 2-byte opcode format: [0x00][actual_opcode][data] result = make([]byte, 2+len(p.Data)) result[0] = 0x00 result[1] = p.Opcode copy(result[2:], p.Data) } else { // 1-byte opcode format: [opcode][data] result = make([]byte, 1+len(p.Data)) result[0] = p.Opcode copy(result[1:], p.Data) } // Add CRC for packets that require it if requiresCRC(p.Opcode) { result = AppendCRC(result) } return result } // String provides human-readable representation for debugging func (p *ProtocolPacket) String() string { return fmt.Sprintf("ProtocolPacket{Opcode: 0x%02X, DataLen: %d}", p.Opcode, len(p.Data)) } // ParseApplicationPacket parses application-level packet from decrypted/decompressed data func ParseApplicationPacket(data []byte) (*ApplicationPacket, error) { if len(data) < 2 { return nil, errors.New("application packet requires at least 2 bytes for opcode") } // Application opcodes are always little-endian 16-bit values opcode := binary.LittleEndian.Uint16(data[0:2]) return &ApplicationPacket{ Opcode: opcode, Data: data[2:], }, nil } // Serialize converts ApplicationPacket to byte array for transmission func (p *ApplicationPacket) Serialize() []byte { result := make([]byte, 2+len(p.Data)) binary.LittleEndian.PutUint16(result[0:2], p.Opcode) copy(result[2:], p.Data) return result } // String provides human-readable representation for debugging func (p *ApplicationPacket) String() string { return fmt.Sprintf("ApplicationPacket{Opcode: 0x%04X, DataLen: %d}", p.Opcode, len(p.Data)) } // requiresCRC determines if a protocol opcode requires CRC validation // Session control packets (SessionRequest, SessionResponse, OutOfSession) don't use CRC func requiresCRC(opcode uint8) bool { switch opcode { case opcodes.OpSessionRequest, opcodes.OpSessionResponse, opcodes.OpOutOfSession: return false default: return true } } // PacketCombiner groups small packets together to reduce UDP overhead type PacketCombiner struct { PendingPackets []*ProtocolPacket // Direct access to pending packets MaxSize int // Direct access to max size } // NewPacketCombiner creates a combiner with specified max size func NewPacketCombiner(maxSize int) *PacketCombiner { return &PacketCombiner{ MaxSize: maxSize, } } // Add queues a packet for potential combining func (pc *PacketCombiner) Add(packet *ProtocolPacket) { pc.PendingPackets = append(pc.PendingPackets, packet) } // Flush returns combined packets and clears the queue func (pc *PacketCombiner) Flush() []*ProtocolPacket { count := len(pc.PendingPackets) if count == 0 { return nil } if count == 1 { // Single packet - no combining needed packet := pc.PendingPackets[0] pc.Clear() return []*ProtocolPacket{packet} } // Combine multiple packets combined := pc.combine() pc.Clear() return []*ProtocolPacket{combined} } // combine merges all pending packets into a single combined packet func (pc *PacketCombiner) combine() *ProtocolPacket { var buf bytes.Buffer for _, packet := range pc.PendingPackets { serialized := packet.Serialize() pc.writeSizeHeader(&buf, len(serialized)) buf.Write(serialized) } return &ProtocolPacket{ Opcode: opcodes.OpCombined, Data: buf.Bytes(), } } // writeSizeHeader writes packet size using variable-length encoding func (pc *PacketCombiner) writeSizeHeader(buf *bytes.Buffer, size int) { if size >= 255 { // Large packet - use 3-byte header [0xFF][low][high] buf.WriteByte(0xFF) buf.WriteByte(byte(size)) buf.WriteByte(byte(size >> 8)) } else { // Small packet - use 1-byte header buf.WriteByte(byte(size)) } } // ShouldCombine determines if packets should be combined based on total size func (pc *PacketCombiner) ShouldCombine() bool { if len(pc.PendingPackets) < 2 { return false } totalSize := 0 for _, packet := range pc.PendingPackets { serialized := packet.Serialize() totalSize += len(serialized) // Add size header overhead if len(serialized) >= 255 { totalSize += 3 } else { totalSize += 1 } } return totalSize <= pc.MaxSize } // Clear removes all pending packets func (pc *PacketCombiner) Clear() { pc.PendingPackets = pc.PendingPackets[:0] // Reuse slice capacity } // ParseCombinedPacket splits combined packet into individual packets func ParseCombinedPacket(data []byte) ([]*ProtocolPacket, error) { var packets []*ProtocolPacket offset := 0 for offset < len(data) { size, headerSize, err := readSizeHeader(data, offset) if err != nil { break } offset += headerSize if offset+size > len(data) { break // Incomplete packet } // Parse individual packet packetData := data[offset : offset+size] if packet, err := ParseProtocolPacket(packetData); err == nil { packets = append(packets, packet) } offset += size } return packets, nil } // readSizeHeader reads variable-length size header func readSizeHeader(data []byte, offset int) (size, headerSize int, err error) { if offset >= len(data) { return 0, 0, errors.New("insufficient data") } if data[offset] == 0xFF { // 3-byte size header if offset+2 >= len(data) { return 0, 0, errors.New("insufficient data for 3-byte header") } size = int(data[offset+1]) | (int(data[offset+2]) << 8) headerSize = 3 } else { // 1-byte size header size = int(data[offset]) headerSize = 1 } return size, headerSize, nil } // ValidateCombinedPacket checks if combined packet data is well-formed func ValidateCombinedPacket(data []byte) error { offset := 0 count := 0 for offset < len(data) { size, headerSize, err := readSizeHeader(data, offset) if err != nil { return err } offset += headerSize if offset+size > len(data) { return errors.New("packet extends beyond data boundary") } offset += size count++ if count > 100 { // Sanity check return errors.New("too many packets in combined packet") } } return nil }