diff --git a/crypto/crc.go b/crypto/crc.go index 1a2c7c1..3b18857 100644 --- a/crypto/crc.go +++ b/crypto/crc.go @@ -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) } diff --git a/game_packet.go b/game_packet.go deleted file mode 100644 index abd20ec..0000000 --- a/game_packet.go +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/login_packet.go b/login_packet.go deleted file mode 100644 index 4a0d616..0000000 --- a/login_packet.go +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/opcodes.go b/opcodes.go deleted file mode 100644 index e1b2022..0000000 --- a/opcodes.go +++ /dev/null @@ -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 -) diff --git a/packet.go b/packet.go deleted file mode 100644 index e4d4864..0000000 --- a/packet.go +++ /dev/null @@ -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) - } -} diff --git a/packet_test.go b/packet_test.go deleted file mode 100644 index 6ad15cb..0000000 --- a/packet_test.go +++ /dev/null @@ -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) - } - }) -} \ No newline at end of file diff --git a/protocol_packet.go b/protocol_packet.go deleted file mode 100644 index e6df127..0000000 --- a/protocol_packet.go +++ /dev/null @@ -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 -} diff --git a/stream.go b/stream.go deleted file mode 100644 index d58b40e..0000000 --- a/stream.go +++ /dev/null @@ -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) - } - } -} diff --git a/stream_factory.go b/stream_factory.go deleted file mode 100644 index ac002a5..0000000 --- a/stream_factory.go +++ /dev/null @@ -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) -} diff --git a/stream_server.go b/stream_server.go deleted file mode 100644 index 9d091d4..0000000 --- a/stream_server.go +++ /dev/null @@ -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 -}