1
0
Protocol/packets/protopacket.go
2025-09-03 20:48:08 -05:00

562 lines
14 KiB
Go

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)
}
}