reset
This commit is contained in:
parent
1a9f194f6a
commit
44b0b86a2e
817
LOGIN.md
817
LOGIN.md
@ -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.
|
209
compression.go
209
compression.go
@ -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
|
||||
}
|
93
crc16.go
93
crc16.go
@ -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
|
||||
}
|
1
go.mod
1
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
|
||||
|
5
go.sum
5
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=
|
||||
|
272
opcodes.go
272
opcodes.go
@ -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<int16, int16>
|
||||
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<int16, OpcodeManager*> 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
|
||||
}
|
@ -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
|
||||
}
|
263
opcodes_test.go
263
opcodes_test.go
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
299
packet.go
299
packet.go
@ -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
|
||||
}
|
207
packet_test.go
207
packet_test.go
@ -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])
|
||||
}
|
||||
}
|
407
server.go
407
server.go
@ -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
|
||||
}
|
567
stream.go
567
stream.go
@ -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
|
||||
}
|
@ -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
|
||||
}
|
194
stream_test.go
194
stream_test.go
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user