improve udp server
This commit is contained in:
parent
576d080f03
commit
768181df8a
488
internal/opcodes/opcodes.go
Normal file
488
internal/opcodes/opcodes.go
Normal file
@ -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
|
||||||
|
)
|
248
internal/udp/combine.go
Normal file
248
internal/udp/combine.go
Normal file
@ -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
|
||||||
|
}
|
@ -6,44 +6,71 @@ import (
|
|||||||
"io"
|
"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) {
|
func Compress(data []byte) ([]byte, error) {
|
||||||
|
// Empty data gets uncompressed marker
|
||||||
|
if len(data) == 0 {
|
||||||
|
return []byte{UncompressedMarker}, nil
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
buf.WriteByte(CompressedMarker)
|
||||||
|
|
||||||
// Write compression marker
|
// Compress data using zlib
|
||||||
buf.WriteByte(0x5A)
|
|
||||||
|
|
||||||
writer := zlib.NewWriter(&buf)
|
writer := zlib.NewWriter(&buf)
|
||||||
_, err := writer.Write(data)
|
if _, err := writer.Write(data); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = writer.Close()
|
// Only use compression if it actually saves space
|
||||||
if err != nil {
|
if buf.Len() >= len(data)+1 {
|
||||||
return nil, err
|
result := make([]byte, len(data)+1)
|
||||||
|
result[0] = UncompressedMarker
|
||||||
|
copy(result[1:], data)
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decompress handles both compressed and uncompressed data based on markers
|
||||||
func Decompress(data []byte) ([]byte, error) {
|
func Decompress(data []byte) ([]byte, error) {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check compression marker
|
switch data[0] {
|
||||||
if data[0] == 0xA5 {
|
case UncompressedMarker:
|
||||||
// Uncompressed data
|
// Data is uncompressed - return without marker
|
||||||
return data[1:], nil
|
return data[1:], nil
|
||||||
}
|
|
||||||
|
|
||||||
if data[0] != 0x5A {
|
case CompressedMarker:
|
||||||
// No compression marker, return as-is
|
// Data is zlib compressed
|
||||||
|
return decompressZlib(data[1:])
|
||||||
|
|
||||||
|
default:
|
||||||
|
// No compression marker - return as-is
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Decompress zlib data
|
// decompressZlib handles zlib decompression
|
||||||
reader := bytes.NewReader(data[1:])
|
func decompressZlib(data []byte) ([]byte, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
zlibReader, err := zlib.NewReader(reader)
|
zlibReader, err := zlib.NewReader(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -51,10 +78,24 @@ func Decompress(data []byte) ([]byte, error) {
|
|||||||
defer zlibReader.Close()
|
defer zlibReader.Close()
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
_, err = io.Copy(&buf, zlibReader)
|
if _, err := io.Copy(&buf, zlibReader); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
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)
|
||||||
|
}
|
||||||
|
@ -3,114 +3,153 @@ package udp
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"eq2emu/internal/opcodes"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ConnectionState represents the current state of a UDP connection
|
||||||
type ConnectionState int
|
type ConnectionState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StateClosed ConnectionState = iota
|
StateClosed ConnectionState = iota // Connection is closed
|
||||||
StateEstablished
|
StateEstablished // Connection is active and ready
|
||||||
StateClosing
|
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 {
|
type Connection struct {
|
||||||
addr *net.UDPAddr
|
// Network details
|
||||||
conn *net.UDPConn
|
addr *net.UDPAddr // Client's UDP address
|
||||||
|
conn *net.UDPConn // Shared UDP socket
|
||||||
handler PacketHandler
|
handler PacketHandler
|
||||||
state ConnectionState
|
state ConnectionState
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex // Protects connection state
|
||||||
|
|
||||||
// Session data
|
// Session parameters
|
||||||
sessionID uint32
|
sessionID uint32 // Unique session identifier
|
||||||
key uint32
|
key uint32 // Encryption key
|
||||||
compressed bool
|
compressed bool // Whether compression is enabled
|
||||||
encoded bool
|
encoded bool // Whether encoding is enabled
|
||||||
maxLength uint32
|
maxLength uint32 // Maximum packet length
|
||||||
|
|
||||||
// Sequence tracking
|
// Sequence tracking for reliable delivery
|
||||||
nextInSeq uint16
|
nextInSeq uint16 // Next expected incoming sequence number
|
||||||
nextOutSeq uint16
|
nextOutSeq uint16 // Next outgoing sequence number
|
||||||
|
windowSize uint16 // Flow control window size
|
||||||
|
|
||||||
// Queues
|
// Protocol components
|
||||||
inboundQueue []*ApplicationPacket
|
retransmitQueue *RetransmitQueue // Handles packet retransmission
|
||||||
outboundQueue []*ProtocolPacket
|
fragmentMgr *FragmentManager // Manages packet fragmentation
|
||||||
ackQueue []uint16
|
combiner *PacketCombiner // Combines small packets
|
||||||
|
outOfOrderMap map[uint16]*ProtocolPacket // Stores out-of-order packets
|
||||||
|
crypto *Crypto // Handles encryption/decryption
|
||||||
|
|
||||||
// Timing
|
// Connection timing
|
||||||
lastPacketTime time.Time
|
lastPacketTime time.Time // Last received packet timestamp
|
||||||
|
lastAckTime time.Time // Last acknowledgment timestamp
|
||||||
|
|
||||||
// Crypto
|
// Flow control
|
||||||
crypto *Crypto
|
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 {
|
func NewConnection(addr *net.UDPAddr, conn *net.UDPConn, handler PacketHandler) *Connection {
|
||||||
return &Connection{
|
return &Connection{
|
||||||
addr: addr,
|
addr: addr,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
state: StateClosed,
|
state: StateClosed,
|
||||||
maxLength: 512,
|
maxLength: MaxPacketSize,
|
||||||
lastPacketTime: time.Now(),
|
windowSize: DefaultWindowSize,
|
||||||
crypto: NewCrypto(),
|
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) {
|
func (c *Connection) ProcessPacket(data []byte) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
c.lastPacketTime = time.Now()
|
c.lastPacketTime = time.Now()
|
||||||
|
|
||||||
packet, err := ParseProtocolPacket(data)
|
packet, err := ParseProtocolPacket(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return // Silently drop malformed packets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Route packet based on opcode
|
||||||
switch packet.Opcode {
|
switch packet.Opcode {
|
||||||
case OpSessionRequest:
|
case opcodes.OpSessionRequest:
|
||||||
c.handleSessionRequest(packet)
|
c.handleSessionRequest(packet)
|
||||||
case OpSessionResponse:
|
case opcodes.OpSessionResponse:
|
||||||
c.handleSessionResponse(packet)
|
c.handleSessionResponse(packet)
|
||||||
case OpPacket:
|
case opcodes.OpPacket:
|
||||||
c.handleDataPacket(packet)
|
c.handleDataPacket(packet)
|
||||||
case OpAck:
|
case opcodes.OpFragment:
|
||||||
|
c.handleFragment(packet)
|
||||||
|
case opcodes.OpCombined:
|
||||||
|
c.handleCombinedPacket(packet)
|
||||||
|
case opcodes.OpAck:
|
||||||
c.handleAck(packet)
|
c.handleAck(packet)
|
||||||
case OpKeepAlive:
|
case opcodes.OpOutOfOrderAck:
|
||||||
|
c.handleOutOfOrderAck(packet)
|
||||||
|
case opcodes.OpKeepAlive:
|
||||||
c.sendKeepAlive()
|
c.sendKeepAlive()
|
||||||
case OpSessionDisconnect:
|
case opcodes.OpSessionDisconnect:
|
||||||
c.Close()
|
c.handleDisconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleSessionRequest processes client session initiation
|
||||||
func (c *Connection) handleSessionRequest(packet *ProtocolPacket) {
|
func (c *Connection) handleSessionRequest(packet *ProtocolPacket) {
|
||||||
if len(packet.Data) < 12 {
|
if len(packet.Data) < 12 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse session request
|
// Extract session parameters from request
|
||||||
c.sessionID = binary.LittleEndian.Uint32(packet.Data[4:8])
|
c.sessionID = binary.LittleEndian.Uint32(packet.Data[4:8])
|
||||||
requestedMaxLen := binary.LittleEndian.Uint32(packet.Data[8:12])
|
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.maxLength = requestedMaxLen
|
||||||
|
c.fragmentMgr = NewFragmentManager(requestedMaxLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate encryption key
|
// Generate random encryption key
|
||||||
keyBytes := make([]byte, 4)
|
keyBytes := make([]byte, 4)
|
||||||
rand.Read(keyBytes)
|
rand.Read(keyBytes)
|
||||||
c.key = binary.LittleEndian.Uint32(keyBytes)
|
c.key = binary.LittleEndian.Uint32(keyBytes)
|
||||||
|
|
||||||
// Send session response
|
// Initialize encryption
|
||||||
|
c.crypto.SetKey(keyBytes)
|
||||||
|
|
||||||
c.sendSessionResponse()
|
c.sendSessionResponse()
|
||||||
c.state = StateEstablished
|
c.state = StateEstablished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleSessionResponse processes server session response (client-side)
|
||||||
func (c *Connection) handleSessionResponse(packet *ProtocolPacket) {
|
func (c *Connection) handleSessionResponse(packet *ProtocolPacket) {
|
||||||
// Client-side session response handling
|
|
||||||
if len(packet.Data) < 20 {
|
if len(packet.Data) < 20 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract session parameters
|
||||||
c.sessionID = binary.LittleEndian.Uint32(packet.Data[0:4])
|
c.sessionID = binary.LittleEndian.Uint32(packet.Data[0:4])
|
||||||
c.key = binary.LittleEndian.Uint32(packet.Data[4:8])
|
c.key = binary.LittleEndian.Uint32(packet.Data[4:8])
|
||||||
format := packet.Data[9]
|
format := packet.Data[9]
|
||||||
@ -118,9 +157,15 @@ func (c *Connection) handleSessionResponse(packet *ProtocolPacket) {
|
|||||||
c.encoded = (format & 0x04) != 0
|
c.encoded = (format & 0x04) != 0
|
||||||
c.maxLength = binary.LittleEndian.Uint32(packet.Data[12:16])
|
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
|
c.state = StateEstablished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleDataPacket processes reliable data packets with sequence numbers
|
||||||
func (c *Connection) handleDataPacket(packet *ProtocolPacket) {
|
func (c *Connection) handleDataPacket(packet *ProtocolPacket) {
|
||||||
if len(packet.Data) < 2 {
|
if len(packet.Data) < 2 {
|
||||||
return
|
return
|
||||||
@ -129,35 +174,149 @@ func (c *Connection) handleDataPacket(packet *ProtocolPacket) {
|
|||||||
seq := binary.BigEndian.Uint16(packet.Data[0:2])
|
seq := binary.BigEndian.Uint16(packet.Data[0:2])
|
||||||
payload := packet.Data[2:]
|
payload := packet.Data[2:]
|
||||||
|
|
||||||
// Simple in-order processing for now
|
|
||||||
if seq == c.nextInSeq {
|
if seq == c.nextInSeq {
|
||||||
c.nextInSeq++
|
// In-order packet - process immediately
|
||||||
c.sendAck(seq)
|
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
|
// processInOrderPacket handles packets received in correct sequence order
|
||||||
if appPacket, err := c.processApplicationData(payload); err == nil {
|
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)
|
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) {
|
func (c *Connection) handleAck(packet *ProtocolPacket) {
|
||||||
if len(packet.Data) < 2 {
|
if len(packet.Data) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
seq := binary.BigEndian.Uint16(packet.Data[0:2])
|
seq := binary.BigEndian.Uint16(packet.Data[0:2])
|
||||||
// Remove acknowledged packets from retransmit queue
|
c.retransmitQueue.Acknowledge(seq)
|
||||||
_ = seq // TODO: implement retransmit queue
|
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) {
|
func (c *Connection) processApplicationData(data []byte) (*ApplicationPacket, error) {
|
||||||
// Decrypt if needed
|
// Decrypt if encryption is enabled
|
||||||
if c.crypto.IsEncrypted() {
|
if c.crypto.IsEncrypted() {
|
||||||
data = c.crypto.Decrypt(data)
|
data = c.crypto.Decrypt(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decompress if needed
|
// Decompress if compression is enabled
|
||||||
if c.compressed && len(data) > 0 {
|
if c.compressed && len(data) > 0 {
|
||||||
var err error
|
var err error
|
||||||
data, err = Decompress(data)
|
data, err = Decompress(data)
|
||||||
@ -169,38 +328,18 @@ func (c *Connection) processApplicationData(data []byte) (*ApplicationPacket, er
|
|||||||
return ParseApplicationPacket(data)
|
return ParseApplicationPacket(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) SendPacket(packet *ApplicationPacket) {
|
// sendProtocolPacketWithRetransmit sends a packet and adds it to retransmit queue if needed
|
||||||
c.mutex.Lock()
|
func (c *Connection) sendProtocolPacketWithRetransmit(packet *ProtocolPacket) {
|
||||||
defer c.mutex.Unlock()
|
// Add reliable packets to retransmit queue
|
||||||
|
if packet.Opcode == opcodes.OpPacket || packet.Opcode == opcodes.OpFragment {
|
||||||
data := packet.Serialize()
|
c.retransmitQueue.Add(packet, c.nextOutSeq)
|
||||||
|
c.nextOutSeq++
|
||||||
// Compress if needed
|
|
||||||
if c.compressed && len(data) > 128 {
|
|
||||||
if compressed, err := Compress(data); err == nil {
|
|
||||||
data = compressed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt if needed
|
c.sendProtocolPacket(packet)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendSessionResponse sends session establishment response to client
|
||||||
func (c *Connection) sendSessionResponse() {
|
func (c *Connection) sendSessionResponse() {
|
||||||
data := make([]byte, 20)
|
data := make([]byte, 20)
|
||||||
binary.LittleEndian.PutUint32(data[0:4], c.sessionID)
|
binary.LittleEndian.PutUint32(data[0:4], c.sessionID)
|
||||||
@ -221,39 +360,61 @@ func (c *Connection) sendSessionResponse() {
|
|||||||
binary.LittleEndian.PutUint32(data[16:20], 0) // UnknownD
|
binary.LittleEndian.PutUint32(data[16:20], 0) // UnknownD
|
||||||
|
|
||||||
packet := &ProtocolPacket{
|
packet := &ProtocolPacket{
|
||||||
Opcode: OpSessionResponse,
|
Opcode: opcodes.OpSessionResponse,
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.sendProtocolPacket(packet)
|
c.sendProtocolPacket(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendAck sends acknowledgment for received packet
|
||||||
func (c *Connection) sendAck(seq uint16) {
|
func (c *Connection) sendAck(seq uint16) {
|
||||||
data := make([]byte, 2)
|
data := make([]byte, 2)
|
||||||
binary.BigEndian.PutUint16(data, seq)
|
binary.BigEndian.PutUint16(data, seq)
|
||||||
|
|
||||||
packet := &ProtocolPacket{
|
packet := &ProtocolPacket{
|
||||||
Opcode: OpAck,
|
Opcode: opcodes.OpAck,
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.sendProtocolPacket(packet)
|
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() {
|
func (c *Connection) sendKeepAlive() {
|
||||||
packet := &ProtocolPacket{
|
packet := &ProtocolPacket{
|
||||||
Opcode: OpKeepAlive,
|
Opcode: opcodes.OpKeepAlive,
|
||||||
Data: []byte{},
|
Data: []byte{},
|
||||||
}
|
}
|
||||||
|
|
||||||
c.sendProtocolPacket(packet)
|
c.sendProtocolPacket(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendProtocolPacket sends a protocol packet over UDP
|
||||||
func (c *Connection) sendProtocolPacket(packet *ProtocolPacket) {
|
func (c *Connection) sendProtocolPacket(packet *ProtocolPacket) {
|
||||||
data := packet.Serialize()
|
data := packet.Serialize()
|
||||||
c.conn.WriteToUDP(data, c.addr)
|
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() {
|
func (c *Connection) Close() {
|
||||||
c.mutex.Lock()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
@ -261,14 +422,14 @@ func (c *Connection) Close() {
|
|||||||
if c.state == StateEstablished {
|
if c.state == StateEstablished {
|
||||||
c.state = StateClosing
|
c.state = StateClosing
|
||||||
|
|
||||||
// Send disconnect
|
// Send disconnect packet
|
||||||
disconnectData := make([]byte, 6)
|
disconnectData := make([]byte, 6)
|
||||||
binary.LittleEndian.PutUint32(disconnectData[0:4], c.sessionID)
|
binary.LittleEndian.PutUint32(disconnectData[0:4], c.sessionID)
|
||||||
disconnectData[4] = 0
|
disconnectData[4] = 0
|
||||||
disconnectData[5] = 6
|
disconnectData[5] = 6
|
||||||
|
|
||||||
packet := &ProtocolPacket{
|
packet := &ProtocolPacket{
|
||||||
Opcode: OpSessionDisconnect,
|
Opcode: opcodes.OpSessionDisconnect,
|
||||||
Data: disconnectData,
|
Data: disconnectData,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,4 +437,45 @@ func (c *Connection) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.state = StateClosed
|
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
|
||||||
}
|
}
|
||||||
|
73
internal/udp/crc.go
Normal file
73
internal/udp/crc.go
Normal file
@ -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
|
||||||
|
}
|
@ -2,27 +2,49 @@ package udp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rc4"
|
"crypto/rc4"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Crypto handles RC4 encryption/decryption for EQ2EMu protocol
|
||||||
type Crypto struct {
|
type Crypto struct {
|
||||||
cipher *rc4.Cipher
|
clientCipher *rc4.Cipher // Cipher for decrypting client data
|
||||||
key []byte
|
serverCipher *rc4.Cipher // Cipher for encrypting server data
|
||||||
encrypted bool
|
key []byte // Encryption key
|
||||||
|
encrypted bool // Whether encryption is active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCrypto creates a new crypto instance with encryption disabled
|
||||||
func NewCrypto() *Crypto {
|
func NewCrypto() *Crypto {
|
||||||
return &Crypto{
|
return &Crypto{
|
||||||
encrypted: false,
|
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 {
|
func (c *Crypto) SetKey(key []byte) error {
|
||||||
cipher, err := rc4.NewCipher(key)
|
if len(key) == 0 {
|
||||||
if err != nil {
|
return fmt.Errorf("encryption key cannot be empty")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
c.key = make([]byte, len(key))
|
||||||
copy(c.key, key)
|
copy(c.key, key)
|
||||||
c.encrypted = true
|
c.encrypted = true
|
||||||
@ -30,28 +52,58 @@ func (c *Crypto) SetKey(key []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEncrypted returns whether encryption is currently active
|
||||||
func (c *Crypto) IsEncrypted() bool {
|
func (c *Crypto) IsEncrypted() bool {
|
||||||
return c.encrypted
|
return c.encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts data for transmission to client
|
||||||
func (c *Crypto) Encrypt(data []byte) []byte {
|
func (c *Crypto) Encrypt(data []byte) []byte {
|
||||||
if !c.encrypted {
|
if !c.encrypted || c.serverCipher == nil {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
encrypted := make([]byte, len(data))
|
encrypted := make([]byte, len(data))
|
||||||
copy(encrypted, data)
|
copy(encrypted, data)
|
||||||
c.cipher.XORKeyStream(encrypted, encrypted)
|
c.serverCipher.XORKeyStream(encrypted, encrypted)
|
||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data received from client
|
||||||
func (c *Crypto) Decrypt(data []byte) []byte {
|
func (c *Crypto) Decrypt(data []byte) []byte {
|
||||||
if !c.encrypted {
|
if !c.encrypted || c.clientCipher == nil {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypted := make([]byte, len(data))
|
decrypted := make([]byte, len(data))
|
||||||
copy(decrypted, data)
|
copy(decrypted, data)
|
||||||
c.cipher.XORKeyStream(decrypted, decrypted)
|
c.clientCipher.XORKeyStream(decrypted, decrypted)
|
||||||
return 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
|
||||||
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
211
internal/udp/fragment.go
Normal file
211
internal/udp/fragment.go
Normal file
@ -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
|
||||||
|
}
|
@ -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
|
|
||||||
)
|
|
@ -2,53 +2,135 @@ package udp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"eq2emu/internal/opcodes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ProtocolPacket represents a low-level UDP protocol packet with opcode and payload
|
||||||
type ProtocolPacket struct {
|
type ProtocolPacket struct {
|
||||||
Opcode uint8
|
Opcode uint8 // Protocol operation code (1-2 bytes when serialized)
|
||||||
Data []byte
|
Data []byte // Packet payload data
|
||||||
|
Raw []byte // Original raw packet data for debugging
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApplicationPacket represents a higher-level game application packet
|
||||||
type ApplicationPacket struct {
|
type ApplicationPacket struct {
|
||||||
Opcode uint16
|
Opcode uint16 // Application-level operation code
|
||||||
Data []byte
|
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) {
|
func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
|
||||||
if len(data) < 2 {
|
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{
|
return &ProtocolPacket{
|
||||||
Opcode: data[1],
|
Opcode: opcode,
|
||||||
Data: data[2:],
|
Data: payload,
|
||||||
|
Raw: data,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialize converts ProtocolPacket back to wire format with proper opcode encoding and CRC
|
||||||
func (p *ProtocolPacket) Serialize() []byte {
|
func (p *ProtocolPacket) Serialize() []byte {
|
||||||
data := make([]byte, 2+len(p.Data))
|
var result []byte
|
||||||
data[0] = 0x00 // Reserved byte
|
|
||||||
data[1] = p.Opcode
|
|
||||||
copy(data[2:], p.Data)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseApplicationPacket(data []byte) (*ApplicationPacket, error) {
|
// Handle variable opcode encoding
|
||||||
if len(data) < 2 {
|
if p.Opcode >= 0xFF {
|
||||||
return nil, errors.New("application packet too small")
|
// 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])
|
opcode := binary.LittleEndian.Uint16(data[0:2])
|
||||||
|
|
||||||
return &ApplicationPacket{
|
return &ApplicationPacket{
|
||||||
Opcode: opcode,
|
Opcode: opcode,
|
||||||
Data: data[2:],
|
Data: data[2:],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialize converts ApplicationPacket to byte array for transmission
|
||||||
func (p *ApplicationPacket) Serialize() []byte {
|
func (p *ApplicationPacket) Serialize() []byte {
|
||||||
data := make([]byte, 2+len(p.Data))
|
result := make([]byte, 2+len(p.Data))
|
||||||
binary.LittleEndian.PutUint16(data[0:2], p.Opcode)
|
binary.LittleEndian.PutUint16(result[0:2], p.Opcode)
|
||||||
copy(data[2:], p.Data)
|
copy(result[2:], p.Data)
|
||||||
return 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
190
internal/udp/retransmit.go
Normal file
190
internal/udp/retransmit.go
Normal file
@ -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
|
||||||
|
}
|
@ -7,44 +7,81 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Server manages multiple UDP connections and handles packet routing
|
||||||
type Server struct {
|
type Server struct {
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn // Main UDP socket
|
||||||
connections map[string]*Connection
|
connections map[string]*Connection // Active connections by address
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex // Protects connections map
|
||||||
handler PacketHandler
|
handler PacketHandler // Application packet handler
|
||||||
running bool
|
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 {
|
type PacketHandler interface {
|
||||||
HandlePacket(conn *Connection, packet *ApplicationPacket)
|
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) {
|
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)
|
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("invalid UDP address %s: %w", addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.ListenUDP("udp", udpAddr)
|
conn, err := net.ListenUDP("udp", udpAddr)
|
||||||
if err != nil {
|
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{
|
return &Server{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
connections: make(map[string]*Connection),
|
connections: make(map[string]*Connection),
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
maxConnections: config.MaxConnections,
|
||||||
|
timeout: config.Timeout,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start begins accepting and processing UDP packets
|
||||||
func (s *Server) Start() error {
|
func (s *Server) Start() error {
|
||||||
s.running = true
|
s.running = true
|
||||||
|
|
||||||
// Start connection timeout checker
|
// Start background management tasks
|
||||||
go s.timeoutChecker()
|
go s.connectionManager()
|
||||||
|
|
||||||
// Main packet receive loop
|
// Main packet receive loop
|
||||||
buffer := make([]byte, 2048)
|
buffer := make([]byte, 8192)
|
||||||
for s.running {
|
for s.running {
|
||||||
n, addr, err := s.conn.ReadFromUDP(buffer)
|
n, addr, err := s.conn.ReadFromUDP(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -54,36 +91,59 @@ func (s *Server) Start() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.handlePacket(buffer[:n], addr)
|
// Handle packet in separate goroutine to avoid blocking
|
||||||
|
go s.handleIncomingPacket(buffer[:n], addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down the server
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
s.running = false
|
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()
|
s.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handlePacket(data []byte, addr *net.UDPAddr) {
|
// handleIncomingPacket processes a single UDP packet
|
||||||
if len(data) < 2 {
|
func (s *Server) handleIncomingPacket(data []byte, addr *net.UDPAddr) {
|
||||||
|
if len(data) < 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
connKey := addr.String()
|
connKey := addr.String()
|
||||||
|
|
||||||
|
// Find or create connection
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
conn, exists := s.connections[connKey]
|
conn, exists := s.connections[connKey]
|
||||||
if !exists {
|
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 = NewConnection(addr, s.conn, s.handler)
|
||||||
|
conn.StartRetransmitLoop()
|
||||||
s.connections[connKey] = conn
|
s.connections[connKey] = conn
|
||||||
}
|
}
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
// Process packet
|
||||||
conn.ProcessPacket(data)
|
conn.ProcessPacket(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) timeoutChecker() {
|
// connectionManager handles connection cleanup and maintenance
|
||||||
|
func (s *Server) connectionManager() {
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@ -92,14 +152,104 @@ func (s *Server) timeoutChecker() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
// Clean up timed out connections
|
||||||
s.mutex.Lock()
|
s.cleanupTimedOutConnections()
|
||||||
for key, conn := range s.connections {
|
|
||||||
if now.Sub(conn.lastPacketTime) > 45*time.Second {
|
|
||||||
conn.Close()
|
|
||||||
delete(s.connections, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mutex.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
422
internal/udp/udp_test.go
Normal file
422
internal/udp/udp_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user