1
0

reset, simplify CRC

This commit is contained in:
Sky Johnson 2025-09-02 21:52:39 -05:00
parent 46642d41d0
commit 0d651853ff
10 changed files with 10 additions and 2960 deletions

View File

@ -5,28 +5,19 @@ import (
"hash/crc32"
)
// Standard CRC32 polynomial
var crcTable = crc32.MakeTable(0xEDB88320)
// EQ2CRCKey is likely the reverse engineered security key for the protocol
const EQ2CRCKey uint32 = 0x33624702
// Fixed key used by EQ2
var EQ2CRCKey = 0x33624702
// CalculateCRC gets the CRC for a given byte slice using a custom key
// CalculateCRC computes CRC32( little_endian(key) || data ) and returns the low 16 bits
// of the final ~CRC, matching the EQ2 behavior
func CalculateCRC(data []byte, key uint32) uint16 {
// Pre-process the key (4 rounds of CRC on key bytes)
crc := uint32(0xFFFFFFFF)
keyBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(keyBytes, key)
crc := ^uint32(0)
for _, b := range keyBytes {
crc = crcTable[(crc^uint32(b))&0xFF] ^ (crc >> 8)
}
var k [4]byte
binary.LittleEndian.PutUint32(k[:], key)
// Process actual data
for _, b := range data {
crc = crcTable[(crc^uint32(b))&0xFF] ^ (crc >> 8)
}
crc = crc32.Update(crc, crc32.IEEETable, k[:])
crc = crc32.Update(crc, crc32.IEEETable, data)
// Return lower 16 bits of inverted result
return uint16(^crc & 0xFFFF)
return uint16(^crc)
}

View File

@ -1,365 +0,0 @@
package eq2net
import (
"encoding/binary"
"fmt"
)
// GameEmuOpcode represents emulator opcodes for the game server
type GameEmuOpcode uint16
// Game server emulator opcodes (partial list - there are hundreds)
const (
GOP_Unknown GameEmuOpcode = iota
GOP_LoginReplyMsg
GOP_LoginByNumRequestMsg
GOP_WSLoginRequestMsg
GOP_ESInitMsg
GOP_ESReadyForClientsMsg
GOP_CreateZoneInstanceMsg
GOP_ZoneInstanceCreateReplyMsg
GOP_ZoneInstanceDestroyedMsg
GOP_ExpectClientAsCharacterRequest
GOP_ExpectClientAsCharacterReplyMs
GOP_ZoneInfoMsg
GOP_CreateCharacterRequestMsg
GOP_DoneLoadingZoneResourcesMsg
GOP_DoneSendingInitialEntitiesMsg
GOP_DoneLoadingEntityResourcesMsg
GOP_DoneLoadingUIResourcesMsg
GOP_PredictionUpdateMsg
GOP_RemoteCmdMsg
GOP_SetRemoteCmdsMsg
GOP_GameWorldTimeMsg
GOP_MOTDMsg
GOP_ZoneMOTDMsg
GOP_GuildRecruitingMemberInfo
GOP_GuildRecruiting
GOP_GuildRecruitingDetails
GOP_GuildRecruitingImage
GOP_AvatarCreatedMsg
GOP_AvatarDestroyedMsg
GOP_RequestCampMsg
GOP_MapRequest
GOP_CampStartedMsg
GOP_CampAbortedMsg
GOP_WhoQueryRequestMsg
GOP_WhoQueryReplyMsg
GOP_RemoveClientFromGroupMsg
GOP_GroupCreatedMsg
GOP_GroupDestroyedMsg
GOP_GroupMemberAddedMsg
GOP_GroupMemberRemovedMsg
GOP_GroupInfoRequestMsg
GOP_GroupInfoUpdateMsg
GOP_GroupRemovedFromGroupMsg
GOP_GroupLeaderChangedMsg
GOP_GroupOptionsMsg
GOP_UpdateDataMsg
GOP_UpdateSpawnMsg
GOP_UpdateCharacterSheetMsg
GOP_UpdateSkillsMsg
GOP_UpdateQuestMsg
GOP_UpdateInventoryMsg
GOP_UpdatePositionMsg
GOP_UpdateRaidMsg
GOP_UpdateTradeMsg
GOP_UpdateTargetMsg
GOP_UpdateTargetLocMsg
GOP_UpdateActorTargetMsg
GOP_UpdatePlayerMailMsg
GOP_UpdatePlayerMail
GOP_UpdateTimeMsg
GOP_MaxGameOpcode // Add more as needed
)
// GamePacket represents a game server packet
type GamePacket struct {
EQPacket // Embed base packet
GameEmuOpcode GameEmuOpcode // Game server emulator opcode
}
// NewGamePacket creates a new game server packet
func NewGamePacket(opcode GameEmuOpcode, data []byte) *GamePacket {
base := NewEQPacket(uint16(opcode), data)
base.EmuOpcode = uint16(opcode)
base.OpcodeSize = 2 // Game server uses 2-byte opcodes
p := &GamePacket{
EQPacket: *base,
GameEmuOpcode: opcode,
}
return p
}
// ParseGamePacket creates a game packet from raw data
func ParseGamePacket(data []byte) (*GamePacket, error) {
if len(data) < 2 {
return nil, fmt.Errorf("packet too small for game opcode")
}
// Extract 2-byte opcode
var opcode GameEmuOpcode
var remainingData []byte
// Handle special encoding for 2-byte opcodes
// Special case: When we have 3+ bytes and first is 0x00 and third is 0x00,
// it's the special encoding for opcodes with low byte = 0x00
if len(data) > 2 && data[0] == 0x00 && data[2] == 0x00 {
// Extra 0x00 prefix for opcodes like 0x1200
opcode = GameEmuOpcode(binary.BigEndian.Uint16(data[1:3]))
remainingData = data[3:] // Skip the extra byte
} else {
opcode = GameEmuOpcode(binary.BigEndian.Uint16(data[0:2]))
remainingData = data[2:]
}
// Create packet with remaining data
base := NewEQPacket(uint16(opcode), remainingData)
base.EmuOpcode = uint16(opcode)
base.OpcodeSize = 2
p := &GamePacket{
EQPacket: *base,
GameEmuOpcode: opcode,
}
return p, nil
}
// GetGameOpcode returns the game emulator opcode
func (p *GamePacket) GetGameOpcode() GameEmuOpcode {
return p.GameEmuOpcode
}
// SetGameOpcode sets the game emulator opcode
func (p *GamePacket) SetGameOpcode(opcode GameEmuOpcode) {
p.GameEmuOpcode = opcode
p.EmuOpcode = uint16(opcode)
p.Opcode = uint16(opcode)
}
// SerializeGame serializes the game packet with proper encoding
func (p *GamePacket) SerializeGame() []byte {
// Handle special encoding rules for 2-byte opcodes
opcodeBytes := 2
extraBytes := 0
if (uint16(p.GameEmuOpcode) & 0x00FF) == 0 {
// Opcodes with low byte = 0x00 need an extra 0x00 prefix
extraBytes = 1
}
// Create buffer
buf := make([]byte, opcodeBytes+extraBytes+len(p.Buffer))
offset := 0
// Write opcode
if extraBytes > 0 {
// Special encoding: add 0x00 prefix
buf[offset] = 0x00
offset++
binary.BigEndian.PutUint16(buf[offset:], uint16(p.GameEmuOpcode))
offset += 2
} else {
binary.BigEndian.PutUint16(buf[offset:], uint16(p.GameEmuOpcode))
offset += 2
}
// Copy packet data
if len(p.Buffer) > 0 {
copy(buf[offset:], p.Buffer)
}
return buf
}
// GetGameOpcodeName returns the string name of a game emulator opcode
func GetGameOpcodeName(opcode GameEmuOpcode) string {
switch opcode {
case GOP_Unknown:
return "GOP_Unknown"
case GOP_LoginReplyMsg:
return "GOP_LoginReplyMsg"
case GOP_LoginByNumRequestMsg:
return "GOP_LoginByNumRequestMsg"
case GOP_WSLoginRequestMsg:
return "GOP_WSLoginRequestMsg"
case GOP_ESInitMsg:
return "GOP_ESInitMsg"
case GOP_ESReadyForClientsMsg:
return "GOP_ESReadyForClientsMsg"
case GOP_CreateZoneInstanceMsg:
return "GOP_CreateZoneInstanceMsg"
case GOP_ZoneInstanceCreateReplyMsg:
return "GOP_ZoneInstanceCreateReplyMsg"
case GOP_ZoneInstanceDestroyedMsg:
return "GOP_ZoneInstanceDestroyedMsg"
case GOP_ExpectClientAsCharacterRequest:
return "GOP_ExpectClientAsCharacterRequest"
case GOP_ExpectClientAsCharacterReplyMs:
return "GOP_ExpectClientAsCharacterReplyMs"
case GOP_ZoneInfoMsg:
return "GOP_ZoneInfoMsg"
case GOP_CreateCharacterRequestMsg:
return "GOP_CreateCharacterRequestMsg"
case GOP_DoneLoadingZoneResourcesMsg:
return "GOP_DoneLoadingZoneResourcesMsg"
case GOP_DoneSendingInitialEntitiesMsg:
return "GOP_DoneSendingInitialEntitiesMsg"
case GOP_DoneLoadingEntityResourcesMsg:
return "GOP_DoneLoadingEntityResourcesMsg"
case GOP_DoneLoadingUIResourcesMsg:
return "GOP_DoneLoadingUIResourcesMsg"
case GOP_PredictionUpdateMsg:
return "GOP_PredictionUpdateMsg"
case GOP_RemoteCmdMsg:
return "GOP_RemoteCmdMsg"
case GOP_SetRemoteCmdsMsg:
return "GOP_SetRemoteCmdsMsg"
case GOP_GameWorldTimeMsg:
return "GOP_GameWorldTimeMsg"
case GOP_MOTDMsg:
return "GOP_MOTDMsg"
case GOP_ZoneMOTDMsg:
return "GOP_ZoneMOTDMsg"
case GOP_GuildRecruitingMemberInfo:
return "GOP_GuildRecruitingMemberInfo"
case GOP_GuildRecruiting:
return "GOP_GuildRecruiting"
case GOP_GuildRecruitingDetails:
return "GOP_GuildRecruitingDetails"
case GOP_GuildRecruitingImage:
return "GOP_GuildRecruitingImage"
case GOP_AvatarCreatedMsg:
return "GOP_AvatarCreatedMsg"
case GOP_AvatarDestroyedMsg:
return "GOP_AvatarDestroyedMsg"
case GOP_RequestCampMsg:
return "GOP_RequestCampMsg"
case GOP_MapRequest:
return "GOP_MapRequest"
case GOP_CampStartedMsg:
return "GOP_CampStartedMsg"
case GOP_CampAbortedMsg:
return "GOP_CampAbortedMsg"
case GOP_WhoQueryRequestMsg:
return "GOP_WhoQueryRequestMsg"
case GOP_WhoQueryReplyMsg:
return "GOP_WhoQueryReplyMsg"
case GOP_RemoveClientFromGroupMsg:
return "GOP_RemoveClientFromGroupMsg"
case GOP_GroupCreatedMsg:
return "GOP_GroupCreatedMsg"
case GOP_GroupDestroyedMsg:
return "GOP_GroupDestroyedMsg"
case GOP_GroupMemberAddedMsg:
return "GOP_GroupMemberAddedMsg"
case GOP_GroupMemberRemovedMsg:
return "GOP_GroupMemberRemovedMsg"
case GOP_GroupInfoRequestMsg:
return "GOP_GroupInfoRequestMsg"
case GOP_GroupInfoUpdateMsg:
return "GOP_GroupInfoUpdateMsg"
case GOP_GroupRemovedFromGroupMsg:
return "GOP_GroupRemovedFromGroupMsg"
case GOP_GroupLeaderChangedMsg:
return "GOP_GroupLeaderChangedMsg"
case GOP_GroupOptionsMsg:
return "GOP_GroupOptionsMsg"
case GOP_UpdateDataMsg:
return "GOP_UpdateDataMsg"
case GOP_UpdateSpawnMsg:
return "GOP_UpdateSpawnMsg"
case GOP_UpdateCharacterSheetMsg:
return "GOP_UpdateCharacterSheetMsg"
case GOP_UpdateSkillsMsg:
return "GOP_UpdateSkillsMsg"
case GOP_UpdateQuestMsg:
return "GOP_UpdateQuestMsg"
case GOP_UpdateInventoryMsg:
return "GOP_UpdateInventoryMsg"
case GOP_UpdatePositionMsg:
return "GOP_UpdatePositionMsg"
case GOP_UpdateRaidMsg:
return "GOP_UpdateRaidMsg"
case GOP_UpdateTradeMsg:
return "GOP_UpdateTradeMsg"
case GOP_UpdateTargetMsg:
return "GOP_UpdateTargetMsg"
case GOP_UpdateTargetLocMsg:
return "GOP_UpdateTargetLocMsg"
case GOP_UpdateActorTargetMsg:
return "GOP_UpdateActorTargetMsg"
case GOP_UpdatePlayerMailMsg:
return "GOP_UpdatePlayerMailMsg"
case GOP_UpdatePlayerMail:
return "GOP_UpdatePlayerMail"
case GOP_UpdateTimeMsg:
return "GOP_UpdateTimeMsg"
default:
return fmt.Sprintf("Unknown(%d)", opcode)
}
}
// ConvertToProtocolPacket wraps this game packet in a protocol packet
func (p *GamePacket) ConvertToProtocolPacket() *ProtocolPacket {
// Serialize the game packet
gameData := p.SerializeGame()
// Create protocol packet with OP_Packet opcode
proto := NewProtocolPacket(OP_Packet, gameData)
// Copy network info
proto.SrcIP = p.SrcIP
proto.SrcPort = p.SrcPort
proto.DstIP = p.DstIP
proto.DstPort = p.DstPort
proto.Timestamp = p.Timestamp
proto.Version = p.Version
return proto
}
// Copy creates a deep copy of the game packet
func (p *GamePacket) Copy() *GamePacket {
newPacket := &GamePacket{
EQPacket: *p.Clone(),
GameEmuOpcode: p.GameEmuOpcode,
}
return newPacket
}
// CombineGamePackets combines multiple game packets for efficient transmission
func CombineGamePackets(packets []*GamePacket) *ProtocolPacket {
if len(packets) == 0 {
return nil
}
if len(packets) == 1 {
return packets[0].ConvertToProtocolPacket()
}
// Build combined buffer
var buf []byte
for _, p := range packets {
data := p.SerializeGame()
size := len(data)
// Add size encoding
if size >= 255 {
// Oversized packet
buf = append(buf, 0xFF)
buf = append(buf, byte(size>>8), byte(size))
} else {
buf = append(buf, byte(size))
}
// Add packet data
buf = append(buf, data...)
}
// Create combined protocol packet
return NewProtocolPacket(OP_AppCombined, buf)
}

View File

@ -1,235 +0,0 @@
package eq2net
import (
"fmt"
)
// LoginEmuOpcode represents emulator opcodes for the login server
type LoginEmuOpcode uint16
// Login server emulator opcodes
const (
LOP_Unknown LoginEmuOpcode = iota
LOP_LoginRequestMsg
LOP_LoginByNumRequestMsg
LOP_WSLoginRequestMsg
LOP_ESLoginRequestMsg
LOP_LoginReplyMsg
LOP_WorldListMsg
LOP_WorldStatusChangeMsg
LOP_AllWSDescRequestMsg
LOP_WSStatusReplyMsg
LOP_AllCharactersDescRequestMsg
LOP_AllCharactersDescReplyMsg
LOP_CreateCharacterRequestMsg
LOP_ReskinCharacterRequestMsg
LOP_CreateCharacterReplyMsg
LOP_WSCreateCharacterRequestMsg
LOP_WSCreateCharacterReplyMsg
LOP_DeleteCharacterRequestMsg
LOP_DeleteCharacterReplyMsg
LOP_PlayCharacterRequestMsg
LOP_PlayCharacterReplyMsg
LOP_ServerPlayCharacterRequestMsg
LOP_ServerPlayCharacterReplyMsg
LOP_KeymapLoadMsg
LOP_KeymapNoneMsg
LOP_KeymapDataMsg
LOP_KeymapSaveMsg
LOP_LSCheckAcctLockMsg
LOP_WSAcctLockStatusMsg
LOP_LsRequestClientCrashLogMsg
LOP_LsClientBaselogReplyMsg
LOP_LsClientCrashlogReplyMsg
LOP_LsClientAlertlogReplyMsg
LOP_LsClientVerifylogReplyMsg
LOP_BadLanguageFilter
LOP_WSServerLockMsg
LOP_WSServerHideMsg
LOP_LSServerLockMsg
LOP_UpdateCharacterSheetMsg
LOP_UpdateInventoryMsg
LOP_MaxLoginOpcode
)
// LoginPacket represents a login server packet
type LoginPacket struct {
EQPacket // Embed base packet
LoginEmuOpcode LoginEmuOpcode // Login server emulator opcode
}
// NewLoginPacket creates a new login server packet
func NewLoginPacket(opcode LoginEmuOpcode, data []byte) *LoginPacket {
base := NewEQPacket(uint16(opcode), data)
base.EmuOpcode = uint16(opcode)
base.OpcodeSize = 1 // Login server uses 1-byte opcodes
p := &LoginPacket{
EQPacket: *base,
LoginEmuOpcode: opcode,
}
return p
}
// ParseLoginPacket creates a login packet from raw data
func ParseLoginPacket(data []byte) (*LoginPacket, error) {
if len(data) < 1 {
return nil, fmt.Errorf("packet too small for login opcode")
}
// Extract 1-byte opcode
opcode := LoginEmuOpcode(data[0])
// Create packet with remaining data
base := NewEQPacket(uint16(opcode), data[1:])
base.EmuOpcode = uint16(opcode)
base.OpcodeSize = 1
p := &LoginPacket{
EQPacket: *base,
LoginEmuOpcode: opcode,
}
return p, nil
}
// GetLoginOpcode returns the login emulator opcode
func (p *LoginPacket) GetLoginOpcode() LoginEmuOpcode {
return p.LoginEmuOpcode
}
// SetLoginOpcode sets the login emulator opcode
func (p *LoginPacket) SetLoginOpcode(opcode LoginEmuOpcode) {
p.LoginEmuOpcode = opcode
p.EmuOpcode = uint16(opcode)
p.Opcode = uint16(opcode)
}
// SerializeLogin serializes the login packet with proper encoding
func (p *LoginPacket) SerializeLogin() []byte {
// Login packets use 1-byte opcodes
buf := make([]byte, 1+len(p.Buffer))
buf[0] = byte(p.LoginEmuOpcode)
if len(p.Buffer) > 0 {
copy(buf[1:], p.Buffer)
}
return buf
}
// GetLoginOpcodeName returns the string name of a login emulator opcode
func GetLoginOpcodeName(opcode LoginEmuOpcode) string {
switch opcode {
case LOP_Unknown:
return "LOP_Unknown"
case LOP_LoginRequestMsg:
return "LOP_LoginRequestMsg"
case LOP_LoginByNumRequestMsg:
return "LOP_LoginByNumRequestMsg"
case LOP_WSLoginRequestMsg:
return "LOP_WSLoginRequestMsg"
case LOP_ESLoginRequestMsg:
return "LOP_ESLoginRequestMsg"
case LOP_LoginReplyMsg:
return "LOP_LoginReplyMsg"
case LOP_WorldListMsg:
return "LOP_WorldListMsg"
case LOP_WorldStatusChangeMsg:
return "LOP_WorldStatusChangeMsg"
case LOP_AllWSDescRequestMsg:
return "LOP_AllWSDescRequestMsg"
case LOP_WSStatusReplyMsg:
return "LOP_WSStatusReplyMsg"
case LOP_AllCharactersDescRequestMsg:
return "LOP_AllCharactersDescRequestMsg"
case LOP_AllCharactersDescReplyMsg:
return "LOP_AllCharactersDescReplyMsg"
case LOP_CreateCharacterRequestMsg:
return "LOP_CreateCharacterRequestMsg"
case LOP_ReskinCharacterRequestMsg:
return "LOP_ReskinCharacterRequestMsg"
case LOP_CreateCharacterReplyMsg:
return "LOP_CreateCharacterReplyMsg"
case LOP_WSCreateCharacterRequestMsg:
return "LOP_WSCreateCharacterRequestMsg"
case LOP_WSCreateCharacterReplyMsg:
return "LOP_WSCreateCharacterReplyMsg"
case LOP_DeleteCharacterRequestMsg:
return "LOP_DeleteCharacterRequestMsg"
case LOP_DeleteCharacterReplyMsg:
return "LOP_DeleteCharacterReplyMsg"
case LOP_PlayCharacterRequestMsg:
return "LOP_PlayCharacterRequestMsg"
case LOP_PlayCharacterReplyMsg:
return "LOP_PlayCharacterReplyMsg"
case LOP_ServerPlayCharacterRequestMsg:
return "LOP_ServerPlayCharacterRequestMsg"
case LOP_ServerPlayCharacterReplyMsg:
return "LOP_ServerPlayCharacterReplyMsg"
case LOP_KeymapLoadMsg:
return "LOP_KeymapLoadMsg"
case LOP_KeymapNoneMsg:
return "LOP_KeymapNoneMsg"
case LOP_KeymapDataMsg:
return "LOP_KeymapDataMsg"
case LOP_KeymapSaveMsg:
return "LOP_KeymapSaveMsg"
case LOP_LSCheckAcctLockMsg:
return "LOP_LSCheckAcctLockMsg"
case LOP_WSAcctLockStatusMsg:
return "LOP_WSAcctLockStatusMsg"
case LOP_LsRequestClientCrashLogMsg:
return "LOP_LsRequestClientCrashLogMsg"
case LOP_LsClientBaselogReplyMsg:
return "LOP_LsClientBaselogReplyMsg"
case LOP_LsClientCrashlogReplyMsg:
return "LOP_LsClientCrashlogReplyMsg"
case LOP_LsClientAlertlogReplyMsg:
return "LOP_LsClientAlertlogReplyMsg"
case LOP_LsClientVerifylogReplyMsg:
return "LOP_LsClientVerifylogReplyMsg"
case LOP_BadLanguageFilter:
return "LOP_BadLanguageFilter"
case LOP_WSServerLockMsg:
return "LOP_WSServerLockMsg"
case LOP_WSServerHideMsg:
return "LOP_WSServerHideMsg"
case LOP_LSServerLockMsg:
return "LOP_LSServerLockMsg"
case LOP_UpdateCharacterSheetMsg:
return "LOP_UpdateCharacterSheetMsg"
case LOP_UpdateInventoryMsg:
return "LOP_UpdateInventoryMsg"
default:
return fmt.Sprintf("Unknown(%d)", opcode)
}
}
// ConvertToProtocolPacket wraps this login packet in a protocol packet
func (p *LoginPacket) ConvertToProtocolPacket() *ProtocolPacket {
// Serialize the login packet
loginData := p.SerializeLogin()
// Create protocol packet with OP_Packet opcode
proto := NewProtocolPacket(OP_Packet, loginData)
// Copy network info
proto.SrcIP = p.SrcIP
proto.SrcPort = p.SrcPort
proto.DstIP = p.DstIP
proto.DstPort = p.DstPort
proto.Timestamp = p.Timestamp
proto.Version = p.Version
return proto
}
// Copy creates a deep copy of the login packet
func (p *LoginPacket) Copy() *LoginPacket {
newPacket := &LoginPacket{
EQPacket: *p.Clone(),
LoginEmuOpcode: p.LoginEmuOpcode,
}
return newPacket
}

View File

@ -1,56 +0,0 @@
package eq2net
// Login/chat use 1-byte opcodes
const AppOpcodeSize = 1
// The game uses 2-byte opcodes
const GameOpcodeSize = 2
// Protocol-level opcodes for the actual UDP stream
const (
OP_SessionRequest = 0x0001
OP_SessionResponse = 0x0002
OP_Combined = 0x0003
OP_SessionDisconnect = 0x0005
OP_KeepAlive = 0x0006
OP_ClientSessionUpdate = 0x0007
OP_SessionStatRequest = 0x0008
OP_SessionStatResponse = 0x0008 // Same opcode, different direction
OP_Packet = 0x0009
OP_Fragment = 0x000D
OP_OutOfOrderAck = 0x0011
OP_Ack = 0x0015
OP_AckFuture = 0x0016
OP_AckPast = 0x0017
OP_AppCombined = 0x0019
OP_OutOfSession = 0x001D
)
// Login server opcodes
const (
OP_LoginInfo = 0x0100
OP_Login2 = 0x0200
OP_GetLoginInfo = 0x0300
OP_Disconnect = 0x0500
OP_SessionId = 0x0900
OP_SendServersFragment = 0x0D00
OP_RegisterAccount = 0x2300
OP_ChangePassword = 0x4500
OP_ServerList = 0x4600
OP_SessionKey = 0x4700
OP_RequestServerStatus = 0x4800
OP_SendServerStatus = 0x4A00
OP_LoginBanner = 0x5200
OP_Version = 0x5900
)
// Common emulator opcodes (internal representation)
const (
OP_Unknown = 0x0000
OP_LoginRequestMsg = 0x0001
OP_LoginByNumRequestMsg = 0x0002
OP_WSLoginRequestMsg = 0x0003
OP_ESLoginRequestMsg = 0x0004
OP_LoginReplyMsg = 0x0005
OP_WSStatusReplyMsg = 0x0006
)

297
packet.go
View File

@ -1,297 +0,0 @@
package eq2net
import (
"encoding/binary"
"fmt"
"net"
"time"
)
const (
PROTOCOL_VERSION = 3 // EQ2 protocol version
)
// EQPacket is the base packet structure for all packet types
// Combines functionality of EQPacket and EQ2Packet from C++ implementation
type EQPacket struct {
Buffer []byte // Raw packet data
Opcode uint16 // Network opcode
SrcIP net.IP // Source IP address
SrcPort uint16 // Source port
DstIP net.IP // Destination IP address
DstPort uint16 // Destination port
Timestamp time.Time // When packet was created/received
Priority uint32 // Priority in processing
Version int16 // Protocol version
// EQ2-specific fields (from EQ2Packet)
EmuOpcode uint16 // Emulator opcode (internal representation)
OpcodeSize uint8 // Size of opcode in bytes (1 or 2)
PacketPrepared bool // Packet has been prepared for wire transmission
PacketEncrypted bool // Packet is encrypted
EQ2Compressed bool // Packet uses EQ2 compression
}
func NewEQPacket(opcode uint16, data []byte) *EQPacket {
p := &EQPacket{
Opcode: opcode,
Timestamp: time.Now(),
Priority: 0,
Version: PROTOCOL_VERSION,
}
if len(data) > 0 {
p.Buffer = make([]byte, len(data))
copy(p.Buffer, data)
}
return p
}
// Size returns the size of the packet data (excluding opcode)
func (p *EQPacket) Size() uint32 {
return uint32(len(p.Buffer))
}
// TotalSize returns the total size including opcode
func (p *EQPacket) TotalSize() uint32 {
opcodeSize := uint32(1)
if p.Opcode > 0xFF {
opcodeSize = 2
}
return p.Size() + opcodeSize
}
// GetRawOpcode returns the raw opcode value
func (p *EQPacket) GetRawOpcode() uint16 {
return p.Opcode
}
// GetOpcodeName returns the string name of the opcode
func (p *EQPacket) GetOpcodeName() string {
switch p.Opcode {
case OP_SessionRequest:
return "OP_SessionRequest"
case OP_SessionResponse:
return "OP_SessionResponse"
case OP_Combined:
return "OP_Combined"
case OP_SessionDisconnect:
return "OP_SessionDisconnect"
case OP_KeepAlive:
return "OP_KeepAlive"
case OP_SessionStatRequest: // Also OP_SessionStatResponse (same value)
return "OP_SessionStat"
case OP_Packet:
return "OP_Packet"
case OP_Fragment:
return "OP_Fragment"
case OP_OutOfOrderAck:
return "OP_OutOfOrderAck"
case OP_Ack:
return "OP_Ack"
case OP_AppCombined:
return "OP_AppCombined"
case OP_OutOfSession:
return "OP_OutOfSession"
default:
return fmt.Sprintf("Unknown(0x%04X)", p.Opcode)
}
}
// Serialize writes the packet to a byte buffer
func (p *EQPacket) Serialize() []byte {
// Determine opcode size
opcodeSize := 1
if p.Opcode > 0xFF {
opcodeSize = 2
}
// Create buffer
buf := make([]byte, opcodeSize+len(p.Buffer))
// Write opcode
if opcodeSize == 2 {
binary.BigEndian.PutUint16(buf[0:2], p.Opcode)
} else {
buf[0] = byte(p.Opcode)
}
// Copy data
if len(p.Buffer) > 0 {
copy(buf[opcodeSize:], p.Buffer)
}
return buf
}
// SetNetworkInfo sets the network address info
func (p *EQPacket) SetNetworkInfo(srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) {
p.SrcIP = srcIP
p.SrcPort = srcPort
p.DstIP = dstIP
p.DstPort = dstPort
}
// Clone creates a deep copy of the packet
func (p *EQPacket) Clone() *EQPacket {
newPacket := &EQPacket{
Opcode: p.Opcode,
SrcIP: make(net.IP, len(p.SrcIP)),
SrcPort: p.SrcPort,
DstIP: make(net.IP, len(p.DstIP)),
DstPort: p.DstPort,
Timestamp: p.Timestamp,
Priority: p.Priority,
Version: p.Version,
EmuOpcode: p.EmuOpcode,
OpcodeSize: p.OpcodeSize,
PacketPrepared: p.PacketPrepared,
PacketEncrypted: p.PacketEncrypted,
EQ2Compressed: p.EQ2Compressed,
}
if p.SrcIP != nil {
copy(newPacket.SrcIP, p.SrcIP)
}
if p.DstIP != nil {
copy(newPacket.DstIP, p.DstIP)
}
if len(p.Buffer) > 0 {
newPacket.Buffer = make([]byte, len(p.Buffer))
copy(newPacket.Buffer, p.Buffer)
}
return newPacket
}
// ParsePacket creates a packet from raw network data
func ParsePacket(data []byte) (*EQPacket, error) {
if len(data) < 1 {
return nil, fmt.Errorf("packet too small: %d bytes", len(data))
}
var opcode uint16
var dataOffset int
// Try to determine if it's a 1-byte or 2-byte opcode
// For simplicity in tests: if first byte > 0 and < 0x80, treat as single byte
// This is a heuristic for test compatibility
if data[0] > 0x00 && data[0] < 0x80 && (len(data) == 1 || data[1] != 0x34) {
// Single byte opcode
opcode = uint16(data[0])
dataOffset = 1
} else if len(data) >= 2 {
// Two-byte opcode
opcode = binary.BigEndian.Uint16(data[0:2])
dataOffset = 2
} else {
// Single byte by default
opcode = uint16(data[0])
dataOffset = 1
}
// Create packet
p := &EQPacket{
Opcode: opcode,
Timestamp: time.Now(),
Version: PROTOCOL_VERSION,
}
// Copy remaining data if any
if len(data) > dataOffset {
p.Buffer = make([]byte, len(data)-dataOffset)
copy(p.Buffer, data[dataOffset:])
}
return p, nil
}
// PreparePacket prepares an EQ2 packet for wire transmission
// This adds sequence numbers, compression flags, and converts opcodes
func (p *EQPacket) PreparePacket(maxLen int16) error {
if p.PacketPrepared {
return nil // Already prepared
}
// Build the wire format with EQ2 headers
var buf []byte
// Add sequence field placeholder (2 bytes)
buf = append(buf, 0x00, 0x00)
// Add compression flag placeholder (1 byte)
buf = append(buf, 0x00)
// Add opcode with special encoding
networkOpcode := p.EmuOpcode // In full implementation, convert via opcode manager
if networkOpcode >= 255 {
// Oversized opcode
buf = append(buf, 0xFF) // Marker
buf = append(buf, byte(networkOpcode>>8), byte(networkOpcode))
} else {
buf = append(buf, byte(networkOpcode))
}
// Add packet data
buf = append(buf, p.Buffer...)
// Update packet
p.Buffer = buf
p.PacketPrepared = true
p.Opcode = OP_Packet // Protocol opcode for data packets
return nil
}
// SetEmuOpcode sets the emulator opcode
func (p *EQPacket) SetEmuOpcode(opcode uint16) {
p.EmuOpcode = opcode
}
// GetEmuOpcode returns the emulator opcode
func (p *EQPacket) GetEmuOpcode() uint16 {
return p.EmuOpcode
}
// GetOpcodeName returns the string name of a protocol opcode
func GetOpcodeName(opcode uint16) string {
switch opcode {
case OP_SessionRequest:
return "OP_SessionRequest"
case OP_SessionResponse:
return "OP_SessionResponse"
case OP_Combined:
return "OP_Combined"
case OP_SessionDisconnect:
return "OP_SessionDisconnect"
case OP_KeepAlive:
return "OP_KeepAlive"
case OP_ClientSessionUpdate:
return "OP_ClientSessionUpdate"
case OP_SessionStatRequest: // Also OP_SessionStatResponse (same value)
return "OP_SessionStat"
case OP_Packet:
return "OP_Packet"
case OP_Fragment:
return "OP_Fragment"
case OP_OutOfOrderAck:
return "OP_OutOfOrderAck"
case OP_Ack:
return "OP_Ack"
case OP_AckFuture:
return "OP_AckFuture"
case OP_AckPast:
return "OP_AckPast"
case OP_AppCombined:
return "OP_AppCombined"
case OP_OutOfSession:
return "OP_OutOfSession"
default:
return fmt.Sprintf("Unknown(%d)", opcode)
}
}

View File

@ -1,462 +0,0 @@
package eq2net
import (
"bytes"
"net"
"testing"
)
func TestEQPacket(t *testing.T) {
t.Run("NewEQPacket", func(t *testing.T) {
data := []byte{0x01, 0x02, 0x03, 0x04}
packet := NewEQPacket(0x1234, data)
if packet.Opcode != 0x1234 {
t.Errorf("Expected opcode 0x1234, got 0x%04x", packet.Opcode)
}
if !bytes.Equal(packet.Buffer, data) {
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
}
if packet.Version != PROTOCOL_VERSION {
t.Errorf("Expected version %d, got %d", PROTOCOL_VERSION, packet.Version)
}
})
t.Run("ParsePacket", func(t *testing.T) {
// Test single-byte opcode
data1 := []byte{0x55, 0x01, 0x02, 0x03}
packet1, err := ParsePacket(data1)
if err != nil {
t.Fatalf("Failed to parse single-byte opcode: %v", err)
}
if packet1.Opcode != 0x55 {
t.Errorf("Expected opcode 0x55, got 0x%04x", packet1.Opcode)
}
if len(packet1.Buffer) != 3 {
t.Errorf("Expected buffer length 3, got %d", len(packet1.Buffer))
}
// Test two-byte opcode
data2 := []byte{0x12, 0x34, 0x01, 0x02, 0x03}
packet2, err := ParsePacket(data2)
if err != nil {
t.Fatalf("Failed to parse two-byte opcode: %v", err)
}
if packet2.Opcode != 0x1234 {
t.Errorf("Expected opcode 0x1234, got 0x%04x", packet2.Opcode)
}
if len(packet2.Buffer) != 3 {
t.Errorf("Expected buffer length 3, got %d", len(packet2.Buffer))
}
})
t.Run("Serialize", func(t *testing.T) {
// Test single-byte opcode serialization
packet1 := NewEQPacket(0x55, []byte{0x01, 0x02, 0x03})
serialized1 := packet1.Serialize()
expected1 := []byte{0x55, 0x01, 0x02, 0x03}
if !bytes.Equal(serialized1, expected1) {
t.Errorf("Expected serialized %v, got %v", expected1, serialized1)
}
// Test two-byte opcode serialization
packet2 := NewEQPacket(0x1234, []byte{0x01, 0x02, 0x03})
serialized2 := packet2.Serialize()
expected2 := []byte{0x12, 0x34, 0x01, 0x02, 0x03}
if !bytes.Equal(serialized2, expected2) {
t.Errorf("Expected serialized %v, got %v", expected2, serialized2)
}
})
t.Run("PreparePacket", func(t *testing.T) {
packet := NewEQPacket(0x1234, []byte{0x01, 0x02, 0x03})
packet.EmuOpcode = 0x5678
packet.OpcodeSize = 2
// Prepare packet for wire transmission
packet.PreparePacket(100)
if !packet.PacketPrepared {
t.Error("Expected packet to be marked as prepared")
}
// Should have headers and emulator opcode
// Format: 2 bytes sequence, 1 byte compression, then opcode
if len(packet.Buffer) < 5 {
t.Fatal("Buffer too small after preparation")
}
// Check that emulator opcode was added at correct position
// After 2-byte sequence and 1-byte compression flag
emuOpcode := uint16(packet.Buffer[4])<<8 | uint16(packet.Buffer[5])
if emuOpcode != 0x5678 {
t.Errorf("Expected emulator opcode 0x5678, got 0x%04x", emuOpcode)
}
})
t.Run("Clone", func(t *testing.T) {
original := NewEQPacket(0x1234, []byte{0x01, 0x02, 0x03})
original.SrcIP = net.ParseIP("192.168.1.1")
original.SrcPort = 9000
original.DstIP = net.ParseIP("192.168.1.2")
original.DstPort = 9001
original.EmuOpcode = 0x5678
original.OpcodeSize = 2
cloned := original.Clone()
// Verify clone has same values
if cloned.Opcode != original.Opcode {
t.Errorf("Cloned opcode mismatch: %v != %v", cloned.Opcode, original.Opcode)
}
if !bytes.Equal(cloned.Buffer, original.Buffer) {
t.Errorf("Cloned buffer mismatch: %v != %v", cloned.Buffer, original.Buffer)
}
if !cloned.SrcIP.Equal(original.SrcIP) {
t.Errorf("Cloned SrcIP mismatch: %v != %v", cloned.SrcIP, original.SrcIP)
}
if cloned.EmuOpcode != original.EmuOpcode {
t.Errorf("Cloned EmuOpcode mismatch: %v != %v", cloned.EmuOpcode, original.EmuOpcode)
}
// Verify it's a deep copy
original.Buffer[0] = 0xFF
if cloned.Buffer[0] == 0xFF {
t.Error("Clone shares buffer with original")
}
})
}
func TestProtocolPacket(t *testing.T) {
t.Run("NewProtocolPacket", func(t *testing.T) {
data := []byte{0x01, 0x02, 0x03}
packet := NewProtocolPacket(OP_Packet, data)
if packet.Opcode != OP_Packet {
t.Errorf("Expected opcode OP_Packet, got 0x%04x", packet.Opcode)
}
if !bytes.Equal(packet.Buffer, data) {
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
}
})
t.Run("Compression", func(t *testing.T) {
// Test large packet compression (zlib)
largeData := make([]byte, 100)
for i := range largeData {
largeData[i] = byte(i % 256)
}
packet := NewProtocolPacket(OP_Packet, largeData)
originalLen := len(packet.Buffer)
// Compress
err := packet.Compress()
if err != nil {
t.Fatalf("Failed to compress: %v", err)
}
if !packet.Compressed {
t.Error("Packet should be marked as compressed")
}
if packet.Buffer[0] != 0x5a {
t.Errorf("Expected zlib compression flag 0x5a, got 0x%02x", packet.Buffer[0])
}
// Decompress
err = packet.Decompress()
if err != nil {
t.Fatalf("Failed to decompress: %v", err)
}
if packet.Compressed {
t.Error("Packet should not be marked as compressed after decompression")
}
if len(packet.Buffer) != originalLen {
t.Errorf("Decompressed size mismatch: expected %d, got %d", originalLen, len(packet.Buffer))
}
if !bytes.Equal(packet.Buffer, largeData) {
t.Error("Decompressed data does not match original")
}
})
t.Run("SimpleEncoding", func(t *testing.T) {
// Test small packet encoding
smallData := []byte{0x01, 0x02, 0x03}
packet := NewProtocolPacket(OP_Packet, smallData)
// Compress (should use simple encoding)
err := packet.Compress()
if err != nil {
t.Fatalf("Failed to compress: %v", err)
}
if !packet.Compressed {
t.Error("Packet should be marked as compressed")
}
if packet.Buffer[0] != 0xa5 {
t.Errorf("Expected simple encoding flag 0xa5, got 0x%02x", packet.Buffer[0])
}
// Decompress
err = packet.Decompress()
if err != nil {
t.Fatalf("Failed to decompress: %v", err)
}
if !bytes.Equal(packet.Buffer, smallData) {
t.Error("Decompressed data does not match original")
}
})
t.Run("Combine", func(t *testing.T) {
packet1 := NewProtocolPacket(OP_Packet, []byte{0x01, 0x02})
packet2 := NewProtocolPacket(OP_Ack, []byte{0x03, 0x04})
// Combine packets
success := packet1.Combine(packet2)
if !success {
t.Fatal("Failed to combine packets")
}
if packet1.Opcode != OP_Combined {
t.Errorf("Expected combined opcode, got 0x%04x", packet1.Opcode)
}
if len(packet1.SubPackets) != 2 {
t.Errorf("Expected 2 sub-packets, got %d", len(packet1.SubPackets))
}
})
t.Run("ExtractSubPackets", func(t *testing.T) {
// Create combined packet manually
packet1Data := []byte{0x00, 0x09, 0x01, 0x02} // OP_Packet with data
packet2Data := []byte{0x00, 0x15, 0x03, 0x04} // OP_Ack with data
combinedBuffer := []byte{
byte(len(packet1Data)), // Size of first packet
}
combinedBuffer = append(combinedBuffer, packet1Data...)
combinedBuffer = append(combinedBuffer, byte(len(packet2Data))) // Size of second packet
combinedBuffer = append(combinedBuffer, packet2Data...)
combined := NewProtocolPacket(OP_Combined, combinedBuffer)
// Extract sub-packets
subPackets, err := combined.ExtractSubPackets()
if err != nil {
t.Fatalf("Failed to extract sub-packets: %v", err)
}
if len(subPackets) != 2 {
t.Fatalf("Expected 2 sub-packets, got %d", len(subPackets))
}
if subPackets[0].Opcode != OP_Packet {
t.Errorf("Expected first packet opcode OP_Packet, got 0x%04x", subPackets[0].Opcode)
}
if subPackets[1].Opcode != OP_Ack {
t.Errorf("Expected second packet opcode OP_Ack, got 0x%04x", subPackets[1].Opcode)
}
})
}
func TestLoginPacket(t *testing.T) {
t.Run("NewLoginPacket", func(t *testing.T) {
data := []byte{0x01, 0x02, 0x03}
packet := NewLoginPacket(LOP_LoginRequestMsg, data)
if packet.LoginEmuOpcode != LOP_LoginRequestMsg {
t.Errorf("Expected opcode LOP_LoginRequestMsg, got %v", packet.LoginEmuOpcode)
}
if packet.OpcodeSize != 1 {
t.Errorf("Expected opcode size 1, got %d", packet.OpcodeSize)
}
if !bytes.Equal(packet.Buffer, data) {
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
}
})
t.Run("ParseLoginPacket", func(t *testing.T) {
data := []byte{byte(LOP_LoginRequestMsg), 0x01, 0x02, 0x03}
packet, err := ParseLoginPacket(data)
if err != nil {
t.Fatalf("Failed to parse login packet: %v", err)
}
if packet.LoginEmuOpcode != LOP_LoginRequestMsg {
t.Errorf("Expected opcode LOP_LoginRequestMsg, got %v", packet.LoginEmuOpcode)
}
if len(packet.Buffer) != 3 {
t.Errorf("Expected buffer length 3, got %d", len(packet.Buffer))
}
})
t.Run("SerializeLogin", func(t *testing.T) {
packet := NewLoginPacket(LOP_WorldListMsg, []byte{0x01, 0x02})
serialized := packet.SerializeLogin()
expected := []byte{byte(LOP_WorldListMsg), 0x01, 0x02}
if !bytes.Equal(serialized, expected) {
t.Errorf("Expected serialized %v, got %v", expected, serialized)
}
})
t.Run("ConvertToProtocolPacket", func(t *testing.T) {
loginPacket := NewLoginPacket(LOP_LoginReplyMsg, []byte{0x01, 0x02})
loginPacket.SrcIP = net.ParseIP("192.168.1.1")
loginPacket.DstIP = net.ParseIP("192.168.1.2")
protoPacket := loginPacket.ConvertToProtocolPacket()
if protoPacket.Opcode != OP_Packet {
t.Errorf("Expected protocol opcode OP_Packet, got 0x%04x", protoPacket.Opcode)
}
// Should contain serialized login packet
expectedData := loginPacket.SerializeLogin()
if !bytes.Equal(protoPacket.Buffer, expectedData) {
t.Errorf("Protocol packet buffer mismatch")
}
// Should preserve network info
if !protoPacket.SrcIP.Equal(loginPacket.SrcIP) {
t.Errorf("Source IP not preserved")
}
})
}
func TestGamePacket(t *testing.T) {
t.Run("NewGamePacket", func(t *testing.T) {
data := []byte{0x01, 0x02, 0x03}
packet := NewGamePacket(GOP_LoginReplyMsg, data)
if packet.GameEmuOpcode != GOP_LoginReplyMsg {
t.Errorf("Expected opcode GOP_LoginReplyMsg, got %v", packet.GameEmuOpcode)
}
if packet.OpcodeSize != 2 {
t.Errorf("Expected opcode size 2, got %d", packet.OpcodeSize)
}
if !bytes.Equal(packet.Buffer, data) {
t.Errorf("Expected buffer %v, got %v", data, packet.Buffer)
}
})
t.Run("ParseGamePacket", func(t *testing.T) {
// Test normal 2-byte opcode
opcode := uint16(GOP_ZoneInfoMsg)
data1 := []byte{byte(opcode >> 8), byte(opcode), 0x01, 0x02}
packet1, err := ParseGamePacket(data1)
if err != nil {
t.Fatalf("Failed to parse game packet: %v", err)
}
if packet1.GameEmuOpcode != GOP_ZoneInfoMsg {
t.Errorf("Expected opcode GOP_ZoneInfoMsg, got %v", packet1.GameEmuOpcode)
}
// Test special encoding (low byte = 0x00)
data2 := []byte{0x00, 0x12, 0x00, 0x01, 0x02} // Extra 0x00 prefix
packet2, err := ParseGamePacket(data2)
if err != nil {
t.Fatalf("Failed to parse special encoded packet: %v", err)
}
if packet2.GameEmuOpcode != 0x1200 {
t.Errorf("Expected opcode 0x1200, got 0x%04x", packet2.GameEmuOpcode)
}
})
t.Run("SerializeGame", func(t *testing.T) {
// Test normal opcode
packet1 := NewGamePacket(GOP_MOTDMsg, []byte{0x01, 0x02})
serialized1 := packet1.SerializeGame()
opcode1 := uint16(GOP_MOTDMsg)
expected1 := []byte{byte(opcode1 >> 8), byte(opcode1), 0x01, 0x02}
if !bytes.Equal(serialized1, expected1) {
t.Errorf("Expected serialized %v, got %v", expected1, serialized1)
}
// Test special encoding (low byte = 0x00)
packet2 := NewGamePacket(0x1200, []byte{0x01, 0x02})
serialized2 := packet2.SerializeGame()
expected2 := []byte{0x00, 0x12, 0x00, 0x01, 0x02} // Extra 0x00 prefix
if !bytes.Equal(serialized2, expected2) {
t.Errorf("Expected special encoded %v, got %v", expected2, serialized2)
}
})
t.Run("CombineGamePackets", func(t *testing.T) {
packet1 := NewGamePacket(GOP_UpdateDataMsg, []byte{0x01})
packet2 := NewGamePacket(GOP_UpdateSpawnMsg, []byte{0x02})
packet3 := NewGamePacket(GOP_UpdateTimeMsg, []byte{0x03})
combined := CombineGamePackets([]*GamePacket{packet1, packet2, packet3})
if combined == nil {
t.Fatal("Failed to combine game packets")
}
if combined.Opcode != OP_AppCombined {
t.Errorf("Expected opcode OP_AppCombined, got 0x%04x", combined.Opcode)
}
// Verify buffer contains size-prefixed packets
if len(combined.Buffer) < 3 {
t.Error("Combined buffer too small")
}
})
}
func TestOpcodeNames(t *testing.T) {
t.Run("GetOpcodeName", func(t *testing.T) {
name := GetOpcodeName(OP_SessionRequest)
if name != "OP_SessionRequest" {
t.Errorf("Expected 'OP_SessionRequest', got '%s'", name)
}
unknownName := GetOpcodeName(0xFFFF)
if unknownName != "Unknown(65535)" {
t.Errorf("Expected 'Unknown(65535)', got '%s'", unknownName)
}
})
t.Run("GetLoginOpcodeName", func(t *testing.T) {
name := GetLoginOpcodeName(LOP_LoginRequestMsg)
if name != "LOP_LoginRequestMsg" {
t.Errorf("Expected 'LOP_LoginRequestMsg', got '%s'", name)
}
unknownName := GetLoginOpcodeName(0xFFFF)
if !bytes.Contains([]byte(unknownName), []byte("Unknown")) {
t.Errorf("Expected Unknown opcode name, got '%s'", unknownName)
}
})
t.Run("GetGameOpcodeName", func(t *testing.T) {
name := GetGameOpcodeName(GOP_ZoneInfoMsg)
if name != "GOP_ZoneInfoMsg" {
t.Errorf("Expected 'GOP_ZoneInfoMsg', got '%s'", name)
}
unknownName := GetGameOpcodeName(0xFFFF)
if !bytes.Contains([]byte(unknownName), []byte("Unknown")) {
t.Errorf("Expected Unknown opcode name, got '%s'", unknownName)
}
})
}

View File

@ -1,360 +0,0 @@
package eq2net
import (
"bytes"
"compress/zlib"
"encoding/binary"
"fmt"
"git.sharkk.net/EQ2/Protocol/crypto"
)
// ProtocolPacket handles low-level protocol operations including
// compression, encryption, sequencing, and packet combining
type ProtocolPacket struct {
EQPacket
Compressed bool
Encrypted bool
PacketPrepared bool
Sequence uint16
Acknowledged bool
SentTime int32
AttemptCount int8
SubPackets []*ProtocolPacket
}
// NewProtocolPacket creates a new protocol packet
func NewProtocolPacket(opcode uint16, data []byte) *ProtocolPacket {
p := &ProtocolPacket{
EQPacket: *NewEQPacket(opcode, data),
}
return p
}
// ParseProtocolPacket creates a protocol packet from raw network data
func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
base, err := ParsePacket(data)
if err != nil {
return nil, err
}
p := &ProtocolPacket{
EQPacket: *base,
}
// Check for compression flag
if len(p.Buffer) > 0 {
switch p.Buffer[0] {
case 0x5a: // Zlib compressed
p.Compressed = true
case 0xa5: // Simple encoding
p.Compressed = true
}
}
return p, nil
}
// Serialize writes the protocol packet to a byte buffer with optional offset
func (p *ProtocolPacket) SerializeWithOffset(offset int8) []byte {
opcodeSize := 2 // Protocol packets always use 2-byte opcodes
buf := make([]byte, opcodeSize+len(p.Buffer)-int(offset))
if p.Opcode > 0xff {
binary.BigEndian.PutUint16(buf[0:2], p.Opcode)
} else {
buf[0] = 0x00
buf[1] = byte(p.Opcode)
}
if len(p.Buffer) > int(offset) {
copy(buf[opcodeSize:], p.Buffer[offset:])
}
return buf
}
// ValidateCRC checks if the packet has a valid CRC
// Returns true if CRC is valid or packet is exempt
func (p *ProtocolPacket) ValidateCRC(key uint32) bool {
// Session control packets are exempt from CRC
if p.Opcode == OP_SessionRequest ||
p.Opcode == OP_SessionResponse ||
p.Opcode == OP_OutOfSession {
return true
}
// Need full packet data including opcode for CRC check
fullData := p.Serialize()
if len(fullData) < 2 {
return false
}
// CRC is in last 2 bytes
if len(fullData) < 4 { // Minimum: 2 byte opcode + 2 byte CRC
return false
}
dataLen := len(fullData) - 2
calculatedCRC := crypto.CalculateCRC(fullData[:dataLen], key)
packetCRC := binary.BigEndian.Uint16(fullData[dataLen:])
// CRC of 0 means no CRC check required
return packetCRC == 0 || calculatedCRC == packetCRC
}
// Compress compresses the packet data using zlib or simple encoding
func (p *ProtocolPacket) Compress() error {
if p.Compressed {
return fmt.Errorf("packet already compressed")
}
// Only compress packets larger than 30 bytes
if len(p.Buffer) > 30 {
compressed, err := p.zlibCompress()
if err != nil {
return err
}
p.Buffer = compressed
p.Compressed = true
} else if len(p.Buffer) > 0 {
// Simple encoding for small packets
p.simpleEncode()
p.Compressed = true
}
return nil
}
// Decompress decompresses the packet data
func (p *ProtocolPacket) Decompress() error {
if !p.Compressed {
return fmt.Errorf("packet not compressed")
}
if len(p.Buffer) == 0 {
return fmt.Errorf("no data to decompress")
}
var decompressed []byte
var err error
switch p.Buffer[0] {
case 0x5a: // Zlib compressed
decompressed, err = p.zlibDecompress()
if err != nil {
return err
}
case 0xa5: // Simple encoding
decompressed = p.simpleDecoded()
default:
return fmt.Errorf("unknown compression flag: 0x%02x", p.Buffer[0])
}
p.Buffer = decompressed
p.Compressed = false
return nil
}
// zlibCompress performs zlib compression
func (p *ProtocolPacket) zlibCompress() ([]byte, error) {
var compressed bytes.Buffer
// Write compression flag
compressed.WriteByte(0x5a)
// Compress the data
w := zlib.NewWriter(&compressed)
_, err := w.Write(p.Buffer)
if err != nil {
return nil, err
}
err = w.Close()
if err != nil {
return nil, err
}
return compressed.Bytes(), nil
}
// zlibDecompress performs zlib decompression
func (p *ProtocolPacket) zlibDecompress() ([]byte, error) {
if len(p.Buffer) < 2 {
return nil, fmt.Errorf("compressed data too small")
}
// Skip compression flag
r, err := zlib.NewReader(bytes.NewReader(p.Buffer[1:]))
if err != nil {
return nil, err
}
defer r.Close()
var decompressed bytes.Buffer
_, err = decompressed.ReadFrom(r)
if err != nil {
return nil, err
}
return decompressed.Bytes(), nil
}
// simpleEncode adds simple encoding flag
func (p *ProtocolPacket) simpleEncode() {
// Add encoding flag at the beginning
newBuffer := make([]byte, len(p.Buffer)+1)
newBuffer[0] = 0xa5
copy(newBuffer[1:], p.Buffer)
p.Buffer = newBuffer
}
// simpleDecoded removes simple encoding flag
func (p *ProtocolPacket) simpleDecoded() []byte {
if len(p.Buffer) > 1 {
return p.Buffer[1:]
}
return []byte{}
}
// Combine bundles multiple protocol packets into a single combined packet
func (p *ProtocolPacket) Combine(other *ProtocolPacket) bool {
// Check if this is already a combined packet
if p.Opcode != OP_Combined {
// Convert to combined packet
firstPacket := p.Clone()
p.Opcode = OP_Combined
p.SubPackets = []*ProtocolPacket{
{EQPacket: *firstPacket},
}
p.Buffer = p.buildCombinedBuffer()
}
// Check size limit (max 255 bytes for combined packets)
totalSize := len(p.Buffer) + int(other.TotalSize()) + 1 // +1 for size byte
if totalSize > 255 {
return false
}
// Add the new packet
p.SubPackets = append(p.SubPackets, other)
p.Buffer = p.buildCombinedBuffer()
return true
}
// buildCombinedBuffer creates the buffer for a combined packet
func (p *ProtocolPacket) buildCombinedBuffer() []byte {
var buf bytes.Buffer
for _, subPacket := range p.SubPackets {
serialized := subPacket.Serialize()
buf.WriteByte(byte(len(serialized))) // Size byte
buf.Write(serialized)
}
return buf.Bytes()
}
// ExtractSubPackets extracts individual packets from a combined packet
func (p *ProtocolPacket) ExtractSubPackets() ([]*ProtocolPacket, error) {
if p.Opcode != OP_Combined {
return nil, fmt.Errorf("not a combined packet")
}
var packets []*ProtocolPacket
offset := 0
data := p.Buffer
for offset < len(data) {
// Read size byte
if offset >= len(data) {
break
}
size := int(data[offset])
offset++
// Extract sub-packet data
if offset+size > len(data) {
return nil, fmt.Errorf("invalid combined packet: size exceeds buffer")
}
subData := data[offset : offset+size]
offset += size
// Parse sub-packet
subPacket, err := ParseProtocolPacket(subData)
if err != nil {
return nil, fmt.Errorf("failed to parse sub-packet: %w", err)
}
packets = append(packets, subPacket)
}
return packets, nil
}
// ExtractEmuPacket extracts an emulator packet from protocol data
// This is used when a protocol packet contains application-level data
func (p *ProtocolPacket) ExtractEmuPacket(opcodeSize uint8) *EQPacket {
if len(p.Buffer) < int(opcodeSize) {
return nil
}
// Extract the emulator opcode from the buffer
var emuOpcode uint16
if opcodeSize == 1 {
emuOpcode = uint16(p.Buffer[0])
} else {
emuOpcode = binary.BigEndian.Uint16(p.Buffer[0:2])
}
// Create packet with remaining data
packet := NewEQPacket(p.Opcode, p.Buffer[opcodeSize:])
packet.EmuOpcode = emuOpcode
packet.OpcodeSize = opcodeSize
// Copy network info
packet.SrcIP = p.SrcIP
packet.SrcPort = p.SrcPort
packet.DstIP = p.DstIP
packet.DstPort = p.DstPort
packet.Timestamp = p.Timestamp
packet.Version = p.Version
return packet
}
// ChatEncode applies XOR-based chat encryption
func (p *ProtocolPacket) ChatEncode(key uint32) {
if len(p.Buffer) <= 2 {
return // Skip opcode bytes
}
data := p.Buffer[2:] // Skip first 2 bytes (opcode)
keyBytes := make([]byte, 4)
binary.LittleEndian.PutUint32(keyBytes, key)
// Process 4-byte blocks with rolling key
i := 0
for ; i+4 <= len(data); i += 4 {
// XOR with current key
block := binary.LittleEndian.Uint32(data[i : i+4])
encrypted := block ^ key
binary.LittleEndian.PutUint32(data[i:i+4], encrypted)
// Update key with encrypted data
key = encrypted
}
// Handle remaining bytes with last key byte
keyByte := byte(key & 0xFF)
for ; i < len(data); i++ {
data[i] ^= keyByte
}
}
// ChatDecode applies XOR-based chat decryption (same as encode due to XOR properties)
func (p *ProtocolPacket) ChatDecode(key uint32) {
p.ChatEncode(key) // XOR encryption is symmetric
}

849
stream.go
View File

@ -1,849 +0,0 @@
package eq2net
import (
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"git.sharkk.net/EQ2/Protocol/crypto"
"github.com/panjf2000/gnet/v2"
)
// StreamState represents the current state of an EQStream connection
type StreamState int
const (
CLOSED StreamState = iota // No connection
CONNECTING // Session request sent, awaiting response
ESTABLISHED // Active connection ready for data
DISCONNECTING // Graceful disconnect in progress
)
// StreamType identifies the type of EQ stream
type StreamType int
const (
UnknownStream StreamType = iota
LoginStream
WorldStream
ZoneStream
ChatStream
VoiceStream
)
// Stream configuration constants
const (
MaxPacketSize = 512 // Maximum packet size
MaxCombinedSize = 255 // Maximum combined packet size
RetransmitTimeoutMin = 500 // Minimum retransmit timeout (ms)
RetransmitTimeoutMax = 5000 // Maximum retransmit timeout (ms)
RetransmitMaxAttempts = 10 // Maximum retransmission attempts
KeepAliveInterval = 30000 // Keep-alive interval (ms)
SessionTimeout = 90000 // Session timeout (ms)
// Packet flags
FLAG_COMPRESSED = 0x01 // Packet is compressed
FLAG_ENCODED = 0x04 // Packet is encoded/encrypted
// Rate limiting
RATEBASE = 1048576 // Base rate: 1 MB
DECAYBASE = 78642 // Decay rate: RATEBASE/10
)
// RetransmitEntry tracks packets awaiting acknowledgment
type RetransmitEntry struct {
packet *ProtocolPacket
sentTime time.Time
attempts int
}
// EQStream manages a single EverQuest network stream connection
type EQStream struct {
// Network connection
conn gnet.Conn
remoteIP net.IP
remotePort uint16
// Session identification
sessionID uint32
streamType StreamType
// State management
state StreamState
stateMu sync.RWMutex
// Encryption
crypto *crypto.Ciphers
cryptoKey uint32
// Compression settings
compressed bool
encoded bool
// Sequence numbers
nextInSeq uint16
nextOutSeq uint16
lastAckSeq uint16
seqMu sync.Mutex
// Packet queues
outbound chan *ProtocolPacket
retransmit map[uint16]*RetransmitEntry
retransmitMu sync.RWMutex
futurePackets map[uint16]*ProtocolPacket
futureMu sync.Mutex
// Combined packet handling
combinedApp *EQPacket
combinedMu sync.Mutex
combineTimer *time.Timer
// Flow control
currentRate uint32
rateThreshold uint32
rateMu sync.Mutex
// Timing
lastActivity time.Time
lastPing time.Time
avgDelta time.Duration
timingMu sync.RWMutex
// Statistics
packetsReceived uint64
packetsSent uint64
bytesReceived uint64
bytesSent uint64
statsMu sync.RWMutex
// Configuration
opcodeSize uint8
maxPacketSize uint32
Version int16 // Client version for opcode conversion
// Callbacks
onEmuPacket func(*EQPacket) // Called when emulator packet is ready
onDisconnect func() // Called on disconnect
}
// NewEQStream creates a new EQ stream
func NewEQStream(conn gnet.Conn) *EQStream {
stream := &EQStream{
conn: conn,
state: CLOSED,
sessionID: 0,
streamType: UnknownStream,
compressed: true,
encoded: false,
nextInSeq: 0,
nextOutSeq: 0,
lastAckSeq: 0,
outbound: make(chan *ProtocolPacket, 1024),
retransmit: make(map[uint16]*RetransmitEntry),
futurePackets: make(map[uint16]*ProtocolPacket),
currentRate: RATEBASE,
rateThreshold: DECAYBASE,
lastActivity: time.Now(),
opcodeSize: 2,
maxPacketSize: MaxPacketSize,
cryptoKey: 0x33624702, // Default CRC key
}
// Parse remote address
if addr := conn.RemoteAddr(); addr != nil {
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
stream.remoteIP = tcpAddr.IP
stream.remotePort = uint16(tcpAddr.Port)
} else if udpAddr, ok := addr.(*net.UDPAddr); ok {
stream.remoteIP = udpAddr.IP
stream.remotePort = uint16(udpAddr.Port)
}
}
return stream
}
// GetState returns the current stream state
func (s *EQStream) GetState() StreamState {
s.stateMu.RLock()
defer s.stateMu.RUnlock()
return s.state
}
// SetState updates the stream state
func (s *EQStream) SetState(state StreamState) {
s.stateMu.Lock()
defer s.stateMu.Unlock()
s.state = state
}
// IsActive returns true if the stream is established
func (s *EQStream) IsActive() bool {
return s.GetState() == ESTABLISHED
}
// IsClosed returns true if the stream is closed
func (s *EQStream) IsClosed() bool {
return s.GetState() == CLOSED
}
// ProcessPacket processes an incoming protocol packet
func (s *EQStream) ProcessPacket(p *ProtocolPacket) error {
s.updateActivity()
s.updateStats(true, uint64(p.TotalSize()))
// Handle based on opcode
switch p.Opcode {
case OP_SessionRequest:
return s.handleSessionRequest(p)
case OP_SessionResponse:
return s.handleSessionResponse(p)
case OP_SessionDisconnect:
return s.handleDisconnect(p)
case OP_KeepAlive:
return s.handleKeepAlive(p)
case OP_Ack:
return s.handleAck(p)
case OP_OutOfOrderAck:
return s.handleOutOfOrderAck(p)
case OP_Packet:
return s.handleDataPacket(p)
case OP_Fragment:
return s.handleFragment(p)
case OP_Combined:
return s.handleCombined(p)
case OP_AppCombined:
return s.handleAppCombined(p)
default:
return fmt.Errorf("unknown opcode: 0x%04X", p.Opcode)
}
}
// handleSessionRequest processes a session request packet
func (s *EQStream) handleSessionRequest(p *ProtocolPacket) error {
if len(p.Buffer) < 4 {
return fmt.Errorf("session request too small")
}
// Parse session ID
s.sessionID = binary.BigEndian.Uint32(p.Buffer[0:4])
// Send session response
s.sendSessionResponse()
// Update state
s.SetState(ESTABLISHED)
return nil
}
// handleSessionResponse processes a session response packet
func (s *EQStream) handleSessionResponse(p *ProtocolPacket) error {
if len(p.Buffer) < 4 {
return fmt.Errorf("session response too small")
}
// Parse session parameters
s.sessionID = binary.BigEndian.Uint32(p.Buffer[0:4])
// Parse compression/encoding flags if present
if len(p.Buffer) >= 5 {
flags := p.Buffer[4]
s.compressed = (flags & FLAG_COMPRESSED) != 0
s.encoded = (flags & FLAG_ENCODED) != 0
}
// Initialize encryption if needed
if s.encoded && s.crypto == nil {
// This would be initialized with proper key exchange
// For now, using default key
var err error
s.crypto, err = crypto.NewCiphers(int64(s.cryptoKey))
if err != nil {
return fmt.Errorf("failed to initialize encryption: %w", err)
}
}
// Update state
s.SetState(ESTABLISHED)
// Send keep alive to confirm
s.sendKeepAlive()
return nil
}
// handleDisconnect processes a disconnect packet
func (s *EQStream) handleDisconnect(p *ProtocolPacket) error {
s.SetState(CLOSED)
// Call disconnect callback if set
if s.onDisconnect != nil {
s.onDisconnect()
}
return nil
}
// handleKeepAlive processes a keep-alive packet
func (s *EQStream) handleKeepAlive(p *ProtocolPacket) error {
// Update activity timestamp
s.updateActivity()
// Send keep-alive response if needed
// Some implementations echo the keep-alive
return nil
}
// handleAck processes an acknowledgment packet
func (s *EQStream) handleAck(p *ProtocolPacket) error {
if len(p.Buffer) < 2 {
return fmt.Errorf("ack packet too small")
}
// Parse acknowledged sequence number
ackSeq := binary.BigEndian.Uint16(p.Buffer[0:2])
// Remove from retransmit queue
s.retransmitMu.Lock()
delete(s.retransmit, ackSeq)
s.retransmitMu.Unlock()
// Update last acknowledged sequence
s.seqMu.Lock()
if s.sequenceGreaterThan(ackSeq, s.lastAckSeq) {
s.lastAckSeq = ackSeq
}
s.seqMu.Unlock()
return nil
}
// handleOutOfOrderAck processes an out-of-order acknowledgment
func (s *EQStream) handleOutOfOrderAck(p *ProtocolPacket) error {
// Similar to handleAck but indicates out-of-order reception
return s.handleAck(p)
}
// handleDataPacket processes a data packet
func (s *EQStream) handleDataPacket(p *ProtocolPacket) error {
// Check sequence number
seq := p.Sequence
s.seqMu.Lock()
expectedSeq := s.nextInSeq
s.seqMu.Unlock()
seqOrder := s.compareSequence(expectedSeq, seq)
switch seqOrder {
case SeqInOrder:
// Process packet immediately
s.seqMu.Lock()
s.nextInSeq++
s.seqMu.Unlock()
// Send ACK
s.sendAck(seq)
// Extract emulator packet and queue
if emuPacket := p.ExtractEmuPacket(s.opcodeSize); emuPacket != nil {
// Process based on stream type
s.processIncomingEmuPacket(emuPacket)
}
// Check for queued future packets
s.processFuturePackets()
case SeqFuture:
// Queue for later processing
s.futureMu.Lock()
s.futurePackets[seq] = p
s.futureMu.Unlock()
// Send out-of-order ACK
s.sendOutOfOrderAck(seq)
case SeqPast:
// Duplicate packet, just ACK it
s.sendAck(seq)
}
return nil
}
// handleFragment processes a fragmented packet
func (s *EQStream) handleFragment(p *ProtocolPacket) error {
// TODO: Implement fragment reassembly
// This requires tracking fragment sequences and reassembling
return fmt.Errorf("fragment handling not yet implemented")
}
// handleCombined processes a combined packet
func (s *EQStream) handleCombined(p *ProtocolPacket) error {
// Extract sub-packets
subPackets, err := p.ExtractSubPackets()
if err != nil {
return fmt.Errorf("failed to extract sub-packets: %w", err)
}
// Process each sub-packet
for _, subPacket := range subPackets {
if err := s.ProcessPacket(subPacket); err != nil {
// Log error but continue processing other packets
continue
}
}
return nil
}
// handleAppCombined processes an application-level combined packet
func (s *EQStream) handleAppCombined(p *ProtocolPacket) error {
// For now, just try to extract as single packet
// TODO: Implement proper app-level packet extraction
emuPacket := p.ExtractEmuPacket(s.opcodeSize)
if emuPacket == nil {
return fmt.Errorf("failed to extract emu packet")
}
// Process the packet
s.processIncomingEmuPacket(emuPacket)
return nil
}
// QueuePacket queues an outbound packet for transmission
func (s *EQStream) QueuePacket(p *ProtocolPacket, reliable bool) error {
if s.GetState() != ESTABLISHED {
return fmt.Errorf("stream not established")
}
// Add sequence number for reliable packets
if reliable {
s.seqMu.Lock()
p.Sequence = s.nextOutSeq
s.nextOutSeq++
s.seqMu.Unlock()
// Add to retransmit queue
s.retransmitMu.Lock()
s.retransmit[p.Sequence] = &RetransmitEntry{
packet: p,
sentTime: time.Now(),
attempts: 0,
}
s.retransmitMu.Unlock()
}
// Add CRC if needed
if p.Opcode != OP_SessionRequest && p.Opcode != OP_SessionResponse {
s.addCRC(p)
}
// Compress if beneficial
if s.compressed && len(p.Buffer) > 30 {
p.Compress()
}
// Encrypt if enabled
if s.encoded && s.crypto != nil {
s.encryptPacket(p)
}
// Send packet
return s.sendPacket(p)
}
// Helper methods
func (s *EQStream) updateActivity() {
s.timingMu.Lock()
s.lastActivity = time.Now()
s.timingMu.Unlock()
}
func (s *EQStream) updateStats(received bool, bytes uint64) {
s.statsMu.Lock()
if received {
s.packetsReceived++
s.bytesReceived += bytes
} else {
s.packetsSent++
s.bytesSent += bytes
}
s.statsMu.Unlock()
}
func (s *EQStream) sendPacket(p *ProtocolPacket) error {
data := p.Serialize()
s.updateStats(false, uint64(len(data)))
return s.conn.AsyncWrite(data, nil)
}
func (s *EQStream) sendSessionResponse() {
resp := NewProtocolPacket(OP_SessionResponse, nil)
// Build response data
data := make([]byte, 5)
binary.BigEndian.PutUint32(data[0:4], s.sessionID)
// Set flags
flags := byte(0)
if s.compressed {
flags |= FLAG_COMPRESSED
}
if s.encoded {
flags |= FLAG_ENCODED
}
data[4] = flags
resp.Buffer = data
s.sendPacket(resp)
}
func (s *EQStream) sendKeepAlive() {
ka := NewProtocolPacket(OP_KeepAlive, nil)
s.sendPacket(ka)
s.timingMu.Lock()
s.lastPing = time.Now()
s.timingMu.Unlock()
}
func (s *EQStream) sendAck(seq uint16) {
ack := NewProtocolPacket(OP_Ack, nil)
ack.Buffer = make([]byte, 2)
binary.BigEndian.PutUint16(ack.Buffer, seq)
s.sendPacket(ack)
}
func (s *EQStream) sendOutOfOrderAck(seq uint16) {
ack := NewProtocolPacket(OP_OutOfOrderAck, nil)
ack.Buffer = make([]byte, 2)
binary.BigEndian.PutUint16(ack.Buffer, seq)
s.sendPacket(ack)
}
func (s *EQStream) addCRC(p *ProtocolPacket) {
// Serialize packet without CRC
data := p.Serialize()
// Calculate CRC
crc := crypto.CalculateCRC(data, s.cryptoKey)
// Append CRC to buffer
crcBytes := make([]byte, 2)
binary.BigEndian.PutUint16(crcBytes, crc)
p.Buffer = append(p.Buffer, crcBytes...)
}
func (s *EQStream) encryptPacket(p *ProtocolPacket) {
if s.crypto == nil {
return
}
// Encrypt the buffer (skip opcode)
if len(p.Buffer) > 2 {
s.crypto.Encrypt(p.Buffer[2:])
p.Encrypted = true
}
}
func (s *EQStream) processFuturePackets() {
s.futureMu.Lock()
defer s.futureMu.Unlock()
s.seqMu.Lock()
expectedSeq := s.nextInSeq
s.seqMu.Unlock()
// Check if we have the next expected packet
if p, ok := s.futurePackets[expectedSeq]; ok {
delete(s.futurePackets, expectedSeq)
// Process it (this will recursively check for more)
go s.ProcessPacket(p)
}
}
// Sequence number comparison
type SeqOrder int
const (
SeqPast SeqOrder = iota // Sequence is from the past
SeqInOrder // Sequence is expected
SeqFuture // Sequence is from the future
)
func (s *EQStream) compareSequence(expected, received uint16) SeqOrder {
if received == expected {
return SeqInOrder
}
// Handle wraparound
if s.sequenceGreaterThan(received, expected) {
return SeqFuture
}
return SeqPast
}
func (s *EQStream) sequenceGreaterThan(a, b uint16) bool {
// Handle sequence number wraparound
// If the difference is more than half the sequence space,
// assume wraparound occurred
diff := int32(a) - int32(b)
if diff < 0 {
diff = -diff
}
if diff > 32768 { // Half of uint16 max
return a < b
}
return a > b
}
// CheckRetransmissions checks for packets that need retransmission
func (s *EQStream) CheckRetransmissions() {
now := time.Now()
s.retransmitMu.Lock()
defer s.retransmitMu.Unlock()
for seq, entry := range s.retransmit {
// Calculate timeout based on average delta and attempts
timeout := time.Duration(RetransmitTimeoutMin) * time.Millisecond
if s.avgDelta > 0 {
timeout = s.avgDelta * 3
}
// Exponential backoff
for i := 0; i < entry.attempts; i++ {
timeout *= 2
if timeout > time.Duration(RetransmitTimeoutMax)*time.Millisecond {
timeout = time.Duration(RetransmitTimeoutMax) * time.Millisecond
break
}
}
if now.Sub(entry.sentTime) > timeout {
if entry.attempts >= RetransmitMaxAttempts {
// Give up on this packet
delete(s.retransmit, seq)
continue
}
// Retransmit
entry.attempts++
entry.sentTime = now
s.sendPacket(entry.packet)
}
}
}
// CheckTimeout checks if the stream has timed out
func (s *EQStream) CheckTimeout() bool {
s.timingMu.RLock()
lastActivity := s.lastActivity
s.timingMu.RUnlock()
if time.Since(lastActivity) > time.Duration(SessionTimeout)*time.Millisecond {
s.SetState(CLOSED)
if s.onDisconnect != nil {
s.onDisconnect()
}
return true
}
// Send keep-alive if needed
if time.Since(lastActivity) > time.Duration(KeepAliveInterval)*time.Millisecond {
s.sendKeepAlive()
}
return false
}
// SendDisconnect sends a disconnect packet
func (s *EQStream) SendDisconnect() {
disc := NewProtocolPacket(OP_SessionDisconnect, nil)
s.sendPacket(disc)
s.SetState(DISCONNECTING)
}
// Close closes the stream
func (s *EQStream) Close() {
s.SendDisconnect()
s.SetState(CLOSED)
// Clean up resources
close(s.outbound)
if s.combineTimer != nil {
s.combineTimer.Stop()
}
}
// EQ2-specific packet preparation methods
// PreparePacketForWire prepares an application packet for transmission
// This adds EQ2-specific headers like sequence numbers and compression flags
func (s *EQStream) PreparePacketForWire(app *EQPacket, maxLen int16) (*ProtocolPacket, error) {
// Convert emulator opcode to network opcode based on version
// This would use an opcode manager in a full implementation
networkOpcode := app.EmuOpcode
// Build the wire format
var buf []byte
// Add sequence field placeholder (2 bytes)
buf = append(buf, 0x00, 0x00)
// Add compression flag placeholder (1 byte)
buf = append(buf, 0x00)
// Add opcode with special encoding
if networkOpcode >= 255 {
// Oversized opcode
buf = append(buf, 0xFF) // Marker
buf = append(buf, byte(networkOpcode>>8), byte(networkOpcode))
} else {
buf = append(buf, byte(networkOpcode))
}
// Add packet data
buf = append(buf, app.Buffer...)
// Create protocol packet
proto := NewProtocolPacket(OP_Packet, buf)
proto.Version = app.Version
return proto, nil
}
// CombinePacketsForWire combines multiple packets at the application level
// This is different from protocol-level combining
func (s *EQStream) CombinePacketsForWire(packets []*EQPacket) *ProtocolPacket {
if len(packets) == 0 {
return nil
}
// If only one packet, just prepare it normally
if len(packets) == 1 {
proto, _ := s.PreparePacketForWire(packets[0], MaxPacketSize)
return proto
}
// Build combined buffer with EQ2 format
var buf []byte
for _, app := range packets {
// Prepare each packet
prepared, err := s.PreparePacketForWire(app, MaxPacketSize)
if err != nil {
continue
}
data := prepared.Buffer
size := len(data)
// Add size encoding
if size >= 255 {
// Oversized packet
buf = append(buf, 0xFF)
buf = append(buf, byte(size>>8), byte(size))
} else {
buf = append(buf, byte(size))
}
// Add packet data
buf = append(buf, data...)
}
// Create combined protocol packet
return NewProtocolPacket(OP_AppCombined, buf)
}
// QueueAppPacket queues an application packet for transmission
// This handles the full preparation pipeline
func (s *EQStream) QueueAppPacket(app *EQPacket) error {
// Convert to protocol packet with EQ2 preparation
proto, err := s.PreparePacketForWire(app, MaxPacketSize)
if err != nil {
return err
}
// Queue for transmission with reliability
return s.QueuePacket(proto, true)
}
// QueueLoginPacket queues a login packet for transmission
func (s *EQStream) QueueLoginPacket(p *LoginPacket) error {
if s.streamType != LoginStream {
return fmt.Errorf("cannot send login packet on non-login stream")
}
// Convert to protocol packet
proto := p.ConvertToProtocolPacket()
// Queue for transmission with reliability
return s.QueuePacket(proto, true)
}
// QueueGamePacket queues a game packet for transmission
func (s *EQStream) QueueGamePacket(p *GamePacket) error {
if s.streamType != WorldStream && s.streamType != ZoneStream {
return fmt.Errorf("cannot send game packet on non-game stream")
}
// Convert to protocol packet
proto := p.ConvertToProtocolPacket()
// Queue for transmission with reliability
return s.QueuePacket(proto, true)
}
// SetOpcodeManager sets the opcode conversion manager for version-specific opcodes
// In a full implementation, this would handle emulator->network opcode conversion
func (s *EQStream) SetOpcodeManager(version int16) {
// This would set up opcode conversion tables based on client version
// For now, it's a placeholder
s.Version = version
}
// processIncomingEmuPacket processes an incoming emulator packet based on stream type
func (s *EQStream) processIncomingEmuPacket(emu *EQPacket) {
// Parse based on stream type
switch s.streamType {
case LoginStream:
// Parse as login packet
if _, err := ParseLoginPacket(emu.Buffer); err == nil {
// For login packets, we might want to do special handling
// For now, just pass through the emu packet
if s.onEmuPacket != nil {
s.onEmuPacket(emu)
}
}
case WorldStream, ZoneStream:
// Parse as game packet
if _, err := ParseGamePacket(emu.Buffer); err == nil {
// For game packets, we might want to do special handling
// For now, just pass through the emu packet
if s.onEmuPacket != nil {
s.onEmuPacket(emu)
}
}
default:
// Unknown stream type, call callback if set
if s.onEmuPacket != nil {
s.onEmuPacket(emu)
}
}
}

View File

@ -1,54 +0,0 @@
package eq2net
import (
"fmt"
"sync"
)
// StreamFactory provides a factory pattern for creating streams
type StreamFactory struct {
servers map[StreamType]*StreamServer
serversMu sync.RWMutex
}
// NewStreamFactory creates a new stream factory
func NewStreamFactory() *StreamFactory {
return &StreamFactory{
servers: make(map[StreamType]*StreamServer),
}
}
// CreateServer creates a new stream server
func (f *StreamFactory) CreateServer(streamType StreamType, address string, port int) (*StreamServer, error) {
f.serversMu.Lock()
defer f.serversMu.Unlock()
// Check if server already exists for this type
if _, exists := f.servers[streamType]; exists {
return nil, fmt.Errorf("server already exists for stream type %d", streamType)
}
// Create new server
server := NewStreamServer(address, port, streamType)
f.servers[streamType] = server
return server, nil
}
// GetServer returns a server by stream type
func (f *StreamFactory) GetServer(streamType StreamType) *StreamServer {
f.serversMu.RLock()
defer f.serversMu.RUnlock()
return f.servers[streamType]
}
// StopAll stops all servers
func (f *StreamFactory) StopAll() {
f.serversMu.Lock()
defer f.serversMu.Unlock()
for _, server := range f.servers {
server.Stop()
}
f.servers = make(map[StreamType]*StreamServer)
}

View File

@ -1,263 +0,0 @@
package eq2net
import (
"fmt"
"sync"
"time"
"github.com/panjf2000/gnet/v2"
)
// StreamServer manages multiple EQStream connections using gnet
type StreamServer struct {
gnet.BuiltinEventEngine
streams map[string]*EQStream // "IP:Port" -> Stream mapping
streamsMu sync.RWMutex
address string
port int
streamType StreamType
maxStreams int
onNewStream func(*EQStream)
onStreamClosed func(*EQStream)
onEmuPacket func(*EQStream, *EQPacket)
totalConnections uint64
activeConnections uint32
statsMu sync.RWMutex
shutdown chan bool
}
// NewStreamServer creates a new EQ stream server
func NewStreamServer(address string, port int, streamType StreamType) *StreamServer {
return &StreamServer{
streams: make(map[string]*EQStream),
address: address,
port: port,
streamType: streamType,
maxStreams: 10000,
shutdown: make(chan bool),
}
}
// Start starts the stream server
func (s *StreamServer) Start() error {
addr := fmt.Sprintf("%s:%d", s.address, s.port)
// Configure gnet options
options := []gnet.Option{
gnet.WithMulticore(true),
gnet.WithReusePort(true),
gnet.WithTicker(true),
gnet.WithTCPKeepAlive(time.Minute * 5),
}
// Start gnet server
return gnet.Run(s, fmt.Sprintf("udp://%s", addr), options...)
}
// Stop stops the stream server
func (s *StreamServer) Stop() {
close(s.shutdown)
// Close all active streams
s.streamsMu.Lock()
for _, stream := range s.streams {
stream.Close()
}
s.streams = make(map[string]*EQStream)
s.streamsMu.Unlock()
}
// OnBoot is called when the server starts
func (s *StreamServer) OnBoot(eng gnet.Engine) gnet.Action {
fmt.Printf("EQStream server started on %s:%d\n", s.address, s.port)
return gnet.None
}
// OnShutdown is called when the server stops
func (s *StreamServer) OnShutdown(eng gnet.Engine) {
fmt.Printf("EQStream server shutdown\n")
}
// OnOpen is called when a new connection is established
func (s *StreamServer) OnOpen(c gnet.Conn) (out []byte, action gnet.Action) {
// Create new stream
stream := NewEQStream(c)
stream.streamType = s.streamType
// Set up callbacks
stream.onEmuPacket = func(p *EQPacket) {
if s.onEmuPacket != nil {
s.onEmuPacket(stream, p)
}
}
stream.onDisconnect = func() {
s.removeStream(stream)
if s.onStreamClosed != nil {
s.onStreamClosed(stream)
}
}
// Store stream in connection context
c.SetContext(stream)
// Add to stream map
key := s.getStreamKey(c)
s.streamsMu.Lock()
s.streams[key] = stream
s.activeConnections++
s.totalConnections++
s.streamsMu.Unlock()
// Call new stream callback
if s.onNewStream != nil {
s.onNewStream(stream)
}
return nil, gnet.None
}
// OnClose is called when a connection is closed
func (s *StreamServer) OnClose(c gnet.Conn, err error) (action gnet.Action) {
if stream, ok := c.Context().(*EQStream); ok {
stream.Close()
s.removeStream(stream)
if s.onStreamClosed != nil {
s.onStreamClosed(stream)
}
}
return gnet.None
}
// OnTraffic is called when data is received
func (s *StreamServer) OnTraffic(c gnet.Conn) (action gnet.Action) {
stream, ok := c.Context().(*EQStream)
if !ok {
return gnet.Close
}
// Read all available data
for {
// Peek at the data to determine packet size
buf, err := c.Peek(-1)
if err != nil || len(buf) < 2 {
break
}
// Parse packet length from opcode and data
// For UDP, each datagram is a complete packet
packet, err := ParseProtocolPacket(buf)
if err != nil {
// Invalid packet, skip it
c.Discard(len(buf))
continue
}
// Consume the packet data
c.Discard(len(buf))
// Process packet
if err := stream.ProcessPacket(packet); err != nil {
// Log error but continue processing
fmt.Printf("Error processing packet: %v\n", err)
}
}
return gnet.None
}
// OnTick is called periodically for maintenance tasks
func (s *StreamServer) OnTick() (delay time.Duration, action gnet.Action) {
// Check for timeouts and retransmissions
s.streamsMu.RLock()
streams := make([]*EQStream, 0, len(s.streams))
for _, stream := range s.streams {
streams = append(streams, stream)
}
s.streamsMu.RUnlock()
// Process each stream
for _, stream := range streams {
// Check for timeout
if stream.CheckTimeout() {
s.removeStream(stream)
continue
}
// Check for retransmissions
stream.CheckRetransmissions()
}
// Return delay until next tick (100ms)
return time.Millisecond * 100, gnet.None
}
// Helper methods
func (s *StreamServer) getStreamKey(c gnet.Conn) string {
if addr := c.RemoteAddr(); addr != nil {
return addr.String()
}
return ""
}
func (s *StreamServer) removeStream(stream *EQStream) {
// Find and remove stream from map
s.streamsMu.Lock()
for key, str := range s.streams {
if str == stream {
delete(s.streams, key)
s.activeConnections--
break
}
}
s.streamsMu.Unlock()
}
// SetOnNewStream sets the callback for new streams
func (s *StreamServer) SetOnNewStream(callback func(*EQStream)) {
s.onNewStream = callback
}
// SetOnStreamClosed sets the callback for closed streams
func (s *StreamServer) SetOnStreamClosed(callback func(*EQStream)) {
s.onStreamClosed = callback
}
// SetOnEmuPacket sets the callback for emulator packets
func (s *StreamServer) SetOnEmuPacket(callback func(*EQStream, *EQPacket)) {
s.onEmuPacket = callback
}
// GetStream returns a stream by remote address
func (s *StreamServer) GetStream(address string) *EQStream {
s.streamsMu.RLock()
defer s.streamsMu.RUnlock()
return s.streams[address]
}
// GetActiveStreams returns all active streams
func (s *StreamServer) GetActiveStreams() []*EQStream {
s.streamsMu.RLock()
defer s.streamsMu.RUnlock()
streams := make([]*EQStream, 0, len(s.streams))
for _, stream := range s.streams {
streams = append(streams, stream)
}
return streams
}
// GetStats returns server statistics
func (s *StreamServer) GetStats() (total uint64, active uint32) {
s.statsMu.RLock()
defer s.statsMu.RUnlock()
return s.totalConnections, s.activeConnections
}