519 lines
13 KiB
Go
519 lines
13 KiB
Go
package packets
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"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
|
|
}
|