diff --git a/internal/opcodes/opcodes.go b/internal/opcodes/opcodes.go new file mode 100644 index 0000000..12780fb --- /dev/null +++ b/internal/opcodes/opcodes.go @@ -0,0 +1,488 @@ +package opcodes + +// Protocol opcodes for UDP packet headers +const ( + OpSessionRequest = 0x01 // Initial connection request from client + OpSessionResponse = 0x02 // Server response to session request + OpCombined = 0x03 // Multiple packets combined into single transmission + OpSessionDisconnect = 0x05 // Clean session termination + OpKeepAlive = 0x06 // Heartbeat to maintain connection + OpServerKeyRequest = 0x07 // Request for server encryption key + OpSessionStatResponse = 0x08 // Session statistics response + OpPacket = 0x09 // Standard data packet + OpFragment = 0x0D // Large packet fragmented for transmission + OpOutOfOrderAck = 0x11 // Acknowledgment for out-of-sequence packet + OpAck = 0x15 // Standard packet acknowledgment + OpAppCombined = 0x19 // Application-level combined packets + OpOutOfSession = 0x1D // Packet received outside valid session +) + +// Login server application opcodes +const ( + // Core login operations + OpLoginRequestMsg = 0x2000 // Initial login request from client + OpLoginByNumRequestMsg = 0x2001 // Login request using account number + OpWSLoginRequestMsg = 0x2002 // World server login request + OpESLoginRequestMsg = 0x2003 // EverQuest station login request + OpLoginReplyMsg = 0x2004 // Server response to login attempt + + // World server operations + OpWorldListMsg = 0x2010 // List of available world servers + OpWorldStatusChangeMsg = 0x2011 // World server status update notification + OpAllWSDescRequestMsg = 0x2012 // Request all world server descriptions + OpWSStatusReplyMsg = 0x2013 // World server status response + + // Character management operations + OpAllCharactersDescRequestMsg = 0x2020 // Request descriptions of all characters + OpAllCharactersDescReplyMsg = 0x2021 // Response with character descriptions + OpCreateCharacterRequestMsg = 0x2022 // Client character creation request + OpReskinCharacterRequestMsg = 0x2023 // Character appearance modification request + OpCreateCharacterReplyMsg = 0x2024 // Server response to character creation + OpWSCreateCharacterRequestMsg = 0x2025 // World server character creation request + OpWSCreateCharacterReplyMsg = 0x2026 // World server character creation response + OpDeleteCharacterRequestMsg = 0x2027 // Character deletion request + OpDeleteCharacterReplyMsg = 0x2028 // Character deletion confirmation + OpPlayCharacterRequestMsg = 0x2029 // Request to enter game with character + OpPlayCharacterReplyMsg = 0x202A // Response to character play request + OpServerPlayCharacterRequestMsg = 0x202B // Server-side character play request + OpServerPlayCharacterReplyMsg = 0x202C // Server-side character play response + + // Key mapping operations + OpKeymapLoadMsg = 0x2030 // Load saved key mappings + OpKeymapNoneMsg = 0x2031 // No key mappings available + OpKeymapDataMsg = 0x2032 // Key mapping data transmission + OpKeymapSaveMsg = 0x2033 // Save current key mappings + + // Account security operations + OpLSCheckAcctLockMsg = 0x2040 // Check account lock status + OpWSAcctLockStatusMsg = 0x2041 // Account lock status update + + // Logging and crash reporting operations + OpLsRequestClientCrashLogMsg = 0x2050 // Request client crash log + OpLsClientBaselogReplyMsg = 0x2051 // Base log response + OpLsClientCrashlogReplyMsg = 0x2052 // Crash log response + OpLsClientAlertlogReplyMsg = 0x2053 // Alert log response + OpLsClientVerifylogReplyMsg = 0x2054 // Verification log response + + // Server administration operations + OpBadLanguageFilter = 0x2060 // Bad language filter message + OpWSServerLockMsg = 0x2061 // World server lock message + OpWSServerHideMsg = 0x2062 // World server hide message + OpLSServerLockMsg = 0x2063 // Login server lock message + + // Character data updates (login context) + OpUpdateCharacterSheetMsg = 0x2070 // Character sheet data update + OpUpdateInventoryMsg = 0x2071 // Character inventory update +) + +// Game server application opcodes +const ( + // Server initialization and zone management + OpESInitMsg = 0x0010 // Server initialization message + OpESReadyForClientsMsg = 0x0011 // Server ready to accept clients + OpCreateZoneInstanceMsg = 0x0012 // Create new zone instance + OpZoneInstanceCreateReplyMsg = 0x0013 // Zone instance creation response + OpZoneInstanceDestroyedMsg = 0x0014 // Zone instance destroyed notification + OpExpectClientAsCharacterRequest = 0x0015 // Expect client with character + OpExpectClientAsCharacterReplyMs = 0x0016 // Character expectation reply + OpZoneInfoMsg = 0x0017 // Zone information message + + // Character loading and resources + OpDoneLoadingZoneResourcesMsg = 0x0020 // Zone resources loaded + OpDoneSendingInitialEntitiesMsg = 0x0021 // Initial entities sent + OpDoneLoadingEntityResourcesMsg = 0x0022 // Entity resources loaded + OpDoneLoadingUIResourcesMsg = 0x0023 // UI resources loaded + + // Game state updates + OpPredictionUpdateMsg = 0x0030 // Client prediction update + OpRemoteCmdMsg = 0x0031 // Remote command message + OpSetRemoteCmdsMsg = 0x0032 // Set remote commands + OpGameWorldTimeMsg = 0x0033 // Game world time sync + OpMOTDMsg = 0x0034 // Message of the day + OpZoneMOTDMsg = 0x0035 // Zone-specific MOTD + + // Guild recruitment system + OpGuildRecruitingMemberInfo = 0x0040 // Guild member recruiting info + OpGuildRecruiting = 0x0041 // Guild recruiting message + OpGuildRecruitingDetails = 0x0042 // Guild recruiting details + OpGuildRecruitingImage = 0x0043 // Guild recruiting image + + // Avatar lifecycle + OpAvatarCreatedMsg = 0x0050 // Avatar created notification + OpAvatarDestroyedMsg = 0x0051 // Avatar destroyed notification + OpAvatarUpdateMsg = 0x0052 // Avatar update message + + // Camping and logout + OpRequestCampMsg = 0x0060 // Request to camp/logout + OpMapRequest = 0x0061 // Map data request + OpCampStartedMsg = 0x0062 // Camp timer started + OpCampAbortedMsg = 0x0063 // Camp timer aborted + + // Player queries and monitoring + OpWhoQueryRequestMsg = 0x0070 // Who query request + OpWhoQueryReplyMsg = 0x0071 // Who query response + OpMonitorReplyMsg = 0x0072 // Monitor command reply + OpMonitorCharacterListMsg = 0x0073 // Character list for monitoring + OpMonitorCharacterListRequestMsg = 0x0074 // Request character list + + // Command dispatching + OpClientCmdMsg = 0x0080 // Client command message + OpLottery = 0x0081 // Lottery system message + OpDispatchClientCmdMsg = 0x0082 // Dispatch client command + OpDispatchESMsg = 0x0083 // Dispatch EverQuest message + + // Target and opportunity updates + OpUpdateTargetMsg = 0x0090 // Update current target + OpUpdateOpportunityMsg = 0x0091 // Update opportunity window + OpUpdateTargetLocMsg = 0x0092 // Update target location + + // Character progression and books + OpUpdateSpellBookMsg = 0x00A0 // Update spell book + OpUpdateSkillBookMsg = 0x00A1 // Update skill book + OpUpdateSkillsMsg = 0x00A2 // Update skills + + // Recipe and crafting system + OpUpdateRecipeBookMsg = 0x00B0 // Update recipe book + OpRequestRecipeDetailsMsg = 0x00B1 // Request recipe details + OpRecipeDetailsMsg = 0x00B2 // Recipe details response + + // Zone transitions and teleportation + OpChangeZoneMsg = 0x00C0 // Change zone request + OpClientTeleportRequestMsg = 0x00C1 // Client teleport request + OpTeleportWithinZoneMsg = 0x00C2 // Teleport within current zone + OpTeleportWithinZoneNoReloadMsg = 0x00C3 // Teleport without zone reload + OpMigrateClientToZoneRequestMsg = 0x00C4 // Migrate client to zone + OpMigrateClientToZoneReplyMsg = 0x00C5 // Zone migration reply + OpReadyToZoneMsg = 0x00C6 // Client ready to zone + + // Group management + OpRemoveClientFromGroupMsg = 0x00D0 // Remove client from group + OpRemoveGroupFromGroupMsg = 0x00D1 // Remove group from group + OpMakeGroupLeaderMsg = 0x00D2 // Make group leader + OpGroupCreatedMsg = 0x00D3 // Group created notification + OpGroupDestroyedMsg = 0x00D4 // Group destroyed notification + OpGroupMemberAddedMsg = 0x00D5 // Group member added + OpGroupMemberRemovedMsg = 0x00D6 // Group member removed + OpGroupRemovedFromGroupMsg = 0x00D7 // Group removed from group + OpGroupLeaderChangedMsg = 0x00D8 // Group leader changed + OpGroupSettingsChangedMsg = 0x00D9 // Group settings changed + + // Data synchronization + OpSendLatestRequestMsg = 0x00E0 // Send latest data request + OpClearDataMsg = 0x00E1 // Clear data message + OpSetSocialMsg = 0x00E2 // Set social information + + // Server status monitoring + OpESStatusMsg = 0x00F0 // EverQuest server status + OpESZoneInstanceStatusMsg = 0x00F1 // Zone instance status + OpZonesStatusRequestMsg = 0x00F2 // Request zones status + OpZonesStatusMsg = 0x00F3 // Zones status response + + // Weather system + OpESWeatherRequestMsg = 0x0100 // Weather request + OpESWeatherRequestEndMsg = 0x0101 // Weather request end + + // Dialog system + OpDialogSelectMsg = 0x0110 // Dialog selection + OpDialogCloseMsg = 0x0111 // Dialog close + + // Spell effects and concentration + OpRemoveSpellEffectMsg = 0x0120 // Remove spell effect + OpRemoveConcentrationMsg = 0x0121 // Remove concentration + + // Quest journal system + OpQuestJournalOpenMsg = 0x0130 // Open quest journal + OpQuestJournalInspectMsg = 0x0131 // Inspect quest journal + OpQuestJournalSetVisibleMsg = 0x0132 // Set quest journal visibility + OpQuestJournalWaypointMsg = 0x0133 // Quest journal waypoint + + // Guild management + OpCreateGuildRequestMsg = 0x0140 // Create guild request + OpCreateGuildReplyMsg = 0x0141 // Create guild reply + OpGuildsayMsg = 0x0142 // Guild say message + OpGuildUpdateMsg = 0x0143 // Guild update message + OpFellowshipExpMsg = 0x0144 // Fellowship experience + + // Trading and consignment + OpConsignmentCloseStoreMsg = 0x0150 // Close consignment store + OpConsignItemRequestMsg = 0x0151 // Consign item request + OpConsignItemResponseMsg = 0x0152 // Consign item response + OpPurchaseConsignmentLoreCheckRe = 0x0153 // Purchase consignment lore check + OpQuestReward = 0x0154 // Quest reward + + // Housing system + OpHouseDeletedRemotelyMsg = 0x0160 // House deleted remotely + OpUpdateHouseDataMsg = 0x0161 // Update house data + OpUpdateHouseAccessDataMsg = 0x0162 // Update house access data + OpPlayerHouseBaseScreenMsg = 0x0163 // Player house base screen + OpPlayerHousePurchaseScreenMsg = 0x0164 // Player house purchase screen + OpPlayerHouseAccessUpdateMsg = 0x0165 // Player house access update + OpPlayerHouseDisplayStatusMsg = 0x0166 // Player house display status + OpPlayerHouseCloseUIMsg = 0x0167 // Player house close UI + OpBuyPlayerHouseMsg = 0x0168 // Buy player house + OpBuyPlayerHouseTintMsg = 0x0169 // Buy player house tint + OpCollectAllHouseItemsMsg = 0x016A // Collect all house items + OpRelinquishHouseMsg = 0x016B // Relinquish house + OpEnterHouseMsg = 0x016C // Enter house + OpExitHouseMsg = 0x016D // Exit house + + // Object examination and placement + OpExamineConsignmentRequestMsg = 0x0170 // Examine consignment request + OpMoveableObjectPlacementCriteri = 0x0171 // Moveable object placement criteria + OpEnterMoveObjectModeMsg = 0x0172 // Enter move object mode + OpPositionMoveableObject = 0x0173 // Position moveable object + OpCancelMoveObjectModeMsg = 0x0174 // Cancel move object mode + + // Visual customization + OpShaderCustomizationMsg = 0x0180 // Shader customization + OpReplaceableSubMeshesMsg = 0x0181 // Replaceable sub meshes + OpExamineConsignmentResponseMsg = 0x0182 // Examine consignment response + + // House access control + OpHouseDefaultAccessSetMsg = 0x0190 // House default access set + OpHouseAccessSetMsg = 0x0191 // House access set + OpHouseAccessRemoveMsg = 0x0192 // House access remove + OpPayHouseUpkeepMsg = 0x0193 // Pay house upkeep + + // UI customization and settings + OpTintWidgetsMsg = 0x01A0 // Tint widgets + OpUISettingsResponseMsg = 0x01A1 // UI settings response + OpUIResetMsg = 0x01A2 // UI reset + + // Spell commands + OpDispatchSpellCmdMsg = 0x01C0 // Dispatch spell command + + // House customization + OpHouseCustomizationScreenMsg = 0x01D0 // House customization screen + OpCustomizationPurchaseRequestMs = 0x01D1 // Customization purchase request + OpCustomizationSetRequestMsg = 0x01D2 // Customization set request + OpCustomizationReplyMsg = 0x01D3 // Customization reply + + // Entity interaction + OpEntityVerbsRequestMsg = 0x01E0 // Entity verbs request + OpEntityVerbsReplyMsg = 0x01E1 // Entity verbs reply + OpEntityVerbsVerbMsg = 0x01E2 // Entity verbs verb + + // Chat system + OpChatRelationshipUpdateMsg = 0x01F0 // Chat relationship update + OpChatCreateChannelMsg = 0x01F1 // Chat create channel + OpChatJoinChannelMsg = 0x01F2 // Chat join channel + OpChatWhoChannelMsg = 0x01F3 // Chat who channel + OpChatLeaveChannelMsg = 0x01F4 // Chat leave channel + OpChatTellChannelMsg = 0x01F5 // Chat tell channel + OpChatTellUserMsg = 0x01F6 // Chat tell user + OpChatToggleFriendMsg = 0x01F7 // Chat toggle friend + OpChatToggleIgnoreMsg = 0x01F8 // Chat toggle ignore + OpChatSendFriendsMsg = 0x01F9 // Chat send friends + OpChatSendIgnoresMsg = 0x01FA // Chat send ignores + OpChatFiltersMsg = 0x01FB // Chat filters + + // Looting system + OpLootItemsRequestMsg = 0x0200 // Loot items request + OpStoppedLootingMsg = 0x0201 // Stopped looting + + // Character positioning + OpSitMsg = 0x0210 // Sit message + OpStandMsg = 0x0211 // Stand message + OpSatMsg = 0x0212 // Sat message + OpStoodMsg = 0x0213 // Stood message + + // Group options and interface + OpDefaultGroupOptionsRequestMsg = 0x0220 // Default group options request + OpDefaultGroupOptionsMsg = 0x0221 // Default group options + OpGroupOptionsMsg = 0x0222 // Group options + OpDisplayGroupOptionsScreenMsg = 0x0223 // Display group options screen + OpDisplayInnVisitScreenMsg = 0x0224 // Display inn visit screen + + // System debugging and monitoring + OpDumpSchedulerMsg = 0x0230 // Dump scheduler + OpRequestHelpRepathMsg = 0x0231 // Request help repath + OpUpdateMotdMsg = 0x0232 // Update MOTD + OpRequestTargetLocMsg = 0x0233 // Request target location + + // Combat effects + OpPerformPlayerKnockbackMsg = 0x0240 // Perform player knockback + OpPerformCameraShakeMsg = 0x0241 // Perform camera shake + + // Skills and abilities + OpPopulateSkillMapsMsg = 0x0250 // Populate skill maps + OpCancelledFeignMsg = 0x0251 // Cancelled feign + OpSignalMsg = 0x0252 // Signal message + OpSkillInfoRequest = 0x0253 // Skill info request + OpSkillInfoResponse = 0x0254 // Skill info response + + // Crafting interface + OpShowCreateFromRecipeUIMsg = 0x0260 // Show create from recipe UI + OpCancelCreateFromRecipeMsg = 0x0261 // Cancel create from recipe + OpBeginItemCreationMsg = 0x0262 // Begin item creation + OpStopItemCreationMsg = 0x0263 // Stop item creation + OpShowItemCreationProcessUIMsg = 0x0264 // Show item creation process UI + OpUpdateItemCreationProcessUIMsg = 0x0265 // Update item creation process UI + OpDisplayTSEventReactionMsg = 0x0266 // Display tradeskill event reaction + OpShowRecipeBookMsg = 0x0267 // Show recipe book + + // Knowledge base and help system + OpKnowledgebaseRequestMsg = 0x0270 // Knowledge base request + OpKnowledgebaseResponseMsg = 0x0271 // Knowledge base response + + // Customer service ticket system + OpCSTicketHeaderRequestMsg = 0x0280 // CS ticket header request + OpCSTicketInfoMsg = 0x0281 // CS ticket info + OpCSTicketCommentRequestMsg = 0x0282 // CS ticket comment request + OpCSTicketCommentResponseMsg = 0x0283 // CS ticket comment response + OpCSTicketCreateMsg = 0x0284 // CS ticket create + OpCSTicketAddCommentMsg = 0x0285 // CS ticket add comment + OpCSTicketDeleteMsg = 0x0286 // CS ticket delete + OpCSTicketChangeNotificationMsg = 0x0287 // CS ticket change notification + + // World data management + OpWorldDataUpdateMsg = 0x0290 // World data update + OpWorldDataChangeMsg = 0x0291 // World data change + OpKnownLanguagesMsg = 0x0292 // Known languages + + // Client control and prediction + OpClientTeleportToLocationMsg = 0x02B0 // Client teleport to location + OpUpdateClientPredFlagsMsg = 0x02B1 // Update client prediction flags + OpChangeServerControlFlagMsg = 0x02B2 // Change server control flag + + // Customer service tools + OpCSToolsRequestMsg = 0x02C0 // CS tools request + OpCSToolsResponseMsg = 0x02C1 // CS tools response + + // Boat transport system + OpCreateBoatTransportsMsg = 0x02D0 // Create boat transports + OpPositionBoatTransportMsg = 0x02D1 // Position boat transport + OpMigrateBoatTransportMsg = 0x02D2 // Migrate boat transport + OpMigrateBoatTransportReplyMsg = 0x02D3 // Migrate boat transport reply + + // Debugging and examination + OpDisplayDebugNLLPointsMsg = 0x02E0 // Display debug NLL points + OpExamineInfoRequestMsg = 0x02E1 // Examine info request + + // Quickbar and macro management + OpQuickbarInitMsg = 0x02F0 // Quickbar init + OpQuickbarUpdateMsg = 0x02F1 // Quickbar update + OpMacroInitMsg = 0x02F2 // Macro init + OpMacroUpdateMsg = 0x02F3 // Macro update + + // EverQuest specific client commands + // Audio and visual commands + OpEqHearChatCmd = 0x1000 // Hear chat command + OpEqDisplayTextCmd = 0x1001 // Display text command + OpEqHearCombatCmd = 0x1002 // Hear combat command + OpEqHearSpellCastCmd = 0x1003 // Hear spell cast command + OpEqHearSpellInterruptCmd = 0x1004 // Hear spell interrupt command + OpEqHearSpellFizzleCmd = 0x1005 // Hear spell fizzle command + OpEqHearConsiderCmd = 0x1006 // Hear consider command + OpEqCannedEmoteCmd = 0x1007 // Canned emote command + OpEqStateCmd = 0x1008 // State command + OpEqPlaySoundCmd = 0x1009 // Play sound command + OpEqPlaySound3DCmd = 0x100A // Play 3D sound command + OpEqPlayVoiceCmd = 0x100B // Play voice command + OpEqHearDrowningCmd = 0x100C // Hear drowning command + OpEqHearDeathCmd = 0x100D // Hear death command + OpEqHearChainEffectCmd = 0x100E // Hear chain effect command + OpEqHearPlayFlavorCmd = 0x100F // Hear play flavor command + OpEqHearHealCmd = 0x1010 // Hear heal command + OpEQHearThreatCmd = 0x1011 // Hear threat command + OpEqHearSpellNoLandCmd = 0x1012 // Hear spell no land command + OpEQHearDispellCmd = 0x1013 // Hear dispell command + + // Ghost and widget management + OpEqCreateGhostCmd = 0x1020 // Create ghost command + OpEqCreateWidgetCmd = 0x1021 // Create widget command + OpEqCreateSignWidgetCmd = 0x1022 // Create sign widget command + OpEqDestroyGhostCmd = 0x1023 // Destroy ghost command + OpEqUpdateGhostCmd = 0x1024 // Update ghost command + OpEqSetControlGhostCmd = 0x1025 // Set control ghost command + OpEqSetPOVGhostCmd = 0x1026 // Set POV ghost command + OpEqUpdateSignWidgetCmd = 0x1027 // Update sign widget command + + // Class and UI management + OpEqUpdateSubClassesCmd = 0x1030 // Update sub classes command + OpEqCreateListBoxCmd = 0x1031 // Create list box command + + // Group and effects + OpEqGroupMemberRemovedCmd = 0x1040 // Group member removed command + OpEqReceiveOfferCmd = 0x1041 // Receive offer command + OpEqInspectPCResultsCmd = 0x1042 // Inspect PC results command + + // Dialog system + OpEqDialogOpenCmd = 0x1050 // Dialog open command + OpEqDialogCloseCmd = 0x1051 // Dialog close command + + // Collections system + OpEqCollectionUpdateCmd = 0x1060 // Collection update command + OpEqCollectionFilterCmd = 0x1061 // Collection filter command + OpEqCollectionItemCmd = 0x1062 // Collection item command + + // Quest system + OpEqQuestJournalUpdateCmd = 0x1070 // Quest journal update command + OpEqQuestJournalReplyCmd = 0x1071 // Quest journal reply command + OpEqQuestGroupCmd = 0x1072 // Quest group command + + // Commerce and trading + OpEqUpdateMerchantCmd = 0x1080 // Update merchant command + OpEqUpdateStoreCmd = 0x1081 // Update store command + OpEqUpdatePlayerTradeCmd = 0x1082 // Update player trade command + OpEqUpdateBankCmd = 0x1083 // Update bank command + OpEqUpdateLootCmd = 0x1084 // Update loot command + OpEqConsignmentItemsCmd = 0x1085 // Consignment items command + OpEqStartBrokerCmd = 0x1086 // Start broker command + + // Navigation and pathfinding + OpEqSetDebugPathPointsCmd = 0x1090 // Set debug path points command + OpEqDrawablePathGraphCmd = 0x1091 // Drawable path graph command + OpEqHelpPathCmd = 0x1092 // Help path command + OpEqHelpPathClearCmd = 0x1093 // Help path clear command + + // Examination and information + OpEqExamineInfoCmd = 0x10A0 // Examine info command + OpEqDebugPVDCmd = 0x10A1 // Debug PVD command + + // Window management + OpEqCloseWindowCmd = 0x10B0 // Close window command + OpEqJunctionListCmd = 0x10B1 // Junction list command + OpEqChoiceWinCmd = 0x10B2 // Choice window command + OpEqInstructionWindowCmd = 0x10B3 // Instruction window command + OpEqInstructionWindowCloseCmd = 0x10B4 // Instruction window close command + OpEqInstructionWindowGoalCmd = 0x10B5 // Instruction window goal command + OpEqInstructionWindowTaskCmd = 0x10B6 // Instruction window task command + OpEqShowWindowCmd = 0x10B7 // Show window command + OpEqEnableWindowCmd = 0x10B8 // Enable window command + OpEqFlashWindowCmd = 0x10B9 // Flash window command + OpEqShowBookCmd = 0x10BA // Show book command + + // Death and spell effects + OpEqShowDeathWindowCmd = 0x10C0 // Show death window command + OpEqDisplaySpellFailCmd = 0x10C1 // Display spell fail command + OpEqSpellCastStartCmd = 0x10C2 // Spell cast start command + OpEqSpellCastEndCmd = 0x10C3 // Spell cast end command + OpEqResurrectedCmd = 0x10C4 // Resurrected command + + // Game events and verbs + OpEqSetDefaultVerbCmd = 0x10D0 // Set default verb command + OpEqEnableGameEventCmd = 0x10D1 // Enable game event command + + // Questionnaire and problems + OpEqQuestionnaireCmd = 0x10E0 // Questionnaire command + OpEqGetProbsCmd = 0x10E1 // Get problems command + + // Chat channels + OpEqChatChannelUpdateCmd = 0x10F0 // Chat channel update command + OpEqWhoChannelQueryReplyCmd = 0x10F1 // Who channel query reply command + OpEqAvailWorldChannelsCmd = 0x10F2 // Available world channels command + + // Target and map systems + OpEqUpdateTargetCmd = 0x1100 // Update target command + OpEqMapExplorationCmd = 0x1101 // Map exploration command + OpEqTargetItemCmd = 0x1102 // Target item command + + // Logging and mail + OpEqStoreLogCmd = 0x1110 // Store log command + OpEqSpellMoveToRangeAndRetryCmd = 0x1111 // Spell move to range and retry command + OpEqUpdatePlayerMailCmd = 0x1112 // Update player mail command + + // Faction updates + OpEqFactionUpdateCmd = 0x1120 // Faction update command +) diff --git a/internal/udp/combine.go b/internal/udp/combine.go new file mode 100644 index 0000000..ea8c7c2 --- /dev/null +++ b/internal/udp/combine.go @@ -0,0 +1,248 @@ +package udp + +import ( + "bytes" + "eq2emu/internal/opcodes" + "errors" +) + +// PacketCombiner groups small packets together to reduce UDP overhead +type PacketCombiner struct { + pendingPackets []*ProtocolPacket // Packets awaiting combination + maxSize int // Maximum combined packet size + timeout int // Combination timeout in milliseconds +} + +// NewPacketCombiner creates a combiner with default settings +func NewPacketCombiner() *PacketCombiner { + return &PacketCombiner{ + maxSize: 256, // Default size threshold for combining + timeout: 10, // Default timeout in ms + } +} + +// NewPacketCombinerWithConfig creates a combiner with custom settings +func NewPacketCombinerWithConfig(maxSize, timeout int) *PacketCombiner { + return &PacketCombiner{ + maxSize: maxSize, + timeout: timeout, + } +} + +// AddPacket queues a packet for potential combining +func (pc *PacketCombiner) AddPacket(packet *ProtocolPacket) { + pc.pendingPackets = append(pc.pendingPackets, packet) +} + +// FlushCombined returns combined packets and clears the queue +func (pc *PacketCombiner) FlushCombined() []*ProtocolPacket { + if len(pc.pendingPackets) == 0 { + return nil + } + + if len(pc.pendingPackets) == 1 { + // Single packet - no combining needed + packet := pc.pendingPackets[0] + pc.pendingPackets = nil + return []*ProtocolPacket{packet} + } + + // Combine multiple packets + combined := pc.combineProtocolPackets(pc.pendingPackets) + pc.pendingPackets = nil + return []*ProtocolPacket{combined} +} + +// combineProtocolPackets merges multiple packets into a single combined packet +func (pc *PacketCombiner) combineProtocolPackets(packets []*ProtocolPacket) *ProtocolPacket { + var buf bytes.Buffer + + for _, packet := range packets { + serialized := packet.Serialize() + pc.writeSizeHeader(&buf, len(serialized)) + buf.Write(serialized) + } + + return &ProtocolPacket{ + Opcode: opcodes.OpCombined, + Data: buf.Bytes(), + } +} + +// writeSizeHeader writes packet size using variable-length encoding +func (pc *PacketCombiner) writeSizeHeader(buf *bytes.Buffer, size int) { + if size >= 255 { + // Large packet - use 3-byte header [0xFF][low][high] + buf.WriteByte(0xFF) + buf.WriteByte(byte(size)) + buf.WriteByte(byte(size >> 8)) + } else { + // Small packet - use 1-byte header + buf.WriteByte(byte(size)) + } +} + +// ParseCombinedPacket splits combined packet into individual packets +func ParseCombinedPacket(data []byte) ([]*ProtocolPacket, error) { + var packets []*ProtocolPacket + offset := 0 + + for offset < len(data) { + size, headerSize, err := readSizeHeader(data, offset) + if err != nil { + break + } + + offset += headerSize + + if offset+size > len(data) { + break // Incomplete packet + } + + // Parse individual packet + packetData := data[offset : offset+size] + if packet, err := ParseProtocolPacket(packetData); err == nil { + packets = append(packets, packet) + } + + offset += size + } + + return packets, nil +} + +// readSizeHeader reads variable-length size header +func readSizeHeader(data []byte, offset int) (size, headerSize int, err error) { + if offset >= len(data) { + return 0, 0, errors.New("insufficient data for size header") + } + + if data[offset] == 0xFF { + // 3-byte size header + if offset+2 >= len(data) { + return 0, 0, errors.New("insufficient data for 3-byte size header") + } + size = int(data[offset+1]) | (int(data[offset+2]) << 8) + headerSize = 3 + } else { + // 1-byte size header + size = int(data[offset]) + headerSize = 1 + } + + return size, headerSize, nil +} + +// ShouldCombine determines if packets should be combined based on total size +func (pc *PacketCombiner) ShouldCombine() bool { + if len(pc.pendingPackets) < 2 { + return false + } + + totalSize := 0 + for _, packet := range pc.pendingPackets { + serialized := packet.Serialize() + totalSize += len(serialized) + + // Add size header overhead + if len(serialized) >= 255 { + totalSize += 3 + } else { + totalSize += 1 + } + } + + return totalSize <= pc.maxSize +} + +// HasPendingPackets returns true if packets are waiting to be combined +func (pc *PacketCombiner) HasPendingPackets() bool { + return len(pc.pendingPackets) > 0 +} + +// GetPendingCount returns the number of packets waiting to be combined +func (pc *PacketCombiner) GetPendingCount() int { + return len(pc.pendingPackets) +} + +// Clear removes all pending packets without combining +func (pc *PacketCombiner) Clear() { + pc.pendingPackets = nil +} + +// SetMaxSize updates the maximum combined packet size +func (pc *PacketCombiner) SetMaxSize(maxSize int) { + pc.maxSize = maxSize +} + +// SetTimeout updates the combination timeout +func (pc *PacketCombiner) SetTimeout(timeout int) { + pc.timeout = timeout +} + +// GetStats returns packet combination statistics +func (pc *PacketCombiner) GetStats() CombinerStats { + return CombinerStats{ + PendingCount: len(pc.pendingPackets), + MaxSize: pc.maxSize, + Timeout: pc.timeout, + } +} + +// CombinerStats contains packet combiner statistics +type CombinerStats struct { + PendingCount int // Number of packets waiting to be combined + MaxSize int // Maximum combined packet size + Timeout int // Combination timeout in milliseconds +} + +// EstimateCombinedSize calculates the size if current packets were combined +func (pc *PacketCombiner) EstimateCombinedSize() int { + if len(pc.pendingPackets) == 0 { + return 0 + } + + totalSize := 0 + for _, packet := range pc.pendingPackets { + serialized := packet.Serialize() + packetSize := len(serialized) + totalSize += packetSize + + // Add size header overhead + if packetSize >= 255 { + totalSize += 3 + } else { + totalSize += 1 + } + } + + return totalSize +} + +// ValidateCombinedPacket checks if combined packet data is well-formed +func ValidateCombinedPacket(data []byte) error { + offset := 0 + count := 0 + + for offset < len(data) { + size, headerSize, err := readSizeHeader(data, offset) + if err != nil { + return err + } + + offset += headerSize + + if offset+size > len(data) { + return errors.New("packet extends beyond data boundary") + } + + offset += size + count++ + + if count > 100 { // Sanity check + return errors.New("too many packets in combined packet") + } + } + + return nil +} diff --git a/internal/udp/compression.go b/internal/udp/compression.go index f1cb4da..a1012f9 100644 --- a/internal/udp/compression.go +++ b/internal/udp/compression.go @@ -6,44 +6,71 @@ import ( "io" ) +// Compression markers used by EQ2EMu protocol +const ( + CompressedMarker = 0x5A // Marks zlib compressed data + UncompressedMarker = 0xA5 // Marks uncompressed data +) + +// Compress applies zlib compression with EQ2EMu protocol markers +// Returns compressed data only if compression provides space savings func Compress(data []byte) ([]byte, error) { + // Empty data gets uncompressed marker + if len(data) == 0 { + return []byte{UncompressedMarker}, nil + } + var buf bytes.Buffer + buf.WriteByte(CompressedMarker) - // Write compression marker - buf.WriteByte(0x5A) - + // Compress data using zlib writer := zlib.NewWriter(&buf) - _, err := writer.Write(data) - if err != nil { + if _, err := writer.Write(data); err != nil { + return nil, err + } + if err := writer.Close(); err != nil { return nil, err } - err = writer.Close() - if err != nil { - return nil, err + // Only use compression if it actually saves space + if buf.Len() >= len(data)+1 { + result := make([]byte, len(data)+1) + result[0] = UncompressedMarker + copy(result[1:], data) + return result, nil } return buf.Bytes(), nil } +// Decompress handles both compressed and uncompressed data based on markers func Decompress(data []byte) ([]byte, error) { if len(data) == 0 { return data, nil } - // Check compression marker - if data[0] == 0xA5 { - // Uncompressed data + switch data[0] { + case UncompressedMarker: + // Data is uncompressed - return without marker return data[1:], nil - } - if data[0] != 0x5A { - // No compression marker, return as-is + case CompressedMarker: + // Data is zlib compressed + return decompressZlib(data[1:]) + + default: + // No compression marker - return as-is return data, nil } +} - // Decompress zlib data - reader := bytes.NewReader(data[1:]) +// decompressZlib handles zlib decompression +func decompressZlib(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, io.ErrUnexpectedEOF + } + + reader := bytes.NewReader(data) zlibReader, err := zlib.NewReader(reader) if err != nil { return nil, err @@ -51,10 +78,24 @@ func Decompress(data []byte) ([]byte, error) { defer zlibReader.Close() var buf bytes.Buffer - _, err = io.Copy(&buf, zlibReader) - if err != nil { + if _, err := io.Copy(&buf, zlibReader); err != nil { return nil, err } return buf.Bytes(), nil } + +// IsCompressed checks if data has compression marker +func IsCompressed(data []byte) bool { + return len(data) > 0 && data[0] == CompressedMarker +} + +// IsUncompressed checks if data has uncompressed marker +func IsUncompressed(data []byte) bool { + return len(data) > 0 && data[0] == UncompressedMarker +} + +// HasCompressionMarker checks if data has any compression marker +func HasCompressionMarker(data []byte) bool { + return IsCompressed(data) || IsUncompressed(data) +} diff --git a/internal/udp/connection.go b/internal/udp/connection.go index 8237d3d..e58b395 100644 --- a/internal/udp/connection.go +++ b/internal/udp/connection.go @@ -3,114 +3,153 @@ package udp import ( "crypto/rand" "encoding/binary" + "eq2emu/internal/opcodes" "net" "sync" "time" ) +// ConnectionState represents the current state of a UDP connection type ConnectionState int const ( - StateClosed ConnectionState = iota - StateEstablished - StateClosing + StateClosed ConnectionState = iota // Connection is closed + StateEstablished // Connection is active and ready + StateClosing // Connection is being closed + StateWaitClose // Waiting for close confirmation ) +const ( + DefaultWindowSize = 2048 // Default sliding window size for flow control + MaxPacketSize = 512 // Maximum packet size before fragmentation +) + +// Connection manages a single client connection over UDP with reliability features type Connection struct { - addr *net.UDPAddr - conn *net.UDPConn + // Network details + addr *net.UDPAddr // Client's UDP address + conn *net.UDPConn // Shared UDP socket handler PacketHandler state ConnectionState - mutex sync.RWMutex + mutex sync.RWMutex // Protects connection state - // Session data - sessionID uint32 - key uint32 - compressed bool - encoded bool - maxLength uint32 + // Session parameters + sessionID uint32 // Unique session identifier + key uint32 // Encryption key + compressed bool // Whether compression is enabled + encoded bool // Whether encoding is enabled + maxLength uint32 // Maximum packet length - // Sequence tracking - nextInSeq uint16 - nextOutSeq uint16 + // Sequence tracking for reliable delivery + nextInSeq uint16 // Next expected incoming sequence number + nextOutSeq uint16 // Next outgoing sequence number + windowSize uint16 // Flow control window size - // Queues - inboundQueue []*ApplicationPacket - outboundQueue []*ProtocolPacket - ackQueue []uint16 + // Protocol components + retransmitQueue *RetransmitQueue // Handles packet retransmission + fragmentMgr *FragmentManager // Manages packet fragmentation + combiner *PacketCombiner // Combines small packets + outOfOrderMap map[uint16]*ProtocolPacket // Stores out-of-order packets + crypto *Crypto // Handles encryption/decryption - // Timing - lastPacketTime time.Time + // Connection timing + lastPacketTime time.Time // Last received packet timestamp + lastAckTime time.Time // Last acknowledgment timestamp - // Crypto - crypto *Crypto + // Flow control + sendWindow []bool // Sliding window for sent packets + windowStart uint16 // Start of the sliding window } +// NewConnection creates a new connection instance with default settings func NewConnection(addr *net.UDPAddr, conn *net.UDPConn, handler PacketHandler) *Connection { return &Connection{ - addr: addr, - conn: conn, - handler: handler, - state: StateClosed, - maxLength: 512, - lastPacketTime: time.Now(), - crypto: NewCrypto(), + addr: addr, + conn: conn, + handler: handler, + state: StateClosed, + maxLength: MaxPacketSize, + windowSize: DefaultWindowSize, + lastPacketTime: time.Now(), + crypto: NewCrypto(), + retransmitQueue: NewRetransmitQueue(), + fragmentMgr: NewFragmentManager(MaxPacketSize), + combiner: NewPacketCombiner(), + outOfOrderMap: make(map[uint16]*ProtocolPacket), + sendWindow: make([]bool, DefaultWindowSize), } } +// ProcessPacket handles incoming UDP packets and routes them based on opcode func (c *Connection) ProcessPacket(data []byte) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.lastPacketTime = time.Now() packet, err := ParseProtocolPacket(data) if err != nil { - return + return // Silently drop malformed packets } + // Route packet based on opcode switch packet.Opcode { - case OpSessionRequest: + case opcodes.OpSessionRequest: c.handleSessionRequest(packet) - case OpSessionResponse: + case opcodes.OpSessionResponse: c.handleSessionResponse(packet) - case OpPacket: + case opcodes.OpPacket: c.handleDataPacket(packet) - case OpAck: + case opcodes.OpFragment: + c.handleFragment(packet) + case opcodes.OpCombined: + c.handleCombinedPacket(packet) + case opcodes.OpAck: c.handleAck(packet) - case OpKeepAlive: + case opcodes.OpOutOfOrderAck: + c.handleOutOfOrderAck(packet) + case opcodes.OpKeepAlive: c.sendKeepAlive() - case OpSessionDisconnect: - c.Close() + case opcodes.OpSessionDisconnect: + c.handleDisconnect() } } +// handleSessionRequest processes client session initiation func (c *Connection) handleSessionRequest(packet *ProtocolPacket) { if len(packet.Data) < 12 { return } - // Parse session request + // Extract session parameters from request c.sessionID = binary.LittleEndian.Uint32(packet.Data[4:8]) requestedMaxLen := binary.LittleEndian.Uint32(packet.Data[8:12]) - if requestedMaxLen > 0 { + // Set maximum packet length if reasonable + if requestedMaxLen > 0 && requestedMaxLen <= 8192 { c.maxLength = requestedMaxLen + c.fragmentMgr = NewFragmentManager(requestedMaxLen) } - // Generate encryption key + // Generate random encryption key keyBytes := make([]byte, 4) rand.Read(keyBytes) c.key = binary.LittleEndian.Uint32(keyBytes) - // Send session response + // Initialize encryption + c.crypto.SetKey(keyBytes) + c.sendSessionResponse() c.state = StateEstablished } +// handleSessionResponse processes server session response (client-side) func (c *Connection) handleSessionResponse(packet *ProtocolPacket) { - // Client-side session response handling if len(packet.Data) < 20 { return } + // Extract session parameters c.sessionID = binary.LittleEndian.Uint32(packet.Data[0:4]) c.key = binary.LittleEndian.Uint32(packet.Data[4:8]) format := packet.Data[9] @@ -118,9 +157,15 @@ func (c *Connection) handleSessionResponse(packet *ProtocolPacket) { c.encoded = (format & 0x04) != 0 c.maxLength = binary.LittleEndian.Uint32(packet.Data[12:16]) + // Initialize encryption with received key + keyBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(keyBytes, c.key) + c.crypto.SetKey(keyBytes) + c.state = StateEstablished } +// handleDataPacket processes reliable data packets with sequence numbers func (c *Connection) handleDataPacket(packet *ProtocolPacket) { if len(packet.Data) < 2 { return @@ -129,35 +174,149 @@ func (c *Connection) handleDataPacket(packet *ProtocolPacket) { seq := binary.BigEndian.Uint16(packet.Data[0:2]) payload := packet.Data[2:] - // Simple in-order processing for now if seq == c.nextInSeq { - c.nextInSeq++ - c.sendAck(seq) + // In-order packet - process immediately + c.processInOrderPacket(seq, payload) + } else if seq > c.nextInSeq { + // Out-of-order packet - store for later processing + c.outOfOrderMap[seq] = packet + c.sendOutOfOrderAck(seq) + } + // Drop packets with seq < nextInSeq (duplicates/old packets) +} - // Process application packet - if appPacket, err := c.processApplicationData(payload); err == nil { +// processInOrderPacket handles packets received in correct sequence order +func (c *Connection) processInOrderPacket(seq uint16, payload []byte) { + c.nextInSeq++ + c.sendAck(seq) + + // Process application data + if appPacket, err := c.processApplicationData(payload); err == nil { + c.handler.HandlePacket(c, appPacket) + } + + // Check for queued out-of-order packets that can now be processed + c.processQueuedPackets() +} + +// processQueuedPackets processes any out-of-order packets that are now in sequence +func (c *Connection) processQueuedPackets() { + for { + packet, exists := c.outOfOrderMap[c.nextInSeq] + if !exists { + break + } + + delete(c.outOfOrderMap, c.nextInSeq) + + if len(packet.Data) >= 2 { + payload := packet.Data[2:] + seq := binary.BigEndian.Uint16(packet.Data[0:2]) + c.nextInSeq++ + c.sendAck(seq) + + if appPacket, err := c.processApplicationData(payload); err == nil { + c.handler.HandlePacket(c, appPacket) + } + } + } +} + +// handleFragment processes fragmented packets and reassembles them +func (c *Connection) handleFragment(packet *ProtocolPacket) { + if data, complete, err := c.fragmentMgr.ProcessFragment(packet); err == nil && complete { + if appPacket, err := c.processApplicationData(data); err == nil { c.handler.HandlePacket(c, appPacket) } } } +// handleCombinedPacket splits combined packets into individual packets +func (c *Connection) handleCombinedPacket(packet *ProtocolPacket) { + if packets, err := ParseCombinedPacket(packet.Data); err == nil { + for _, p := range packets { + c.ProcessPacket(p.Raw) + } + } +} + +// handleAck processes acknowledgment packets func (c *Connection) handleAck(packet *ProtocolPacket) { if len(packet.Data) < 2 { return } seq := binary.BigEndian.Uint16(packet.Data[0:2]) - // Remove acknowledged packets from retransmit queue - _ = seq // TODO: implement retransmit queue + c.retransmitQueue.Acknowledge(seq) + c.lastAckTime = time.Now() } +// handleOutOfOrderAck processes out-of-order acknowledgments +func (c *Connection) handleOutOfOrderAck(packet *ProtocolPacket) { + if len(packet.Data) < 2 { + return + } + + seq := binary.BigEndian.Uint16(packet.Data[0:2]) + c.retransmitQueue.Acknowledge(seq) +} + +// SendPacket sends an application packet with fragmentation and reliability +func (c *Connection) SendPacket(packet *ApplicationPacket) { + c.mutex.Lock() + defer c.mutex.Unlock() + + data := packet.Serialize() + + // Handle large packets with fragmentation + if fragments := c.fragmentMgr.FragmentPacket(data, c.nextOutSeq); fragments != nil { + for _, frag := range fragments { + c.sendProtocolPacketWithRetransmit(frag) + } + return + } + + // Process outbound data (compression, encryption) + processedData := c.processOutboundData(data) + + // Create protocol packet with sequence number + protocolData := make([]byte, 2+len(processedData)) + binary.BigEndian.PutUint16(protocolData[0:2], c.nextOutSeq) + copy(protocolData[2:], processedData) + + protocolPacket := &ProtocolPacket{ + Opcode: opcodes.OpPacket, + Data: protocolData, + } + + c.sendProtocolPacketWithRetransmit(protocolPacket) +} + +// processOutboundData applies compression and encryption to outgoing data +func (c *Connection) processOutboundData(data []byte) []byte { + // Compress large packets if compression is enabled + if c.compressed && len(data) > 128 { + if compressed, err := Compress(data); err == nil { + data = compressed + } + } + + // Encrypt data if encryption is enabled + if c.crypto.IsEncrypted() { + data = c.crypto.Encrypt(data) + } + + return data +} + +// processApplicationData decrypts and decompresses incoming application data func (c *Connection) processApplicationData(data []byte) (*ApplicationPacket, error) { - // Decrypt if needed + // Decrypt if encryption is enabled if c.crypto.IsEncrypted() { data = c.crypto.Decrypt(data) } - // Decompress if needed + // Decompress if compression is enabled if c.compressed && len(data) > 0 { var err error data, err = Decompress(data) @@ -169,38 +328,18 @@ func (c *Connection) processApplicationData(data []byte) (*ApplicationPacket, er return ParseApplicationPacket(data) } -func (c *Connection) SendPacket(packet *ApplicationPacket) { - c.mutex.Lock() - defer c.mutex.Unlock() - - data := packet.Serialize() - - // Compress if needed - if c.compressed && len(data) > 128 { - if compressed, err := Compress(data); err == nil { - data = compressed - } +// sendProtocolPacketWithRetransmit sends a packet and adds it to retransmit queue if needed +func (c *Connection) sendProtocolPacketWithRetransmit(packet *ProtocolPacket) { + // Add reliable packets to retransmit queue + if packet.Opcode == opcodes.OpPacket || packet.Opcode == opcodes.OpFragment { + c.retransmitQueue.Add(packet, c.nextOutSeq) + c.nextOutSeq++ } - // Encrypt if needed - if c.crypto.IsEncrypted() { - data = c.crypto.Encrypt(data) - } - - // Create protocol packet - protocolData := make([]byte, 2+len(data)) - binary.BigEndian.PutUint16(protocolData[0:2], c.nextOutSeq) - copy(protocolData[2:], data) - c.nextOutSeq++ - - protocolPacket := &ProtocolPacket{ - Opcode: OpPacket, - Data: protocolData, - } - - c.sendProtocolPacket(protocolPacket) + c.sendProtocolPacket(packet) } +// sendSessionResponse sends session establishment response to client func (c *Connection) sendSessionResponse() { data := make([]byte, 20) binary.LittleEndian.PutUint32(data[0:4], c.sessionID) @@ -221,39 +360,61 @@ func (c *Connection) sendSessionResponse() { binary.LittleEndian.PutUint32(data[16:20], 0) // UnknownD packet := &ProtocolPacket{ - Opcode: OpSessionResponse, + Opcode: opcodes.OpSessionResponse, Data: data, } c.sendProtocolPacket(packet) } +// sendAck sends acknowledgment for received packet func (c *Connection) sendAck(seq uint16) { data := make([]byte, 2) binary.BigEndian.PutUint16(data, seq) packet := &ProtocolPacket{ - Opcode: OpAck, + Opcode: opcodes.OpAck, Data: data, } c.sendProtocolPacket(packet) } +// sendOutOfOrderAck sends acknowledgment for out-of-order packet +func (c *Connection) sendOutOfOrderAck(seq uint16) { + data := make([]byte, 2) + binary.BigEndian.PutUint16(data, seq) + + packet := &ProtocolPacket{ + Opcode: opcodes.OpOutOfOrderAck, + Data: data, + } + + c.sendProtocolPacket(packet) +} + +// sendKeepAlive sends keep-alive packet to maintain connection func (c *Connection) sendKeepAlive() { packet := &ProtocolPacket{ - Opcode: OpKeepAlive, + Opcode: opcodes.OpKeepAlive, Data: []byte{}, } c.sendProtocolPacket(packet) } +// sendProtocolPacket sends a protocol packet over UDP func (c *Connection) sendProtocolPacket(packet *ProtocolPacket) { data := packet.Serialize() c.conn.WriteToUDP(data, c.addr) } +// handleDisconnect processes disconnection request +func (c *Connection) handleDisconnect() { + c.state = StateClosing +} + +// Close gracefully closes the connection func (c *Connection) Close() { c.mutex.Lock() defer c.mutex.Unlock() @@ -261,14 +422,14 @@ func (c *Connection) Close() { if c.state == StateEstablished { c.state = StateClosing - // Send disconnect + // Send disconnect packet disconnectData := make([]byte, 6) binary.LittleEndian.PutUint32(disconnectData[0:4], c.sessionID) disconnectData[4] = 0 disconnectData[5] = 6 packet := &ProtocolPacket{ - Opcode: OpSessionDisconnect, + Opcode: opcodes.OpSessionDisconnect, Data: disconnectData, } @@ -276,4 +437,45 @@ func (c *Connection) Close() { } c.state = StateClosed + c.retransmitQueue.Clear() +} + +// StartRetransmitLoop begins the retransmission management goroutine +func (c *Connection) StartRetransmitLoop() { + go func() { + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for range ticker.C { + if c.state == StateClosed { + return + } + + // Retransmit expired packets + for _, entry := range c.retransmitQueue.GetExpired() { + c.sendProtocolPacket(entry.Packet) + } + } + }() +} + +// GetState returns the current connection state (thread-safe) +func (c *Connection) GetState() ConnectionState { + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.state +} + +// GetSessionID returns the session ID (thread-safe) +func (c *Connection) GetSessionID() uint32 { + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.sessionID +} + +// IsTimedOut checks if connection has timed out +func (c *Connection) IsTimedOut(timeout time.Duration) bool { + c.mutex.RLock() + defer c.mutex.RUnlock() + return time.Since(c.lastPacketTime) > timeout } diff --git a/internal/udp/crc.go b/internal/udp/crc.go new file mode 100644 index 0000000..456554f --- /dev/null +++ b/internal/udp/crc.go @@ -0,0 +1,73 @@ +package udp + +// EQ2EMu uses a specific CRC32 polynomial (reversed) +const crcPolynomial = 0xEDB88320 + +// Pre-computed CRC32 lookup table for fast calculation +var crcTable [256]uint32 + +// init builds the CRC lookup table at package initialization +func init() { + for i := range crcTable { + crc := uint32(i) + for range 8 { + if crc&1 == 1 { + crc = (crc >> 1) ^ crcPolynomial + } else { + crc >>= 1 + } + } + crcTable[i] = crc + } +} + +// CalculateCRC32 computes CRC32 using EQ2EMu's algorithm +// Returns 16-bit value by truncating the upper bits +func CalculateCRC32(data []byte) uint16 { + crc := uint32(0xFFFFFFFF) + + // Use lookup table for efficient calculation + for _, b := range data { + crc = crcTable[byte(crc)^b] ^ (crc >> 8) + } + + // Return inverted result truncated to 16 bits + return uint16(^crc) +} + +// ValidateCRC checks if packet has valid CRC +// Expects CRC to be the last 2 bytes of data +func ValidateCRC(data []byte) bool { + if len(data) < 2 { + return false + } + + // Split payload and CRC + payload := data[:len(data)-2] + expectedCRC := uint16(data[len(data)-2]) | (uint16(data[len(data)-1]) << 8) + + // Calculate and compare + actualCRC := CalculateCRC32(payload) + return expectedCRC == actualCRC +} + +// AppendCRC adds 16-bit CRC to the end of data +func AppendCRC(data []byte) []byte { + crc := CalculateCRC32(data) + result := make([]byte, len(data)+2) + copy(result, data) + + // Append CRC in little-endian format + result[len(data)] = byte(crc) + result[len(data)+1] = byte(crc >> 8) + + return result +} + +// ValidateAndStrip validates CRC and returns data without CRC suffix +func ValidateAndStrip(data []byte) ([]byte, bool) { + if !ValidateCRC(data) { + return nil, false + } + return data[:len(data)-2], true +} diff --git a/internal/udp/crypto.go b/internal/udp/crypto.go index 6fd522a..1e969cf 100644 --- a/internal/udp/crypto.go +++ b/internal/udp/crypto.go @@ -2,27 +2,49 @@ package udp import ( "crypto/rc4" + "fmt" ) +// Crypto handles RC4 encryption/decryption for EQ2EMu protocol type Crypto struct { - cipher *rc4.Cipher - key []byte - encrypted bool + clientCipher *rc4.Cipher // Cipher for decrypting client data + serverCipher *rc4.Cipher // Cipher for encrypting server data + key []byte // Encryption key + encrypted bool // Whether encryption is active } +// NewCrypto creates a new crypto instance with encryption disabled func NewCrypto() *Crypto { return &Crypto{ encrypted: false, } } +// SetKey initializes RC4 encryption with the given key +// Creates separate ciphers for client and server with 20-byte priming func (c *Crypto) SetKey(key []byte) error { - cipher, err := rc4.NewCipher(key) - if err != nil { - return err + if len(key) == 0 { + return fmt.Errorf("encryption key cannot be empty") } - c.cipher = cipher + // Create separate RC4 ciphers for bidirectional communication + clientCipher, err := rc4.NewCipher(key) + if err != nil { + return fmt.Errorf("failed to create client cipher: %w", err) + } + + serverCipher, err := rc4.NewCipher(key) + if err != nil { + return fmt.Errorf("failed to create server cipher: %w", err) + } + + // Prime both ciphers with 20 dummy bytes per EQ2EMu protocol + dummy := make([]byte, 20) + clientCipher.XORKeyStream(dummy, dummy) + serverCipher.XORKeyStream(dummy, dummy) + + c.clientCipher = clientCipher + c.serverCipher = serverCipher c.key = make([]byte, len(key)) copy(c.key, key) c.encrypted = true @@ -30,28 +52,58 @@ func (c *Crypto) SetKey(key []byte) error { return nil } +// IsEncrypted returns whether encryption is currently active func (c *Crypto) IsEncrypted() bool { return c.encrypted } +// Encrypt encrypts data for transmission to client func (c *Crypto) Encrypt(data []byte) []byte { - if !c.encrypted { + if !c.encrypted || c.serverCipher == nil { return data } encrypted := make([]byte, len(data)) copy(encrypted, data) - c.cipher.XORKeyStream(encrypted, encrypted) + c.serverCipher.XORKeyStream(encrypted, encrypted) return encrypted } +// Decrypt decrypts data received from client func (c *Crypto) Decrypt(data []byte) []byte { - if !c.encrypted { + if !c.encrypted || c.clientCipher == nil { return data } decrypted := make([]byte, len(data)) copy(decrypted, data) - c.cipher.XORKeyStream(decrypted, decrypted) + c.clientCipher.XORKeyStream(decrypted, decrypted) return decrypted } + +// GetKey returns a copy of the encryption key +func (c *Crypto) GetKey() []byte { + if c.key == nil { + return nil + } + keyCopy := make([]byte, len(c.key)) + copy(keyCopy, c.key) + return keyCopy +} + +// Reset disables encryption and clears keys +func (c *Crypto) Reset() { + c.clientCipher = nil + c.serverCipher = nil + c.key = nil + c.encrypted = false +} + +// Clone creates a copy of the crypto instance with the same key +func (c *Crypto) Clone() (*Crypto, error) { + newCrypto := NewCrypto() + if c.encrypted && c.key != nil { + return newCrypto, newCrypto.SetKey(c.key) + } + return newCrypto, nil +} diff --git a/internal/udp/example_test.go b/internal/udp/example_test.go deleted file mode 100644 index d370a78..0000000 --- a/internal/udp/example_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package udp - -import ( - "fmt" - "testing" - "time" -) - -type TestHandler struct{} - -func (h *TestHandler) HandlePacket(conn *Connection, packet *ApplicationPacket) { - fmt.Printf("Received packet - Opcode: 0x%04X, Data length: %d\n", - packet.Opcode, len(packet.Data)) - - // Echo back a response - response := &ApplicationPacket{ - Opcode: OpLoginReplyMsg, - Data: []byte("Hello from server"), - } - conn.SendPacket(response) -} - -func TestServer(t *testing.T) { - handler := &TestHandler{} - server, err := NewServer(":9999", handler) - if err != nil { - t.Fatalf("Failed to create server: %v", err) - } - - go func() { - if err := server.Start(); err != nil { - t.Errorf("Server error: %v", err) - } - }() - - // Let it run for a bit - time.Sleep(100 * time.Millisecond) - - server.Stop() -} diff --git a/internal/udp/fragment.go b/internal/udp/fragment.go new file mode 100644 index 0000000..d040e10 --- /dev/null +++ b/internal/udp/fragment.go @@ -0,0 +1,211 @@ +package udp + +import ( + "encoding/binary" + "eq2emu/internal/opcodes" + "errors" + "fmt" + "sort" +) + +// FragmentManager handles packet fragmentation and reassembly +type FragmentManager struct { + fragments map[uint16]*FragmentGroup // Active fragment groups by base sequence + maxLength uint32 // Maximum packet size before fragmentation +} + +// FragmentGroup tracks fragments belonging to the same original packet +type FragmentGroup struct { + BaseSequence uint16 // Base sequence number + TotalLength uint32 // Expected total reassembled length + Fragments []FragmentPiece // Individual fragment pieces + FirstSeen bool // Whether we've seen the first fragment +} + +// FragmentPiece represents a single fragment +type FragmentPiece struct { + Sequence uint16 // Fragment sequence number + Data []byte // Fragment payload data + IsFirst bool // Whether this is the first fragment +} + +// NewFragmentManager creates a manager with specified maximum packet length +func NewFragmentManager(maxLength uint32) *FragmentManager { + return &FragmentManager{ + fragments: make(map[uint16]*FragmentGroup), + maxLength: maxLength, + } +} + +// FragmentPacket splits large packets into fragments +// Returns nil if packet doesn't need fragmentation +func (fm *FragmentManager) FragmentPacket(data []byte, startSeq uint16) []*ProtocolPacket { + if uint32(len(data)) <= fm.maxLength { + return nil // No fragmentation needed + } + + totalLength := uint32(len(data)) + chunkSize := int(fm.maxLength - 6) // Reserve 6 bytes for headers + if chunkSize <= 0 { + chunkSize = 1 + } + + var packets []*ProtocolPacket + seq := startSeq + + for offset := 0; offset < len(data); offset += chunkSize { + end := offset + chunkSize + if end > len(data) { + end = len(data) + } + + var fragmentData []byte + if offset == 0 { + // First fragment includes total length + fragmentData = make([]byte, 6+end-offset) + binary.BigEndian.PutUint16(fragmentData[0:2], seq) + binary.LittleEndian.PutUint32(fragmentData[2:6], totalLength) + copy(fragmentData[6:], data[offset:end]) + } else { + // Subsequent fragments + fragmentData = make([]byte, 2+end-offset) + binary.BigEndian.PutUint16(fragmentData[0:2], seq) + copy(fragmentData[2:], data[offset:end]) + } + + packets = append(packets, &ProtocolPacket{ + Opcode: opcodes.OpFragment, + Data: fragmentData, + }) + seq++ + } + + return packets +} + +// ProcessFragment handles incoming fragments and returns complete packet when ready +func (fm *FragmentManager) ProcessFragment(packet *ProtocolPacket) ([]byte, bool, error) { + if len(packet.Data) < 2 { + return nil, false, errors.New("fragment too small") + } + + seq := binary.BigEndian.Uint16(packet.Data[0:2]) + + // Parse fragment data + fragment := FragmentPiece{Sequence: seq} + + if len(packet.Data) >= 6 { + // Check if this is the first fragment (has total length) + possibleLength := binary.LittleEndian.Uint32(packet.Data[2:6]) + if possibleLength > 0 && possibleLength < 10*1024*1024 { // Reasonable limit + fragment.IsFirst = true + fragment.Data = packet.Data[6:] + + // Create new fragment group + group := &FragmentGroup{ + BaseSequence: seq, + TotalLength: possibleLength, + Fragments: []FragmentPiece{fragment}, + FirstSeen: true, + } + fm.fragments[seq] = group + + return fm.tryAssemble(seq) + } + } + + // Not first fragment - find matching group + fragment.Data = packet.Data[2:] + group := fm.findFragmentGroup(seq) + if group == nil { + return nil, false, errors.New("orphaned fragment") + } + + group.Fragments = append(group.Fragments, fragment) + return fm.tryAssemble(group.BaseSequence) +} + +// findFragmentGroup locates the fragment group for a sequence number +func (fm *FragmentManager) findFragmentGroup(seq uint16) *FragmentGroup { + // Look for groups where this sequence fits + for baseSeq, group := range fm.fragments { + if seq >= baseSeq && seq < baseSeq+100 { // Reasonable window + return group + } + } + return nil +} + +// tryAssemble attempts to reassemble fragments into complete packet +func (fm *FragmentManager) tryAssemble(baseSeq uint16) ([]byte, bool, error) { + group, exists := fm.fragments[baseSeq] + if !exists || !group.FirstSeen { + return nil, false, nil + } + + // Calculate expected fragment count + chunkSize := int(fm.maxLength - 6) + expectedCount := int(group.TotalLength) / chunkSize + if int(group.TotalLength)%chunkSize != 0 { + expectedCount++ + } + + if len(group.Fragments) < expectedCount { + return nil, false, nil // Still waiting for fragments + } + + // Sort fragments by sequence number + sort.Slice(group.Fragments, func(i, j int) bool { + return group.Fragments[i].Sequence < group.Fragments[j].Sequence + }) + + // Reassemble packet + result := make([]byte, 0, group.TotalLength) + for _, frag := range group.Fragments[:expectedCount] { + result = append(result, frag.Data...) + } + + // Validate length + if uint32(len(result)) != group.TotalLength { + delete(fm.fragments, baseSeq) + return nil, false, fmt.Errorf("assembled length %d != expected %d", len(result), group.TotalLength) + } + + // Clean up + delete(fm.fragments, baseSeq) + return result, true, nil +} + +// CleanupStale removes old incomplete fragment groups +func (fm *FragmentManager) CleanupStale(maxAge uint16) { + // Simple cleanup - remove groups with very old base sequences + for baseSeq := range fm.fragments { + if baseSeq < maxAge { + delete(fm.fragments, baseSeq) + } + } +} + +// GetStats returns fragmentation statistics +func (fm *FragmentManager) GetStats() FragmentStats { + return FragmentStats{ + ActiveGroups: len(fm.fragments), + MaxLength: fm.maxLength, + } +} + +// FragmentStats contains fragmentation statistics +type FragmentStats struct { + ActiveGroups int // Number of incomplete fragment groups + MaxLength uint32 // Maximum packet length setting +} + +// Clear removes all pending fragments +func (fm *FragmentManager) Clear() { + fm.fragments = make(map[uint16]*FragmentGroup) +} + +// SetMaxLength updates the maximum packet length +func (fm *FragmentManager) SetMaxLength(maxLength uint32) { + fm.maxLength = maxLength +} diff --git a/internal/udp/opcodes.go b/internal/udp/opcodes.go deleted file mode 100644 index 29d0f94..0000000 --- a/internal/udp/opcodes.go +++ /dev/null @@ -1,28 +0,0 @@ -package udp - -const ( - // Protocol opcodes - OpSessionRequest = 0x01 - OpSessionResponse = 0x02 - OpCombined = 0x03 - OpSessionDisconnect = 0x05 - OpKeepAlive = 0x06 - OpServerKeyRequest = 0x07 - OpSessionStatResponse = 0x08 - OpPacket = 0x09 - OpFragment = 0x0D - OpOutOfOrderAck = 0x11 - OpAck = 0x15 - OpAppCombined = 0x19 - OpOutOfSession = 0x1D -) - -// Application opcodes (examples) -const ( - OpLoginRequestMsg = 0x0001 - OpLoginReplyMsg = 0x0002 - OpAllCharactersDescRequestMsg = 0x0003 - OpAllCharactersDescReplyMsg = 0x0004 - OpCreateCharacterRequestMsg = 0x0005 - OpCreateCharacterReplyMsg = 0x0006 -) diff --git a/internal/udp/packet.go b/internal/udp/packet.go index 0aec430..f22f43d 100644 --- a/internal/udp/packet.go +++ b/internal/udp/packet.go @@ -2,53 +2,135 @@ package udp import ( "encoding/binary" + "eq2emu/internal/opcodes" "errors" + "fmt" ) +// ProtocolPacket represents a low-level UDP protocol packet with opcode and payload type ProtocolPacket struct { - Opcode uint8 - Data []byte + Opcode uint8 // Protocol operation code (1-2 bytes when serialized) + Data []byte // Packet payload data + Raw []byte // Original raw packet data for debugging } +// ApplicationPacket represents a higher-level game application packet type ApplicationPacket struct { - Opcode uint16 - Data []byte + Opcode uint16 // Application-level operation code + Data []byte // Application payload data } +// ParseProtocolPacket parses raw UDP data into a ProtocolPacket +// Handles variable opcode sizing and CRC validation based on EQ2 protocol func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) { if len(data) < 2 { - return nil, errors.New("packet too small") + return nil, errors.New("packet too small for valid protocol packet") + } + + var opcode uint8 + var dataStart int + + // EQ2 protocol uses 1-byte opcodes normally, 2-byte for opcodes >= 0xFF + // When opcode >= 0xFF, it's prefixed with 0x00 + if data[0] == 0x00 && len(data) > 2 { + opcode = data[1] + dataStart = 2 + } else { + opcode = data[0] + dataStart = 1 + } + + // Extract payload, handling CRC for non-session packets + var payload []byte + if requiresCRC(opcode) { + if len(data) < dataStart+2 { + return nil, errors.New("packet too small for CRC validation") + } + + // Payload excludes the 2-byte CRC suffix + payload = data[dataStart : len(data)-2] + + // Validate CRC on the entire packet from beginning + if !ValidateCRC(data) { + return nil, fmt.Errorf("CRC validation failed for opcode 0x%02X", opcode) + } + } else { + payload = data[dataStart:] } return &ProtocolPacket{ - Opcode: data[1], - Data: data[2:], + Opcode: opcode, + Data: payload, + Raw: data, }, nil } +// Serialize converts ProtocolPacket back to wire format with proper opcode encoding and CRC func (p *ProtocolPacket) Serialize() []byte { - data := make([]byte, 2+len(p.Data)) - data[0] = 0x00 // Reserved byte - data[1] = p.Opcode - copy(data[2:], p.Data) - return data -} + var result []byte -func ParseApplicationPacket(data []byte) (*ApplicationPacket, error) { - if len(data) < 2 { - return nil, errors.New("application packet too small") + // Handle variable opcode encoding + if p.Opcode >= 0xFF { + // 2-byte opcode format: [0x00][actual_opcode][data] + result = make([]byte, 2+len(p.Data)) + result[0] = 0x00 + result[1] = p.Opcode + copy(result[2:], p.Data) + } else { + // 1-byte opcode format: [opcode][data] + result = make([]byte, 1+len(p.Data)) + result[0] = p.Opcode + copy(result[1:], p.Data) } + // Add CRC for packets that require it + if requiresCRC(p.Opcode) { + result = AppendCRC(result) + } + + return result +} + +// ParseApplicationPacket parses application-level packet from decrypted/decompressed data +func ParseApplicationPacket(data []byte) (*ApplicationPacket, error) { + if len(data) < 2 { + return nil, errors.New("application packet requires at least 2 bytes for opcode") + } + + // Application opcodes are always little-endian 16-bit values opcode := binary.LittleEndian.Uint16(data[0:2]) + return &ApplicationPacket{ Opcode: opcode, Data: data[2:], }, nil } +// Serialize converts ApplicationPacket to byte array for transmission func (p *ApplicationPacket) Serialize() []byte { - data := make([]byte, 2+len(p.Data)) - binary.LittleEndian.PutUint16(data[0:2], p.Opcode) - copy(data[2:], p.Data) - return data + result := make([]byte, 2+len(p.Data)) + binary.LittleEndian.PutUint16(result[0:2], p.Opcode) + copy(result[2:], p.Data) + return result +} + +// String provides human-readable representation for debugging +func (p *ProtocolPacket) String() string { + return fmt.Sprintf("ProtocolPacket{Opcode: 0x%02X, DataLen: %d}", p.Opcode, len(p.Data)) +} + +// String provides human-readable representation for debugging +func (p *ApplicationPacket) String() string { + return fmt.Sprintf("ApplicationPacket{Opcode: 0x%04X, DataLen: %d}", p.Opcode, len(p.Data)) +} + +// requiresCRC determines if a protocol opcode requires CRC validation +// Session control packets (SessionRequest, SessionResponse, OutOfSession) don't use CRC +func requiresCRC(opcode uint8) bool { + switch opcode { + case opcodes.OpSessionRequest, opcodes.OpSessionResponse, opcodes.OpOutOfSession: + return false + default: + return true + } } diff --git a/internal/udp/retransmit.go b/internal/udp/retransmit.go new file mode 100644 index 0000000..d125f26 --- /dev/null +++ b/internal/udp/retransmit.go @@ -0,0 +1,190 @@ +package udp + +import ( + "sync" + "time" +) + +// RetransmitEntry tracks a packet awaiting acknowledgment +type RetransmitEntry struct { + Packet *ProtocolPacket // The packet to retransmit + Sequence uint16 // Packet sequence number + Timestamp time.Time // When packet was last sent + Attempts int // Number of transmission attempts +} + +// RetransmitQueue manages reliable packet delivery with exponential backoff +type RetransmitQueue struct { + entries map[uint16]*RetransmitEntry // Pending packets by sequence + mutex sync.RWMutex // Thread-safe access + baseTimeout time.Duration // Base retransmission timeout + maxAttempts int // Maximum retry attempts + maxTimeout time.Duration // Maximum timeout cap +} + +// NewRetransmitQueue creates a queue with default settings +func NewRetransmitQueue() *RetransmitQueue { + return &RetransmitQueue{ + entries: make(map[uint16]*RetransmitEntry), + baseTimeout: 500 * time.Millisecond, + maxAttempts: 5, + maxTimeout: 5 * time.Second, + } +} + +// NewRetransmitQueueWithConfig creates a queue with custom settings +func NewRetransmitQueueWithConfig(baseTimeout, maxTimeout time.Duration, maxAttempts int) *RetransmitQueue { + return &RetransmitQueue{ + entries: make(map[uint16]*RetransmitEntry), + baseTimeout: baseTimeout, + maxAttempts: maxAttempts, + maxTimeout: maxTimeout, + } +} + +// Add queues a packet for potential retransmission +func (rq *RetransmitQueue) Add(packet *ProtocolPacket, sequence uint16) { + rq.mutex.Lock() + defer rq.mutex.Unlock() + + rq.entries[sequence] = &RetransmitEntry{ + Packet: packet, + Sequence: sequence, + Timestamp: time.Now(), + Attempts: 1, + } +} + +// Acknowledge removes a packet from the retransmit queue +func (rq *RetransmitQueue) Acknowledge(sequence uint16) bool { + rq.mutex.Lock() + defer rq.mutex.Unlock() + + _, existed := rq.entries[sequence] + delete(rq.entries, sequence) + return existed +} + +// GetExpired returns packets that need retransmission +func (rq *RetransmitQueue) GetExpired() []*RetransmitEntry { + rq.mutex.Lock() + defer rq.mutex.Unlock() + + now := time.Now() + var expired []*RetransmitEntry + + for seq, entry := range rq.entries { + timeout := rq.calculateTimeout(entry.Attempts) + + if now.Sub(entry.Timestamp) > timeout { + if entry.Attempts >= rq.maxAttempts { + // Give up after max attempts + delete(rq.entries, seq) + } else { + // Schedule for retransmission + entry.Attempts++ + entry.Timestamp = now + expired = append(expired, entry) + } + } + } + + return expired +} + +// calculateTimeout computes timeout with exponential backoff +func (rq *RetransmitQueue) calculateTimeout(attempts int) time.Duration { + timeout := rq.baseTimeout * time.Duration(attempts*attempts) // Quadratic backoff + if timeout > rq.maxTimeout { + timeout = rq.maxTimeout + } + return timeout +} + +// Clear removes all pending packets +func (rq *RetransmitQueue) Clear() { + rq.mutex.Lock() + defer rq.mutex.Unlock() + rq.entries = make(map[uint16]*RetransmitEntry) +} + +// Size returns the number of pending packets +func (rq *RetransmitQueue) Size() int { + rq.mutex.RLock() + defer rq.mutex.RUnlock() + return len(rq.entries) +} + +// GetPendingSequences returns all sequence numbers awaiting acknowledgment +func (rq *RetransmitQueue) GetPendingSequences() []uint16 { + rq.mutex.RLock() + defer rq.mutex.RUnlock() + + sequences := make([]uint16, 0, len(rq.entries)) + for seq := range rq.entries { + sequences = append(sequences, seq) + } + return sequences +} + +// IsEmpty returns true if no packets are pending +func (rq *RetransmitQueue) IsEmpty() bool { + rq.mutex.RLock() + defer rq.mutex.RUnlock() + return len(rq.entries) == 0 +} + +// SetBaseTimeout updates the base retransmission timeout +func (rq *RetransmitQueue) SetBaseTimeout(timeout time.Duration) { + rq.mutex.Lock() + defer rq.mutex.Unlock() + rq.baseTimeout = timeout +} + +// SetMaxAttempts updates the maximum retry attempts +func (rq *RetransmitQueue) SetMaxAttempts(attempts int) { + rq.mutex.Lock() + defer rq.mutex.Unlock() + rq.maxAttempts = attempts +} + +// SetMaxTimeout updates the maximum timeout cap +func (rq *RetransmitQueue) SetMaxTimeout(timeout time.Duration) { + rq.mutex.Lock() + defer rq.mutex.Unlock() + rq.maxTimeout = timeout +} + +// GetStats returns retransmission statistics +func (rq *RetransmitQueue) GetStats() RetransmitStats { + rq.mutex.RLock() + defer rq.mutex.RUnlock() + + stats := RetransmitStats{ + PendingCount: len(rq.entries), + BaseTimeout: rq.baseTimeout, + MaxAttempts: rq.maxAttempts, + MaxTimeout: rq.maxTimeout, + } + + // Calculate attempt distribution + for _, entry := range rq.entries { + if entry.Attempts == 1 { + stats.FirstAttempts++ + } else { + stats.Retransmissions++ + } + } + + return stats +} + +// RetransmitStats contains retransmission queue statistics +type RetransmitStats struct { + PendingCount int // Total pending packets + FirstAttempts int // Packets on first attempt + Retransmissions int // Packets being retransmitted + BaseTimeout time.Duration // Base timeout setting + MaxAttempts int // Maximum attempts setting + MaxTimeout time.Duration // Maximum timeout setting +} diff --git a/internal/udp/server.go b/internal/udp/server.go index 4c1f3f9..3af785c 100644 --- a/internal/udp/server.go +++ b/internal/udp/server.go @@ -7,44 +7,81 @@ import ( "time" ) +// Server manages multiple UDP connections and handles packet routing type Server struct { - conn *net.UDPConn - connections map[string]*Connection - mutex sync.RWMutex - handler PacketHandler - running bool + conn *net.UDPConn // Main UDP socket + connections map[string]*Connection // Active connections by address + mutex sync.RWMutex // Protects connections map + handler PacketHandler // Application packet handler + running bool // Server running state + + // Configuration + maxConnections int // Maximum concurrent connections + timeout time.Duration // Connection timeout duration } +// PacketHandler processes application-level packets for connections type PacketHandler interface { HandlePacket(conn *Connection, packet *ApplicationPacket) } +// ServerConfig holds server configuration options +type ServerConfig struct { + MaxConnections int // Maximum concurrent connections (default: 1000) + Timeout time.Duration // Connection timeout (default: 45s) + BufferSize int // UDP receive buffer size (default: 8192) +} + +// DefaultServerConfig returns sensible default configuration +func DefaultServerConfig() ServerConfig { + return ServerConfig{ + MaxConnections: 1000, + Timeout: 45 * time.Second, + BufferSize: 8192, + } +} + +// NewServer creates a new UDP server instance func NewServer(addr string, handler PacketHandler) (*Server, error) { + return NewServerWithConfig(addr, handler, DefaultServerConfig()) +} + +// NewServerWithConfig creates a server with custom configuration +func NewServerWithConfig(addr string, handler PacketHandler, config ServerConfig) (*Server, error) { udpAddr, err := net.ResolveUDPAddr("udp", addr) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid UDP address %s: %w", addr, err) } conn, err := net.ListenUDP("udp", udpAddr) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to listen on %s: %w", addr, err) + } + + // Set socket buffer size for better performance + if config.BufferSize > 0 { + conn.SetReadBuffer(config.BufferSize) + conn.SetWriteBuffer(config.BufferSize) } return &Server{ - conn: conn, - connections: make(map[string]*Connection), - handler: handler, + conn: conn, + connections: make(map[string]*Connection), + handler: handler, + maxConnections: config.MaxConnections, + timeout: config.Timeout, }, nil } +// Start begins accepting and processing UDP packets func (s *Server) Start() error { s.running = true - // Start connection timeout checker - go s.timeoutChecker() + // Start background management tasks + go s.connectionManager() // Main packet receive loop - buffer := make([]byte, 2048) + buffer := make([]byte, 8192) for s.running { n, addr, err := s.conn.ReadFromUDP(buffer) if err != nil { @@ -54,36 +91,59 @@ func (s *Server) Start() error { continue } - go s.handlePacket(buffer[:n], addr) + // Handle packet in separate goroutine to avoid blocking + go s.handleIncomingPacket(buffer[:n], addr) } return nil } +// Stop gracefully shuts down the server func (s *Server) Stop() { s.running = false + + // Close all connections + s.mutex.Lock() + for _, conn := range s.connections { + conn.Close() + } + s.connections = make(map[string]*Connection) + s.mutex.Unlock() + + // Close UDP socket s.conn.Close() } -func (s *Server) handlePacket(data []byte, addr *net.UDPAddr) { - if len(data) < 2 { +// handleIncomingPacket processes a single UDP packet +func (s *Server) handleIncomingPacket(data []byte, addr *net.UDPAddr) { + if len(data) < 1 { return } connKey := addr.String() + // Find or create connection s.mutex.Lock() conn, exists := s.connections[connKey] if !exists { + // Check connection limit + if len(s.connections) >= s.maxConnections { + s.mutex.Unlock() + return // Drop packet if at capacity + } + conn = NewConnection(addr, s.conn, s.handler) + conn.StartRetransmitLoop() s.connections[connKey] = conn } s.mutex.Unlock() + // Process packet conn.ProcessPacket(data) } -func (s *Server) timeoutChecker() { +// connectionManager handles connection cleanup and maintenance +func (s *Server) connectionManager() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() @@ -92,14 +152,104 @@ func (s *Server) timeoutChecker() { return } - now := time.Now() - s.mutex.Lock() - for key, conn := range s.connections { - if now.Sub(conn.lastPacketTime) > 45*time.Second { - conn.Close() - delete(s.connections, key) - } - } - s.mutex.Unlock() + // Clean up timed out connections + s.cleanupTimedOutConnections() } } + +// cleanupTimedOutConnections removes connections that have timed out +func (s *Server) cleanupTimedOutConnections() { + s.mutex.Lock() + defer s.mutex.Unlock() + + for key, conn := range s.connections { + if conn.IsTimedOut(s.timeout) { + conn.Close() + delete(s.connections, key) + } + } +} + +// GetConnectionCount returns the number of active connections +func (s *Server) GetConnectionCount() int { + s.mutex.RLock() + defer s.mutex.RUnlock() + return len(s.connections) +} + +// GetConnection returns a connection by address string +func (s *Server) GetConnection(addr string) *Connection { + s.mutex.RLock() + defer s.mutex.RUnlock() + return s.connections[addr] +} + +// GetAllConnections returns a snapshot of all active connections +func (s *Server) GetAllConnections() []*Connection { + s.mutex.RLock() + defer s.mutex.RUnlock() + + connections := make([]*Connection, 0, len(s.connections)) + for _, conn := range s.connections { + connections = append(connections, conn) + } + return connections +} + +// BroadcastPacket sends a packet to all connected clients +func (s *Server) BroadcastPacket(packet *ApplicationPacket) { + connections := s.GetAllConnections() + for _, conn := range connections { + if conn.GetState() == StateEstablished { + conn.SendPacket(packet) + } + } +} + +// DisconnectClient forcibly disconnects a client by address +func (s *Server) DisconnectClient(addr string) bool { + s.mutex.Lock() + defer s.mutex.Unlock() + + if conn, exists := s.connections[addr]; exists { + conn.Close() + delete(s.connections, addr) + return true + } + return false +} + +// GetStats returns server statistics +func (s *Server) GetStats() ServerStats { + s.mutex.RLock() + defer s.mutex.RUnlock() + + return ServerStats{ + ConnectionCount: len(s.connections), + MaxConnections: s.maxConnections, + Running: s.running, + Timeout: s.timeout, + } +} + +// ServerStats contains server runtime statistics +type ServerStats struct { + ConnectionCount int // Current number of connections + MaxConnections int // Maximum allowed connections + Running bool // Whether server is running + Timeout time.Duration // Connection timeout setting +} + +// SetConnectionLimit updates the maximum connection limit +func (s *Server) SetConnectionLimit(limit int) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.maxConnections = limit +} + +// SetTimeout updates the connection timeout duration +func (s *Server) SetTimeout(timeout time.Duration) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.timeout = timeout +} diff --git a/internal/udp/udp_test.go b/internal/udp/udp_test.go new file mode 100644 index 0000000..7b9b574 --- /dev/null +++ b/internal/udp/udp_test.go @@ -0,0 +1,422 @@ +package udp + +import ( + "eq2emu/internal/opcodes" + "fmt" + "net" + "sync" + "testing" + "time" +) + +// TestHandler implements PacketHandler for testing +type TestHandler struct { + receivedPackets []*ApplicationPacket + mutex sync.Mutex +} + +// HandlePacket processes received packets and stores them for verification +func (h *TestHandler) HandlePacket(conn *Connection, packet *ApplicationPacket) { + h.mutex.Lock() + defer h.mutex.Unlock() + + h.receivedPackets = append(h.receivedPackets, packet) + fmt.Printf("Received packet - Opcode: 0x%04X, Data length: %d\n", + packet.Opcode, len(packet.Data)) + + // Echo back a response for interactive testing + response := &ApplicationPacket{ + Opcode: opcodes.OpLoginReplyMsg, + Data: []byte("Hello from server"), + } + conn.SendPacket(response) +} + +// GetReceivedPackets returns a copy of all received packets +func (h *TestHandler) GetReceivedPackets() []*ApplicationPacket { + h.mutex.Lock() + defer h.mutex.Unlock() + + packets := make([]*ApplicationPacket, len(h.receivedPackets)) + copy(packets, h.receivedPackets) + return packets +} + +// Clear removes all received packets +func (h *TestHandler) Clear() { + h.mutex.Lock() + defer h.mutex.Unlock() + h.receivedPackets = nil +} + +// TestServer tests basic server creation and startup +func TestServer(t *testing.T) { + handler := &TestHandler{} + server, err := NewServer(":9999", handler) + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + // Start server in goroutine + go func() { + if err := server.Start(); err != nil { + t.Errorf("Server error: %v", err) + } + }() + + // Let it run briefly + time.Sleep(100 * time.Millisecond) + + // Verify server is running + if server.GetConnectionCount() != 0 { + t.Errorf("Expected 0 connections, got %d", server.GetConnectionCount()) + } + + server.Stop() +} + +// TestServerConfig tests server configuration options +func TestServerConfig(t *testing.T) { + handler := &TestHandler{} + config := ServerConfig{ + MaxConnections: 10, + Timeout: 30 * time.Second, + BufferSize: 4096, + } + + server, err := NewServerWithConfig(":9998", handler, config) + if err != nil { + t.Fatalf("Failed to create server with config: %v", err) + } + + stats := server.GetStats() + if stats.MaxConnections != 10 { + t.Errorf("Expected max connections 10, got %d", stats.MaxConnections) + } + if stats.Timeout != 30*time.Second { + t.Errorf("Expected timeout 30s, got %v", stats.Timeout) + } + + server.Stop() +} + +// TestPacketParsing tests protocol packet parsing +func TestPacketParsing(t *testing.T) { + // Test 1-byte opcode with CRC + payload1 := []byte{0x01, 0x48, 0x65, 0x6C, 0x6C, 0x6F} + data1 := AppendCRC(payload1) + packet1, err := ParseProtocolPacket(data1) + if err != nil { + t.Fatalf("Failed to parse 1-byte opcode: %v", err) + } + if packet1.Opcode != 0x01 { + t.Errorf("Expected opcode 0x01, got 0x%02X", packet1.Opcode) + } + + // Test 2-byte opcode with CRC + payload2 := []byte{0x00, 0xFF, 0x48, 0x65, 0x6C, 0x6C, 0x6F} + data2 := AppendCRC(payload2) + packet2, err := ParseProtocolPacket(data2) + if err != nil { + t.Fatalf("Failed to parse 2-byte opcode: %v", err) + } + if packet2.Opcode != 0xFF { + t.Errorf("Expected opcode 0xFF, got 0x%02X", packet2.Opcode) + } + + // Test session packet (no CRC required) + sessionData := []byte{opcodes.OpSessionRequest, 0x48, 0x65, 0x6C, 0x6C, 0x6F} + sessionPacket, err := ParseProtocolPacket(sessionData) + if err != nil { + t.Fatalf("Failed to parse session packet: %v", err) + } + if sessionPacket.Opcode != opcodes.OpSessionRequest { + t.Errorf("Expected session request opcode, got 0x%02X", sessionPacket.Opcode) + } +} + +// TestCRC tests CRC calculation and validation +func TestCRC(t *testing.T) { + data := []byte("Hello, World!") + + // Append CRC and validate + withCRC := AppendCRC(data) + if !ValidateCRC(withCRC) { + t.Error("CRC validation failed for correct data") + } + + // Test with corrupted data + corrupted := make([]byte, len(withCRC)) + copy(corrupted, withCRC) + corrupted[0] ^= 0xFF // Flip bits + + if ValidateCRC(corrupted) { + t.Error("CRC validation passed for corrupted data") + } + + // Test ValidateAndStrip + stripped, valid := ValidateAndStrip(withCRC) + if !valid { + t.Error("ValidateAndStrip failed for valid data") + } + if string(stripped) != string(data) { + t.Error("Stripped data doesn't match original") + } +} + +// TestCompression tests data compression and decompression +func TestCompression(t *testing.T) { + testData := []byte("This is a test string that should compress well because it has repetitive patterns.") + + compressed, err := Compress(testData) + if err != nil { + t.Fatalf("Compression failed: %v", err) + } + + decompressed, err := Decompress(compressed) + if err != nil { + t.Fatalf("Decompression failed: %v", err) + } + + if string(decompressed) != string(testData) { + t.Error("Decompressed data doesn't match original") + } + + // Test empty data + empty, err := Compress([]byte{}) + if err != nil { + t.Fatalf("Empty compression failed: %v", err) + } + if len(empty) != 1 || empty[0] != UncompressedMarker { + t.Error("Empty data compression incorrect") + } +} + +// TestCrypto tests RC4 encryption and decryption +func TestCrypto(t *testing.T) { + crypto := NewCrypto() + key := []byte{0x01, 0x02, 0x03, 0x04} + + err := crypto.SetKey(key) + if err != nil { + t.Fatalf("SetKey failed: %v", err) + } + + if !crypto.IsEncrypted() { + t.Error("Crypto should be encrypted after SetKey") + } + + testData := []byte("Hello, World!") + encrypted := crypto.Encrypt(testData) + decrypted := crypto.Decrypt(encrypted) + + if string(decrypted) != string(testData) { + t.Error("Decrypted data doesn't match original") + } +} + +// TestRetransmitQueue tests packet retransmission logic +func TestRetransmitQueue(t *testing.T) { + rq := NewRetransmitQueue() + + packet := &ProtocolPacket{ + Opcode: opcodes.OpPacket, + Data: []byte("test"), + } + + // Add packet + rq.Add(packet, 1) + if rq.Size() != 1 { + t.Errorf("Expected size 1, got %d", rq.Size()) + } + + // Acknowledge packet + acked := rq.Acknowledge(1) + if !acked { + t.Error("Acknowledge should return true for existing packet") + } + if rq.Size() != 0 { + t.Errorf("Expected size 0 after ack, got %d", rq.Size()) + } + + // Test non-existent acknowledgment + acked = rq.Acknowledge(999) + if acked { + t.Error("Acknowledge should return false for non-existent packet") + } +} + +// TestFragmentation tests packet fragmentation and reassembly +func TestFragmentation(t *testing.T) { + fm := NewFragmentManager(100) // Small max length to force fragmentation + + // Create large test data + largeData := make([]byte, 300) + for i := range largeData { + largeData[i] = byte(i % 256) + } + + // Fragment the data + fragments := fm.FragmentPacket(largeData, 1) + if fragments == nil { + t.Fatal("Expected fragmentation for large data") + } + if len(fragments) < 2 { + t.Error("Expected multiple fragments") + } + + // Reassemble fragments + var reassembled []byte + complete := false + for _, frag := range fragments { + data, isComplete, err := fm.ProcessFragment(frag) + if err != nil { + t.Fatalf("Fragment processing failed: %v", err) + } + if isComplete { + reassembled = data + complete = true + break + } + } + + if !complete { + t.Error("Fragmentation did not complete") + } + if len(reassembled) != len(largeData) { + t.Errorf("Reassembled length %d != original %d", len(reassembled), len(largeData)) + } + for i, b := range reassembled { + if b != largeData[i] { + t.Errorf("Reassembled data differs at position %d", i) + break + } + } +} + +// TestPacketCombining tests packet combination functionality +func TestPacketCombining(t *testing.T) { + combiner := NewPacketCombiner() + + // Add small packets - use session opcodes that don't require CRC + packet1 := &ProtocolPacket{Opcode: opcodes.OpSessionRequest, Data: []byte("test1")} + packet2 := &ProtocolPacket{Opcode: opcodes.OpSessionRequest, Data: []byte("test2")} + + combiner.AddPacket(packet1) + combiner.AddPacket(packet2) + + if combiner.GetPendingCount() != 2 { + t.Errorf("Expected 2 pending packets, got %d", combiner.GetPendingCount()) + } + + // Flush combined + combined := combiner.FlushCombined() + if len(combined) != 1 { + t.Errorf("Expected 1 combined packet, got %d", len(combined)) + } + if combined[0].Opcode != opcodes.OpCombined { + t.Error("Combined packet should have OpCombined opcode") + } + + // Parse combined packet + parsed, err := ParseCombinedPacket(combined[0].Data) + if err != nil { + t.Fatalf("Failed to parse combined packet: %v", err) + } + if len(parsed) != 2 { + t.Errorf("Expected 2 parsed packets, got %d", len(parsed)) + } +} + +// TestConnection tests basic connection functionality +func TestConnection(t *testing.T) { + handler := &TestHandler{} + addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:0") + conn, _ := net.ListenUDP("udp", addr) + defer conn.Close() + + connection := NewConnection(addr, conn, handler) + + if connection.GetState() != StateClosed { + t.Error("New connection should be in closed state") + } + + // Test session ID + if connection.GetSessionID() != 0 { + t.Error("New connection should have session ID 0") + } + + // Test timeout + if !connection.IsTimedOut(time.Nanosecond) { + t.Error("New connection should be timed out with very short timeout") + } +} + +// BenchmarkCRC benchmarks CRC calculation performance +func BenchmarkCRC(b *testing.B) { + data := make([]byte, 1024) + for i := range data { + data[i] = byte(i) + } + + b.ResetTimer() + for range b.N { + CalculateCRC32(data) + } +} + +// BenchmarkCompression benchmarks compression performance +func BenchmarkCompression(b *testing.B) { + data := make([]byte, 1024) + for i := range data { + data[i] = byte(i % 256) + } + + b.ResetTimer() + for range b.N { + compressed, _ := Compress(data) + Decompress(compressed) + } +} + +// BenchmarkEncryption benchmarks encryption performance +func BenchmarkEncryption(b *testing.B) { + crypto := NewCrypto() + crypto.SetKey([]byte{1, 2, 3, 4}) + + data := make([]byte, 1024) + for i := range data { + data[i] = byte(i) + } + + b.ResetTimer() + for range b.N { + encrypted := crypto.Encrypt(data) + crypto.Decrypt(encrypted) + } +} + +// TestIntegration performs a basic integration test +func TestIntegration(t *testing.T) { + handler := &TestHandler{} + server, err := NewServer(":0", handler) // Use any available port + if err != nil { + t.Fatalf("Failed to create server: %v", err) + } + + // Start server + go server.Start() + defer server.Stop() + + // Wait for server to start + time.Sleep(50 * time.Millisecond) + + // Basic integration test - server should be running with 0 connections + stats := server.GetStats() + if !stats.Running { + t.Error("Server should be running") + } + if stats.ConnectionCount != 0 { + t.Errorf("Expected 0 connections, got %d", stats.ConnectionCount) + } +}