eq2go/internal/udp/packet.go
2025-07-21 23:23:26 -05:00

137 lines
3.8 KiB
Go

package udp
import (
"encoding/binary"
"eq2emu/internal/opcodes"
"errors"
"fmt"
)
// 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, errors.New("packet too small for valid protocol packet")
}
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, errors.New("packet too small for CRC validation")
}
// 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("CRC validation failed for opcode 0x%02X", 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
}
// 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 *ProtocolPacket) String() string {
return fmt.Sprintf("ProtocolPacket{Opcode: 0x%02X, DataLen: %d}", p.Opcode, len(p.Data))
}
// 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
}
}