1
0

compat pass 3

This commit is contained in:
Sky Johnson 2025-09-03 13:50:03 -05:00
parent c692b25ac0
commit d70daa98be
4 changed files with 513 additions and 690 deletions

View File

@ -9,8 +9,7 @@ import (
// DefaultOpcodeSize is the default opcode size for application packets // DefaultOpcodeSize is the default opcode size for application packets
var DefaultOpcodeSize uint8 = 2 var DefaultOpcodeSize uint8 = 2
// AppPacket handles high-level game opcodes and application data // AppPacket handles high-level game opcodes (matches EQApplicationPacket)
// This is the main packet type used by game logic
type AppPacket struct { type AppPacket struct {
*Packet *Packet
@ -29,7 +28,7 @@ func NewAppPacket(manager opcodes.Manager) *AppPacket {
} }
} }
// NewAppPacketWithOp creates a new application packet with opcode // NewAppPacketWithOp creates with opcode
func NewAppPacketWithOp(op opcodes.EmuOpcode, manager opcodes.Manager) *AppPacket { func NewAppPacketWithOp(op opcodes.EmuOpcode, manager opcodes.Manager) *AppPacket {
app := &AppPacket{ app := &AppPacket{
Packet: NewPacket(0, nil), Packet: NewPacket(0, nil),
@ -40,7 +39,7 @@ func NewAppPacketWithOp(op opcodes.EmuOpcode, manager opcodes.Manager) *AppPacke
return app return app
} }
// NewAppPacketWithSize creates a new application packet with opcode and size // NewAppPacketWithSize creates with opcode and size
func NewAppPacketWithSize(op opcodes.EmuOpcode, size uint32, manager opcodes.Manager) *AppPacket { func NewAppPacketWithSize(op opcodes.EmuOpcode, size uint32, manager opcodes.Manager) *AppPacket {
app := &AppPacket{ app := &AppPacket{
Packet: NewPacket(0, make([]byte, size)), Packet: NewPacket(0, make([]byte, size)),
@ -51,7 +50,7 @@ func NewAppPacketWithSize(op opcodes.EmuOpcode, size uint32, manager opcodes.Man
return app return app
} }
// NewAppPacketWithData creates a new application packet with opcode and data // NewAppPacketWithData creates with opcode and data
func NewAppPacketWithData(op opcodes.EmuOpcode, data []byte, manager opcodes.Manager) *AppPacket { func NewAppPacketWithData(op opcodes.EmuOpcode, data []byte, manager opcodes.Manager) *AppPacket {
app := &AppPacket{ app := &AppPacket{
Packet: NewPacket(0, data), Packet: NewPacket(0, data),
@ -62,8 +61,7 @@ func NewAppPacketWithData(op opcodes.EmuOpcode, data []byte, manager opcodes.Man
return app return app
} }
// NewAppPacketFromRaw creates app packet from raw buffer (used by ProtoPacket) // NewAppPacketFromRaw creates from raw buffer (matches EQApplicationPacket constructor)
// Assumes first bytes are opcode based on opcodeSize
func NewAppPacketFromRaw(buf []byte, opcodeSize uint8, manager opcodes.Manager) *AppPacket { func NewAppPacketFromRaw(buf []byte, opcodeSize uint8, manager opcodes.Manager) *AppPacket {
if opcodeSize == 0 { if opcodeSize == 0 {
opcodeSize = DefaultOpcodeSize opcodeSize = DefaultOpcodeSize
@ -75,23 +73,28 @@ func NewAppPacketFromRaw(buf []byte, opcodeSize uint8, manager opcodes.Manager)
manager: manager, manager: manager,
} }
offset := 0
// Extract opcode based on size
if opcodeSize == 1 && len(buf) >= 1 { if opcodeSize == 1 && len(buf) >= 1 {
app.Opcode = uint16(buf[0]) app.Opcode = uint16(buf[0])
if len(buf) > 1 { offset = 1
app.Buffer = make([]byte, len(buf)-1)
copy(app.Buffer, buf[1:])
}
} else if len(buf) >= 2 { } else if len(buf) >= 2 {
app.Opcode = binary.BigEndian.Uint16(buf[:2]) app.Opcode = binary.BigEndian.Uint16(buf[:2])
if len(buf) > 2 { offset = 2
app.Buffer = make([]byte, len(buf)-2) }
copy(app.Buffer, buf[2:])
} // Copy remaining data as payload
if len(buf) > offset {
app.Buffer = make([]byte, len(buf)-offset)
copy(app.Buffer, buf[offset:])
} }
// Convert EQ opcode to emulator opcode // Convert EQ opcode to emulator opcode
if app.manager != nil { if app.manager != nil {
app.emuOpcode = app.manager.EQToEmu(app.Opcode) app.emuOpcode = app.manager.EQToEmu(app.Opcode)
} else {
app.emuOpcode = opcodes.EmuOpcode(app.Opcode)
} }
return app return app
@ -99,21 +102,12 @@ func NewAppPacketFromRaw(buf []byte, opcodeSize uint8, manager opcodes.Manager)
// Size returns total packet size including opcode // Size returns total packet size including opcode
func (a *AppPacket) Size() uint32 { func (a *AppPacket) Size() uint32 {
return uint32(len(a.Buffer)) + uint32(a.opcodeSize) // Handle special encoding where low byte = 0x00 needs extra byte
} extraBytes := uint32(0)
if a.opcodeSize == 2 && (a.Opcode&0x00ff) == 0 {
// SetOpcodeSize sets the opcode size extraBytes = 1
func (a *AppPacket) SetOpcodeSize(size uint8) {
a.opcodeSize = size
}
// SetManager sets the opcode manager for translation
func (a *AppPacket) SetManager(manager opcodes.Manager) {
a.manager = manager
// Re-translate if we have an opcode
if a.emuOpcode != opcodes.OP_Unknown && a.manager != nil {
a.Opcode = a.manager.EmuToEQ(a.emuOpcode)
} }
return uint32(len(a.Buffer)) + uint32(a.opcodeSize) + extraBytes
} }
// SetOpcode sets the emulator opcode and translates to EQ opcode // SetOpcode sets the emulator opcode and translates to EQ opcode
@ -122,15 +116,13 @@ func (a *AppPacket) SetOpcode(op opcodes.EmuOpcode) {
if a.manager != nil { if a.manager != nil {
a.Opcode = a.manager.EmuToEQ(op) a.Opcode = a.manager.EmuToEQ(op)
} else { } else {
// Fallback to direct assignment if no manager
a.Opcode = uint16(op) a.Opcode = uint16(op)
} }
} }
// GetOpcode returns the emulator opcode (with caching) // GetOpcode returns the emulator opcode
func (a *AppPacket) GetOpcode() opcodes.EmuOpcode { func (a *AppPacket) GetOpcode() opcodes.EmuOpcode {
if a.emuOpcode == opcodes.OP_Unknown && a.manager != nil { if a.emuOpcode == opcodes.OP_Unknown && a.manager != nil {
// Convert from protocol opcode
a.emuOpcode = a.manager.EQToEmu(a.Opcode) a.emuOpcode = a.manager.EQToEmu(a.Opcode)
} }
return a.emuOpcode return a.emuOpcode
@ -144,20 +136,16 @@ func (a *AppPacket) GetOpcodeName() string {
return "OP_Unknown" return "OP_Unknown"
} }
// Copy creates a deep copy of this application packet // SetManager sets the opcode manager
func (a *AppPacket) Copy() *AppPacket { func (a *AppPacket) SetManager(manager opcodes.Manager) {
newApp := &AppPacket{ a.manager = manager
Packet: NewPacket(a.Opcode, a.Buffer), // Re-translate if we have an opcode
emuOpcode: a.emuOpcode, if a.emuOpcode != opcodes.OP_Unknown && a.manager != nil {
opcodeSize: a.opcodeSize, a.Opcode = a.manager.EmuToEQ(a.emuOpcode)
manager: a.manager,
} }
newApp.Packet.CopyInfo(a.Packet)
return newApp
} }
// Serialize writes the application packet to a destination buffer // Serialize writes to destination (matches EQApplicationPacket::serialize)
// Handles special opcode encoding rules for application-level packets
func (a *AppPacket) Serialize(dest []byte) uint32 { func (a *AppPacket) Serialize(dest []byte) uint32 {
opcodeBytes := a.opcodeSize opcodeBytes := a.opcodeSize
pos := 0 pos := 0
@ -187,84 +175,34 @@ func (a *AppPacket) Serialize(dest []byte) uint32 {
return uint32(len(a.Buffer)) + uint32(opcodeBytes) return uint32(len(a.Buffer)) + uint32(opcodeBytes)
} }
// Combine combines this packet with another application packet // Combine combines with another app packet (matches EQApplicationPacket::combine)
func (a *AppPacket) Combine(rhs *AppPacket) bool { func (a *AppPacket) Combine(rhs *AppPacket) bool {
const opAppCombined = 0x19 // OP_AppCombined // Application packet combining is not implemented in original
// Use protocol-level combining instead
// Check if we can combine these packets return false
totalSize := a.Size() + rhs.Size() }
if totalSize > 512 { // Reasonable max combined packet size
return false // Copy creates a deep copy
} func (a *AppPacket) Copy() *AppPacket {
newApp := &AppPacket{
// If this isn't already a combined packet, convert it Packet: NewPacket(a.Opcode, a.Buffer),
if a.Opcode != opAppCombined { emuOpcode: a.emuOpcode,
// Create combined packet structure opcodeSize: a.opcodeSize,
oldSize := a.Size() manager: a.manager,
newSize := oldSize + rhs.Size() + 2 // +2 for size headers }
newApp.Packet.CopyInfo(a.Packet)
newBuffer := make([]byte, newSize) return newApp
pos := 0 }
// Write first packet size and data // ToProto converts to protocol packet
newBuffer[pos] = byte(oldSize) func (a *AppPacket) ToProto() *ProtoPacket {
pos++ // Serialize the application packet
tmpBuf := make([]byte, a.Size())
// Serialize first packet a.Serialize(tmpBuf)
tmpBuf := make([]byte, oldSize)
a.Serialize(tmpBuf) proto := NewProtoPacket(opcodes.OP_Packet, tmpBuf, a.manager)
copy(newBuffer[pos:], tmpBuf) proto.LoginOp = a.emuOpcode
pos += int(oldSize) proto.CopyInfo(a.Packet)
// Write second packet size and data return proto
rhsSize := rhs.Size()
newBuffer[pos] = byte(rhsSize)
pos++
// Serialize second packet
tmpBuf = make([]byte, rhsSize)
rhs.Serialize(tmpBuf)
copy(newBuffer[pos:], tmpBuf)
// Update this packet to be a combined packet
a.Opcode = opAppCombined
a.Buffer = newBuffer
return true
}
// This is already a combined packet, add to it
rhsSize := rhs.Size()
if rhsSize >= 255 {
// Oversized packet handling
newBuffer := make([]byte, len(a.Buffer)+int(rhsSize)+3)
copy(newBuffer, a.Buffer)
pos := len(a.Buffer)
newBuffer[pos] = 255 // Oversized marker
pos++
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rhsSize))
pos += 2
tmpBuf := make([]byte, rhsSize)
rhs.Serialize(tmpBuf)
copy(newBuffer[pos:], tmpBuf)
a.Buffer = newBuffer
} else {
// Normal sized addition
newBuffer := make([]byte, len(a.Buffer)+int(rhsSize)+1)
copy(newBuffer, a.Buffer)
pos := len(a.Buffer)
newBuffer[pos] = byte(rhsSize)
pos++
tmpBuf := make([]byte, rhsSize)
rhs.Serialize(tmpBuf)
copy(newBuffer[pos:], tmpBuf)
a.Buffer = newBuffer
}
return true
} }

View File

@ -5,33 +5,42 @@ import (
"compress/zlib" "compress/zlib"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"hash/crc32"
"io" "io"
"git.sharkk.net/EQ2/Protocol/crypto" "git.sharkk.net/EQ2/Protocol/crypto"
) )
// ValidateCRC validates packet CRC using EQ's CRC32 implementation // ValidateCRC validates packet CRC using EQ2's custom CRC16
func ValidateCRC(buffer []byte, key uint32) bool { func ValidateCRC(buffer []byte, key uint32) bool {
if len(buffer) < 4 { if len(buffer) < 2 {
return false return false
} }
// Extract CRC from last 4 bytes // Extract CRC from last 2 bytes (EQ2 uses CRC16)
packetCRC := binary.BigEndian.Uint32(buffer[len(buffer)-4:]) packetCRC := binary.BigEndian.Uint16(buffer[len(buffer)-2:])
// Calculate CRC on data portion (excluding CRC bytes) // Calculate CRC on data portion (excluding CRC bytes)
data := buffer[:len(buffer)-4] data := buffer[:len(buffer)-2]
calculatedCRC := CalculateCRC(data, key) calculatedCRC := crypto.CalculateCRC(data, key)
return packetCRC == calculatedCRC return packetCRC == calculatedCRC
} }
// CalculateCRC calculates CRC32 for packet data // AppendCRC appends CRC16 to packet buffer using EQ2's custom CRC
func CalculateCRC(data []byte, key uint32) uint32 { func AppendCRC(buffer []byte, key uint32) []byte {
// EQ uses standard CRC32 with XOR key crc := crypto.CalculateCRC(buffer, key)
crc := crc32.ChecksumIEEE(data) result := make([]byte, len(buffer)+2)
return crc ^ key copy(result, buffer)
binary.BigEndian.PutUint16(result[len(buffer):], crc)
return result
}
// StripCRC removes CRC16 from packet buffer
func StripCRC(buffer []byte) []byte {
if len(buffer) < 2 {
return buffer
}
return buffer[:len(buffer)-2]
} }
// Compress compresses packet data using zlib (matches EQ compression) // Compress compresses packet data using zlib (matches EQ compression)
@ -42,16 +51,13 @@ func Compress(src []byte) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
// EQ uses default compression level
w := zlib.NewWriter(&buf)
// Write uncompressed length first (4 bytes) - EQ protocol requirement // Write uncompressed length first (4 bytes) - EQ protocol requirement
uncompressedLen := uint32(len(src)) if err := binary.Write(&buf, binary.BigEndian, uint32(len(src))); err != nil {
if err := binary.Write(&buf, binary.BigEndian, uncompressedLen); err != nil {
return nil, err return nil, err
} }
// Compress the data // Compress the data
w := zlib.NewWriter(&buf)
if _, err := w.Write(src); err != nil { if _, err := w.Write(src); err != nil {
w.Close() w.Close()
return nil, err return nil, err
@ -73,7 +79,7 @@ func Decompress(src []byte) ([]byte, error) {
// Read uncompressed length (first 4 bytes) // Read uncompressed length (first 4 bytes)
uncompressedLen := binary.BigEndian.Uint32(src[:4]) uncompressedLen := binary.BigEndian.Uint32(src[:4])
// Sanity check - prevent decompression bombs // Sanity check
if uncompressedLen > MaxPacketSize { if uncompressedLen > MaxPacketSize {
return nil, fmt.Errorf("uncompressed size %d exceeds max packet size", uncompressedLen) return nil, fmt.Errorf("uncompressed size %d exceeds max packet size", uncompressedLen)
} }
@ -95,16 +101,13 @@ func Decompress(src []byte) ([]byte, error) {
} }
// ChatEncode encodes chat data using EQ's XOR-based encoding // ChatEncode encodes chat data using EQ's XOR-based encoding
// EQ uses a simple rotating XOR with the encode key
func ChatEncode(buffer []byte, encodeKey int) { func ChatEncode(buffer []byte, encodeKey int) {
if len(buffer) == 0 || encodeKey == 0 { if len(buffer) == 0 || encodeKey == 0 {
return return
} }
// EQ chat encoding algorithm
key := byte(encodeKey & 0xFF) key := byte(encodeKey & 0xFF)
for i := range buffer { for i := range buffer {
// XOR with rotating key based on position
buffer[i] ^= key buffer[i] ^= key
// Rotate key for next byte // Rotate key for next byte
key = ((key << 1) | (key >> 7)) & 0xFF key = ((key << 1) | (key >> 7)) & 0xFF
@ -115,23 +118,38 @@ func ChatEncode(buffer []byte, encodeKey int) {
} }
} }
// ChatDecode decodes chat data using EQ's XOR-based encoding // ChatDecode decodes chat data (XOR is symmetric)
// Decoding is the same as encoding for XOR
func ChatDecode(buffer []byte, decodeKey int) { func ChatDecode(buffer []byte, decodeKey int) {
// XOR encoding is symmetric - encode and decode are the same operation
ChatEncode(buffer, decodeKey) ChatEncode(buffer, decodeKey)
} }
// IsChatPacket checks if opcode is a chat-related packet
func IsChatPacket(opcode uint16) bool {
chatOpcodes := map[uint16]bool{
0x0300: true, // OP_ChatMsg
0x0302: true, // OP_TellMsg
0x0307: true, // OP_ChatLeaveChannelMsg
0x0308: true, // OP_ChatTellChannelMsg
0x0309: true, // OP_ChatTellUserMsg
0x0e07: true, // OP_GuildsayMsg
}
return chatOpcodes[opcode]
}
// longToIP converts uint32 IP to string
func longToIP(ip uint32) string {
return fmt.Sprintf("%d.%d.%d.%d",
byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
}
// IsProtocolPacket checks if buffer contains a valid protocol packet // IsProtocolPacket checks if buffer contains a valid protocol packet
func IsProtocolPacket(buffer []byte, trimCRC bool) bool { func IsProtocolPacket(buffer []byte) bool {
if len(buffer) < 2 { if len(buffer) < 2 {
return false return false
} }
// Check for valid protocol opcodes
opcode := binary.BigEndian.Uint16(buffer[:2]) opcode := binary.BigEndian.Uint16(buffer[:2])
// Protocol opcodes from protocol.go
validOpcodes := map[uint16]bool{ validOpcodes := map[uint16]bool{
0x0001: true, // OP_SessionRequest 0x0001: true, // OP_SessionRequest
0x0002: true, // OP_SessionResponse 0x0002: true, // OP_SessionResponse
@ -148,95 +166,5 @@ func IsProtocolPacket(buffer []byte, trimCRC bool) bool {
0x001e: true, // OP_OutOfSession 0x001e: true, // OP_OutOfSession
} }
if !validOpcodes[opcode] { return validOpcodes[opcode]
return false
}
// If checking CRC, validate it
if trimCRC && len(buffer) >= 6 {
// Protocol packets have 2-byte opcode + data + 4-byte CRC
return ValidateCRC(buffer, 0)
}
return true
}
// EncodePacket applies encoding/compression based on flags
func EncodePacket(packet *ProtoPacket, compressThreshold int, encodeKey int) error {
// Apply compression if packet is large enough
if len(packet.Buffer) > compressThreshold && !packet.IsCompressed() {
compressed, err := Compress(packet.Buffer)
if err != nil {
return err
}
packet.Buffer = compressed
packet.SetCompressed(true)
}
// Apply chat encoding if this is a chat packet
if IsChatPacket(packet.Opcode) && encodeKey != 0 {
ChatEncode(packet.Buffer, encodeKey)
packet.SetEncrypted(true)
}
return nil
}
// DecodePacket reverses encoding/compression
func DecodePacket(packet *ProtoPacket, decodeKey int) error {
// Decrypt if encrypted
if packet.IsEncrypted() && decodeKey != 0 {
ChatDecode(packet.Buffer, decodeKey)
packet.SetEncrypted(false)
}
// Decompress if compressed
if packet.IsCompressed() {
decompressed, err := Decompress(packet.Buffer)
if err != nil {
return err
}
packet.Buffer = decompressed
packet.SetCompressed(false)
}
return nil
}
// IsChatPacket checks if opcode is a chat-related packet
func IsChatPacket(opcode uint16) bool {
// Chat-related opcodes that need encoding
// These would map to OP_ChatMsg, OP_TellMsg, etc in the opcodes package
chatOpcodes := map[uint16]bool{
0x0300: true, // OP_ChatMsg
0x0302: true, // OP_TellMsg
0x0307: true, // OP_ChatLeaveChannelMsg
0x0308: true, // OP_ChatTellChannelMsg
0x0309: true, // OP_ChatTellUserMsg
0x0e07: true, // OP_GuildsayMsg
}
return chatOpcodes[opcode]
}
// Helper function to convert uint32 IP to string
func longToIP(ip uint32) string {
return fmt.Sprintf("%d.%d.%d.%d",
byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
}
// AppendCRC appends CRC16 to packet buffer using EQ2's custom CRC
func AppendCRC(buffer []byte, key uint32) []byte {
crc := crypto.CalculateCRC(buffer, key)
result := make([]byte, len(buffer)+2)
copy(result, buffer)
binary.BigEndian.PutUint16(result[len(buffer):], crc)
return result
}
// StripCRC removes CRC16 from packet buffer
func StripCRC(buffer []byte) []byte {
if len(buffer) < 2 {
return buffer
}
return buffer[:len(buffer)-2]
} }

View File

@ -7,12 +7,15 @@ import (
"git.sharkk.net/EQ2/Protocol/opcodes" "git.sharkk.net/EQ2/Protocol/opcodes"
) )
// ProtoPacket handles low-level protocol features including EQ2-specific operations // ProtoPacket handles low-level protocol features (matches EQProtocolPacket/EQ2Packet)
type ProtoPacket struct { type ProtoPacket struct {
*Packet *Packet
// Protocol state flags (using bitfield) // Protocol state flags
flags uint8 // bit 0: compressed, bit 1: prepared, bit 2: encrypted, bit 3: acked eq2Compressed bool
packetPrepared bool
packetEncrypted bool
acked bool
// EQ2-specific // EQ2-specific
LoginOp opcodes.EmuOpcode LoginOp opcodes.EmuOpcode
@ -30,15 +33,6 @@ type ProtoPacket struct {
EncodeKey int EncodeKey int
} }
// Protocol flag constants
const (
FlagCompressed = 1 << iota
FlagPrepared
FlagEncrypted
FlagAcked
)
// Default compression threshold
const DefaultCompressThreshold = 100 const DefaultCompressThreshold = 100
// NewProtoPacket creates a protocol packet with opcode and buffer // NewProtoPacket creates a protocol packet with opcode and buffer
@ -50,7 +44,7 @@ func NewProtoPacket(op uint16, buf []byte, manager opcodes.Manager) *ProtoPacket
} }
} }
// NewProtoPacketFromRaw creates a protocol packet from raw buffer // NewProtoPacketFromRaw creates from raw buffer (matches EQProtocolPacket constructor)
func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manager) *ProtoPacket { func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manager) *ProtoPacket {
var offset uint32 var offset uint32
var opcode uint16 var opcode uint16
@ -78,7 +72,6 @@ func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manag
CompressThreshold: DefaultCompressThreshold, CompressThreshold: DefaultCompressThreshold,
} }
// Convert EQ opcode to emulator opcode if manager available
if pp.manager != nil { if pp.manager != nil {
pp.LoginOp = pp.manager.EQToEmu(opcode) pp.LoginOp = pp.manager.EQToEmu(opcode)
} }
@ -86,249 +79,63 @@ func NewProtoPacketFromRaw(buf []byte, opcodeOverride int, manager opcodes.Manag
return pp return pp
} }
// SetManager sets the opcode manager for translation // PreparePacket prepares an EQ2 packet for transmission (matches EQ2Packet::PreparePacket)
func (p *ProtoPacket) SetManager(manager opcodes.Manager) {
p.manager = manager
}
// IsCompressed returns true if packet is compressed
func (p *ProtoPacket) IsCompressed() bool {
return p.flags&FlagCompressed != 0
}
// SetCompressed sets the compressed flag
func (p *ProtoPacket) SetCompressed(compressed bool) {
if compressed {
p.flags |= FlagCompressed
} else {
p.flags &^= FlagCompressed
}
}
// IsPrepared returns true if packet has been prepared for sending
func (p *ProtoPacket) IsPrepared() bool {
return p.flags&FlagPrepared != 0
}
// SetPrepared sets the prepared flag
func (p *ProtoPacket) SetPrepared(prepared bool) {
if prepared {
p.flags |= FlagPrepared
} else {
p.flags &^= FlagPrepared
}
}
// IsEncrypted returns true if packet is encrypted
func (p *ProtoPacket) IsEncrypted() bool {
return p.flags&FlagEncrypted != 0
}
// SetEncrypted sets the encrypted flag
func (p *ProtoPacket) SetEncrypted(encrypted bool) {
if encrypted {
p.flags |= FlagEncrypted
} else {
p.flags &^= FlagEncrypted
}
}
// IsAcked returns true if packet has been acknowledged
func (p *ProtoPacket) IsAcked() bool {
return p.flags&FlagAcked != 0
}
// SetAcked sets the acknowledged flag
func (p *ProtoPacket) SetAcked(acked bool) {
if acked {
p.flags |= FlagAcked
} else {
p.flags &^= FlagAcked
}
}
// CompressPacket compresses the packet data if needed
func (p *ProtoPacket) CompressPacket() error {
if p.IsCompressed() || len(p.Buffer) <= p.CompressThreshold {
return nil
}
compressed, err := Compress(p.Buffer)
if err != nil {
return err
}
// Only use compression if it actually saves space
if len(compressed) < len(p.Buffer) {
p.Buffer = compressed
p.SetCompressed(true)
}
return nil
}
// DecompressPacket decompresses the packet data
func (p *ProtoPacket) DecompressPacket() error {
if !p.IsCompressed() {
return nil
}
decompressed, err := Decompress(p.Buffer)
if err != nil {
return err
}
p.Buffer = decompressed
p.SetCompressed(false)
return nil
}
// EncodeChat applies chat encoding if this is a chat packet
func (p *ProtoPacket) EncodeChat() {
if p.EncodeKey == 0 || p.IsEncrypted() {
return
}
if p.IsChatPacket() {
ChatEncode(p.Buffer, p.EncodeKey)
p.SetEncrypted(true)
}
}
// DecodeChat reverses chat encoding
func (p *ProtoPacket) DecodeChat() {
if p.EncodeKey == 0 || !p.IsEncrypted() {
return
}
ChatDecode(p.Buffer, p.EncodeKey)
p.SetEncrypted(false)
}
// IsChatPacket checks if this is a chat packet using opcode manager
func (p *ProtoPacket) IsChatPacket() bool {
if p.manager == nil {
return false
}
// Get emulator opcode
emuOp := p.manager.EQToEmu(p.Opcode)
// Check against known chat opcodes
switch emuOp {
case opcodes.OP_ChatMsg,
opcodes.OP_TellMsg,
opcodes.OP_ChatLeaveChannelMsg,
opcodes.OP_ChatTellChannelMsg,
opcodes.OP_ChatTellUserMsg,
opcodes.OP_GuildsayMsg:
return true
default:
return false
}
}
// Copy creates a deep copy of this protocol packet
func (p *ProtoPacket) Copy() *ProtoPacket {
newPacket := &ProtoPacket{
Packet: NewPacket(p.Opcode, p.Buffer),
flags: p.flags,
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
}
// Serialize writes the protocol packet to a destination buffer
func (p *ProtoPacket) Serialize(dest []byte, offset int8) uint32 {
// Apply compression before serialization
p.CompressPacket()
// Apply chat encoding if needed
p.EncodeChat()
// Write compression flag if compressed
pos := 0
if p.IsCompressed() {
dest[pos] = 0x5a // EQ compression flag
pos++
}
// Write opcode (2 bytes)
if p.Opcode > 0xff {
binary.BigEndian.PutUint16(dest[pos:], p.Opcode)
} else {
dest[pos] = 0
dest[pos+1] = byte(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)
}
// PreparePacket prepares an EQ2 packet for transmission
func (p *ProtoPacket) PreparePacket(maxLen int16) int8 { func (p *ProtoPacket) PreparePacket(maxLen int16) int8 {
if p.IsPrepared() { if p.packetPrepared {
return 0 return 0
} }
p.SetPrepared(true) if p.manager == nil {
// Apply compression if needed
if err := p.CompressPacket(); err != nil {
return -1 return -1
} }
// Apply chat encoding if needed p.packetPrepared = true
p.EncodeChat()
// Convert emulator opcode to network opcode using manager // Convert emulator opcode to network opcode
var loginOpcode uint16 loginOpcode := p.manager.EmuToEQ(p.LoginOp)
if p.manager != nil {
loginOpcode = p.manager.EmuToEQ(p.LoginOp)
} else {
loginOpcode = uint16(p.LoginOp)
}
var offset int8 // Apply compression if needed
newSize := len(p.Buffer) + 2 + 1 // sequence(2) + compressed_flag(1) if !p.eq2Compressed && len(p.Buffer) > p.CompressThreshold {
oversized := false compressed, err := Compress(p.Buffer)
if err == nil && len(compressed) < len(p.Buffer) {
// Add compression flag space if compressed p.Buffer = compressed
if p.IsCompressed() { p.eq2Compressed = true
newSize++
}
// Handle different opcode sizes and formats
if loginOpcode != 2 {
newSize++ // opcode type byte
if loginOpcode >= 255 {
newSize += 2 // oversized opcode
oversized = true
} }
} }
// Build new packet buffer // Build new packet buffer with proper headers
newBuffer := make([]byte, newSize) var offset int8
ptr := 2 // Skip sequence field newSize := len(p.Buffer) + 2 // Base size with sequence
// Add compression flag if needed // Add compression flag space if compressed
if p.IsCompressed() { if p.eq2Compressed {
newBuffer[ptr] = 0x5a // EQ compression flag newSize++
}
// Handle opcode encoding (matches C++ implementation)
oversized := false
if loginOpcode != 2 { // Not OP_SessionResponse
if loginOpcode >= 255 {
newSize += 3 // oversized opcode (0xFF + 2 bytes)
oversized = true
} else {
newSize += 2 // normal opcode
}
} else {
newSize++ // single byte for OP_SessionResponse
}
// Build new buffer
newBuffer := make([]byte, newSize)
ptr := 2 // Skip sequence field (filled by stream)
// Add compression flag
if p.eq2Compressed {
newBuffer[ptr] = 0x5a // EQ2 compression flag
ptr++ ptr++
} }
// Encode opcode
if loginOpcode != 2 { if loginOpcode != 2 {
if oversized { if oversized {
newBuffer[ptr] = 0xff // Oversized marker newBuffer[ptr] = 0xff // Oversized marker
@ -348,52 +155,94 @@ func (p *ProtoPacket) PreparePacket(maxLen int16) int8 {
copy(newBuffer[ptr:], p.Buffer) copy(newBuffer[ptr:], p.Buffer)
p.Buffer = newBuffer p.Buffer = newBuffer
offset = int8(newSize - len(p.Buffer) - 1) offset = int8(ptr - 2) // Return offset past sequence field
return offset return offset
} }
// MakeApplicationPacket converts protocol packet to application packet // Serialize writes the protocol packet to a destination buffer
func (p *ProtoPacket) MakeApplicationPacket(opcodeSize uint8) *AppPacket { func (p *ProtoPacket) Serialize(dest []byte, offset int8) uint32 {
// Decompress if needed pos := 0
p.DecompressPacket()
// Decode chat if needed // Write compression flag if compressed
p.DecodeChat() if p.eq2Compressed {
dest[pos] = 0x5a
if opcodeSize == 0 { pos++
opcodeSize = DefaultOpcodeSize
} }
app := &AppPacket{ // Write opcode (2 bytes)
Packet: NewPacket(0, p.Buffer), binary.BigEndian.PutUint16(dest[pos:], p.Opcode)
opcodeSize: opcodeSize, pos += 2
manager: p.manager,
}
app.CopyInfo(p.Packet)
// Parse opcode from buffer based on size // Copy packet data after opcode
if opcodeSize == 1 && len(p.Buffer) >= 1 { if offset < int8(len(p.Buffer)) {
app.Opcode = uint16(p.Buffer[0]) copy(dest[pos:], p.Buffer[offset:])
app.Buffer = p.Buffer[1:] return uint32(len(p.Buffer)-int(offset)) + uint32(pos)
} else if len(p.Buffer) >= 2 {
app.Opcode = binary.BigEndian.Uint16(p.Buffer[:2])
app.Buffer = p.Buffer[2:]
} }
// Convert EQ opcode to emulator opcode if manager available return uint32(pos)
if app.manager != nil {
app.emuOpcode = app.manager.EQToEmu(app.Opcode)
}
return app
} }
// AppCombine combines this packet with another for efficient transmission // 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 { func (p *ProtoPacket) AppCombine(rhs *ProtoPacket) bool {
const opAppCombined = 0x19 // OP_AppCombined const opAppCombined = 0x19 // OP_AppCombined
// Calculate sizes
lhsSize := p.Size() lhsSize := p.Size()
rhsSize := rhs.Size() rhsSize := rhs.Size()
@ -402,109 +251,136 @@ func (p *ProtoPacket) AppCombine(rhs *ProtoPacket) bool {
return false return false
} }
// If this packet is already combined, add to it // If already combined, add to it
if p.Opcode == opAppCombined { if p.Opcode == opAppCombined {
// Calculate new size tmpSize := rhsSize - 2 // Subtract opcode bytes
var newSize int var newSize int
if rhsSize >= 255 {
newSize = len(p.Buffer) + int(rhsSize) + 3 // oversized header if tmpSize >= 255 {
newSize = len(p.Buffer) + int(tmpSize) + 3 // oversized header
} else { } else {
newSize = len(p.Buffer) + int(rhsSize) + 1 // normal header newSize = len(p.Buffer) + int(tmpSize) + 1 // normal header
} }
// Check size limit
if newSize > MaxCombinedSize {
return false
}
// Create new buffer
newBuffer := make([]byte, newSize) newBuffer := make([]byte, newSize)
pos := 0
// Copy existing combined data
copy(newBuffer, p.Buffer) copy(newBuffer, p.Buffer)
pos = len(p.Buffer) pos := len(p.Buffer)
// Add new packet with size header // Add size header
if rhsSize >= 255 { if tmpSize >= 255 {
newBuffer[pos] = 255 // Oversized marker newBuffer[pos] = 255 // Oversized marker
pos++ pos++
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rhsSize)) binary.BigEndian.PutUint16(newBuffer[pos:], uint16(tmpSize))
pos += 2 pos += 2
} else { } else {
newBuffer[pos] = byte(rhsSize) newBuffer[pos] = byte(tmpSize)
pos++ pos++
} }
// Serialize rhs packet // Serialize rhs packet (skip first 2 bytes - opcode)
tmpBuf := make([]byte, rhsSize) if len(rhs.Buffer) > 2 {
rhs.Serialize(tmpBuf, 0) copy(newBuffer[pos:], rhs.Buffer[2:])
copy(newBuffer[pos:], tmpBuf) }
p.Buffer = newBuffer p.Buffer = newBuffer
return true return true
} }
// Create new combined packet from two non-combined packets // Create new combined packet
// Calculate total size: size1(1-3) + packet1 + size2(1-3) + packet2 lSize := lhsSize - 2
var totalSize int rSize := rhsSize - 2
if lhsSize >= 255 { totalSize := 0
totalSize += 3 + int(lhsSize)
if lSize >= 255 {
totalSize += 3 + int(lSize)
} else { } else {
totalSize += 1 + int(lhsSize) totalSize += 1 + int(lSize)
} }
if rhsSize >= 255 {
totalSize += 3 + int(rhsSize) if rSize >= 255 {
} else { totalSize += 3 + int(rSize)
totalSize += 1 + int(rhsSize) } else {
totalSize += 1 + int(rSize)
} }
// Check size limit
if totalSize > MaxCombinedSize { if totalSize > MaxCombinedSize {
return false return false
} }
// Build combined packet
newBuffer := make([]byte, totalSize) newBuffer := make([]byte, totalSize)
pos := 0 pos := 0
// Add first packet with size header // Add first packet
if lhsSize >= 255 { if lSize >= 255 {
newBuffer[pos] = 255 newBuffer[pos] = 255
pos++ pos++
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(lhsSize)) binary.BigEndian.PutUint16(newBuffer[pos:], uint16(lSize))
pos += 2 pos += 2
} else { } else {
newBuffer[pos] = byte(lhsSize) newBuffer[pos] = byte(lSize)
pos++ pos++
} }
if len(p.Buffer) > 2 {
copy(newBuffer[pos:], p.Buffer[2:])
pos += int(lSize)
}
// Serialize first packet // Add second packet
tmpBuf := make([]byte, lhsSize) if rSize >= 255 {
p.Serialize(tmpBuf, 0)
copy(newBuffer[pos:], tmpBuf)
pos += int(lhsSize)
// Add second packet with size header
if rhsSize >= 255 {
newBuffer[pos] = 255 newBuffer[pos] = 255
pos++ pos++
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rhsSize)) binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rSize))
pos += 2 pos += 2
} else { } else {
newBuffer[pos] = byte(rhsSize) newBuffer[pos] = byte(rSize)
pos++ pos++
} }
if len(rhs.Buffer) > 2 {
copy(newBuffer[pos:], rhs.Buffer[2:])
}
// Serialize second packet
tmpBuf = make([]byte, rhsSize)
rhs.Serialize(tmpBuf, 0)
copy(newBuffer[pos:], tmpBuf)
// Update this packet to be combined
p.Opcode = opAppCombined p.Opcode = opAppCombined
p.Buffer = newBuffer p.Buffer = newBuffer
p.SetPrepared(false) // Need to re-prepare p.packetPrepared = false
return true return true
} }
// MakeApplicationPacket converts to app packet (matches EQProtocolPacket::MakeApplicationPacket)
func (p *ProtoPacket) MakeApplicationPacket(opcodeSize uint8) *AppPacket {
// Decompress if needed
if p.eq2Compressed {
if decompressed, err := Decompress(p.Buffer); err == nil {
p.Buffer = decompressed
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
}

View File

@ -13,7 +13,7 @@ import (
"github.com/panjf2000/gnet/v2" "github.com/panjf2000/gnet/v2"
) )
// Stream implements EQ2's reliable UDP protocol // Stream implements EQ2's reliable UDP protocol (matches EQStream)
type Stream struct { type Stream struct {
conn gnet.Conn conn gnet.Conn
mu sync.RWMutex mu sync.RWMutex
@ -41,12 +41,15 @@ type Stream struct {
duplicateAckCnt map[uint16]int duplicateAckCnt map[uint16]int
// Fragment assembly with expiry // Fragment assembly with expiry
fragments map[uint16]*fragmentBuffer fragments map[uint16]*fragmentBuffer
nextFragID uint16 currentFragment *fragmentBuffer
// Out of order handling with expiry // Out of order handling with expiry
outOfOrder map[uint16]*outOfOrderPacket outOfOrder map[uint16]*outOfOrderPacket
// Combined packet for efficient sending
combinedPacket *packets.ProtoPacket
// Packet queues // Packet queues
reliableQueue []*packets.ProtoPacket reliableQueue []*packets.ProtoPacket
unreliableQueue []*packets.ProtoPacket unreliableQueue []*packets.ProtoPacket
@ -64,6 +67,7 @@ type Stream struct {
timeoutTimer *time.Timer timeoutTimer *time.Timer
retransmitTimer *time.Timer retransmitTimer *time.Timer
cleanupTimer *time.Timer cleanupTimer *time.Timer
combineTimer *time.Timer
// Retransmission settings // Retransmission settings
rtt time.Duration rtt time.Duration
@ -121,6 +125,7 @@ const (
fragmentTimeout = 30 * time.Second fragmentTimeout = 30 * time.Second
outOfOrderTimeout = 10 * time.Second outOfOrderTimeout = 10 * time.Second
duplicateAckThreshold = 3 duplicateAckThreshold = 3
combineFlushInterval = 10 * time.Millisecond
) )
// Config holds stream configuration // Config holds stream configuration
@ -188,24 +193,23 @@ func NewStream(conn gnet.Conn, cfg *Config) *Stream {
} }
s.retransmitTimer = time.AfterFunc(retransmitInterval, s.processRetransmits) s.retransmitTimer = time.AfterFunc(retransmitInterval, s.processRetransmits)
s.cleanupTimer = time.AfterFunc(cleanupInterval, s.cleanup) s.cleanupTimer = time.AfterFunc(cleanupInterval, s.cleanup)
s.combineTimer = time.AfterFunc(combineFlushInterval, s.flushCombined)
return s return s
} }
// Process handles incoming data from gnet // Process handles incoming data with proper CRC validation
func (s *Stream) Process(data []byte) error { func (s *Stream) Process(data []byte) error {
if len(data) < 2 { if len(data) < 2 {
return nil return nil
} }
// Check for CRC // Validate and strip CRC16
if len(data) > 2 { if len(data) > 2 {
providedCRC := binary.BigEndian.Uint16(data[len(data)-2:]) if !packets.ValidateCRC(data, s.crcKey) {
calculatedCRC := crypto.CalculateCRC(data[:len(data)-2], s.crcKey)
if providedCRC != calculatedCRC {
return nil return nil
} }
data = data[:len(data)-2] data = packets.StripCRC(data)
} }
// Decrypt if needed // Decrypt if needed
@ -248,32 +252,71 @@ func (s *Stream) Process(data []byte) error {
return nil return nil
} }
// SendPacket sends an application packet // SendPacket sends an application packet with proper preparation
func (s *Stream) SendPacket(app *packets.AppPacket) error { func (s *Stream) SendPacket(app *packets.AppPacket) error {
if s.state.Load() != StateEstablished { if s.state.Load() != StateEstablished {
return fmt.Errorf("stream not established") return fmt.Errorf("stream not established")
} }
// Validate packet size // Convert to protocol packet
if app.Size() > uint32(s.maxLen) { proto := app.ToProto()
proto := s.appToProto(app) proto.CompressThreshold = 100
proto.EncodeKey = s.encodeKey
// Check if packet needs fragmentation
if proto.Size() > uint32(s.maxLen-8) {
return s.sendFragmented(proto) return s.sendFragmented(proto)
} }
proto := s.appToProto(app) // Try to combine with pending combined packet
s.mu.Lock()
defer s.mu.Unlock()
if s.combinedPacket != nil {
if s.combinedPacket.AppCombine(proto) {
if s.combinedPacket.Size() > uint32(s.maxLen/2) {
s.flushCombinedPacketLocked()
}
return nil
}
s.flushCombinedPacketLocked()
}
// Queue based on reliability
isUnreliable := s.isUnreliableOpcode(app.GetOpcode()) isUnreliable := s.isUnreliableOpcode(app.GetOpcode())
s.mu.Lock()
if isUnreliable { if isUnreliable {
s.unreliableQueue = append(s.unreliableQueue, proto) s.unreliableQueue = append(s.unreliableQueue, proto)
} else { } else {
s.reliableQueue = append(s.reliableQueue, proto) // Start new combined packet if small enough
if proto.Size() < uint32(s.maxLen/4) {
s.combinedPacket = proto
s.combineTimer.Reset(combineFlushInterval)
} else {
s.reliableQueue = append(s.reliableQueue, proto)
}
} }
s.mu.Unlock()
return s.processQueues() return s.processQueues()
} }
// flushCombined timer callback
func (s *Stream) flushCombined() {
s.mu.Lock()
s.flushCombinedPacketLocked()
s.mu.Unlock()
s.processQueues()
s.combineTimer.Reset(combineFlushInterval)
}
// flushCombinedPacketLocked sends any pending combined packet (must hold lock)
func (s *Stream) flushCombinedPacketLocked() {
if s.combinedPacket != nil {
s.reliableQueue = append(s.reliableQueue, s.combinedPacket)
s.combinedPacket = nil
}
}
// processQueues processes all packet queues // processQueues processes all packet queues
func (s *Stream) processQueues() error { func (s *Stream) processQueues() error {
s.mu.Lock() s.mu.Lock()
@ -340,6 +383,75 @@ func (s *Stream) processQueues() error {
return nil return nil
} }
// sendFragmented sends a fragmented packet (matches EQStream::SendPacket)
func (s *Stream) sendFragmented(proto *packets.ProtoPacket) error {
// Prepare packet first
offset := proto.PreparePacket(int16(s.maxLen))
if offset < 0 {
return fmt.Errorf("failed to prepare packet")
}
data := proto.Buffer
length := uint32(len(data))
// First fragment with total length
out := packets.NewProtoPacket(opcodes.OP_Fragment, nil, s.opcodeManager)
out.Buffer = make([]byte, s.maxLen-4)
binary.BigEndian.PutUint32(out.Buffer[:4], length)
used := copy(out.Buffer[4:], data)
out.Buffer = out.Buffer[:4+used]
if err := s.sendReliable(out); err != nil {
return err
}
// Send remaining fragments
pos := used
for pos < int(length) {
chunkSize := int(length) - pos
if chunkSize > int(s.maxLen)-4 {
chunkSize = int(s.maxLen) - 4
}
frag := packets.NewProtoPacket(opcodes.OP_Fragment, data[pos:pos+chunkSize], s.opcodeManager)
if err := s.sendReliable(frag); err != nil {
return err
}
pos += chunkSize
}
return nil
}
// sendReliable sends a packet reliably with sequencing
func (s *Stream) sendReliable(proto *packets.ProtoPacket) error {
s.mu.Lock()
seq := s.seqOut
s.seqOut = s.incrementSequence(s.seqOut)
s.mu.Unlock()
data := make([]byte, proto.Size()+2)
binary.BigEndian.PutUint16(data[:2], seq)
proto.Serialize(data[2:], 0)
pending := &pendingPacket{
packet: proto.Copy(),
seq: seq,
sentTime: time.Now(),
attempts: 1,
nextRetry: time.Now().Add(s.rto),
}
s.mu.Lock()
s.pendingAcks[seq] = pending
s.mu.Unlock()
return s.sendRaw(opcodes.OP_Packet, data)
}
// Helper methods // Helper methods
func (s *Stream) isUnreliableOpcode(emuOp opcodes.EmuOpcode) bool { func (s *Stream) isUnreliableOpcode(emuOp opcodes.EmuOpcode) bool {
switch emuOp { switch emuOp {
@ -363,6 +475,7 @@ func (s *Stream) isSequenceAhead(seq, base uint16) bool {
return diff > 0 && diff < 0x8000 return diff > 0 && diff < 0x8000
} }
// Protocol handlers
func (s *Stream) handleSessionRequest(data []byte) error { func (s *Stream) handleSessionRequest(data []byte) error {
if len(data) < 10 { if len(data) < 10 {
return fmt.Errorf("session request too small") return fmt.Errorf("session request too small")
@ -430,6 +543,7 @@ func (s *Stream) handlePacket(data []byte) error {
return err return err
} }
// Process any buffered out-of-order packets
for { for {
if buffered, exists := s.outOfOrder[s.seqExpected]; exists { if buffered, exists := s.outOfOrder[s.seqExpected]; exists {
delete(s.outOfOrder, s.seqExpected) delete(s.outOfOrder, s.seqExpected)
@ -443,12 +557,14 @@ func (s *Stream) handlePacket(data []byte) error {
} }
} }
} else if s.isSequenceAhead(seq, s.seqExpected) { } else if s.isSequenceAhead(seq, s.seqExpected) {
// Out of order - buffer it
s.outOfOrder[seq] = &outOfOrderPacket{ s.outOfOrder[seq] = &outOfOrderPacket{
data: append([]byte(nil), data...), data: append([]byte(nil), data...),
timestamp: time.Now(), timestamp: time.Now(),
} }
go s.sendOutOfOrderAck(seq) go s.sendOutOfOrderAck(seq)
} else { } else {
// Duplicate packet - just ack it
go s.sendAckImmediate(seq) go s.sendAckImmediate(seq)
} }
@ -457,43 +573,46 @@ func (s *Stream) handlePacket(data []byte) error {
} }
func (s *Stream) handleFragment(data []byte) error { func (s *Stream) handleFragment(data []byte) error {
if len(data) < 10 { if len(data) < 2 {
return nil return nil
} }
seq := binary.BigEndian.Uint16(data[:2]) seq := binary.BigEndian.Uint16(data[:2])
fragID := binary.BigEndian.Uint32(data[2:6]) data = data[2:]
fragTotal := binary.BigEndian.Uint16(data[6:8])
fragCur := binary.BigEndian.Uint16(data[8:10])
data = data[10:]
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
frag, exists := s.fragments[uint16(fragID)] // Check if this is start of new fragment stream
if !exists { if s.currentFragment == nil && len(data) >= 4 {
frag = &fragmentBuffer{ totalLen := binary.BigEndian.Uint32(data[:4])
totalSize: uint32(fragTotal), s.currentFragment = &fragmentBuffer{
totalSize: totalLen,
chunks: make(map[uint16][]byte), chunks: make(map[uint16][]byte),
startTime: time.Now(), startTime: time.Now(),
} }
s.fragments[uint16(fragID)] = frag
}
frag.chunks[fragCur] = append([]byte(nil), data...) if len(data) > 4 {
frag.received++ s.currentFragment.chunks[0] = append([]byte(nil), data[4:]...)
s.currentFragment.received = uint32(len(data) - 4)
if frag.received == uint32(fragTotal) { }
complete := make([]byte, 0) } else if s.currentFragment != nil {
for i := uint16(0); i < fragTotal; i++ { // Continuation fragment
if chunk, ok := frag.chunks[i]; ok { chunkNum := uint16(len(s.currentFragment.chunks))
complete = append(complete, chunk...) s.currentFragment.chunks[chunkNum] = append([]byte(nil), data...)
} else { s.currentFragment.received += uint32(len(data))
return nil
} // Check if complete
if s.currentFragment.received >= s.currentFragment.totalSize {
complete := make([]byte, 0, s.currentFragment.totalSize)
for i := uint16(0); i < uint16(len(s.currentFragment.chunks)); i++ {
if chunk, ok := s.currentFragment.chunks[i]; ok {
complete = append(complete, chunk...)
}
}
s.currentFragment = nil
return s.processPacketData(complete)
} }
delete(s.fragments, uint16(fragID))
return s.processPacketData(complete)
} }
s.ackQueue = append(s.ackQueue, seq) s.ackQueue = append(s.ackQueue, seq)
@ -515,6 +634,7 @@ func (s *Stream) handleAck(data []byte) error {
delete(s.pendingAcks, ackSeq) delete(s.pendingAcks, ackSeq)
delete(s.duplicateAckCnt, ackSeq) delete(s.duplicateAckCnt, ackSeq)
// Update RTT
sample := time.Since(pending.sentTime) sample := time.Since(pending.sentTime)
if s.rtt == 0 { if s.rtt == 0 {
s.rtt = sample s.rtt = sample
@ -533,9 +653,11 @@ func (s *Stream) handleAck(data []byte) error {
s.rto = s.maxRTO s.rto = s.maxRTO
} }
} else { } else {
// Duplicate ACK
s.duplicateAckCnt[ackSeq]++ s.duplicateAckCnt[ackSeq]++
if s.duplicateAckCnt[ackSeq] >= duplicateAckThreshold { if s.duplicateAckCnt[ackSeq] >= duplicateAckThreshold {
// Fast retransmit
for seq, pending := range s.pendingAcks { for seq, pending := range s.pendingAcks {
if s.isSequenceAhead(seq, ackSeq) { if s.isSequenceAhead(seq, ackSeq) {
s.resendQueue = append(s.resendQueue, pending) s.resendQueue = append(s.resendQueue, pending)
@ -641,83 +763,19 @@ func (s *Stream) handleDisconnect() error {
return nil return nil
} }
func (s *Stream) sendFragmented(proto *packets.ProtoPacket) error { // processPacketData processes received packet data
data := make([]byte, proto.Size())
proto.Serialize(data, 0)
fragSize := int(s.maxLen) - 12
numFrags := (len(data) + fragSize - 1) / fragSize
if numFrags > 0xFFFF {
return fmt.Errorf("packet too large to fragment")
}
s.mu.Lock()
fragID := s.nextFragID
s.nextFragID++
s.mu.Unlock()
for i := 0; i < numFrags; i++ {
start := i * fragSize
end := start + fragSize
if end > len(data) {
end = len(data)
}
fragHeader := make([]byte, 10)
binary.BigEndian.PutUint32(fragHeader[0:4], uint32(fragID))
binary.BigEndian.PutUint16(fragHeader[4:6], uint16(numFrags))
binary.BigEndian.PutUint16(fragHeader[6:8], uint16(i))
binary.BigEndian.PutUint16(fragHeader[8:10], uint16(end-start))
fragment := append(fragHeader, data[start:end]...)
fragProto := packets.NewProtoPacket(opcodes.OP_Fragment, fragment, s.opcodeManager)
if err := s.sendReliable(fragProto); err != nil {
return err
}
}
return nil
}
func (s *Stream) sendReliable(proto *packets.ProtoPacket) error {
s.mu.Lock()
seq := s.seqOut
s.seqOut = s.incrementSequence(s.seqOut)
s.mu.Unlock()
data := make([]byte, proto.Size()+2)
binary.BigEndian.PutUint16(data[:2], seq)
proto.Serialize(data[2:], 0)
pending := &pendingPacket{
packet: proto.Copy(),
seq: seq,
sentTime: time.Now(),
attempts: 1,
nextRetry: time.Now().Add(s.rto),
}
s.mu.Lock()
s.pendingAcks[seq] = pending
s.mu.Unlock()
return s.sendRaw(opcodes.OP_Packet, data)
}
func (s *Stream) processPacketData(data []byte) error { func (s *Stream) processPacketData(data []byte) error {
proto := packets.NewProtoPacketFromRaw(data, -1, s.opcodeManager) proto := packets.NewProtoPacketFromRaw(data, -1, s.opcodeManager)
if err := proto.DecompressPacket(); err != nil {
return err
}
app := proto.MakeApplicationPacket(s.opcodeSize) app := proto.MakeApplicationPacket(s.opcodeSize)
if s.onPacket != nil { if s.onPacket != nil {
go s.onPacket(app) go s.onPacket(app)
} }
return nil return nil
} }
// Timer handlers
func (s *Stream) processRetransmits() { func (s *Stream) processRetransmits() {
if s.state.Load() != StateEstablished { if s.state.Load() != StateEstablished {
return return
@ -757,18 +815,21 @@ func (s *Stream) cleanup() {
s.mu.Lock() s.mu.Lock()
now := time.Now() now := time.Now()
// Clean up old fragments
for id, frag := range s.fragments { for id, frag := range s.fragments {
if now.Sub(frag.startTime) > fragmentTimeout { if now.Sub(frag.startTime) > fragmentTimeout {
delete(s.fragments, id) delete(s.fragments, id)
} }
} }
// Clean up old out-of-order packets
for seq, oop := range s.outOfOrder { for seq, oop := range s.outOfOrder {
if now.Sub(oop.timestamp) > outOfOrderTimeout { if now.Sub(oop.timestamp) > outOfOrderTimeout {
delete(s.outOfOrder, seq) delete(s.outOfOrder, seq)
} }
} }
// Clean up duplicate ACK counts
for seq := range s.duplicateAckCnt { for seq := range s.duplicateAckCnt {
if _, exists := s.pendingAcks[seq]; !exists { if _, exists := s.pendingAcks[seq]; !exists {
delete(s.duplicateAckCnt, seq) delete(s.duplicateAckCnt, seq)
@ -841,14 +902,16 @@ func (s *Stream) handleTimeout() {
} }
} }
// sendRaw sends raw packet with CRC16
func (s *Stream) sendRaw(opcode uint16, data []byte) error { func (s *Stream) sendRaw(opcode uint16, data []byte) error {
packet := make([]byte, 2+len(data)+2) packet := make([]byte, 2+len(data))
binary.BigEndian.PutUint16(packet[:2], opcode) binary.BigEndian.PutUint16(packet[:2], opcode)
copy(packet[2:], data) copy(packet[2:], data)
crc := crypto.CalculateCRC(packet[:len(packet)-2], s.crcKey) // Add CRC16
binary.BigEndian.PutUint16(packet[len(packet)-2:], crc) packet = packets.AppendCRC(packet, s.crcKey)
// Encrypt if needed
if s.cipher != nil { if s.cipher != nil {
s.cipher.Encrypt(packet) s.cipher.Encrypt(packet)
} }
@ -859,14 +922,6 @@ func (s *Stream) sendRaw(opcode uint16, data []byte) error {
return s.conn.AsyncWrite(packet, nil) return s.conn.AsyncWrite(packet, nil)
} }
func (s *Stream) appToProto(app *packets.AppPacket) *packets.ProtoPacket {
proto := packets.NewProtoPacket(app.Opcode, app.Buffer, s.opcodeManager)
proto.CopyInfo(app.Packet)
proto.CompressThreshold = 100
proto.EncodeKey = s.encodeKey
return proto
}
// Public methods // Public methods
func (s *Stream) SetPacketCallback(fn func(*packets.AppPacket)) { func (s *Stream) SetPacketCallback(fn func(*packets.AppPacket)) {
s.onPacket = fn s.onPacket = fn
@ -886,7 +941,7 @@ func (s *Stream) IsConnected() bool {
func (s *Stream) SendSessionRequest() error { func (s *Stream) SendSessionRequest() error {
data := make([]byte, 10) data := make([]byte, 10)
binary.BigEndian.PutUint32(data[0:4], 2) binary.BigEndian.PutUint32(data[0:4], 2) // protocol version
binary.BigEndian.PutUint32(data[4:8], s.sessionID) binary.BigEndian.PutUint32(data[4:8], s.sessionID)
binary.BigEndian.PutUint16(data[8:10], s.maxLen) binary.BigEndian.PutUint16(data[8:10], s.maxLen)
@ -921,6 +976,7 @@ func (s *Stream) Close() error {
s.state.Store(StateClosed) s.state.Store(StateClosed)
s.sendRaw(opcodes.OP_SessionDisconnect, nil) s.sendRaw(opcodes.OP_SessionDisconnect, nil)
// Stop all timers
if s.keepAliveTimer != nil { if s.keepAliveTimer != nil {
s.keepAliveTimer.Stop() s.keepAliveTimer.Stop()
} }
@ -936,7 +992,11 @@ func (s *Stream) Close() error {
if s.cleanupTimer != nil { if s.cleanupTimer != nil {
s.cleanupTimer.Stop() s.cleanupTimer.Stop()
} }
if s.combineTimer != nil {
s.combineTimer.Stop()
}
// Clear queues
s.mu.Lock() s.mu.Lock()
s.reliableQueue = nil s.reliableQueue = nil
s.unreliableQueue = nil s.unreliableQueue = nil
@ -944,11 +1004,32 @@ func (s *Stream) Close() error {
s.pendingAcks = nil s.pendingAcks = nil
s.fragments = nil s.fragments = nil
s.outOfOrder = nil s.outOfOrder = nil
s.combinedPacket = nil
s.currentFragment = nil
s.mu.Unlock() s.mu.Unlock()
return nil return nil
} }
// GetStats returns stream statistics
func (s *Stream) GetStats() map[string]interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
return map[string]interface{}{
"packets_out": atomic.LoadUint64(&s.packetsOut),
"packets_in": atomic.LoadUint64(&s.packetsIn),
"bytes_out": atomic.LoadUint64(&s.bytesOut),
"bytes_in": atomic.LoadUint64(&s.bytesIn),
"retransmits": atomic.LoadUint64(&s.retransmits),
"pending_acks": len(s.pendingAcks),
"out_of_order": len(s.outOfOrder),
"fragments": len(s.fragments),
"rtt": s.rtt,
"rto": s.rto,
}
}
func absTime(d time.Duration) time.Duration { func absTime(d time.Duration) time.Duration {
if d < 0 { if d < 0 {
return -d return -d