511 lines
11 KiB
Go
511 lines
11 KiB
Go
package packets
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"time"
|
|
|
|
"git.sharkk.net/EQ2/Protocol/opcodes"
|
|
)
|
|
|
|
// ProtoPacket handles low-level protocol features including EQ2-specific operations
|
|
type ProtoPacket struct {
|
|
*Packet
|
|
|
|
// Protocol state flags (using bitfield)
|
|
flags uint8 // bit 0: compressed, bit 1: prepared, bit 2: encrypted, bit 3: acked
|
|
|
|
// 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
|
|
}
|
|
|
|
// Protocol flag constants
|
|
const (
|
|
FlagCompressed = 1 << iota
|
|
FlagPrepared
|
|
FlagEncrypted
|
|
FlagAcked
|
|
)
|
|
|
|
// Default compression threshold
|
|
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,
|
|
}
|
|
}
|
|
|
|
// NewProtoPacketFromRaw creates a protocol packet from raw buffer
|
|
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,
|
|
}
|
|
|
|
// Convert EQ opcode to emulator opcode if manager available
|
|
if pp.manager != nil {
|
|
pp.LoginOp = pp.manager.EQToEmu(opcode)
|
|
}
|
|
|
|
return pp
|
|
}
|
|
|
|
// SetManager sets the opcode manager for translation
|
|
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 {
|
|
if p.IsPrepared() {
|
|
return 0
|
|
}
|
|
|
|
p.SetPrepared(true)
|
|
|
|
// Apply compression if needed
|
|
if err := p.CompressPacket(); err != nil {
|
|
return -1
|
|
}
|
|
|
|
// Apply chat encoding if needed
|
|
p.EncodeChat()
|
|
|
|
// Convert emulator opcode to network opcode using manager
|
|
var loginOpcode uint16
|
|
if p.manager != nil {
|
|
loginOpcode = p.manager.EmuToEQ(p.LoginOp)
|
|
} else {
|
|
loginOpcode = uint16(p.LoginOp)
|
|
}
|
|
|
|
var offset int8
|
|
newSize := len(p.Buffer) + 2 + 1 // sequence(2) + compressed_flag(1)
|
|
oversized := false
|
|
|
|
// Add compression flag space if compressed
|
|
if p.IsCompressed() {
|
|
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
|
|
newBuffer := make([]byte, newSize)
|
|
ptr := 2 // Skip sequence field
|
|
|
|
// Add compression flag if needed
|
|
if p.IsCompressed() {
|
|
newBuffer[ptr] = 0x5a // EQ compression flag
|
|
ptr++
|
|
}
|
|
|
|
if loginOpcode != 2 {
|
|
if oversized {
|
|
newBuffer[ptr] = 0xff // Oversized marker
|
|
ptr++
|
|
binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode)
|
|
ptr += 2
|
|
} else {
|
|
binary.BigEndian.PutUint16(newBuffer[ptr:], loginOpcode)
|
|
ptr += 2
|
|
}
|
|
} else {
|
|
newBuffer[ptr] = byte(loginOpcode)
|
|
ptr++
|
|
}
|
|
|
|
// Copy original packet data
|
|
copy(newBuffer[ptr:], p.Buffer)
|
|
|
|
p.Buffer = newBuffer
|
|
offset = int8(newSize - len(p.Buffer) - 1)
|
|
|
|
return offset
|
|
}
|
|
|
|
// MakeApplicationPacket converts protocol packet to application packet
|
|
func (p *ProtoPacket) MakeApplicationPacket(opcodeSize uint8) *AppPacket {
|
|
// Decompress if needed
|
|
p.DecompressPacket()
|
|
|
|
// Decode chat if needed
|
|
p.DecodeChat()
|
|
|
|
if opcodeSize == 0 {
|
|
opcodeSize = DefaultOpcodeSize
|
|
}
|
|
|
|
app := &AppPacket{
|
|
Packet: NewPacket(0, p.Buffer),
|
|
opcodeSize: opcodeSize,
|
|
manager: p.manager,
|
|
}
|
|
app.CopyInfo(p.Packet)
|
|
|
|
// Parse opcode from buffer based on size
|
|
if opcodeSize == 1 && len(p.Buffer) >= 1 {
|
|
app.Opcode = uint16(p.Buffer[0])
|
|
app.Buffer = p.Buffer[1:]
|
|
} 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
|
|
if app.manager != nil {
|
|
app.emuOpcode = app.manager.EQToEmu(app.Opcode)
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
// AppCombine combines this packet with another for efficient transmission
|
|
func (p *ProtoPacket) AppCombine(rhs *ProtoPacket) bool {
|
|
const opAppCombined = 0x19 // OP_AppCombined
|
|
|
|
// Calculate sizes
|
|
lhsSize := p.Size()
|
|
rhsSize := rhs.Size()
|
|
|
|
// Check max combined size
|
|
if lhsSize+rhsSize > MaxCombinedSize {
|
|
return false
|
|
}
|
|
|
|
// If this packet is already combined, add to it
|
|
if p.Opcode == opAppCombined {
|
|
// Calculate new size
|
|
var newSize int
|
|
if rhsSize >= 255 {
|
|
newSize = len(p.Buffer) + int(rhsSize) + 3 // oversized header
|
|
} else {
|
|
newSize = len(p.Buffer) + int(rhsSize) + 1 // normal header
|
|
}
|
|
|
|
// Check size limit
|
|
if newSize > MaxCombinedSize {
|
|
return false
|
|
}
|
|
|
|
// Create new buffer
|
|
newBuffer := make([]byte, newSize)
|
|
pos := 0
|
|
|
|
// Copy existing combined data
|
|
copy(newBuffer, p.Buffer)
|
|
pos = len(p.Buffer)
|
|
|
|
// Add new packet with size header
|
|
if rhsSize >= 255 {
|
|
newBuffer[pos] = 255 // Oversized marker
|
|
pos++
|
|
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rhsSize))
|
|
pos += 2
|
|
} else {
|
|
newBuffer[pos] = byte(rhsSize)
|
|
pos++
|
|
}
|
|
|
|
// Serialize rhs packet
|
|
tmpBuf := make([]byte, rhsSize)
|
|
rhs.Serialize(tmpBuf, 0)
|
|
copy(newBuffer[pos:], tmpBuf)
|
|
|
|
p.Buffer = newBuffer
|
|
return true
|
|
}
|
|
|
|
// Create new combined packet from two non-combined packets
|
|
// Calculate total size: size1(1-3) + packet1 + size2(1-3) + packet2
|
|
var totalSize int
|
|
if lhsSize >= 255 {
|
|
totalSize += 3 + int(lhsSize)
|
|
} else {
|
|
totalSize += 1 + int(lhsSize)
|
|
}
|
|
if rhsSize >= 255 {
|
|
totalSize += 3 + int(rhsSize)
|
|
} else {
|
|
totalSize += 1 + int(rhsSize)
|
|
}
|
|
|
|
// Check size limit
|
|
if totalSize > MaxCombinedSize {
|
|
return false
|
|
}
|
|
|
|
// Build combined packet
|
|
newBuffer := make([]byte, totalSize)
|
|
pos := 0
|
|
|
|
// Add first packet with size header
|
|
if lhsSize >= 255 {
|
|
newBuffer[pos] = 255
|
|
pos++
|
|
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(lhsSize))
|
|
pos += 2
|
|
} else {
|
|
newBuffer[pos] = byte(lhsSize)
|
|
pos++
|
|
}
|
|
|
|
// Serialize first packet
|
|
tmpBuf := make([]byte, lhsSize)
|
|
p.Serialize(tmpBuf, 0)
|
|
copy(newBuffer[pos:], tmpBuf)
|
|
pos += int(lhsSize)
|
|
|
|
// Add second packet with size header
|
|
if rhsSize >= 255 {
|
|
newBuffer[pos] = 255
|
|
pos++
|
|
binary.BigEndian.PutUint16(newBuffer[pos:], uint16(rhsSize))
|
|
pos += 2
|
|
} else {
|
|
newBuffer[pos] = byte(rhsSize)
|
|
pos++
|
|
}
|
|
|
|
// 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.Buffer = newBuffer
|
|
p.SetPrepared(false) // Need to re-prepare
|
|
|
|
return true
|
|
}
|