From 44b0b86a2e59fb1414aca3ed687b272dd98e517c Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 2 Sep 2025 09:41:39 -0500 Subject: [PATCH] reset --- EQPACKET.md => DOCS/EQPACKET.md | 0 EQSTREAM.md => DOCS/EQSTREAM.md | 0 LOGIN.md | 817 -------------------------------- compression.go | 209 -------- crc16.go | 93 ---- go.mod | 1 + go.sum | 5 + opcodes.go | 272 ----------- opcodes_db_example.go | 252 ---------- opcodes_test.go | 263 ---------- packet.go | 299 ------------ packet_test.go | 207 -------- server.go | 407 ---------------- stream.go | 567 ---------------------- stream_packet_handler.go | 506 -------------------- stream_test.go | 194 -------- 16 files changed, 6 insertions(+), 4086 deletions(-) rename EQPACKET.md => DOCS/EQPACKET.md (100%) rename EQSTREAM.md => DOCS/EQSTREAM.md (100%) delete mode 100644 LOGIN.md delete mode 100644 compression.go delete mode 100644 crc16.go delete mode 100644 opcodes.go delete mode 100644 opcodes_db_example.go delete mode 100644 opcodes_test.go delete mode 100644 packet.go delete mode 100644 packet_test.go delete mode 100644 server.go delete mode 100644 stream.go delete mode 100644 stream_packet_handler.go delete mode 100644 stream_test.go diff --git a/EQPACKET.md b/DOCS/EQPACKET.md similarity index 100% rename from EQPACKET.md rename to DOCS/EQPACKET.md diff --git a/EQSTREAM.md b/DOCS/EQSTREAM.md similarity index 100% rename from EQSTREAM.md rename to DOCS/EQSTREAM.md diff --git a/LOGIN.md b/LOGIN.md deleted file mode 100644 index 600935e..0000000 --- a/LOGIN.md +++ /dev/null @@ -1,817 +0,0 @@ -# EverQuest II Login Server - Complete Technical Documentation - -## Table of Contents -1. [Overview](#overview) -2. [Architecture](#architecture) -3. [Network Protocol Stack](#network-protocol-stack) -4. [Encryption and Security](#encryption-and-security) -5. [Packet Structure](#packet-structure) -6. [Complete Login Flow](#complete-login-flow) -7. [Character Management](#character-management) -8. [World Server Communication](#world-server-communication) -9. [Error Handling](#error-handling) -10. [Database Operations](#database-operations) - -## Overview - -The EverQuest II Login Server is responsible for: -- Client authentication and authorization -- World server discovery and status management -- Character listing and management -- Secure handoff to world servers -- Account management and creation - -### Key Components -- **NetConnection**: Main network manager -- **Client**: Individual client connection handler -- **ClientList**: Thread-safe client container -- **LWorld**: World server representation -- **LWorldList**: World server manager -- **LoginDatabase**: Database interface -- **EQStream**: Network stream handler with encryption - -## Architecture - -``` -┌─────────────┐ UDP ┌──────────────┐ -│ EQ2 Client ├─────────────────►│ Login Server │ -└─────────────┘ └──────┬───────┘ - │ TCP - ▼ - ┌──────────────────┐ - │ World Servers │ - └──────────────────┘ -``` - -### Port Configuration -- **Default Login Port**: 5999 (UDP for game clients) -- **Web API Port**: Configurable (TCP/HTTPS) -- **World Server Communication**: TCP (internal) -- **World Server Game Port**: Dynamic per world (UDP for game clients) - -## Network Protocol Stack - -### Layer Structure -``` -Application Layer: [Game Packets] -Presentation Layer: [PacketStruct Serialization] -Session Layer: [EQStream with RC4/CRC] -Transport Layer: [UDP Datagram Protocol] -Network Layer: [IP Protocol] -``` - -**Note**: EQStream implements its own reliability layer on top of UDP, providing: -- Sequence numbers for ordering -- Acknowledgments for reliability -- Retransmission for lost packets -- This gives the benefits of TCP while maintaining lower latency - -### EQStream Protocol - -The EQStream protocol provides: -1. **Sequencing**: Packet ordering and acknowledgment -2. **Fragmentation**: Large packet splitting -3. **Compression**: zlib compression for efficiency -4. **Encryption**: RC4 stream cipher -5. **Integrity**: CRC32 checksums - -#### Packet Types -```cpp -enum EQStreamOp { - OP_SessionRequest = 0x0001, // Initial connection - OP_SessionResponse = 0x0002, // Server accepts connection - OP_Combined = 0x0003, // Multiple packets in one - OP_SessionDisconnect = 0x0005, // Connection termination - OP_KeepAlive = 0x0006, // Connection heartbeat - OP_SessionStatRequest = 0x0007, // Statistics request - OP_SessionStatResponse = 0x0008,// Statistics response - OP_Packet = 0x0009, // Application data - OP_Fragment = 0x000d, // Fragmented packet piece - OP_Ack = 0x0015, // Acknowledgment - OP_AckFuture = 0x0016, // Future acknowledgment - OP_AckPast = 0x0017 // Past acknowledgment -}; -``` - -## Encryption and Security - -### RC4 Encryption - -The login server uses RC4 stream cipher for packet encryption: - -1. **Key Exchange**: - ```cpp - // Initial session setup - struct SessionRequest { - uint32_t unknown; // Protocol version - uint32_t session_id; // Client session ID - uint32_t max_length; // Max packet size - }; - - struct SessionResponse { - uint32_t session_id; // Server session ID - uint32_t key; // RC4 key seed - uint8_t crc_length; // CRC bytes (2) - uint8_t compression; // Compression flag - uint32_t unknown; // Reserved - uint32_t max_length; // Max packet size - }; - ``` - -2. **RC4 Key Generation**: - ```cpp - // Key is derived from session response - void GenerateRC4Key(uint32_t key_seed) { - // Initialize RC4 state with key_seed - RC4_KEY encrypt_key, decrypt_key; - unsigned char key_bytes[4]; - memcpy(key_bytes, &key_seed, 4); - - // Setup encryption/decryption keys - RC4_set_key(&encrypt_key, 4, key_bytes); - RC4_set_key(&decrypt_key, 4, key_bytes); - } - ``` - -3. **Packet Encryption**: - ```cpp - void EncryptPacket(uint8_t* data, size_t length) { - // Skip protocol header (2 bytes) - RC4(&encrypt_key, length - 2, data + 2, data + 2); - } - ``` - -### CRC32 Integrity Check - -Every packet includes a CRC32 checksum: - -```cpp -struct EQProtocolPacket { - uint16_t opcode; // Operation code - uint8_t data[]; // Packet data - uint32_t crc32; // CRC32 of opcode + data -}; - -uint32_t CalculateCRC(const uint8_t* data, size_t length) { - // Standard CRC32 calculation - return crc32(0, data, length); -} -``` - -### CRC Verification Process: -1. Extract CRC from packet end -2. Calculate CRC of packet data -3. Compare calculated vs received CRC -4. Reject packet if mismatch - -## Packet Structure - -### Base Packet Header -```cpp -struct EQ2Packet { - uint16_t opcode; // Operation identifier - uint16_t sequence; // Packet sequence number - uint32_t size; // Payload size - uint8_t compressed; // Compression flag - uint8_t data[]; // Actual payload -}; -``` - -### Application Packet Structure -```cpp -struct EQApplicationPacket { - uint16_t opcode; // Game operation code - uint32_t version; // Client version - uint8_t data[]; // Serialized data -}; -``` - -## Complete Login Flow - -### Phase 1: Connection Establishment - -1. **Client Initiates UDP Communication**: - ``` - Client → Server: UDP Datagram with OP_SessionRequest - (No TCP handshake - connectionless protocol) - ``` - -2. **EQStream Session**: - ``` - Client → Server: OP_SessionRequest - { - protocol_version: 0x0003 - session_id: random() - max_packet_size: 512 - } - - Server → Client: OP_SessionResponse - { - session_id: server_session - rc4_key: generated_key - crc_length: 2 - compression: true - max_packet_size: 512 - } - ``` - -3. **Encryption Initialization**: - - Both sides initialize RC4 with the shared key - - All subsequent packets are encrypted - -### Phase 2: Authentication - -1. **Login Request**: - ```cpp - Client → Server: OP_LoginRequestMsg - { - version: client_version // e.g., 1208 - username: "account_name" // EQ2_16BitString - password: "hashed_password" // EQ2_16BitString - unknown3: cl_eqversion // From eq2_defaults.ini - } - ``` - -2. **Version Validation**: - ```cpp - // Server checks version compatibility - if (!EQOpcodeManager.count(GetOpcodeVersion(version))) { - // Send incompatible version error - SendLoginDeniedBadVersion(); - return; - } - ``` - -3. **Account Verification**: - ```cpp - // Database lookup - LoginAccount* acct = database.LoadAccount(username, password); - - // Check for duplicate login - Client* existing = client_list.FindByLSID(acct->getLoginAccountID()); - if (existing) { - existing->getConnection()->SendDisconnect(); - } - - // Update account info - database.UpdateAccountIPAddress(acct->id, client_ip); - database.UpdateAccountClientDataVersion(acct->id, version); - ``` - -4. **Login Response**: - ```cpp - Server → Client: OP_LoginReplyMsg - { - account_id: account_id - login_response: 0 // 0 = success - sub_level: 0xFFFFFFFF // Subscription level - race_flag: 0x1FFFFF // Available races - class_flag: 0x7FFFFFE // Available classes - expansion_flag: 0x7CFF // Enabled expansions - cities_flag: 0xFF // Starting cities - enabled_races: 0xFFFF // Race availability - } - ``` - -### Phase 3: World List Request - -1. **Client Requests World List**: - ```cpp - Client → Server: OP_AllWSDescRequestMsg - {} // Empty request - ``` - -2. **Server Builds World List**: - ```cpp - Server → Client: OP_AllWSDescReplyMsg - { - num_worlds: count - worlds[]: { - world_id: id - world_name: "ServerName" - world_status: 1 // 0=down, 1=up, 2=locked - num_players: current - max_players: maximum - language: "en" - recommended: false - } - } - ``` - -3. **World Status Updates**: - - Server sends periodic updates (every 10 seconds) - - Updates include player counts and status changes - -### Phase 4: Character List - -1. **Character Loading**: - ```cpp - // Server loads characters from database - database.LoadCharacters(GetLoginAccount(), GetVersion()); - ``` - -2. **Character List Response**: - ```cpp - Server → Client: LS_CharSelectList - { - account_id: account_id - num_characters: count - characters[]: { - char_id: database_id - server_id: world_id - name: "CharacterName" - race: race_id - class: class_id - level: current_level - zone: "ZoneName" - gender: 0/1 - deity: deity_id - body_size: size - body_age: age - soga_race_type: soga_id - x: position_x - y: position_y - z: position_z - } - } - ``` - -### Phase 5: Character Creation - -1. **Creation Request**: - ```cpp - Client → Server: OP_CreateCharacterRequestMsg - { - server_id: target_world - name: "NewCharacter" - race: selected_race - class: selected_class - gender: 0/1 - deity: selected_deity - body_size: size_value - body_age: age_value - // Appearance data... - } - ``` - -2. **Forward to World Server**: - ```cpp - Login → World: ServerOP_CharacterCreate - { - version: client_version - account_id: account_id - [original_request_data] - } - ``` - -3. **World Server Validation**: - - Check name availability - - Validate race/class combination - - Check character limits - -4. **Creation Response**: - ```cpp - World → Login: ServerOP_CharacterCreateResponse - { - success: true/false - char_id: new_character_id // If successful - reason: error_code // If failed - } - - Login → Client: OP_CreateCharacterReplyMsg - { - response: CREATESUCCESS_REPLY or error_code - name: "NewCharacter" - account_id: account_id - } - ``` - -### Phase 6: World Entry - -1. **Play Request**: - ```cpp - Client → Server: OP_PlayCharacterRequestMsg - { - char_id: selected_character - server_id: world_server // Version > 283 - name: "CharacterName" // Version <= 283 - } - ``` - -2. **World Server Handoff**: - ```cpp - Login → World: ServerOP_UsertoWorldReq - { - char_id: character_id - lsaccountid: account_id - worldid: server_id - ip_address: client_ip - } - ``` - -3. **World Server Response**: - ```cpp - World → Login: ServerOP_UsertoWorldResp - { - worldid: server_id - response: 1 // 1 = success - ip_address: world_ip - port: world_port - access_key: session_key // For authentication - } - ``` - -4. **Client Redirect**: - ```cpp - Login → Client: OP_PlayCharacterReplyMsg - { - response: 1 // Success - server: "world.ip.address" - port: world_port - account_id: account_id - access_code: session_key - } - ``` - -5. **Client Connects to World**: - - Client disconnects from login server - - Connects to world server using provided IP:port - - Authenticates using access_key - -## Character Management - -### Character Deletion - -1. **Delete Request**: - ```cpp - Client → Server: OP_DeleteCharacterRequestMsg - { - char_id: character_id - server_id: world_id - name: "CharacterName" - } - ``` - -2. **Verification**: - ```cpp - // Verify ownership - bool valid = database.VerifyDelete(account_id, char_id, name); - ``` - -3. **World Server Notification**: - ```cpp - Login → World: ServerOP_DeleteCharacter - { - char_id: character_id - account_id: account_id - } - ``` - -4. **Delete Response**: - ```cpp - Server → Client: OP_DeleteCharacterReplyMsg - { - response: 1 // 1 = success - char_id: deleted_id - server_id: world_id - name: "CharacterName" - } - ``` - -## World Server Communication - -### Inter-Server Protocol - -1. **World Server Registration**: - ```cpp - World → Login: ServerOP_LSInfo - { - name: "WorldName" - address: "public_ip" - port: game_port - admin_port: admin_port - status: 1 // 0=down, 1=up, 2=locked - players: current_count - max_players: maximum - } - ``` - -2. **Heartbeat**: - ```cpp - World → Login: ServerOP_LSStatus - { - status: current_status - players: current_count - zones: active_zones - } - // Sent every 30 seconds - ``` - -3. **Player Updates**: - ```cpp - World → Login: ServerOP_UsertoWorldResp - { - lsaccountid: account_id - worldid: server_id - response: status - // 0 = rejected - // 1 = accepted - // -1 = world full - // -2 = character not found - // -3 = world locked - } - ``` - -## Error Handling - -### Login Error Codes -```cpp -enum LoginResponseCodes { - LOGIN_SUCCESS = 0, - LOGIN_BADPASS = 1, // Invalid username/password - LOGIN_BADVERSION = 6, // Client version mismatch - LOGIN_SUSPENDED = 7, // Account suspended - LOGIN_BANNED = 9, // Account banned - LOGIN_WORLDFULL = 10, // World at capacity - LOGIN_DISCONNECT = 100 // Generic disconnect -}; -``` - -### Play Error Codes -```cpp -enum PlayResponseCodes { - PLAY_SUCCESS = 1, - PLAY_ERROR_PROBLEM = 2, // Generic error - PLAY_ERROR_WORLDFULL = 3, // World full - PLAY_ERROR_LOCKED = 4, // World locked - PLAY_ERROR_BANNED = 5, // Character banned - PLAY_ERROR_SUSPENDED = 6, // Character suspended - PLAY_ERROR_SERVER_TIMEOUT = 7, // Timeout waiting for world - PLAY_ERROR_CHAR_NOT_LOADED = 8 // Character data issue -}; -``` - -### Character Creation Error Codes -```cpp -enum CreateResponseCodes { - CREATESUCCESS_REPLY = 1, - INVALIDRACE_REPLY = 2, // Invalid race selection - INVALIDGENDER_REPLY = 3, // Invalid gender - BADNAMELENGTH_REPLY = 9, // Name too short/long - NAMEINVALID_REPLY = 10, // Invalid characters - NAMEFILTER_REPLY = 11, // Profanity filter - NAMETAKEN_REPLY = 12, // Name already exists - OVERLOADEDSERVER_REPLY = 13, // Server overloaded - UNKNOWNERROR_REPLY = 20 // Generic error -}; -``` - -## Database Operations - -### Account Management - -1. **Account Loading**: - ```sql - SELECT id, name, password, suspended, banned - FROM login_accounts - WHERE name = ? AND password = SHA2(?, 256) - ``` - -2. **Account Creation** (if enabled): - ```sql - INSERT INTO login_accounts (name, password, created) - VALUES (?, SHA2(?, 256), NOW()) - ``` - -3. **IP Address Update**: - ```sql - UPDATE login_accounts - SET last_ip = ?, last_login = NOW() - WHERE id = ? - ``` - -### Character Operations - -1. **Character List**: - ```sql - SELECT c.id, c.name, c.server_id, c.level, c.race, - c.class, c.zone, c.gender, c.deity - FROM characters c - WHERE c.account_id = ? AND c.deleted = 0 - ORDER BY c.server_id, c.name - ``` - -2. **Character Creation**: - ```sql - INSERT INTO characters - (account_id, server_id, name, race, class, gender, - deity, body_size, body_age, created) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) - ``` - -3. **Character Deletion**: - ```sql - UPDATE characters - SET deleted = 1, deleted_date = NOW() - WHERE id = ? AND account_id = ? - ``` - -### World Server Tracking - -1. **World Registration**: - ```sql - INSERT INTO login_worldservers - (name, ip_address, port, status) - VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - ip_address = VALUES(ip_address), - port = VALUES(port), - status = VALUES(status) - ``` - -2. **Status Updates**: - ```sql - UPDATE login_worldservers - SET status = ?, players = ?, last_update = NOW() - WHERE id = ? - ``` - -## Security Considerations - -### Password Handling -- Passwords are hashed using SHA256 before transmission -- Never store plaintext passwords -- Use prepared statements to prevent SQL injection - -### Session Management -- Generate unique session keys for world handoff -- Session keys expire after use or timeout -- IP address validation between login and world entry - -### Rate Limiting -- Limit login attempts per IP -- Throttle character creation requests -- Monitor for abnormal packet patterns - -### Packet Validation -- Verify packet size limits -- Check sequence numbers for replay attacks -- Validate all input data ranges - -## Performance Optimizations - -### Connection Pooling -- Maintain persistent database connections -- Reuse world server connections -- Implement connection timeout and retry logic - -### Caching -- Cache world server status for 10 seconds -- Cache character lists until modified -- Cache opcode mappings per version - -### Threading Model -- Main thread handles network I/O -- Database operations on thread pool -- World server communication async -- Client processing single-threaded per client - -## Monitoring and Logging - -### Key Metrics -- Active client connections -- Login success/failure rates -- World server availability -- Database query performance -- Packet processing latency - -### Log Levels -```cpp -enum LogLevel { - LOGIN__INFO, // General information - LOGIN__DEBUG, // Detailed debugging - LOGIN__ERROR, // Error conditions - LOGIN__WARNING, // Warning conditions - WORLD__INFO, // World server info - WORLD__ERROR, // World server errors - OPCODE__DEBUG, // Packet opcodes - INIT__INFO, // Initialization - INIT__ERROR // Init failures -}; -``` - -### Critical Events to Log -- Failed login attempts -- Account creation -- Character creation/deletion -- World server registration/deregistration -- Network errors and timeouts -- Database connection issues - -## Configuration - -### Main Configuration (config.json) -```json -{ - "loginconfig": { - "serverport": 5999, - "serverip": "", - "accountcreation": 1, - "expansionflag": 32463, - "citiesflag": 255, - "defaultsubscriptionlevel": -1, - "enabledraces": 65535, - "webloginaddress": "0.0.0.0", - "webloginport": 8080, - "webcertfile": "", - "webkeyfile": "", - "webkeypassword": "", - "webhardcodeuser": "admin", - "webhardcodepassword": "password" - } -} -``` - -### Database Configuration -```json -{ - "database": { - "host": "localhost", - "port": 3306, - "username": "eq2login", - "password": "password", - "database": "eq2login", - "max_connections": 10, - "connection_timeout": 5 - } -} -``` - -## Troubleshooting - -### Common Issues - -1. **"Version Mismatch" Error**: - - Client version not in opcode database - - Solution: Update opcodes.sql - -2. **"Cannot Connect to Login Server"**: - - Firewall blocking port 5999 - - Server not running - - Network configuration issue - -3. **"World Server Unavailable"**: - - World server not registered - - World server crashed - - Network issue between login and world - -4. **Character Creation Fails**: - - Name already taken - - Invalid race/class combination - - World server communication timeout - -5. **Cannot Enter World**: - - Session key mismatch - - World server full - - Character data corruption - -### Debug Commands - -The login server supports console commands (Linux): -- `l` - List all connected world servers -- `v` - Display version information -- `h` - Show help menu -- `q` - Quit server gracefully - -## Protocol Evolution - -### Version Differences - -**Version <= 283**: -- Server ID not included in play request -- Character name used for lookup - -**Version <= 561**: -- Auto-enter world after character creation -- Different packet structure alignment - -**Version >= 546**: -- Enhanced error logging format -- Larger packet size support - -**Version >= 1208**: -- New login request structure -- Additional client version field - -## Conclusion - -The EverQuest II login server implements a sophisticated authentication and routing system with multiple layers of security and error handling. The combination of RC4 encryption, CRC32 integrity checking, and session-based authentication provides a robust framework for managing player connections and character data. - -Key design principles: -- **Security First**: All communications encrypted -- **Scalability**: Support for multiple world servers -- **Reliability**: Comprehensive error handling -- **Performance**: Efficient caching and threading -- **Maintainability**: Clear separation of concerns - -The login server acts as the gateway to the game world, ensuring only authenticated players can access their characters while maintaining the integrity and security of the game environment. \ No newline at end of file diff --git a/compression.go b/compression.go deleted file mode 100644 index bd4350f..0000000 --- a/compression.go +++ /dev/null @@ -1,209 +0,0 @@ -package eq2net - -import ( - "bytes" - "compress/zlib" - "encoding/binary" - "io" -) - -const ( - // Compression flags - CompressionFlagZlib = 0x5A // Zlib compression - CompressionFlagSimple = 0xA5 // Simple encoding (no actual compression) - - // Compression threshold - packets larger than this use zlib - CompressionThreshold = 30 -) - -// CompressPacket compresses a packet using zlib or simple encoding -func CompressPacket(data []byte) ([]byte, error) { - if len(data) < 2 { - return data, nil - } - - // Determine opcode size - flagOffset := 1 - if data[0] == 0 { - flagOffset = 2 // Two-byte opcode - } - - // Don't compress if too small - if len(data) <= flagOffset { - return data, nil - } - - result := make([]byte, 0, len(data)+1) - - // Copy opcode bytes - result = append(result, data[:flagOffset]...) - - if len(data) > CompressionThreshold { - // Use zlib compression for larger packets - result = append(result, CompressionFlagZlib) - - // Compress the data after opcode - var compressed bytes.Buffer - w := zlib.NewWriter(&compressed) - if _, err := w.Write(data[flagOffset:]); err != nil { - return nil, err - } - if err := w.Close(); err != nil { - return nil, err - } - - result = append(result, compressed.Bytes()...) - } else { - // Use simple encoding for smaller packets - result = append(result, CompressionFlagSimple) - result = append(result, data[flagOffset:]...) - } - - return result, nil -} - -// DecompressPacket decompresses a packet -func DecompressPacket(data []byte) ([]byte, error) { - if len(data) < 3 { - return data, nil - } - - // Determine opcode size and compression flag position - flagOffset := 1 - if data[0] == 0 { - flagOffset = 2 - } - - if len(data) <= flagOffset { - return data, nil - } - - compressionFlag := data[flagOffset] - - // Check compression type - switch compressionFlag { - case CompressionFlagZlib: - // Zlib decompression - result := make([]byte, 0, len(data)*2) - - // Copy opcode - result = append(result, data[:flagOffset]...) - - // Decompress data (skip flag byte) - r, err := zlib.NewReader(bytes.NewReader(data[flagOffset+1:])) - if err != nil { - return nil, err - } - defer r.Close() - - decompressed, err := io.ReadAll(r) - if err != nil { - return nil, err - } - - result = append(result, decompressed...) - return result, nil - - case CompressionFlagSimple: - // Simple encoding - just remove the flag byte - result := make([]byte, 0, len(data)-1) - result = append(result, data[:flagOffset]...) - result = append(result, data[flagOffset+1:]...) - return result, nil - - default: - // No compression - return data, nil - } -} - -// IsCompressed checks if a packet is compressed -func IsCompressed(data []byte) bool { - if len(data) < 2 { - return false - } - - flagOffset := 1 - if data[0] == 0 { - flagOffset = 2 - } - - if len(data) <= flagOffset { - return false - } - - flag := data[flagOffset] - return flag == CompressionFlagZlib || flag == CompressionFlagSimple -} - -// ChatEncode encodes chat data using XOR encryption with rolling key -func ChatEncode(data []byte, encodeKey uint32) []byte { - // Skip certain packet types - if len(data) >= 2 && (data[1] == 0x01 || data[0] == 0x02 || data[0] == 0x1d) { - return data - } - - // Work with data after opcode - if len(data) <= 2 { - return data - } - - result := make([]byte, len(data)) - copy(result[:2], data[:2]) // Copy opcode - - key := encodeKey - offset := 2 - - // Process 4-byte blocks with rolling key - for i := offset; i+4 <= len(data); i += 4 { - block := binary.LittleEndian.Uint32(data[i : i+4]) - encrypted := block ^ key - binary.LittleEndian.PutUint32(result[i:i+4], encrypted) - key = encrypted // Update key with encrypted data - } - - // Handle remaining bytes - keyByte := byte(key & 0xFF) - alignedEnd := offset + ((len(data)-offset)/4)*4 - for i := alignedEnd; i < len(data); i++ { - result[i] = data[i] ^ keyByte - } - - return result -} - -// ChatDecode decodes chat data using XOR encryption with rolling key -func ChatDecode(data []byte, decodeKey uint32) []byte { - // Skip certain packet types - if len(data) >= 2 && (data[1] == 0x01 || data[0] == 0x02 || data[0] == 0x1d) { - return data - } - - // Work with data after opcode - if len(data) <= 2 { - return data - } - - result := make([]byte, len(data)) - copy(result[:2], data[:2]) // Copy opcode - - key := decodeKey - offset := 2 - - // Process 4-byte blocks with rolling key - for i := offset; i+4 <= len(data); i += 4 { - encrypted := binary.LittleEndian.Uint32(data[i : i+4]) - decrypted := encrypted ^ key - binary.LittleEndian.PutUint32(result[i:i+4], decrypted) - key = encrypted // Update key with encrypted data (before decryption) - } - - // Handle remaining bytes - keyByte := byte(key & 0xFF) - alignedEnd := offset + ((len(data)-offset)/4)*4 - for i := alignedEnd; i < len(data); i++ { - result[i] = data[i] ^ keyByte - } - - return result -} \ No newline at end of file diff --git a/crc16.go b/crc16.go deleted file mode 100644 index 7b7e47f..0000000 --- a/crc16.go +++ /dev/null @@ -1,93 +0,0 @@ -package eq2net - -// CRC16 table for CCITT polynomial (0x1021) -var crc16Table [256]uint16 - -func init() { - // Initialize CRC16 lookup table - for i := 0; i < 256; i++ { - crc := uint16(i << 8) - for j := 0; j < 8; j++ { - if (crc & 0x8000) != 0 { - crc = (crc << 1) ^ 0x1021 - } else { - crc = crc << 1 - } - } - crc16Table[i] = crc - } -} - -// CRC16 calculates the CRC16-CCITT checksum with a key -func CRC16(data []byte, length int, key uint32) uint16 { - if length <= 0 || len(data) < length { - return 0 - } - - // Mix the key into initial CRC value - crc := uint16(0xFFFF) - keyBytes := []byte{ - byte(key), - byte(key >> 8), - byte(key >> 16), - byte(key >> 24), - } - - // Process key bytes first - for _, b := range keyBytes { - tableIndex := (uint8(crc>>8) ^ b) & 0xFF - crc = (crc << 8) ^ crc16Table[tableIndex] - } - - // Process data - for i := 0; i < length; i++ { - tableIndex := (uint8(crc>>8) ^ data[i]) & 0xFF - crc = (crc << 8) ^ crc16Table[tableIndex] - } - - return crc ^ 0xFFFF -} - -// ValidateCRC checks if a packet has a valid CRC -func ValidateCRC(buffer []byte, key uint32) bool { - if len(buffer) < 3 { - return false - } - - // Check for CRC-exempt packets - if len(buffer) >= 2 && buffer[0] == 0x00 { - switch buffer[1] { - case byte(OPSessionRequest), byte(OPSessionResponse), byte(OPOutOfSession): - return true // Session packets don't require CRC - } - } - - // Check for combined application packets (also CRC-exempt) - if len(buffer) >= 4 && buffer[2] == 0x00 && buffer[3] == 0x19 { - return true - } - - // Calculate CRC for the packet (excluding last 2 CRC bytes) - dataLen := len(buffer) - 2 - calculatedCRC := CRC16(buffer, dataLen, key) - - // Extract packet CRC (big-endian in last 2 bytes) - packetCRC := uint16(buffer[dataLen])<<8 | uint16(buffer[dataLen+1]) - - // Valid if no CRC present (0) or CRCs match - return packetCRC == 0 || calculatedCRC == packetCRC -} - -// AppendCRC adds CRC to the end of a packet -func AppendCRC(buffer []byte, key uint32) []byte { - // Calculate CRC for current buffer - crc := CRC16(buffer, len(buffer), key) - - // Append CRC in big-endian format - result := make([]byte, len(buffer)+2) - copy(result, buffer) - result[len(buffer)] = byte(crc >> 8) - result[len(buffer)+1] = byte(crc) - - return result -} \ No newline at end of file diff --git a/go.mod b/go.mod index 4a5d069..811cfba 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21.0 require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/panjf2000/ants/v2 v2.11.3 // indirect github.com/panjf2000/gnet/v2 v2.9.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 1de0f99..7e8fdb9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,12 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/panjf2000/gnet/v2 v2.9.3 h1:auV3/A9Na3jiBDmYAAU00rPhFKnsAI+TnI1F7YUJMHQ= diff --git a/opcodes.go b/opcodes.go deleted file mode 100644 index d6df85d..0000000 --- a/opcodes.go +++ /dev/null @@ -1,272 +0,0 @@ -package eq2net - -import ( - "fmt" - "sync" -) - -// VersionRange represents a client version range mapped to an opcode version -type VersionRange struct { - MinVersion uint16 // version_range1 in database - MaxVersion uint16 // version_range2 in database - OpcodeVersion uint16 // The opcode version for this range (usually MinVersion) -} - -// OpcodeVersionMap maps version ranges for opcode lookups -// This replaces the C++ EQOpcodeVersions map -type OpcodeVersionMap map[uint16]uint16 // Key: version_range1, Value: version_range2 - -// GetOpcodeVersion returns the opcode version for a given client version -// This is a direct port of the C++ GetOpcodeVersion function -func GetOpcodeVersion(clientVersion uint16, versionMap OpcodeVersionMap) uint16 { - ret := clientVersion - - // Iterate through version ranges to find a match - for minVersion, maxVersion := range versionMap { - if clientVersion >= minVersion && clientVersion <= maxVersion { - ret = minVersion - break - } - } - - return ret -} - -// EmuOpcode represents an emulator-side opcode -type EmuOpcode uint16 - -// Common emulator opcodes - these match the C++ emu_opcodes.h -const ( - OP_Unknown EmuOpcode = 0x0000 - OP_LoginRequestMsg EmuOpcode = 0x0001 - OP_LoginByNumRequestMsg EmuOpcode = 0x0002 - OP_WSLoginRequestMsg EmuOpcode = 0x0003 - OP_ESLoginRequestMsg EmuOpcode = 0x0004 - OP_LoginReplyMsg EmuOpcode = 0x0005 - OP_WSStatusReplyMsg EmuOpcode = 0x0006 - OP_WorldListMsg EmuOpcode = 0x0007 - OP_WorldStatusMsg EmuOpcode = 0x0008 - OP_DeleteCharacterRequestMsg EmuOpcode = 0x0009 - OP_DeleteCharacterReplyMsg EmuOpcode = 0x000A - OP_CreateCharacterRequestMsg EmuOpcode = 0x000B - OP_CreateCharacterReplyMsg EmuOpcode = 0x000C - OP_PlayCharacterRequestMsg EmuOpcode = 0x000D - OP_PlayCharacterReplyMsg EmuOpcode = 0x000E - OP_ServerListRequestMsg EmuOpcode = 0x000F - OP_ServerListReplyMsg EmuOpcode = 0x0010 - OP_CharacterListRequestMsg EmuOpcode = 0x0011 - OP_CharacterListReplyMsg EmuOpcode = 0x0012 - // Add more opcodes as needed -) - -// OpcodeNames maps emulator opcodes to their string names -// This matches the C++ OpcodeNames array -var OpcodeNames = map[EmuOpcode]string{ - OP_Unknown: "OP_Unknown", - OP_LoginRequestMsg: "OP_LoginRequestMsg", - OP_LoginByNumRequestMsg: "OP_LoginByNumRequestMsg", - OP_WSLoginRequestMsg: "OP_WSLoginRequestMsg", - OP_ESLoginRequestMsg: "OP_ESLoginRequestMsg", - OP_LoginReplyMsg: "OP_LoginReplyMsg", - OP_WSStatusReplyMsg: "OP_WSStatusReplyMsg", - OP_WorldListMsg: "OP_WorldListMsg", - OP_WorldStatusMsg: "OP_WorldStatusMsg", - OP_DeleteCharacterRequestMsg: "OP_DeleteCharacterRequestMsg", - OP_DeleteCharacterReplyMsg: "OP_DeleteCharacterReplyMsg", - OP_CreateCharacterRequestMsg: "OP_CreateCharacterRequestMsg", - OP_CreateCharacterReplyMsg: "OP_CreateCharacterReplyMsg", - OP_PlayCharacterRequestMsg: "OP_PlayCharacterRequestMsg", - OP_PlayCharacterReplyMsg: "OP_PlayCharacterReplyMsg", - OP_ServerListRequestMsg: "OP_ServerListRequestMsg", - OP_ServerListReplyMsg: "OP_ServerListReplyMsg", - OP_CharacterListRequestMsg: "OP_CharacterListRequestMsg", - OP_CharacterListReplyMsg: "OP_CharacterListReplyMsg", -} - -// RegularOpcodeManager manages opcode mappings for a specific version -// This is equivalent to the C++ RegularOpcodeManager class -type RegularOpcodeManager struct { - version uint16 - emuToEQ map[EmuOpcode]uint16 - eqToEmu map[uint16]EmuOpcode - mu sync.RWMutex -} - -// NewRegularOpcodeManager creates a new opcode manager -func NewRegularOpcodeManager(version uint16) *RegularOpcodeManager { - return &RegularOpcodeManager{ - version: version, - emuToEQ: make(map[EmuOpcode]uint16), - eqToEmu: make(map[uint16]EmuOpcode), - } -} - -// LoadOpcodes loads opcode mappings from a map -// Input format matches database: map[opcode_name]opcode_value -func (om *RegularOpcodeManager) LoadOpcodes(opcodes map[string]uint16) bool { - om.mu.Lock() - defer om.mu.Unlock() - - // Clear existing mappings - om.emuToEQ = make(map[EmuOpcode]uint16) - om.eqToEmu = make(map[uint16]EmuOpcode) - - // Build bidirectional mappings - for name, eqOpcode := range opcodes { - // Find the emulator opcode by name - var emuOpcode EmuOpcode = OP_Unknown - for emu, opcName := range OpcodeNames { - if opcName == name { - emuOpcode = emu - break - } - } - - if emuOpcode != OP_Unknown { - om.emuToEQ[emuOpcode] = eqOpcode - om.eqToEmu[eqOpcode] = emuOpcode - } - } - - return true -} - -// EmuToEQ converts an emulator opcode to EQ network opcode -func (om *RegularOpcodeManager) EmuToEQ(emu EmuOpcode) uint16 { - om.mu.RLock() - defer om.mu.RUnlock() - - if eq, exists := om.emuToEQ[emu]; exists { - return eq - } - return 0xCDCD // Invalid opcode marker (matches C++) -} - -// EQToEmu converts an EQ network opcode to emulator opcode -func (om *RegularOpcodeManager) EQToEmu(eq uint16) EmuOpcode { - om.mu.RLock() - defer om.mu.RUnlock() - - if emu, exists := om.eqToEmu[eq]; exists { - return emu - } - return OP_Unknown -} - -// EmuToName returns the name of an emulator opcode -func (om *RegularOpcodeManager) EmuToName(emu EmuOpcode) string { - if name, exists := OpcodeNames[emu]; exists { - return name - } - return "OP_Unknown" -} - -// EQToName returns the name of an EQ network opcode -func (om *RegularOpcodeManager) EQToName(eq uint16) string { - emu := om.EQToEmu(eq) - return om.EmuToName(emu) -} - -// NameSearch finds an emulator opcode by name -func NameSearch(name string) EmuOpcode { - for opcode, opcName := range OpcodeNames { - if opcName == name { - return opcode - } - } - return OP_Unknown -} - -// EQOpcodeManager is the global opcode manager map -// Maps opcode version to manager instance -// This replaces the C++ map EQOpcodeManager -type EQOpcodeManagerMap map[uint16]*RegularOpcodeManager - -// NewEQOpcodeManager creates and initializes the global opcode manager -func NewEQOpcodeManager() EQOpcodeManagerMap { - return make(EQOpcodeManagerMap) -} - -// LoadFromDatabase simulates loading opcodes from database results -// This would be called by your application after querying the database -func (m EQOpcodeManagerMap) LoadFromDatabase(versions OpcodeVersionMap, opcodesByVersion map[uint16]map[string]uint16) error { - // For each version range, create an opcode manager - for minVersion := range versions { - manager := NewRegularOpcodeManager(minVersion) - - // Load opcodes for this version - if opcodes, exists := opcodesByVersion[minVersion]; exists { - if !manager.LoadOpcodes(opcodes) { - return fmt.Errorf("failed to load opcodes for version %d", minVersion) - } - } else { - return fmt.Errorf("no opcodes found for version %d", minVersion) - } - - m[minVersion] = manager - } - - return nil -} - -// GetManagerForClient returns the appropriate opcode manager for a client version -func (m EQOpcodeManagerMap) GetManagerForClient(clientVersion uint16, versionMap OpcodeVersionMap) *RegularOpcodeManager { - opcodeVersion := GetOpcodeVersion(clientVersion, versionMap) - return m[opcodeVersion] -} - -// Example helper functions for database integration -// These would be implemented by the application using this library - -// LoadVersionsFromDB would execute: -// SELECT DISTINCT version_range1, version_range2 FROM opcodes -func LoadVersionsFromDB() OpcodeVersionMap { - // This is just an example - actual implementation would query the database - return OpcodeVersionMap{ - 1: 546, // Version range 1-546 uses opcode version 1 - 547: 889, // Version range 547-889 uses opcode version 547 - 890: 1027, // etc. - 1028: 1048, - 1049: 1095, - 1096: 1184, - 1185: 1197, - 1198: 1207, - 1208: 1211, - 1212: 9999, - } -} - -// LoadOpcodesFromDB would execute: -// SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2 -func LoadOpcodesFromDB(version uint16) map[string]uint16 { - // This is just an example - actual implementation would query the database - return map[string]uint16{ - "OP_LoginRequestMsg": 0x00B3, - "OP_LoginReplyMsg": 0x00B6, - // ... etc - } -} - -// InitializeOpcodeSystem shows how to initialize the opcode system -// This would be called during server startup -func InitializeOpcodeSystem() (EQOpcodeManagerMap, OpcodeVersionMap, error) { - // Load version ranges from database - versions := LoadVersionsFromDB() - - // Create the global opcode manager - opcodeManager := NewEQOpcodeManager() - - // Load opcodes for each version - opcodesByVersion := make(map[uint16]map[string]uint16) - for minVersion := range versions { - opcodes := LoadOpcodesFromDB(minVersion) - opcodesByVersion[minVersion] = opcodes - } - - // Initialize the manager - if err := opcodeManager.LoadFromDatabase(versions, opcodesByVersion); err != nil { - return nil, nil, err - } - - return opcodeManager, versions, nil -} \ No newline at end of file diff --git a/opcodes_db_example.go b/opcodes_db_example.go deleted file mode 100644 index 5cdf0ef..0000000 --- a/opcodes_db_example.go +++ /dev/null @@ -1,252 +0,0 @@ -package eq2net - -import ( - "database/sql" - "fmt" - "log" - "time" - - _ "github.com/go-sql-driver/mysql" -) - -// OpcodeDBLoader handles loading opcodes from a MySQL database -// This keeps database concerns separate from the core opcode system -type OpcodeDBLoader struct { - db *sql.DB -} - -// NewOpcodeDBLoader creates a new database loader -func NewOpcodeDBLoader(db *sql.DB) *OpcodeDBLoader { - return &OpcodeDBLoader{db: db} -} - -// LoadVersions loads version ranges from the database -// Executes: SELECT DISTINCT version_range1, version_range2 FROM opcodes -func (l *OpcodeDBLoader) LoadVersions() (OpcodeVersionMap, error) { - query := `SELECT DISTINCT version_range1, version_range2 FROM opcodes ORDER BY version_range1` - - rows, err := l.db.Query(query) - if err != nil { - return nil, fmt.Errorf("failed to query version ranges: %w", err) - } - defer rows.Close() - - versions := make(OpcodeVersionMap) - for rows.Next() { - var minVersion, maxVersion uint16 - if err := rows.Scan(&minVersion, &maxVersion); err != nil { - return nil, fmt.Errorf("failed to scan version range: %w", err) - } - versions[minVersion] = maxVersion - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating version rows: %w", err) - } - - return versions, nil -} - -// LoadOpcodes loads opcodes for a specific version -// Executes: SELECT name, opcode FROM opcodes WHERE ? BETWEEN version_range1 AND version_range2 -func (l *OpcodeDBLoader) LoadOpcodes(version uint16) (map[string]uint16, error) { - query := ` - SELECT name, opcode - FROM opcodes - WHERE ? BETWEEN version_range1 AND version_range2 - ORDER BY version_range1, id - ` - - rows, err := l.db.Query(query, version) - if err != nil { - return nil, fmt.Errorf("failed to query opcodes for version %d: %w", version, err) - } - defer rows.Close() - - opcodes := make(map[string]uint16) - for rows.Next() { - var name string - var opcode uint16 - if err := rows.Scan(&name, &opcode); err != nil { - return nil, fmt.Errorf("failed to scan opcode row: %w", err) - } - opcodes[name] = opcode - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating opcode rows: %w", err) - } - - return opcodes, nil -} - -// LoadAllOpcodes loads all opcodes for all versions at once -// More efficient for server initialization -func (l *OpcodeDBLoader) LoadAllOpcodes() (map[uint16]map[string]uint16, error) { - // First get all unique version ranges - versions, err := l.LoadVersions() - if err != nil { - return nil, err - } - - // Load opcodes for each version - result := make(map[uint16]map[string]uint16) - for minVersion := range versions { - opcodes, err := l.LoadOpcodes(minVersion) - if err != nil { - return nil, fmt.Errorf("failed to load opcodes for version %d: %w", minVersion, err) - } - result[minVersion] = opcodes - } - - return result, nil -} - -// InitializeOpcodeSystemFromDB initializes the opcode system from a database -func InitializeOpcodeSystemFromDB(db *sql.DB) (EQOpcodeManagerMap, OpcodeVersionMap, error) { - loader := NewOpcodeDBLoader(db) - - // Load version ranges - versions, err := loader.LoadVersions() - if err != nil { - return nil, nil, fmt.Errorf("failed to load version ranges: %w", err) - } - - // Load all opcodes - opcodesByVersion, err := loader.LoadAllOpcodes() - if err != nil { - return nil, nil, fmt.Errorf("failed to load opcodes: %w", err) - } - - // Create and initialize the opcode manager - opcodeManager := NewEQOpcodeManager() - if err := opcodeManager.LoadFromDatabase(versions, opcodesByVersion); err != nil { - return nil, nil, fmt.Errorf("failed to initialize opcode manager: %w", err) - } - - log.Printf("Loaded opcodes for %d version ranges", len(versions)) - for minVersion, maxVersion := range versions { - if opcodes, exists := opcodesByVersion[minVersion]; exists { - log.Printf(" Version %d-%d: %d opcodes", minVersion, maxVersion, len(opcodes)) - } - } - - return opcodeManager, versions, nil -} - -// Example usage showing how to use the opcode system with a database -func ExampleDatabaseUsage() { - // Connect to database - dsn := "root:Root12!@tcp(localhost:3306)/eq2db?parseTime=true" - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatalf("Failed to open database: %v", err) - } - defer db.Close() - - // Configure connection pool - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(5) - db.SetConnMaxLifetime(5 * time.Minute) - - // Test connection - if err := db.Ping(); err != nil { - log.Fatalf("Failed to ping database: %v", err) - } - - // Initialize the opcode system - opcodeManager, versionMap, err := InitializeOpcodeSystemFromDB(db) - if err != nil { - log.Fatalf("Failed to initialize opcode system: %v", err) - } - - // Example: Handle a client with version 1193 - clientVersion := uint16(1193) - - // Get the appropriate opcode manager for this client - manager := opcodeManager.GetManagerForClient(clientVersion, versionMap) - if manager == nil { - log.Fatalf("No opcode manager available for client version %d", clientVersion) - } - - // Convert opcodes as needed - emuOpcode := OP_LoginRequestMsg - eqOpcode := manager.EmuToEQ(emuOpcode) - log.Printf("Client %d: %s -> 0x%04X", clientVersion, manager.EmuToName(emuOpcode), eqOpcode) - - // Reverse conversion - emuOpcode = manager.EQToEmu(eqOpcode) - log.Printf("Client %d: 0x%04X -> %s", clientVersion, eqOpcode, manager.EmuToName(emuOpcode)) -} - -// CreateOpcodeTableSQL returns the SQL to create the opcodes table -// This matches the existing EQ2 schema -func CreateOpcodeTableSQL() string { - return ` -CREATE TABLE IF NOT EXISTS opcodes ( - id INT AUTO_INCREMENT PRIMARY KEY, - version_range1 INT NOT NULL, - version_range2 INT NOT NULL, - name VARCHAR(64) NOT NULL, - opcode INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - INDEX idx_version_range (version_range1, version_range2), - INDEX idx_name (name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 -` -} - -// InsertExampleOpcodes inserts example opcodes for testing -func InsertExampleOpcodes(db *sql.DB) error { - // Example data for version 1185-1197 (includes client version 1193) - opcodes := []struct { - versionMin uint16 - versionMax uint16 - name string - opcode uint16 - }{ - {1185, 1197, "OP_LoginRequestMsg", 0x00B3}, - {1185, 1197, "OP_LoginReplyMsg", 0x00B6}, - {1185, 1197, "OP_WorldListMsg", 0x00B8}, - {1185, 1197, "OP_PlayCharacterRequestMsg", 0x00BE}, - {1185, 1197, "OP_PlayCharacterReplyMsg", 0x00BF}, - {1185, 1197, "OP_DeleteCharacterRequestMsg", 0x00BA}, - {1185, 1197, "OP_CreateCharacterRequestMsg", 0x00BC}, - - // Example data for version 1198-1207 - {1198, 1207, "OP_LoginRequestMsg", 0x00C3}, - {1198, 1207, "OP_LoginReplyMsg", 0x00C6}, - {1198, 1207, "OP_WorldListMsg", 0x00C8}, - {1198, 1207, "OP_PlayCharacterRequestMsg", 0x00CE}, - {1198, 1207, "OP_PlayCharacterReplyMsg", 0x00CF}, - - // Example data for version 1208-1211 - {1208, 1211, "OP_LoginRequestMsg", 0x00D3}, - {1208, 1211, "OP_LoginReplyMsg", 0x00D6}, - {1208, 1211, "OP_WorldListMsg", 0x00D8}, - {1208, 1211, "OP_PlayCharacterRequestMsg", 0x00DE}, - {1208, 1211, "OP_PlayCharacterReplyMsg", 0x00DF}, - } - - // Prepare insert statement - stmt, err := db.Prepare(` - INSERT INTO opcodes (version_range1, version_range2, name, opcode) - VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE opcode = VALUES(opcode) - `) - if err != nil { - return fmt.Errorf("failed to prepare statement: %w", err) - } - defer stmt.Close() - - // Insert all opcodes - for _, op := range opcodes { - if _, err := stmt.Exec(op.versionMin, op.versionMax, op.name, op.opcode); err != nil { - return fmt.Errorf("failed to insert opcode %s: %w", op.name, err) - } - } - - log.Printf("Inserted %d example opcodes", len(opcodes)) - return nil -} diff --git a/opcodes_test.go b/opcodes_test.go deleted file mode 100644 index ecf3416..0000000 --- a/opcodes_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package eq2net - -import ( - "fmt" - "testing" -) - -func TestGetOpcodeVersionV2(t *testing.T) { - // Create a version map that matches the C++ implementation - versionMap := OpcodeVersionMap{ - 1: 546, - 547: 889, - 890: 1027, - 1028: 1048, - 1049: 1095, - 1096: 1184, - 1185: 1197, - 1198: 1207, - 1208: 1211, - 1212: 9999, - } - - tests := []struct { - clientVersion uint16 - expectedOpcode uint16 - }{ - // Test first range - {1, 1}, - {100, 1}, - {546, 1}, - - // Test second range - {547, 547}, - {700, 547}, - {889, 547}, - - // Test middle ranges - {890, 890}, - {1000, 890}, - {1027, 890}, - - {1028, 1028}, - {1048, 1028}, - - {1096, 1096}, - {1100, 1096}, - {1184, 1096}, - - {1185, 1185}, - {1193, 1185}, - {1197, 1185}, - - {1198, 1198}, - {1200, 1198}, - {1207, 1198}, - - {1208, 1208}, - {1210, 1208}, - {1211, 1208}, - - // Test last range - {1212, 1212}, - {2000, 1212}, - {9999, 1212}, - - // Test out of range (should return client version) - {10000, 10000}, - } - - for _, tt := range tests { - result := GetOpcodeVersion(tt.clientVersion, versionMap) - if result != tt.expectedOpcode { - t.Errorf("GetOpcodeVersion(%d) = %d, want %d", - tt.clientVersion, result, tt.expectedOpcode) - } - } -} - -func TestRegularOpcodeManagerV2(t *testing.T) { - manager := NewRegularOpcodeManager(1193) - - // Test loading opcodes (simulating database results) - opcodes := map[string]uint16{ - "OP_LoginRequestMsg": 0x0001, - "OP_LoginReplyMsg": 0x0002, - "OP_WorldListMsg": 0x0003, - "OP_PlayCharacterRequestMsg": 0x0004, - } - - if !manager.LoadOpcodes(opcodes) { - t.Fatal("Failed to load opcodes") - } - - // Test EmuToEQ conversion - eq := manager.EmuToEQ(OP_LoginRequestMsg) - if eq != 0x0001 { - t.Errorf("Expected EQ opcode 0x0001, got 0x%04x", eq) - } - - // Test invalid opcode returns 0xCDCD - eq = manager.EmuToEQ(OP_Unknown) - if eq != 0xCDCD { - t.Errorf("Expected 0xCDCD for unknown opcode, got 0x%04x", eq) - } - - // Test EQToEmu conversion - emu := manager.EQToEmu(0x0002) - if emu != OP_LoginReplyMsg { - t.Errorf("Expected emu opcode %v, got %v", OP_LoginReplyMsg, emu) - } - - // Test unknown EQ opcode - emu = manager.EQToEmu(0xFFFF) - if emu != OP_Unknown { - t.Errorf("Expected OP_Unknown for unknown EQ opcode, got %v", emu) - } - - // Test name lookups - name := manager.EmuToName(OP_LoginRequestMsg) - if name != "OP_LoginRequestMsg" { - t.Errorf("Expected 'OP_LoginRequestMsg', got '%s'", name) - } - - name = manager.EQToName(0x0003) - if name != "OP_WorldListMsg" { - t.Errorf("Expected 'OP_WorldListMsg', got '%s'", name) - } -} - -func TestEQOpcodeManagerMapV2(t *testing.T) { - // Create version map - versions := OpcodeVersionMap{ - 1185: 1197, - 1198: 1207, - 1208: 1211, - } - - // Create opcodes for each version - opcodesByVersion := map[uint16]map[string]uint16{ - 1185: { - "OP_LoginRequestMsg": 0x00B3, - "OP_LoginReplyMsg": 0x00B6, - }, - 1198: { - "OP_LoginRequestMsg": 0x00C1, - "OP_LoginReplyMsg": 0x00C4, - }, - 1208: { - "OP_LoginRequestMsg": 0x00D1, - "OP_LoginReplyMsg": 0x00D4, - }, - } - - // Initialize the manager map - managerMap := NewEQOpcodeManager() - err := managerMap.LoadFromDatabase(versions, opcodesByVersion) - if err != nil { - t.Fatalf("Failed to load from database: %v", err) - } - - // Test getting manager for client version 1193 (should use 1185 opcodes) - manager := managerMap.GetManagerForClient(1193, versions) - if manager == nil { - t.Fatal("Failed to get manager for client version 1193") - } - - eq := manager.EmuToEQ(OP_LoginRequestMsg) - if eq != 0x00B3 { - t.Errorf("Expected 0x00B3 for version 1193, got 0x%04x", eq) - } - - // Test getting manager for client version 1200 (should use 1198 opcodes) - manager = managerMap.GetManagerForClient(1200, versions) - if manager == nil { - t.Fatal("Failed to get manager for client version 1200") - } - - eq = manager.EmuToEQ(OP_LoginRequestMsg) - if eq != 0x00C1 { - t.Errorf("Expected 0x00C1 for version 1200, got 0x%04x", eq) - } - - // Test getting manager for client version 1210 (should use 1208 opcodes) - manager = managerMap.GetManagerForClient(1210, versions) - if manager == nil { - t.Fatal("Failed to get manager for client version 1210") - } - - eq = manager.EmuToEQ(OP_LoginRequestMsg) - if eq != 0x00D1 { - t.Errorf("Expected 0x00D1 for version 1210, got 0x%04x", eq) - } -} - -func TestNameSearch(t *testing.T) { - // Test finding opcodes by name - opcode := NameSearch("OP_LoginRequestMsg") - if opcode != OP_LoginRequestMsg { - t.Errorf("Expected %v, got %v", OP_LoginRequestMsg, opcode) - } - - opcode = NameSearch("OP_WorldListMsg") - if opcode != OP_WorldListMsg { - t.Errorf("Expected %v, got %v", OP_WorldListMsg, opcode) - } - - // Test unknown name - opcode = NameSearch("OP_NonExistent") - if opcode != OP_Unknown { - t.Errorf("Expected OP_Unknown for non-existent name, got %v", opcode) - } -} - -// BenchmarkOpcodeConversionV2 benchmarks the new implementation -func BenchmarkOpcodeConversionV2(b *testing.B) { - manager := NewRegularOpcodeManager(1193) - opcodes := make(map[string]uint16) - - // Add many opcodes for benchmarking - for i := uint16(1); i <= 100; i++ { - name := OpcodeNames[EmuOpcode(i)] - if name == "" { - name = fmt.Sprintf("OP_Test%d", i) - OpcodeNames[EmuOpcode(i)] = name - } - opcodes[name] = i - } - - manager.LoadOpcodes(opcodes) - - b.ResetTimer() - - b.Run("EmuToEQ", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = manager.EmuToEQ(EmuOpcode(i%100 + 1)) - } - }) - - b.Run("EQToEmu", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = manager.EQToEmu(uint16(i%100 + 1)) - } - }) - - b.Run("GetOpcodeVersion", func(b *testing.B) { - versionMap := OpcodeVersionMap{ - 1: 546, - 547: 889, - 890: 1027, - 1028: 1048, - 1049: 1095, - 1096: 1184, - 1185: 1197, - 1198: 1207, - 1208: 1211, - 1212: 9999, - } - - for i := 0; i < b.N; i++ { - _ = GetOpcodeVersion(uint16(i%2000+1), versionMap) - } - }) -} \ No newline at end of file diff --git a/packet.go b/packet.go deleted file mode 100644 index 1b1db0a..0000000 --- a/packet.go +++ /dev/null @@ -1,299 +0,0 @@ -// Package eq2net implements the EverQuest 2 network protocol -package eq2net - -import ( - "encoding/binary" - "fmt" - "net" - "time" -) - -// Protocol opcodes for low-level packet control -const ( - OPSessionRequest = 0x0001 - OPSessionResponse = 0x0002 - OPCombined = 0x0003 - OPSessionDisconnect = 0x0005 - OPKeepAlive = 0x0006 - OPSessionStatRequest = 0x0007 - OPSessionStatResponse = 0x0008 - OPPacket = 0x0009 - OPFragment = 0x000D - OPOutOfOrderAck = 0x0011 - OPAck = 0x0015 - OPAppCombined = 0x0019 - OPOutOfSession = 0x001D -) - -// EQPacket is the base packet type for all EverQuest packets -type EQPacket struct { - // Core packet data - Buffer []byte - Size uint32 - Opcode uint16 - - // Network information - SrcIP net.IP - DstIP net.IP - SrcPort uint16 - DstPort uint16 - - // Metadata - Priority uint32 - Timestamp time.Time - Version int16 -} - -// NewEQPacket creates a new packet with the specified opcode and data -func NewEQPacket(opcode uint16, data []byte) *EQPacket { - p := &EQPacket{ - Opcode: opcode, - Timestamp: time.Now(), - } - - if len(data) > 0 { - p.Buffer = make([]byte, len(data)) - copy(p.Buffer, data) - p.Size = uint32(len(data)) - } - - return p -} - -// TotalSize returns the total packet size including opcode -func (p *EQPacket) TotalSize() uint32 { - return p.Size + 2 // +2 for opcode -} - -// SetNetworkInfo sets the source and destination network information -func (p *EQPacket) SetNetworkInfo(srcIP net.IP, srcPort uint16, dstIP net.IP, dstPort uint16) { - p.SrcIP = srcIP - p.SrcPort = srcPort - p.DstIP = dstIP - p.DstPort = dstPort -} - -// CopyInfo copies network and timing information from another packet -func (p *EQPacket) CopyInfo(other *EQPacket) { - p.SrcIP = other.SrcIP - p.SrcPort = other.SrcPort - p.DstIP = other.DstIP - p.DstPort = other.DstPort - p.Timestamp = other.Timestamp - p.Version = other.Version -} - -// EQProtocolPacket handles low-level protocol operations -type EQProtocolPacket struct { - *EQPacket - - // Protocol state flags - Compressed bool - Prepared bool - Encrypted bool - Acked bool - - // Reliability tracking - SentTime time.Time - AttemptCount uint8 - Sequence uint32 -} - -// NewEQProtocolPacket creates a new protocol packet -func NewEQProtocolPacket(opcode uint16, data []byte) *EQProtocolPacket { - return &EQProtocolPacket{ - EQPacket: NewEQPacket(opcode, data), - } -} - -// NewEQProtocolPacketFromBuffer creates a protocol packet from raw buffer -func NewEQProtocolPacketFromBuffer(buffer []byte, opcodeOverride int) (*EQProtocolPacket, error) { - if len(buffer) < 2 { - return nil, fmt.Errorf("buffer too small for opcode") - } - - var opcode uint16 - var dataOffset int - - if opcodeOverride >= 0 { - opcode = uint16(opcodeOverride) - dataOffset = 0 - } else { - opcode = binary.BigEndian.Uint16(buffer[:2]) - dataOffset = 2 - } - - var data []byte - if len(buffer) > dataOffset { - data = buffer[dataOffset:] - } - - return NewEQProtocolPacket(opcode, data), nil -} - -// Serialize writes the protocol packet to a byte buffer -func (p *EQProtocolPacket) Serialize(offset int) []byte { - // Allocate buffer for opcode + data - result := make([]byte, 2+len(p.Buffer)-offset) - - // Write opcode (big-endian) - if p.Opcode > 0xFF { - binary.BigEndian.PutUint16(result[0:2], p.Opcode) - } else { - result[0] = 0 - result[1] = byte(p.Opcode) - } - - // Copy packet data - if len(p.Buffer) > offset { - copy(result[2:], p.Buffer[offset:]) - } - - return result -} - -// IsProtocolPacket checks if the opcode is a valid protocol packet -func IsProtocolPacket(opcode uint16) bool { - switch opcode { - case OPSessionRequest, OPSessionDisconnect, OPKeepAlive, - OPSessionStatResponse, OPPacket, OPCombined, OPFragment, - OPAck, OPOutOfOrderAck, OPOutOfSession: - return true - default: - return false - } -} - -// Copy creates a deep copy of the protocol packet -func (p *EQProtocolPacket) Copy() *EQProtocolPacket { - newPacket := &EQProtocolPacket{ - EQPacket: NewEQPacket(p.Opcode, p.Buffer), - Compressed: p.Compressed, - Prepared: p.Prepared, - Encrypted: p.Encrypted, - Acked: p.Acked, - SentTime: p.SentTime, - AttemptCount: p.AttemptCount, - Sequence: p.Sequence, - } - newPacket.CopyInfo(p.EQPacket) - return newPacket -} - -// EQApplicationPacket represents high-level application packets -type EQApplicationPacket struct { - *EQPacket - - // Cached emulator opcode - EmuOpcode uint16 - - // Opcode size (1 or 2 bytes) - OpcodeSize uint8 -} - -// DefaultOpcodeSize is the default size for application opcodes -var DefaultOpcodeSize uint8 = 2 - -// NewEQApplicationPacket creates a new application packet -func NewEQApplicationPacket(opcode uint16, data []byte) *EQApplicationPacket { - return &EQApplicationPacket{ - EQPacket: NewEQPacket(opcode, data), - EmuOpcode: opcode, - OpcodeSize: DefaultOpcodeSize, - } -} - -// Serialize writes the application packet to a byte buffer -func (p *EQApplicationPacket) Serialize() []byte { - opcodeBytes := p.OpcodeSize - - // Special handling for opcodes with low byte = 0x00 - if p.OpcodeSize == 2 && (p.Opcode&0x00FF) == 0 { - opcodeBytes = 3 - } - - result := make([]byte, uint32(opcodeBytes)+p.Size) - - if p.OpcodeSize == 1 { - result[0] = byte(p.Opcode) - } else { - if (p.Opcode & 0x00FF) == 0 { - result[0] = 0 - binary.BigEndian.PutUint16(result[1:3], p.Opcode) - } else { - binary.BigEndian.PutUint16(result[0:2], p.Opcode) - } - } - - // Copy data after opcode - if p.Size > 0 { - copy(result[opcodeBytes:], p.Buffer) - } - - return result -} - -// Copy creates a deep copy of the application packet -func (p *EQApplicationPacket) Copy() *EQApplicationPacket { - newPacket := &EQApplicationPacket{ - EQPacket: NewEQPacket(p.Opcode, p.Buffer), - EmuOpcode: p.EmuOpcode, - OpcodeSize: p.OpcodeSize, - } - newPacket.CopyInfo(p.EQPacket) - return newPacket -} - -// PacketCombiner handles combining multiple packets for efficient transmission -type PacketCombiner struct { - maxSize int -} - -// NewPacketCombiner creates a new packet combiner with max size limit -func NewPacketCombiner(maxSize int) *PacketCombiner { - return &PacketCombiner{maxSize: maxSize} -} - -// CombineProtocolPackets combines multiple protocol packets into one -func (c *PacketCombiner) CombineProtocolPackets(packets []*EQProtocolPacket) *EQProtocolPacket { - if len(packets) == 0 { - return nil - } - - if len(packets) == 1 { - return packets[0] - } - - // Calculate total size needed - totalSize := 0 - for _, p := range packets { - totalSize += 1 + int(p.TotalSize()) // 1 byte for size prefix - } - - if totalSize > c.maxSize { - return nil // Too large to combine - } - - // Build combined packet buffer - buffer := make([]byte, totalSize) - offset := 0 - - for _, p := range packets { - // Write size prefix - buffer[offset] = byte(p.TotalSize()) - offset++ - - // Serialize packet - serialized := p.Serialize(0) - copy(buffer[offset:], serialized) - offset += len(serialized) - } - - // Create combined packet - combined := NewEQProtocolPacket(OPCombined, buffer) - if len(packets) > 0 { - combined.CopyInfo(packets[0].EQPacket) - } - - return combined -} \ No newline at end of file diff --git a/packet_test.go b/packet_test.go deleted file mode 100644 index 639d694..0000000 --- a/packet_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package eq2net - -import ( - "bytes" - "testing" -) - -func TestEQPacket(t *testing.T) { - data := []byte("Hello, World!") - packet := NewEQPacket(OPPacket, data) - - if packet.Opcode != OPPacket { - t.Errorf("Expected opcode %04x, got %04x", OPPacket, packet.Opcode) - } - - if packet.Size != uint32(len(data)) { - t.Errorf("Expected size %d, got %d", len(data), packet.Size) - } - - if !bytes.Equal(packet.Buffer, data) { - t.Errorf("Buffer mismatch") - } -} - -func TestProtocolPacketSerialization(t *testing.T) { - data := []byte("Test Data") - packet := NewEQProtocolPacket(OPPacket, data) - - serialized := packet.Serialize(0) - - // Check opcode (first 2 bytes) - if len(serialized) < 2 { - t.Fatal("Serialized packet too small") - } - - // OPPacket = 0x0009, should be [0x00, 0x09] in big-endian - if serialized[0] != 0x00 || serialized[1] != 0x09 { - t.Errorf("Opcode not serialized correctly: %02x %02x", serialized[0], serialized[1]) - } - - // Check data - if !bytes.Equal(serialized[2:], data) { - t.Error("Data not serialized correctly") - } -} - -func TestCRC16(t *testing.T) { - tests := []struct { - data []byte - key uint32 - }{ - {[]byte{0x00, 0x09, 0x00, 0x00, 0x00}, 0x12345678}, - {[]byte{0x00, 0x01}, 0}, - {[]byte("Hello"), 0}, - } - - for _, tt := range tests { - // Just test that CRC16 produces consistent results - got1 := CRC16(tt.data, len(tt.data), tt.key) - got2 := CRC16(tt.data, len(tt.data), tt.key) - if got1 != got2 { - t.Errorf("CRC16 not consistent: %04x != %04x", got1, got2) - } - // Test that different keys produce different CRCs - if tt.key != 0 { - got3 := CRC16(tt.data, len(tt.data), 0) - if got1 == got3 { - t.Errorf("Different keys should produce different CRCs") - } - } - } -} - -func TestValidateCRC(t *testing.T) { - // Test session packet (CRC exempt) - sessionPacket := []byte{0x00, byte(OPSessionRequest), 0x00, 0x00} - if !ValidateCRC(sessionPacket, 0) { - t.Error("Session packet should be CRC exempt") - } - - // Test packet with valid CRC - data := []byte{0x00, 0x09, 0x48, 0x65, 0x6C, 0x6C, 0x6F} // "Hello" - crc := CRC16(data, len(data), 0x1234) - dataWithCRC := append(data, byte(crc>>8), byte(crc)) - - if !ValidateCRC(dataWithCRC, 0x1234) { - t.Error("Packet with valid CRC should validate") - } - - // Test packet with invalid CRC - dataWithCRC[len(dataWithCRC)-1] ^= 0xFF // Corrupt CRC - if ValidateCRC(dataWithCRC, 0x1234) { - t.Error("Packet with invalid CRC should not validate") - } -} - -func TestCompression(t *testing.T) { - // Test simple encoding (small packet) - smallData := []byte{0x00, 0x09, 0x01, 0x02, 0x03} - compressed, err := CompressPacket(smallData) - if err != nil { - t.Fatal(err) - } - - if compressed[2] != CompressionFlagSimple { - t.Errorf("Small packet should use simple encoding, got flag %02x", compressed[2]) - } - - decompressed, err := DecompressPacket(compressed) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(decompressed, smallData) { - t.Error("Decompressed data doesn't match original") - } - - // Test zlib compression (large packet) - largeData := make([]byte, 100) - largeData[0] = 0x00 - largeData[1] = 0x09 - for i := 2; i < len(largeData); i++ { - largeData[i] = byte(i) - } - - compressed, err = CompressPacket(largeData) - if err != nil { - t.Fatal(err) - } - - if compressed[2] != CompressionFlagZlib { - t.Errorf("Large packet should use zlib compression, got flag %02x", compressed[2]) - } - - decompressed, err = DecompressPacket(compressed) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(decompressed, largeData) { - t.Error("Decompressed large data doesn't match original") - } -} - -func TestChatEncryption(t *testing.T) { - // Test chat encoding/decoding - original := []byte{0x00, 0x09, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x21} // "Hello!" - key := uint32(0x12345678) - - encoded := ChatEncode(original, key) - if bytes.Equal(encoded[2:], original[2:]) { - t.Error("Encoded data should differ from original") - } - - decoded := ChatDecode(encoded, key) - if !bytes.Equal(decoded, original) { - t.Errorf("Decoded data doesn't match original\nOriginal: %v\nDecoded: %v", original, decoded) - } - - // Test exempt packet types - exemptPacket := []byte{0x00, 0x01, 0x12, 0x34} - encoded = ChatEncode(exemptPacket, key) - if !bytes.Equal(encoded, exemptPacket) { - t.Error("Exempt packet should not be encoded") - } -} - -func TestPacketCombiner(t *testing.T) { - combiner := NewPacketCombiner(256) - - p1 := NewEQProtocolPacket(OPPacket, []byte{0x01, 0x02}) - p2 := NewEQProtocolPacket(OPAck, []byte{0x03, 0x04}) - p3 := NewEQProtocolPacket(OPKeepAlive, []byte{0x05}) - - combined := combiner.CombineProtocolPackets([]*EQProtocolPacket{p1, p2, p3}) - - if combined == nil { - t.Fatal("Failed to combine packets") - } - - if combined.Opcode != OPCombined { - t.Errorf("Combined packet should have opcode %04x, got %04x", OPCombined, combined.Opcode) - } - - // Verify combined packet structure - buffer := combined.Buffer - offset := 0 - - // First packet - if buffer[offset] != byte(p1.TotalSize()) { - t.Errorf("First packet size incorrect: %d", buffer[offset]) - } - offset++ - offset += int(p1.TotalSize()) - - // Second packet - if buffer[offset] != byte(p2.TotalSize()) { - t.Errorf("Second packet size incorrect: %d", buffer[offset]) - } - offset++ - offset += int(p2.TotalSize()) - - // Third packet - if buffer[offset] != byte(p3.TotalSize()) { - t.Errorf("Third packet size incorrect: %d", buffer[offset]) - } -} \ No newline at end of file diff --git a/server.go b/server.go deleted file mode 100644 index 3539f3b..0000000 --- a/server.go +++ /dev/null @@ -1,407 +0,0 @@ -package eq2net - -import ( - "context" - "fmt" - "net" - "sync" - "time" - - "github.com/panjf2000/gnet/v2" -) - -// ServerConfig contains configuration for the EQ2 server -type ServerConfig struct { - // Network settings - Address string // Listen address (e.g., ":9000") - MaxConnections int // Maximum concurrent connections - ReadBufferSize int // UDP read buffer size - WriteBufferSize int // UDP write buffer size - - // Stream settings - StreamConfig *StreamConfig // Default config for new streams - - // Performance settings - NumEventLoops int // Number of gnet event loops (0 = NumCPU) - ReusePort bool // Enable SO_REUSEPORT for load balancing -} - -// DefaultServerConfig returns a default server configuration -func DefaultServerConfig() *ServerConfig { - return &ServerConfig{ - Address: ":9000", - MaxConnections: 10000, - ReadBufferSize: 65536, - WriteBufferSize: 65536, - StreamConfig: DefaultStreamConfig(), - NumEventLoops: 0, - ReusePort: true, - } -} - -// EQ2Server implements a gnet-based EverQuest 2 server -type EQ2Server struct { - gnet.BuiltinEventEngine - - config *ServerConfig - engine gnet.Engine - engineSet bool // Track if engine has been set - addr net.Addr - - // Connection management - streams map[string]*serverStream // Key: remote address string - streamsMu sync.RWMutex - - // Callbacks - onNewConnection func(*EQStream) - onConnectionClosed func(*EQStream, string) - - // Lifecycle - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup -} - -// serverStream wraps an EQStream with server-specific data -type serverStream struct { - stream *EQStream - lastActive time.Time - conn gnet.Conn -} - -// NewEQ2Server creates a new EQ2 server -func NewEQ2Server(config *ServerConfig) *EQ2Server { - if config == nil { - config = DefaultServerConfig() - } - - ctx, cancel := context.WithCancel(context.Background()) - - return &EQ2Server{ - config: config, - streams: make(map[string]*serverStream), - ctx: ctx, - cancel: cancel, - } -} - -// Start begins listening for connections -func (s *EQ2Server) Start() error { - // Configure gnet options - opts := []gnet.Option{ - gnet.WithMulticore(true), - gnet.WithReusePort(s.config.ReusePort), - gnet.WithSocketRecvBuffer(s.config.ReadBufferSize), - gnet.WithSocketSendBuffer(s.config.WriteBufferSize), - gnet.WithTicker(true), - } - - if s.config.NumEventLoops > 0 { - opts = append(opts, gnet.WithNumEventLoop(s.config.NumEventLoops)) - } - - // Start cleanup worker - s.wg.Add(1) - go s.cleanupWorker() - - // Start gnet server - return gnet.Run(s, "udp://"+s.config.Address, opts...) -} - -// Stop gracefully shuts down the server -func (s *EQ2Server) Stop() error { - // Signal shutdown - s.cancel() - - // Close all streams - s.streamsMu.Lock() - for _, ss := range s.streams { - ss.stream.Close() - } - s.streamsMu.Unlock() - - // Wait for cleanup - done := make(chan struct{}) - go func() { - s.wg.Wait() - close(done) - }() - - select { - case <-done: - case <-time.After(10 * time.Second): - // Force shutdown after timeout - } - - // Stop gnet engine - if s.engineSet { - return s.engine.Stop(s.ctx) - } - - return nil -} - -// gnet event handlers - -// OnBoot is called when the server starts -func (s *EQ2Server) OnBoot(eng gnet.Engine) (action gnet.Action) { - s.engine = eng - s.engineSet = true - // Parse and store the address - addr, err := net.ResolveUDPAddr("udp", s.config.Address) - if err == nil { - s.addr = addr - } - fmt.Printf("EQ2 server started on %s\n", s.config.Address) - return gnet.None -} - -// OnShutdown is called when the server stops -func (s *EQ2Server) OnShutdown(eng gnet.Engine) { - fmt.Println("EQ2 server shutting down") -} - -// OnTraffic handles incoming UDP packets -func (s *EQ2Server) OnTraffic(c gnet.Conn) (action gnet.Action) { - // Read the packet - buf, err := c.Next(-1) - if err != nil { - return gnet.None - } - - // Get remote address - remoteAddr := c.RemoteAddr() - if remoteAddr == nil { - return gnet.None - } - - addrStr := remoteAddr.String() - - // Look up or create stream - s.streamsMu.RLock() - ss, exists := s.streams[addrStr] - s.streamsMu.RUnlock() - - if !exists { - // Check for session request - if len(buf) >= 2 { - opcode := uint16(buf[0])<<8 | uint16(buf[1]) - if opcode == OPSessionRequest { - // Create new stream - ss = s.createStream(c, remoteAddr) - if ss == nil { - return gnet.None - } - } else { - // Not a session request, send out-of-session - s.sendOutOfSession(c, remoteAddr) - return gnet.None - } - } else { - return gnet.None - } - } - - // Update last activity - ss.lastActive = time.Now() - - // Process packet in stream - ss.stream.handleIncomingPacket(buf) - - return gnet.None -} - -// OnTick is called periodically -func (s *EQ2Server) OnTick() (delay time.Duration, action gnet.Action) { - // Tick interval for maintenance tasks - return 100 * time.Millisecond, gnet.None -} - -// createStream creates a new stream for a client -func (s *EQ2Server) createStream(c gnet.Conn, remoteAddr net.Addr) *serverStream { - // Check connection limit - s.streamsMu.Lock() - defer s.streamsMu.Unlock() - - if len(s.streams) >= s.config.MaxConnections { - return nil - } - - // Create stream config (copy from default) - streamConfig := *s.config.StreamConfig - - // Create new stream - stream := NewEQStream(&streamConfig) - - // Set up callbacks - stream.SetCallbacks( - func() { - // On connect - if s.onNewConnection != nil { - s.onNewConnection(stream) - } - }, - func(reason string) { - // On disconnect - s.removeStream(remoteAddr.String()) - if s.onConnectionClosed != nil { - s.onConnectionClosed(stream, reason) - } - }, - nil, // Error handler - ) - - // Create server stream wrapper - ss := &serverStream{ - stream: stream, - lastActive: time.Now(), - conn: c, - } - - // Store in map - s.streams[remoteAddr.String()] = ss - - // Create a custom PacketConn wrapper for this stream - packetConn := &gnetPacketConn{ - conn: c, - localAddr: s.addr, - remoteAddr: remoteAddr, - server: s, - } - - // Connect the stream (in server mode, this just sets up the connection) - go func() { - if err := stream.Connect(packetConn, remoteAddr); err != nil { - s.removeStream(remoteAddr.String()) - } - }() - - return ss -} - -// removeStream removes a stream from the server -func (s *EQ2Server) removeStream(addrStr string) { - s.streamsMu.Lock() - defer s.streamsMu.Unlock() - - if ss, exists := s.streams[addrStr]; exists { - ss.stream.Close() - delete(s.streams, addrStr) - } -} - -// sendOutOfSession sends an out-of-session packet -func (s *EQ2Server) sendOutOfSession(c gnet.Conn, remoteAddr net.Addr) { - packet := NewEQProtocolPacket(OPOutOfSession, nil) - data := packet.Serialize(0) - c.AsyncWrite(data, nil) -} - -// cleanupWorker periodically cleans up inactive connections -func (s *EQ2Server) cleanupWorker() { - defer s.wg.Done() - - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-s.ctx.Done(): - return - - case <-ticker.C: - s.cleanupInactiveStreams() - } - } -} - -// cleanupInactiveStreams removes streams that have been inactive too long -func (s *EQ2Server) cleanupInactiveStreams() { - timeout := 5 * time.Minute - now := time.Now() - - s.streamsMu.Lock() - defer s.streamsMu.Unlock() - - for addr, ss := range s.streams { - if now.Sub(ss.lastActive) > timeout { - ss.stream.Close() - delete(s.streams, addr) - } - } -} - -// SetCallbacks sets server event callbacks -func (s *EQ2Server) SetCallbacks(onNew func(*EQStream), onClosed func(*EQStream, string)) { - s.onNewConnection = onNew - s.onConnectionClosed = onClosed -} - -// GetStream returns the stream for a given address -func (s *EQ2Server) GetStream(addr string) *EQStream { - s.streamsMu.RLock() - defer s.streamsMu.RUnlock() - - if ss, exists := s.streams[addr]; exists { - return ss.stream - } - return nil -} - -// GetAllStreams returns all active streams -func (s *EQ2Server) GetAllStreams() []*EQStream { - s.streamsMu.RLock() - defer s.streamsMu.RUnlock() - - streams := make([]*EQStream, 0, len(s.streams)) - for _, ss := range s.streams { - streams = append(streams, ss.stream) - } - return streams -} - -// gnetPacketConn implements net.PacketConn for gnet connections -type gnetPacketConn struct { - conn gnet.Conn - localAddr net.Addr - remoteAddr net.Addr - server *EQ2Server -} - -func (g *gnetPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { - // This is handled by OnTraffic, not used in server mode - return 0, nil, fmt.Errorf("not implemented for server mode") -} - -func (g *gnetPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { - // Write to the gnet connection - err = g.conn.AsyncWrite(p, nil) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (g *gnetPacketConn) Close() error { - // Connection lifecycle is managed by server - return nil -} - -func (g *gnetPacketConn) LocalAddr() net.Addr { - return g.localAddr -} - -func (g *gnetPacketConn) SetDeadline(t time.Time) error { - // Not implemented for UDP - return nil -} - -func (g *gnetPacketConn) SetReadDeadline(t time.Time) error { - // Not implemented for UDP - return nil -} - -func (g *gnetPacketConn) SetWriteDeadline(t time.Time) error { - // Not implemented for UDP - return nil -} \ No newline at end of file diff --git a/stream.go b/stream.go deleted file mode 100644 index 63ae152..0000000 --- a/stream.go +++ /dev/null @@ -1,567 +0,0 @@ -package eq2net - -import ( - "context" - "encoding/binary" - "fmt" - "net" - "sync" - "sync/atomic" - "time" -) - -// StreamState represents the state of an EQStream connection -type StreamState int32 - -const ( - StreamStateDisconnected StreamState = iota - StreamStateConnecting - StreamStateConnected - StreamStateDisconnecting - StreamStateClosed -) - -// StreamConfig contains configuration for an EQStream -type StreamConfig struct { - // Network settings - MaxPacketSize int // Maximum packet size (default: 512) - WindowSize uint16 // Sliding window size for flow control (default: 2048) - RetransmitTimeMs int64 // Initial retransmit time in milliseconds (default: 500) - MaxRetransmits int // Maximum retransmission attempts (default: 5) - ConnectTimeout time.Duration // Connection timeout (default: 30s) - KeepAliveTime time.Duration // Keep-alive interval (default: 5s) - - // Session settings - SessionID uint32 // Session identifier - MaxBandwidth uint32 // Maximum bandwidth in bytes/sec (0 = unlimited) - CRCKey uint32 // CRC key for packet validation - EncodeKey uint32 // Encryption key for chat packets - DecodeKey uint32 // Decryption key for chat packets - CompressEnable bool // Enable packet compression - - // Performance settings - SendBufferSize int // Size of send buffer (default: 1024) - RecvBufferSize int // Size of receive buffer (default: 1024) -} - -// DefaultStreamConfig returns a default configuration -func DefaultStreamConfig() *StreamConfig { - return &StreamConfig{ - MaxPacketSize: 512, - WindowSize: 2048, - RetransmitTimeMs: 500, - MaxRetransmits: 5, - ConnectTimeout: 30 * time.Second, - KeepAliveTime: 5 * time.Second, - SendBufferSize: 1024, - RecvBufferSize: 1024, - CompressEnable: true, - } -} - -// StreamStats tracks stream statistics -type StreamStats struct { - PacketsSent atomic.Uint64 - PacketsReceived atomic.Uint64 - BytesSent atomic.Uint64 - BytesReceived atomic.Uint64 - PacketsDropped atomic.Uint64 - Retransmits atomic.Uint64 - RTT atomic.Int64 // Round-trip time in microseconds - Bandwidth atomic.Uint64 -} - -// EQStream implements reliable UDP communication for EQ2 -type EQStream struct { - // Configuration - config *StreamConfig - - // Network - conn net.PacketConn - remoteAddr net.Addr - localAddr net.Addr - - // State management - state atomic.Int32 // StreamState - sessionID uint32 - nextSeqOut atomic.Uint32 - nextSeqIn atomic.Uint32 - lastAckSeq atomic.Uint32 - - // Packet queues - using channels for lock-free operations - sendQueue chan *EQProtocolPacket - recvQueue chan *EQApplicationPacket - ackQueue chan uint32 - resendQueue chan *EQProtocolPacket - fragmentQueue map[uint32][]*EQProtocolPacket // Fragments being assembled - - // Sliding window for flow control - sendWindow map[uint32]*sendPacket - sendWindowMu sync.RWMutex - recvWindow map[uint32]*EQProtocolPacket - recvWindowMu sync.RWMutex - - // Retransmission management - rtt atomic.Int64 // Smoothed RTT in microseconds - rttVar atomic.Int64 // RTT variance - rto atomic.Int64 // Retransmission timeout - - // Statistics - stats *StreamStats - - // Lifecycle management - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - - // Callbacks - onConnect func() - onDisconnect func(reason string) - onError func(error) -} - -// sendPacket tracks packets awaiting acknowledgment -type sendPacket struct { - packet *EQProtocolPacket - sentTime time.Time - attempts int - nextRetry time.Time -} - -// NewEQStream creates a new EQ2 stream -func NewEQStream(config *StreamConfig) *EQStream { - if config == nil { - config = DefaultStreamConfig() - } - - ctx, cancel := context.WithCancel(context.Background()) - - s := &EQStream{ - config: config, - sendQueue: make(chan *EQProtocolPacket, config.SendBufferSize), - recvQueue: make(chan *EQApplicationPacket, config.RecvBufferSize), - ackQueue: make(chan uint32, 256), - resendQueue: make(chan *EQProtocolPacket, 256), - fragmentQueue: make(map[uint32][]*EQProtocolPacket), - sendWindow: make(map[uint32]*sendPacket), - recvWindow: make(map[uint32]*EQProtocolPacket), - stats: &StreamStats{}, - ctx: ctx, - cancel: cancel, - } - - // Initialize state - s.state.Store(int32(StreamStateDisconnected)) - s.sessionID = config.SessionID - - // Set initial RTO - s.rto.Store(config.RetransmitTimeMs * 1000) // Convert to microseconds - - return s -} - -// Connect establishes a connection to the remote endpoint -func (s *EQStream) Connect(conn net.PacketConn, remoteAddr net.Addr) error { - // Check state - if !s.compareAndSwapState(StreamStateDisconnected, StreamStateConnecting) { - return fmt.Errorf("stream not in disconnected state") - } - - s.conn = conn - s.remoteAddr = remoteAddr - s.localAddr = conn.LocalAddr() - - // Start workers - s.wg.Add(4) - go s.sendWorker() - go s.recvWorker() - go s.retransmitWorker() - go s.keepAliveWorker() - - // Send session request - sessionReq := s.createSessionRequest() - if err := s.sendPacket(sessionReq); err != nil { - s.Close() - return fmt.Errorf("failed to send session request: %w", err) - } - - // Wait for connection or timeout - timer := time.NewTimer(s.config.ConnectTimeout) - defer timer.Stop() - - for { - select { - case <-timer.C: - s.Close() - return fmt.Errorf("connection timeout") - - case <-s.ctx.Done(): - return fmt.Errorf("connection cancelled") - - default: - if s.GetState() == StreamStateConnected { - if s.onConnect != nil { - s.onConnect() - } - return nil - } - time.Sleep(10 * time.Millisecond) - } - } -} - -// Send queues an application packet for transmission -func (s *EQStream) Send(packet *EQApplicationPacket) error { - if s.GetState() != StreamStateConnected { - return fmt.Errorf("stream not connected") - } - - // Convert to protocol packet - protoPacket := s.applicationToProtocol(packet) - - select { - case s.sendQueue <- protoPacket: - return nil - case <-s.ctx.Done(): - return fmt.Errorf("stream closed") - default: - return fmt.Errorf("send queue full") - } -} - -// Receive gets the next application packet from the receive queue -func (s *EQStream) Receive() (*EQApplicationPacket, error) { - select { - case packet := <-s.recvQueue: - return packet, nil - case <-s.ctx.Done(): - return nil, fmt.Errorf("stream closed") - default: - return nil, nil // Non-blocking - } -} - -// sendWorker handles outgoing packets -func (s *EQStream) sendWorker() { - defer s.wg.Done() - - combiner := NewPacketCombiner(s.config.MaxPacketSize - 10) // Leave room for headers/CRC - combineTimer := time.NewTicker(1 * time.Millisecond) - defer combineTimer.Stop() - - var pendingPackets []*EQProtocolPacket - - for { - select { - case <-s.ctx.Done(): - return - - case packet := <-s.sendQueue: - // Add sequence number if needed - if s.needsSequence(packet.Opcode) { - packet.Sequence = s.nextSeqOut.Add(1) - - // Track in send window - s.sendWindowMu.Lock() - s.sendWindow[packet.Sequence] = &sendPacket{ - packet: packet.Copy(), - sentTime: time.Now(), - attempts: 1, - nextRetry: time.Now().Add(time.Duration(s.rto.Load()) * time.Microsecond), - } - s.sendWindowMu.Unlock() - } - - pendingPackets = append(pendingPackets, packet) - - case packet := <-s.resendQueue: - // Priority resend - s.sendPacketNow(packet) - - case <-combineTimer.C: - // Send any pending combined packets - if len(pendingPackets) > 0 { - s.sendCombined(pendingPackets, combiner) - pendingPackets = nil - } - } - - // Try to combine and send if we have enough packets - if len(pendingPackets) >= 3 { - s.sendCombined(pendingPackets, combiner) - pendingPackets = nil - } - } -} - -// recvWorker handles incoming packets from the network -func (s *EQStream) recvWorker() { - defer s.wg.Done() - - buffer := make([]byte, s.config.MaxPacketSize) - - for { - select { - case <-s.ctx.Done(): - return - default: - } - - // Read from network with timeout - s.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - n, addr, err := s.conn.ReadFrom(buffer) - if err != nil { - if netErr, ok := err.(net.Error); ok && netErr.Timeout() { - continue - } - if s.onError != nil { - s.onError(err) - } - continue - } - - // Verify source address - if addr.String() != s.remoteAddr.String() { - continue // Ignore packets from other sources - } - - // Process packet - s.handleIncomingPacket(buffer[:n]) - } -} - -// retransmitWorker handles packet retransmissions -func (s *EQStream) retransmitWorker() { - defer s.wg.Done() - - ticker := time.NewTicker(10 * time.Millisecond) - defer ticker.Stop() - - for { - select { - case <-s.ctx.Done(): - return - - case <-ticker.C: - now := time.Now() - - s.sendWindowMu.Lock() - for seq, sp := range s.sendWindow { - if now.After(sp.nextRetry) { - if sp.attempts >= s.config.MaxRetransmits { - // Max retransmits reached, connection is dead - delete(s.sendWindow, seq) - s.stats.PacketsDropped.Add(1) - continue - } - - // Retransmit - sp.attempts++ - sp.nextRetry = now.Add(time.Duration(s.rto.Load()) * time.Microsecond * time.Duration(sp.attempts)) - s.stats.Retransmits.Add(1) - - // Queue for immediate send - select { - case s.resendQueue <- sp.packet: - default: - // Resend queue full, try next time - } - } - } - s.sendWindowMu.Unlock() - } - } -} - -// keepAliveWorker sends periodic keep-alive packets -func (s *EQStream) keepAliveWorker() { - defer s.wg.Done() - - ticker := time.NewTicker(s.config.KeepAliveTime) - defer ticker.Stop() - - for { - select { - case <-s.ctx.Done(): - return - - case <-ticker.C: - if s.GetState() == StreamStateConnected { - keepAlive := NewEQProtocolPacket(OPKeepAlive, nil) - select { - case s.sendQueue <- keepAlive: - default: - // Queue full, skip this keep-alive - } - } - } - } -} - -// Helper methods - -func (s *EQStream) GetState() StreamState { - return StreamState(s.state.Load()) -} - -func (s *EQStream) compareAndSwapState(old, new StreamState) bool { - return s.state.CompareAndSwap(int32(old), int32(new)) -} - -func (s *EQStream) createSessionRequest() *EQProtocolPacket { - data := make([]byte, 10) - binary.BigEndian.PutUint32(data[0:4], 2) // Protocol version - binary.BigEndian.PutUint32(data[4:8], s.sessionID) - binary.BigEndian.PutUint16(data[8:10], s.config.WindowSize) - - return NewEQProtocolPacket(OPSessionRequest, data) -} - -func (s *EQStream) needsSequence(opcode uint16) bool { - switch opcode { - case OPPacket, OPFragment: - return true - default: - return false - } -} - -func (s *EQStream) sendPacket(packet *EQProtocolPacket) error { - // Check if connection is established - if s.conn == nil { - return fmt.Errorf("no connection") - } - - // Prepare packet data - data := packet.Serialize(0) - - // Add CRC if not exempt - if !s.isCRCExempt(packet.Opcode) { - data = AppendCRC(data, s.config.CRCKey) - } - - // Send to network - n, err := s.conn.WriteTo(data, s.remoteAddr) - if err != nil { - return err - } - - // Update statistics - s.stats.PacketsSent.Add(1) - s.stats.BytesSent.Add(uint64(n)) - - return nil -} - -func (s *EQStream) sendPacketNow(packet *EQProtocolPacket) { - if err := s.sendPacket(packet); err != nil && s.onError != nil { - s.onError(err) - } -} - -func (s *EQStream) sendCombined(packets []*EQProtocolPacket, combiner *PacketCombiner) { - if len(packets) == 1 { - s.sendPacketNow(packets[0]) - return - } - - combined := combiner.CombineProtocolPackets(packets) - if combined != nil { - s.sendPacketNow(combined) - } else { - // Couldn't combine, send individually - for _, p := range packets { - s.sendPacketNow(p) - } - } -} - -func (s *EQStream) isCRCExempt(opcode uint16) bool { - switch opcode { - case OPSessionRequest, OPSessionResponse, OPOutOfSession: - return true - default: - return false - } -} - -func (s *EQStream) applicationToProtocol(app *EQApplicationPacket) *EQProtocolPacket { - // Serialize application packet - data := app.Serialize() - - // Create protocol packet - proto := NewEQProtocolPacket(OPPacket, data) - proto.CopyInfo(app.EQPacket) - - // Apply compression if enabled - if s.config.CompressEnable && len(data) > CompressionThreshold { - if compressed, err := CompressPacket(data); err == nil { - proto.Buffer = compressed - proto.Size = uint32(len(compressed)) - proto.Compressed = true - } - } - - return proto -} - -// handleIncomingPacket is implemented in stream_packet_handler.go - -// Close gracefully shuts down the stream -func (s *EQStream) Close() error { - if !s.compareAndSwapState(StreamStateConnected, StreamStateDisconnecting) && - !s.compareAndSwapState(StreamStateConnecting, StreamStateDisconnecting) { - return nil // Already closing or closed - } - - // Send disconnect packet if we have a connection - if s.conn != nil { - disconnect := NewEQProtocolPacket(OPSessionDisconnect, nil) - s.sendPacketNow(disconnect) - } - - // Cancel context to stop workers - s.cancel() - - // Wait for workers to finish - done := make(chan struct{}) - go func() { - s.wg.Wait() - close(done) - }() - - select { - case <-done: - case <-time.After(5 * time.Second): - // Force close after timeout - } - - s.state.Store(int32(StreamStateClosed)) - - if s.onDisconnect != nil { - s.onDisconnect("closed") - } - - return nil -} - -// GetStats returns a copy of the current statistics -func (s *EQStream) GetStats() StreamStats { - return StreamStats{ - PacketsSent: atomic.Uint64{}, - PacketsReceived: atomic.Uint64{}, - BytesSent: atomic.Uint64{}, - BytesReceived: atomic.Uint64{}, - PacketsDropped: atomic.Uint64{}, - Retransmits: atomic.Uint64{}, - RTT: atomic.Int64{}, - Bandwidth: atomic.Uint64{}, - } -} - -// SetCallbacks sets the stream event callbacks -func (s *EQStream) SetCallbacks(onConnect func(), onDisconnect func(string), onError func(error)) { - s.onConnect = onConnect - s.onDisconnect = onDisconnect - s.onError = onError -} \ No newline at end of file diff --git a/stream_packet_handler.go b/stream_packet_handler.go deleted file mode 100644 index c02830a..0000000 --- a/stream_packet_handler.go +++ /dev/null @@ -1,506 +0,0 @@ -package eq2net - -import ( - "encoding/binary" - "time" -) - -// handleIncomingPacket processes incoming network packets -func (s *EQStream) handleIncomingPacket(data []byte) { - // Update statistics - s.stats.PacketsReceived.Add(1) - s.stats.BytesReceived.Add(uint64(len(data))) - - // Validate minimum size - if len(data) < 2 { - return - } - - // Extract opcode - opcode := binary.BigEndian.Uint16(data[0:2]) - - // Check CRC for non-exempt packets - if !s.isCRCExempt(opcode) { - if !ValidateCRC(data, s.config.CRCKey) { - s.stats.PacketsDropped.Add(1) - return - } - // Remove CRC bytes for further processing - if len(data) > 2 { - data = data[:len(data)-2] - } - } - - // Create protocol packet - packet, err := NewEQProtocolPacketFromBuffer(data, -1) - if err != nil { - return - } - - // Handle based on opcode - switch packet.Opcode { - case OPSessionRequest: - s.handleSessionRequest(packet) - - case OPSessionResponse: - s.handleSessionResponse(packet) - - case OPSessionDisconnect: - s.handleDisconnect(packet) - - case OPKeepAlive: - s.handleKeepAlive(packet) - - case OPAck: - s.handleAck(packet) - - case OPOutOfOrderAck: - s.handleOutOfOrderAck(packet) - - case OPPacket: - s.handleDataPacket(packet) - - case OPFragment: - s.handleFragment(packet) - - case OPCombined: - s.handleCombined(packet) - - case OPOutOfSession: - s.handleOutOfSession(packet) - - default: - // Unknown opcode, ignore - } -} - -// handleSessionRequest processes incoming session requests -func (s *EQStream) handleSessionRequest(packet *EQProtocolPacket) { - if s.GetState() != StreamStateDisconnected { - // We're not accepting new connections - s.sendSessionResponse(false) - return - } - - // Parse request - if len(packet.Buffer) < 10 { - return - } - - version := binary.BigEndian.Uint32(packet.Buffer[0:4]) - sessionID := binary.BigEndian.Uint32(packet.Buffer[4:8]) - maxLength := binary.BigEndian.Uint16(packet.Buffer[8:10]) - - // Validate version - if version != 2 { - s.sendSessionResponse(false) - return - } - - // Update session info - s.sessionID = sessionID - if int(maxLength) < s.config.MaxPacketSize { - s.config.MaxPacketSize = int(maxLength) - } - - // Accept connection - s.state.Store(int32(StreamStateConnected)) - s.sendSessionResponse(true) - - if s.onConnect != nil { - s.onConnect() - } -} - -// handleSessionResponse processes session response packets -func (s *EQStream) handleSessionResponse(packet *EQProtocolPacket) { - if s.GetState() != StreamStateConnecting { - return - } - - // Parse response - if len(packet.Buffer) < 11 { - return - } - - sessionID := binary.BigEndian.Uint32(packet.Buffer[0:4]) - crcKey := binary.BigEndian.Uint32(packet.Buffer[4:8]) - validation := packet.Buffer[8] - format := packet.Buffer[9] - unknownByte := packet.Buffer[10] - - // Check if accepted - if validation == 0 { - // Connection rejected - s.state.Store(int32(StreamStateDisconnected)) - if s.onDisconnect != nil { - s.onDisconnect("connection rejected") - } - return - } - - // Update session info - s.sessionID = sessionID - s.config.CRCKey = crcKey - _ = format // Store for later use if needed - _ = unknownByte - - // Connection established - s.state.Store(int32(StreamStateConnected)) -} - -// handleDisconnect processes disconnect packets -func (s *EQStream) handleDisconnect(packet *EQProtocolPacket) { - s.state.Store(int32(StreamStateDisconnecting)) - - // Send acknowledgment - ack := NewEQProtocolPacket(OPSessionDisconnect, nil) - s.sendPacketNow(ack) - - // Clean shutdown - if s.onDisconnect != nil { - s.onDisconnect("remote disconnect") - } - - s.Close() -} - -// handleKeepAlive processes keep-alive packets -func (s *EQStream) handleKeepAlive(packet *EQProtocolPacket) { - // Keep-alives don't require a response, just update last activity time - // This helps detect dead connections -} - -// handleAck processes acknowledgment packets -func (s *EQStream) handleAck(packet *EQProtocolPacket) { - if len(packet.Buffer) < 2 { - return - } - - ackSeq := binary.BigEndian.Uint16(packet.Buffer[0:2]) - s.processAck(uint32(ackSeq)) -} - -// handleOutOfOrderAck processes out-of-order acknowledgments -func (s *EQStream) handleOutOfOrderAck(packet *EQProtocolPacket) { - if len(packet.Buffer) < 2 { - return - } - - ackSeq := binary.BigEndian.Uint16(packet.Buffer[0:2]) - s.processAck(uint32(ackSeq)) -} - -// processAck handles acknowledgment of a sent packet -func (s *EQStream) processAck(seq uint32) { - s.sendWindowMu.Lock() - defer s.sendWindowMu.Unlock() - - if sp, exists := s.sendWindow[seq]; exists { - // Calculate RTT and update estimates - rtt := time.Since(sp.sentTime).Microseconds() - s.updateRTT(rtt) - - // Remove from send window - delete(s.sendWindow, seq) - - // Update last acknowledged sequence - if seq > s.lastAckSeq.Load() { - s.lastAckSeq.Store(seq) - } - } -} - -// updateRTT updates the RTT estimates using Jacobson/Karels algorithm -func (s *EQStream) updateRTT(sampleRTT int64) { - // First sample - if s.rtt.Load() == 0 { - s.rtt.Store(sampleRTT) - s.rttVar.Store(sampleRTT / 2) - s.rto.Store(sampleRTT + 4*s.rttVar.Load()) - return - } - - // Subsequent samples (RFC 6298) - alpha := int64(125) // 1/8 in fixed point (multiply by 1000) - beta := int64(250) // 1/4 in fixed point - - // SRTT = (1-alpha) * SRTT + alpha * RTT - srtt := s.rtt.Load() - srtt = ((1000-alpha)*srtt + alpha*sampleRTT) / 1000 - - // RTTVAR = (1-beta) * RTTVAR + beta * |SRTT - RTT| - var diff int64 - if srtt > sampleRTT { - diff = srtt - sampleRTT - } else { - diff = sampleRTT - srtt - } - - rttVar := s.rttVar.Load() - rttVar = ((1000-beta)*rttVar + beta*diff) / 1000 - - // RTO = SRTT + 4 * RTTVAR - rto := srtt + 4*rttVar - - // Minimum RTO of 200ms, maximum of 60s - if rto < 200000 { - rto = 200000 - } else if rto > 60000000 { - rto = 60000000 - } - - s.rtt.Store(srtt) - s.rttVar.Store(rttVar) - s.rto.Store(rto) - s.stats.RTT.Store(srtt) -} - -// handleDataPacket processes regular data packets -func (s *EQStream) handleDataPacket(packet *EQProtocolPacket) { - // Extract sequence number (first 2 bytes after opcode) - if len(packet.Buffer) < 2 { - return - } - - seq := uint32(binary.BigEndian.Uint16(packet.Buffer[0:2])) - - // Send acknowledgment - s.sendAck(seq) - - // Check if it's in order - expectedSeq := s.nextSeqIn.Load() - if seq == expectedSeq { - // In order, process immediately - s.nextSeqIn.Add(1) - s.processDataPacket(packet.Buffer[2:]) - - // Check if we have any queued packets that can now be processed - s.processQueuedPackets() - } else if seq > expectedSeq { - // Future packet, queue it - s.recvWindowMu.Lock() - s.recvWindow[seq] = packet - s.recvWindowMu.Unlock() - - // Send out-of-order ACK - s.sendOutOfOrderAck(seq) - } - // If seq < expectedSeq, it's a duplicate, ignore (we already sent ACK) -} - -// processQueuedPackets processes any queued packets that are now in order -func (s *EQStream) processQueuedPackets() { - for { - expectedSeq := s.nextSeqIn.Load() - - s.recvWindowMu.Lock() - packet, exists := s.recvWindow[expectedSeq] - if !exists { - s.recvWindowMu.Unlock() - break - } - delete(s.recvWindow, expectedSeq) - s.recvWindowMu.Unlock() - - s.nextSeqIn.Add(1) - if len(packet.Buffer) > 2 { - s.processDataPacket(packet.Buffer[2:]) - } - } -} - -// processDataPacket processes the data portion of a packet -func (s *EQStream) processDataPacket(data []byte) { - // Decompress if needed - if IsCompressed(data) { - decompressed, err := DecompressPacket(data) - if err != nil { - return - } - data = decompressed - } - - // Decrypt chat if needed (check for chat opcodes) - // This would need opcode inspection - - // Convert to application packet - if len(data) < 2 { - return - } - - app := &EQApplicationPacket{ - EQPacket: NewEQPacket(binary.BigEndian.Uint16(data[0:2]), nil), - } - - if len(data) > 2 { - app.Buffer = make([]byte, len(data)-2) - copy(app.Buffer, data[2:]) - app.Size = uint32(len(app.Buffer)) - } - - // Queue for application - select { - case s.recvQueue <- app: - default: - // Receive queue full, drop packet - s.stats.PacketsDropped.Add(1) - } -} - -// handleFragment processes fragmented packets -func (s *EQStream) handleFragment(packet *EQProtocolPacket) { - if len(packet.Buffer) < 6 { - return - } - - // Parse fragment header - seq := uint32(binary.BigEndian.Uint16(packet.Buffer[0:2])) - totalSize := binary.BigEndian.Uint32(packet.Buffer[2:6]) - - // Send acknowledgment - s.sendAck(seq) - - // Store fragment - s.recvWindowMu.Lock() - s.fragmentQueue[seq] = append(s.fragmentQueue[seq], packet) - - // Check if we have all fragments - currentSize := uint32(0) - for _, frag := range s.fragmentQueue[seq] { - if len(frag.Buffer) > 6 { - currentSize += uint32(len(frag.Buffer) - 6) - } - } - - if currentSize >= totalSize { - // Reassemble packet - reassembled := make([]byte, 0, totalSize) - for _, frag := range s.fragmentQueue[seq] { - if len(frag.Buffer) > 6 { - reassembled = append(reassembled, frag.Buffer[6:]...) - } - } - - // Clean up fragment queue - delete(s.fragmentQueue, seq) - s.recvWindowMu.Unlock() - - // Process reassembled packet - s.processDataPacket(reassembled) - } else { - s.recvWindowMu.Unlock() - } -} - -// handleCombined processes combined packets -func (s *EQStream) handleCombined(packet *EQProtocolPacket) { - data := packet.Buffer - offset := 0 - - for offset < len(data) { - if offset+1 > len(data) { - break - } - - // Get sub-packet size - size := int(data[offset]) - offset++ - - // Handle oversized packets (size == 255) - if size == 255 && offset+2 <= len(data) { - size = int(binary.BigEndian.Uint16(data[offset:offset+2])) - offset += 2 - } - - if offset+size > len(data) { - break - } - - // Process sub-packet - subData := data[offset : offset+size] - s.handleIncomingPacket(subData) - - offset += size - } -} - -// handleOutOfSession processes out-of-session packets -func (s *EQStream) handleOutOfSession(packet *EQProtocolPacket) { - // Server is telling us we're not in a session - s.state.Store(int32(StreamStateDisconnected)) - - if s.onDisconnect != nil { - s.onDisconnect("out of session") - } -} - -// sendAck sends an acknowledgment packet -func (s *EQStream) sendAck(seq uint32) { - data := make([]byte, 2) - binary.BigEndian.PutUint16(data, uint16(seq)) - - ack := NewEQProtocolPacket(OPAck, data) - s.sendPacketNow(ack) -} - -// sendOutOfOrderAck sends an out-of-order acknowledgment -func (s *EQStream) sendOutOfOrderAck(seq uint32) { - data := make([]byte, 2) - binary.BigEndian.PutUint16(data, uint16(seq)) - - ack := NewEQProtocolPacket(OPOutOfOrderAck, data) - s.sendPacketNow(ack) -} - -// sendSessionResponse sends a session response packet -func (s *EQStream) sendSessionResponse(accept bool) { - data := make([]byte, 11) - binary.BigEndian.PutUint32(data[0:4], s.sessionID) - binary.BigEndian.PutUint32(data[4:8], s.config.CRCKey) - - if accept { - data[8] = 1 // Validation byte - } else { - data[8] = 0 // Rejection - } - - data[9] = 0 // Format - data[10] = 0 // Unknown - - response := NewEQProtocolPacket(OPSessionResponse, data) - s.sendPacketNow(response) -} - -// FragmentPacket breaks a large packet into fragments -func (s *EQStream) FragmentPacket(data []byte, maxSize int) []*EQProtocolPacket { - if len(data) <= maxSize { - // No fragmentation needed - return []*EQProtocolPacket{NewEQProtocolPacket(OPPacket, data)} - } - - // Calculate fragment sizes - headerSize := 6 // seq(2) + total_size(4) - fragmentDataSize := maxSize - headerSize - numFragments := (len(data) + fragmentDataSize - 1) / fragmentDataSize - - fragments := make([]*EQProtocolPacket, 0, numFragments) - totalSize := uint32(len(data)) - - for offset := 0; offset < len(data); offset += fragmentDataSize { - end := offset + fragmentDataSize - if end > len(data) { - end = len(data) - } - - // Build fragment packet - fragData := make([]byte, headerSize+end-offset) - // Sequence will be set by send worker - binary.BigEndian.PutUint32(fragData[2:6], totalSize) - copy(fragData[6:], data[offset:end]) - - fragments = append(fragments, NewEQProtocolPacket(OPFragment, fragData)) - } - - return fragments -} \ No newline at end of file diff --git a/stream_test.go b/stream_test.go deleted file mode 100644 index aa565fb..0000000 --- a/stream_test.go +++ /dev/null @@ -1,194 +0,0 @@ -package eq2net - -import ( - "net" - "testing" - "time" -) - -func TestStreamCreation(t *testing.T) { - config := DefaultStreamConfig() - stream := NewEQStream(config) - - if stream == nil { - t.Fatal("Failed to create stream") - } - - if stream.GetState() != StreamStateDisconnected { - t.Errorf("Expected disconnected state, got %v", stream.GetState()) - } - - // Test state transitions - if !stream.compareAndSwapState(StreamStateDisconnected, StreamStateConnecting) { - t.Error("Failed to transition to connecting state") - } - - if stream.GetState() != StreamStateConnecting { - t.Errorf("Expected connecting state, got %v", stream.GetState()) - } - - // Clean up - stream.Close() -} - -func TestStreamConfig(t *testing.T) { - config := DefaultStreamConfig() - - if config.MaxPacketSize != 512 { - t.Errorf("Expected max packet size 512, got %d", config.MaxPacketSize) - } - - if config.WindowSize != 2048 { - t.Errorf("Expected window size 2048, got %d", config.WindowSize) - } - - if config.RetransmitTimeMs != 500 { - t.Errorf("Expected retransmit time 500ms, got %d", config.RetransmitTimeMs) - } -} - -func TestRTTCalculation(t *testing.T) { - stream := NewEQStream(nil) - - // Test first RTT sample - stream.updateRTT(100000) // 100ms in microseconds - - if stream.rtt.Load() != 100000 { - t.Errorf("Expected RTT 100000, got %d", stream.rtt.Load()) - } - - // Test subsequent samples - stream.updateRTT(120000) // 120ms - stream.updateRTT(80000) // 80ms - - // RTT should be smoothed - rtt := stream.rtt.Load() - if rtt < 80000 || rtt > 120000 { - t.Errorf("RTT outside expected range: %d", rtt) - } - - // RTO should be set - rto := stream.rto.Load() - if rto < 200000 { // Minimum 200ms - t.Errorf("RTO below minimum: %d", rto) - } -} - -func TestPacketSequencing(t *testing.T) { - stream := NewEQStream(nil) - - // Test sequence number generation - seq1 := stream.nextSeqOut.Add(1) - seq2 := stream.nextSeqOut.Add(1) - seq3 := stream.nextSeqOut.Add(1) - - if seq1 != 1 || seq2 != 2 || seq3 != 3 { - t.Errorf("Sequence numbers not incrementing correctly: %d, %d, %d", seq1, seq2, seq3) - } -} - -func TestSendWindow(t *testing.T) { - stream := NewEQStream(nil) - - // Add packet to send window - packet := NewEQProtocolPacket(OPPacket, []byte("test")) - packet.Sequence = 1 - - stream.sendWindowMu.Lock() - stream.sendWindow[1] = &sendPacket{ - packet: packet, - sentTime: time.Now(), - attempts: 1, - nextRetry: time.Now().Add(500 * time.Millisecond), - } - stream.sendWindowMu.Unlock() - - // Process ACK - stream.processAck(1) - - // Verify packet removed from window - stream.sendWindowMu.RLock() - _, exists := stream.sendWindow[1] - stream.sendWindowMu.RUnlock() - - if exists { - t.Error("Packet not removed from send window after ACK") - } -} - -func TestFragmentation(t *testing.T) { - stream := NewEQStream(nil) - - // Create large data that needs fragmentation - largeData := make([]byte, 1000) - for i := range largeData { - largeData[i] = byte(i % 256) - } - - // Fragment the data - fragments := stream.FragmentPacket(largeData, 100) - - if len(fragments) == 0 { - t.Fatal("No fragments created") - } - - // Verify fragments - expectedFragments := (len(largeData) + 93) / 94 // 100 - 6 header bytes - if len(fragments) != expectedFragments { - t.Errorf("Expected %d fragments, got %d", expectedFragments, len(fragments)) - } - - // Verify each fragment has correct opcode - for _, frag := range fragments { - if frag.Opcode != OPFragment { - t.Errorf("Fragment has wrong opcode: %04x", frag.Opcode) - } - } -} - -// TestMockConnection tests basic packet flow without real network -func TestMockConnection(t *testing.T) { - // Create mock packet conn - clientConn, serverConn := net.Pipe() - defer clientConn.Close() - defer serverConn.Close() - - // Note: net.Pipe creates a stream connection, not packet-based - // For a real test, we'd need to use actual UDP sockets - // This is just to verify compilation - - config := DefaultStreamConfig() - stream := NewEQStream(config) - - // Verify stream creation - if stream == nil { - t.Fatal("Failed to create stream") - } - - stream.Close() -} - -func TestServerCreation(t *testing.T) { - config := DefaultServerConfig() - server := NewEQ2Server(config) - - if server == nil { - t.Fatal("Failed to create server") - } - - // Set callbacks - connectCount := 0 - disconnectCount := 0 - - server.SetCallbacks( - func(s *EQStream) { - connectCount++ - }, - func(s *EQStream, reason string) { - disconnectCount++ - }, - ) - - // Note: We don't actually start the server in unit tests - // as it would require binding to a real port -} \ No newline at end of file