update claude, add luajit wrapper source as reference

This commit is contained in:
Sky Johnson 2025-08-06 18:12:08 -05:00
parent 1987d48a77
commit 789729a07e
15 changed files with 57 additions and 3305 deletions

3
.gitignore vendored
View File

@ -17,3 +17,6 @@
# Go workspace file
go.work
# LuaJIT wrapper source
luawrapper

View File

@ -234,6 +234,22 @@ XML-driven packet definitions with version-specific formats, conditional fields,
- `internal/languages/manager.go`: High-level language management with database integration, statistics, and command processing
- `internal/languages/interfaces.go`: Integration interfaces with database, players, chat processing, and event handling systems
**Player System:**
- `internal/player/player.go`: Core Player struct extending Entity with character data, experience, skills, spells, combat, social features
- `internal/player/player_info.go`: PlayerInfo struct for detailed character sheet data and serialization
- `internal/player/character_flags.go`: Character flag management for all EQ2 player states (anonymous, AFK, LFG, etc.)
- `internal/player/currency.go`: Coin and currency handling with validation and transaction support
- `internal/player/experience.go`: XP, leveling, and vitality systems with automatic level progression
- `internal/player/combat.go`: Combat mechanics, auto-attack, and weapon handling integration
- `internal/player/quest_management.go`: Quest system integration with tracking, progress, and completion
- `internal/player/spell_management.go`: Spell book and casting management with passive spell support
- `internal/player/skill_management.go`: Skill system integration with progression and bonuses
- `internal/player/spawn_management.go`: Spawn visibility and tracking for player view management
- `internal/player/manager.go`: Multi-player management system with statistics, events, and background processing
- `internal/player/interfaces.go`: System integration interfaces and player adapter for other systems
- `internal/player/database.go`: SQLite database operations for player persistence with zombiezen integration
- `internal/player/README.md`: Complete player system documentation with usage examples
**Quests System:**
- `internal/quests/constants.go`: Quest step types, display status flags, sharing constants, and validation limits
- `internal/quests/types.go`: Core Quest and QuestStep structures with complete quest data management and thread-safe operations
@ -340,6 +356,8 @@ Command-line flags override JSON configuration.
**Languages System**: Multilingual character communication system managing language learning and chat processing. Features master language registry with ID-based and name-based lookups, individual player language collections with thread-safe operations, language validation and persistence, and comprehensive multilingual chat processing. Supports all EverQuest II racial languages (Common, Elvish, Dwarven, Halfling, Gnomish, Iksar, Trollish, Ogrish, Fae, Arasai, Sarnak, Froglok), language learning/forgetting mechanics, primary language selection, and message scrambling for unknown languages. Includes PlayerLanguageAdapter for seamless player integration, ChatLanguageProcessor for multilingual communication, and statistics tracking for language usage patterns. Thread-safe operations with efficient hash-based lookups and comprehensive validation systems.
**Player System**: Complete player character management extending Entity with comprehensive MMO player functionality. Features Player struct with character data (ID, level, class, race), experience systems (adventure/tradeskill XP with automatic leveling), skill progression, spell management (spell books, casting, passive spells), combat mechanics (auto-attack, combat state, weapon handling), social features (friends/ignore lists), currency management (coin transactions with validation), quest integration (tracking, progress, completion), spawn management (visibility, tracking for player view), character flags (anonymous, AFK, LFG, roleplaying, etc.), movement and positioning, housing integration, and comprehensive cleanup systems. Includes PlayerManager for multi-player coordination with statistics, events, background processing (save/update loops), zone management, and thread-safe operations. Database integration uses zombiezen SQLite with complete persistence for all player data. All systems are thread-safe with proper Go concurrency patterns and extensive test coverage including concurrent access testing.
**Quests System**: Complete quest management with quest definitions, step system, and real-time progress tracking. Features quest system with multiple step types (kill, chat, obtain item, location, spell, normal, craft, harvest, kill race requirements), comprehensive prerequisite system (level, class, race, faction, quest dependencies), flexible reward system (coins, experience, status points, faction reputation, items), step-based progress tracking with percentage-based success chances, task group organization for complex quests, Lua action system for completion/progress/failure events, quest sharing system with configurable sharing rules, repeatable quest support, player quest management with active quest tracking, master quest list with categorization and search, validation system for quest integrity, and thread-safe operations with proper mutex usage. Includes QuestSystemAdapter for complete quest lifecycle management and integration with player, client, spawn, and item systems.
**NPC System**: Non-player character system extending Entity with complete AI, combat, and spell casting capabilities. Features NPC struct with brain system, spell management (cast-on-spawn/aggro triggers), skill bonuses, movement with runback mechanics, appearance randomization (33+ flags for race/gender/colors/features), AI strategies (balanced/offensive/defensive), and combat state management. Includes NPCSpell configurations with HP ratio requirements, skill bonus system with spell-based modifications, movement locations with navigation pathfinding, timer system for pause/movement control, and comprehensive appearance randomization covering all EQ2 races and visual elements. Manager provides zone-based indexing, appearance tracking, combat processing, AI processing, statistics collection, and command interface. Integration interfaces support database persistence, spell/skill/appearance systems, combat management, movement control, and entity adapters for seamless system integration. Thread-safe operations with proper mutex usage and atomic flags for state management.
@ -348,7 +366,40 @@ Command-line flags override JSON configuration.
All systems are converted from C++ with TODO comments marking areas for future implementation (LUA integration, advanced mechanics, etc.).
**Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility.
**Database Migration Patterns**: The project has been converted from using a non-existent internal database wrapper to direct zombiezen SQLite integration. Key patterns include:
- Using `sqlite.Conn` instead of `sql.DB` for connections
- `sqlitex.Execute` with `ExecOptions` and `ResultFunc` for queries
- Proper `found` flag handling to detect when no rows are returned
- Thread-safe operations with connection management
- Complete conversion in player system serves as reference implementation
**Testing**: Focus testing on the UDP protocol layer and packet parsing, as these are critical for client compatibility. All systems include comprehensive test suites with concurrency testing for thread safety validation.
## Development Patterns and Conventions
**Package Structure**: Each system follows a consistent structure:
- `types.go`: Core data structures and type definitions
- `constants.go`: System constants, IDs, and configuration values
- `manager.go`: High-level system management and coordination
- `interfaces.go`: Integration interfaces and adapter patterns
- `database.go`: Database persistence layer (where applicable)
- `README.md`: Package-specific documentation with usage examples
**Thread Safety**: All systems implement proper Go concurrency patterns:
- `sync.RWMutex` for read-heavy operations (maps, lists)
- `atomic` operations for simple flags and counters
- Proper lock ordering to prevent deadlocks
- Channel-based communication for background processing
**Integration Patterns**:
- `*Aware` interfaces for feature detection and loose coupling
- Adapter pattern for bridging different system interfaces
- Manager pattern for centralized system coordination
- Event-driven architecture for system notifications
**Entity-Pass-By-Pointer**: Always pass `*entity.Entity` (not `entity.Entity`) to avoid copying locks and improve performance. This applies to all entity-related method signatures.
**Name Padding Handling**: Player names from database may have null byte padding from `[128]byte` arrays. Always use `strings.TrimSpace(strings.Trim(name, "\x00"))` when working with player names from database or similar fixed-size string fields.
## Code Documentation Standards

1
cmd/login_server/TODO.md Normal file
View File

@ -0,0 +1 @@
need to implement

View File

@ -1,230 +0,0 @@
package main
import (
"eq2emu/internal/common/opcodes"
"eq2emu/internal/udp"
"fmt"
"log"
"time"
)
// LoginClient represents a connected client session
type LoginClient struct {
connection *udp.Connection
server *LoginServer
account *Account
lastActivity time.Time
authenticated bool
version uint16
sessionID string
// Client state
needsWorldList bool
sentCharacterList bool
pendingPlayCharID int32
createRequest *CharacterCreateRequest
}
// Account represents an authenticated user account
type Account struct {
ID int32
Username string
LSAdmin bool
WorldAdmin bool
Characters []*Character
LastLogin time.Time
IPAddress string
ClientVersion uint16
}
// Character represents a character in the database
type Character struct {
ID int32
AccountID int32
ServerID int32
Name string
Level int8
Race int8
Class int8
Gender int8
CreatedDate time.Time
Deleted bool
}
// CharacterCreateRequest holds pending character creation data
type CharacterCreateRequest struct {
ServerID int32
Name string
Race int8
Gender int8
Class int8
Face int8
Hair int8
HairColor int8
SkinColor int8
EyeColor int8
Timestamp time.Time
}
// NewLoginClient creates a new login client instance
func NewLoginClient(conn *udp.Connection, server *LoginServer) *LoginClient {
return &LoginClient{
connection: conn,
server: server,
lastActivity: time.Now(),
sessionID: fmt.Sprintf("%d", conn.GetSessionID()),
needsWorldList: true,
sentCharacterList: false,
}
}
// ProcessPacket handles incoming packets from the client
func (lc *LoginClient) ProcessPacket(packet *udp.ApplicationPacket) {
lc.lastActivity = time.Now()
switch packet.Opcode {
case opcodes.OpLoginRequestMsg:
lc.handleLoginRequest(packet)
case opcodes.OpAllWSDescRequestMsg:
lc.handleWorldListRequest(packet)
case opcodes.OpAllCharactersDescRequestMsg:
lc.handleCharacterListRequest(packet)
case opcodes.OpCreateCharacterRequestMsg:
lc.handleCharacterCreateRequest(packet)
case opcodes.OpDeleteCharacterRequestMsg:
lc.handleCharacterDeleteRequest(packet)
case opcodes.OpPlayCharacterRequestMsg:
lc.handlePlayCharacterRequest(packet)
case opcodes.OpKeymapLoadMsg:
// Client keymap request - usually ignored
break
default:
log.Printf("Unknown packet opcode from client %s: 0x%04X", lc.sessionID, packet.Opcode)
}
}
// handleLoginRequest processes client login attempts
func (lc *LoginClient) handleLoginRequest(packet *udp.ApplicationPacket) {
lc.server.IncrementLoginAttempts()
// Parse login request packet
loginReq, err := lc.parseLoginRequest(packet.Data)
if err != nil {
log.Printf("Failed to parse login request from %s: %v", lc.sessionID, err)
lc.sendLoginDenied()
return
}
lc.version = loginReq.Version
// Check if client version is supported
if !lc.server.GetConfig().IsVersionSupported(lc.version) {
log.Printf("Unsupported client version %d from %s", lc.version, lc.sessionID)
lc.sendLoginDeniedBadVersion()
return
}
// Authenticate with database
account, err := lc.server.database.AuthenticateAccount(loginReq.Username, loginReq.Password)
if err != nil {
log.Printf("Authentication failed for %s: %v", loginReq.Username, err)
lc.sendLoginDenied()
return
}
if account == nil {
log.Printf("Invalid credentials for %s", loginReq.Username)
lc.sendLoginDenied()
return
}
// Check for existing session
lc.server.clientMutex.RLock()
for _, existingClient := range lc.server.clients {
if existingClient.account != nil && existingClient.account.ID == account.ID && existingClient != lc {
log.Printf("Account %s already logged in, disconnecting previous session", account.Username)
existingClient.Disconnect()
break
}
}
lc.server.clientMutex.RUnlock()
// Update account info
account.LastLogin = time.Now()
account.IPAddress = lc.connection.GetClientAddr().IP.String()
account.ClientVersion = lc.version
lc.server.database.UpdateAccountLogin(account)
lc.account = account
lc.authenticated = true
lc.server.IncrementSuccessfulLogins()
log.Printf("User %s successfully authenticated", account.Username)
lc.sendLoginAccepted()
}
// handleWorldListRequest sends the list of available world servers
func (lc *LoginClient) handleWorldListRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
lc.sendWorldList()
lc.needsWorldList = false
// Load and send character list
if !lc.sentCharacterList {
characters, err := lc.server.database.LoadCharacters(lc.account.ID, lc.version)
if err != nil {
log.Printf("Failed to load characters for account %d: %v", lc.account.ID, err)
} else {
lc.account.Characters = characters
lc.sentCharacterList = true
}
lc.sendCharacterList()
}
}
// handleCharacterListRequest handles explicit character list requests
func (lc *LoginClient) handleCharacterListRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
lc.sendCharacterList()
}
// IsStale returns true if the client connection should be cleaned up
func (lc *LoginClient) IsStale() bool {
return time.Since(lc.lastActivity) > 5*time.Minute
}
// Disconnect closes the client connection and cleans up
func (lc *LoginClient) Disconnect() {
if lc.connection != nil {
lc.connection.Close()
}
// Clean up any pending requests
lc.createRequest = nil
log.Printf("Client %s disconnected", lc.sessionID)
lc.server.RemoveClient(lc.sessionID)
}
// GetAccount returns the authenticated account
func (lc *LoginClient) GetAccount() *Account {
return lc.account
}
// GetVersion returns the client version
func (lc *LoginClient) GetVersion() uint16 {
return lc.version
}
// GetSessionID returns the session identifier
func (lc *LoginClient) GetSessionID() string {
return lc.sessionID
}

View File

@ -1,163 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
// Config holds all login server configuration
type Config struct {
Port int `json:"port"`
MaxConnections int `json:"max_connections"`
TimeoutSeconds int `json:"timeout_seconds"`
EnableCompression bool `json:"enable_compression"`
EnableEncryption bool `json:"enable_encryption"`
AllowAccountCreation bool `json:"allow_account_creation"`
DefaultSubscriptionLevel uint32 `json:"default_subscription_level"`
ExpansionFlag uint16 `json:"expansion_flag"`
CitiesFlag uint8 `json:"cities_flag"`
EnabledRaces uint32 `json:"enabled_races"`
SupportedVersions []uint16 `json:"supported_versions"`
Database DatabaseConfig `json:"database"`
WebServer WebServerConfig `json:"web_server"`
WorldServers []WorldServerConfig `json:"world_servers"`
}
// WebServerConfig holds web server settings
type WebServerConfig struct {
Enabled bool `json:"enabled"`
Address string `json:"address"`
Port int `json:"port"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
KeyPassword string `json:"key_password"`
Username string `json:"username"`
Password string `json:"password"`
}
// WorldServerConfig holds world server connection info
type WorldServerConfig struct {
ID int32 `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
Port int `json:"port"`
SharedKey string `json:"shared_key"`
AutoConnect bool `json:"auto_connect"`
}
// DefaultConfig returns sensible default configuration
func DefaultConfig() *Config {
return &Config{
Port: 5999,
MaxConnections: 1000,
TimeoutSeconds: 45,
EnableCompression: true,
EnableEncryption: true,
AllowAccountCreation: false,
DefaultSubscriptionLevel: 0xFFFFFFFF,
ExpansionFlag: 0x7CFF,
CitiesFlag: 0xFF,
EnabledRaces: 0xFFFF,
SupportedVersions: []uint16{283, 373, 546, 561, 1096, 1208},
Database: DatabaseConfig{
FilePath: "eq2login.db",
MaxConnections: 10,
BusyTimeout: 5000,
},
WebServer: WebServerConfig{
Enabled: true,
Address: "0.0.0.0",
Port: 8080,
},
}
}
// LoadConfig loads configuration from a JSON file
func LoadConfig(filename string) (*Config, error) {
// Start with defaults
config := DefaultConfig()
// Check if config file exists
if _, err := os.Stat(filename); os.IsNotExist(err) {
// Create default config file
if err := SaveConfig(filename, config); err != nil {
return nil, fmt.Errorf("failed to create default config: %w", err)
}
fmt.Printf("Created default configuration file: %s\n", filename)
return config, nil
}
// Read existing config file
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Parse JSON
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("failed to parse config JSON: %w", err)
}
// Validate configuration
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}
return config, nil
}
// SaveConfig saves configuration to a JSON file
func SaveConfig(filename string, config *Config) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// Validate checks if the configuration is valid
func (c *Config) Validate() error {
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("invalid port: %d", c.Port)
}
if c.MaxConnections < 1 {
return fmt.Errorf("max_connections must be positive")
}
if c.TimeoutSeconds < 1 {
return fmt.Errorf("timeout_seconds must be positive")
}
if len(c.SupportedVersions) == 0 {
return fmt.Errorf("must specify at least one supported version")
}
return nil
}
// IsVersionSupported checks if a client version is supported
func (c *Config) IsVersionSupported(version uint16) bool {
for _, supported := range c.SupportedVersions {
if supported == version {
return true
}
}
return false
}
// GetWorldServerConfig returns configuration for a specific world server
func (c *Config) GetWorldServerConfig(id int32) *WorldServerConfig {
for i := range c.WorldServers {
if c.WorldServers[i].ID == id {
return &c.WorldServers[i]
}
}
return nil
}

View File

@ -1,325 +0,0 @@
package main
import (
"eq2emu/internal/database"
"fmt"
"log"
"time"
"golang.org/x/crypto/bcrypt"
)
// Database handles all database operations for the login server
type Database struct {
db *database.DB
}
// DatabaseConfig holds database connection settings
type DatabaseConfig struct {
FilePath string `json:"file_path"`
MaxConnections int `json:"max_connections"`
BusyTimeout int `json:"busy_timeout_ms"`
}
// NewDatabase creates a new database connection
func NewDatabase(config DatabaseConfig) (*Database, error) {
db, err := database.Open(config.FilePath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Set busy timeout if specified
if config.BusyTimeout > 0 {
query := fmt.Sprintf("PRAGMA busy_timeout = %d", config.BusyTimeout)
if err := db.Exec(query); err != nil {
return nil, fmt.Errorf("failed to set busy timeout: %w", err)
}
}
log.Println("SQLite database connection established")
return &Database{db: db}, nil
}
// Close closes the database connection
func (d *Database) Close() error {
return d.db.Close()
}
// AuthenticateAccount verifies user credentials and returns account info
func (d *Database) AuthenticateAccount(username, password string) (*Account, error) {
query := `
SELECT id, username, password_hash, ls_admin, world_admin,
created_date, last_login, client_version
FROM accounts
WHERE username = ? AND active = 1`
row, err := d.db.QueryRow(query, username)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
if row == nil {
return nil, nil // Account not found
}
defer row.Close()
var account Account
account.ID = int32(row.Int64(0))
account.Username = row.Text(1)
passwordHash := row.Text(2)
account.LSAdmin = row.Bool(3)
account.WorldAdmin = row.Bool(4)
// Skip created_date at index 5 - not needed for authentication
lastLogin := row.Text(6)
account.ClientVersion = uint16(row.Int64(7))
// Verify password
if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(password)); err != nil {
return nil, nil // Invalid password
}
// Parse timestamp
if lastLogin != "" {
if t, err := time.Parse("2006-01-02 15:04:05", lastLogin); err == nil {
account.LastLogin = t
}
}
return &account, nil
}
// UpdateAccountLogin updates account login timestamp and IP
func (d *Database) UpdateAccountLogin(account *Account) error {
query := `
UPDATE accounts
SET last_login = ?, last_ip = ?, client_version = ?
WHERE id = ?`
return d.db.Exec(query,
account.LastLogin.Format("2006-01-02 15:04:05"),
account.IPAddress,
account.ClientVersion,
account.ID,
)
}
// LoadCharacters loads all characters for an account
func (d *Database) LoadCharacters(accountID int32, version uint16) ([]*Character, error) {
query := `
SELECT id, server_id, name, level, race, gender, class,
created_date, deleted
FROM characters
WHERE account_id = ?
ORDER BY created_date ASC`
var characters []*Character
err := d.db.Query(query, func(row *database.Row) error {
char := &Character{AccountID: accountID}
char.ID = int32(row.Int64(0))
char.ServerID = int32(row.Int64(1))
char.Name = row.Text(2)
char.Level = int8(row.Int(3))
char.Race = int8(row.Int(4))
char.Gender = int8(row.Int(5))
char.Class = int8(row.Int(6))
if dateStr := row.Text(7); dateStr != "" {
if t, err := time.Parse("2006-01-02 15:04:05", dateStr); err == nil {
char.CreatedDate = t
}
}
char.Deleted = row.Bool(8)
characters = append(characters, char)
return nil
}, accountID)
return characters, err
}
// CharacterNameExists checks if a character name is already taken
func (d *Database) CharacterNameExists(name string, serverID int32) (bool, error) {
query := `
SELECT COUNT(*)
FROM characters
WHERE name = ? AND server_id = ? AND deleted = 0`
row, err := d.db.QueryRow(query, name, serverID)
if err != nil {
return false, err
}
if row == nil {
return false, nil
}
defer row.Close()
return row.Int(0) > 0, nil
}
// CreateCharacter creates a new character in the database
func (d *Database) CreateCharacter(char *Character) (int32, error) {
query := `
INSERT INTO characters (account_id, server_id, name, level, race,
gender, class, created_date)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
err := d.db.Exec(query,
char.AccountID, char.ServerID, char.Name, char.Level,
char.Race, char.Gender, char.Class,
char.CreatedDate.Format("2006-01-02 15:04:05"),
)
if err != nil {
return 0, fmt.Errorf("failed to create character: %w", err)
}
id := int32(d.db.LastInsertID())
log.Printf("Created character %s (ID: %d) for account %d",
char.Name, id, char.AccountID)
return id, nil
}
// DeleteCharacter marks a character as deleted
func (d *Database) DeleteCharacter(charID, accountID int32) error {
query := `
UPDATE characters
SET deleted = 1, deleted_date = CURRENT_TIMESTAMP
WHERE id = ? AND account_id = ?`
err := d.db.Exec(query, charID, accountID)
if err != nil {
return fmt.Errorf("failed to delete character: %w", err)
}
// Check if any rows were affected
if d.db.Changes() == 0 {
return fmt.Errorf("character not found or not owned by account")
}
log.Printf("Deleted character %d for account %d", charID, accountID)
return nil
}
// GetWorldServers returns all configured world servers
func (d *Database) GetWorldServers() ([]*WorldServer, error) {
query := `
SELECT id, name, description, ip_address, port, status,
population, locked, hidden, created_date
FROM world_servers
WHERE active = 1
ORDER BY sort_order, name`
var servers []*WorldServer
err := d.db.Query(query, func(row *database.Row) error {
server := &WorldServer{}
server.ID = int32(row.Int64(0))
server.Name = row.Text(1)
server.Description = row.Text(2)
server.IPAddress = row.Text(3)
server.Port = row.Int(4)
server.Status = row.Text(5)
server.Population = int32(row.Int64(6))
server.Locked = row.Bool(7)
server.Hidden = row.Bool(8)
if dateStr := row.Text(9); dateStr != "" {
if t, err := time.Parse("2006-01-02 15:04:05", dateStr); err == nil {
server.CreatedDate = t
}
}
server.Online = server.Status == "online"
server.PopulationLevel = calculatePopulationLevel(server.Population)
servers = append(servers, server)
return nil
})
return servers, err
}
// UpdateWorldServerStats updates world server statistics
func (d *Database) UpdateWorldServerStats(serverID int32, stats *WorldServerStats) error {
query := `
INSERT OR REPLACE INTO world_server_stats
(server_id, timestamp, population, zones_active, players_online, uptime_seconds)
VALUES (?, CURRENT_TIMESTAMP, ?, ?, ?, ?)`
return d.db.Exec(query,
serverID, stats.Population,
stats.ZonesActive, stats.PlayersOnline, stats.UptimeSeconds,
)
}
// CleanupOldEntries removes old log entries and statistics
func (d *Database) CleanupOldEntries() error {
queries := []string{
"DELETE FROM login_attempts WHERE timestamp < datetime('now', '-30 days')",
"DELETE FROM world_server_stats WHERE timestamp < datetime('now', '-7 days')",
"DELETE FROM client_logs WHERE timestamp < datetime('now', '-14 days')",
}
for _, query := range queries {
if err := d.db.Exec(query); err != nil {
log.Printf("Cleanup query failed: %v", err)
}
}
return nil
}
// LogLoginAttempt records a login attempt for security monitoring
func (d *Database) LogLoginAttempt(username, ipAddress string, success bool) error {
query := `
INSERT INTO login_attempts (username, ip_address, success, timestamp)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)`
return d.db.Exec(query, username, ipAddress, success)
}
// GetMaxCharsSetting returns the maximum characters per account
func (d *Database) GetMaxCharsSetting() int32 {
var maxChars int32 = 7 // Default
query := "SELECT value FROM server_settings WHERE name = 'max_characters_per_account'"
row, err := d.db.QueryRow(query)
if err != nil || row == nil {
return maxChars
}
defer row.Close()
if !row.IsNull(0) {
if val := row.Int64(0); val > 0 {
maxChars = int32(val)
}
}
return maxChars
}
// GetAccountBonus returns veteran bonus flags for an account
func (d *Database) GetAccountBonus(accountID int32) uint8 {
var bonus uint8 = 0
query := "SELECT veteran_bonus FROM accounts WHERE id = ?"
row, err := d.db.QueryRow(query, accountID)
if err != nil || row == nil {
return bonus
}
defer row.Close()
bonus = uint8(row.Int(0))
return bonus
}
// calculatePopulationLevel converts population to display level
func calculatePopulationLevel(population int32) uint8 {
switch {
case population >= 1000:
return 3 // Full
case population >= 500:
return 2 // High
case population >= 100:
return 1 // Medium
default:
return 0 // Low
}
}

View File

@ -1,536 +0,0 @@
package main
import (
"encoding/binary"
"eq2emu/internal/common/opcodes"
"eq2emu/internal/packets"
"eq2emu/internal/udp"
"fmt"
"log"
"time"
)
// LoginRequest represents parsed login request data
type LoginRequest struct {
Username string
Password string
Version uint16
}
// opcodeToPacketName maps opcodes to packet definition names
var opcodeToPacketName = map[uint16]string{
opcodes.OpLoginRequestMsg: "LS_LoginRequest",
opcodes.OpAllWSDescRequestMsg: "LS_WorldListRequest",
opcodes.OpAllCharactersDescRequestMsg: "LS_CharacterListRequest",
opcodes.OpCreateCharacterRequestMsg: "LS_CreateCharacterRequest",
opcodes.OpDeleteCharacterRequestMsg: "LS_DeleteCharacterRequest",
opcodes.OpPlayCharacterRequestMsg: "LS_PlayCharacterRequest",
}
// parsePacketWithDefinition parses packet using definitions or fails
func (lc *LoginClient) parsePacketWithDefinition(opcode uint16, data []byte) (map[string]any, error) {
packetName, exists := opcodeToPacketName[opcode]
if !exists {
return nil, fmt.Errorf("no packet name mapping for opcode 0x%04X", opcode)
}
packetDef, exists := packets.GetPacket(packetName)
if !exists {
return nil, fmt.Errorf("no packet definition found for %s", packetName)
}
// Use client version for parsing, default flags
result, err := packetDef.Parse(data, uint32(lc.version), 0)
if err != nil {
return nil, fmt.Errorf("packet parsing failed for %s: %w", packetName, err)
}
return result, nil
}
// parseLoginRequest parses the login request packet data
func (lc *LoginClient) parseLoginRequest(data []byte) (*LoginRequest, error) {
parsed, err := lc.parsePacketWithDefinition(opcodes.OpLoginRequestMsg, data)
if err != nil {
return nil, err
}
req := &LoginRequest{}
if username, ok := parsed["username"].(string); ok {
req.Username = username
}
if password, ok := parsed["password"].(string); ok {
req.Password = password
}
if version, ok := parsed["version"].(uint16); ok {
req.Version = version
}
return req, nil
}
// sendLoginDenied sends login failure response
func (lc *LoginClient) sendLoginDenied() {
data := make([]byte, 12)
data[0] = 1 // reply_code: Invalid username or password
binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF)
binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF)
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpLoginReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
// Disconnect after short delay
time.AfterFunc(1*time.Second, func() {
lc.Disconnect()
})
}
// sendLoginDeniedBadVersion sends bad version response
func (lc *LoginClient) sendLoginDeniedBadVersion() {
data := make([]byte, 12)
data[0] = 6 // reply_code: Version mismatch
binary.LittleEndian.PutUint32(data[4:], 0xFFFFFFFF)
binary.LittleEndian.PutUint32(data[8:], 0xFFFFFFFF)
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpLoginReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
time.AfterFunc(1*time.Second, func() {
lc.Disconnect()
})
}
// sendLoginAccepted sends successful login response
func (lc *LoginClient) sendLoginAccepted() {
config := lc.server.GetConfig()
// Build login response packet
data := make([]byte, 64) // Base size, will expand as needed
offset := 0
// Account ID
binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID))
offset += 4
// Login response code (0 = success)
data[offset] = 0
offset++
// Do not force SOGA flag
data[offset] = 1
offset++
// Subscription level
binary.LittleEndian.PutUint32(data[offset:], config.DefaultSubscriptionLevel)
offset += 4
// Race flags (enabled races)
binary.LittleEndian.PutUint32(data[offset:], 0x1FFFFF)
offset += 4
// Class flags (enabled classes)
binary.LittleEndian.PutUint32(data[offset:], 0x7FFFFFE)
offset += 4
// Username (16-bit string)
username := lc.account.Username
binary.LittleEndian.PutUint16(data[offset:], uint16(len(username)))
offset += 2
copy(data[offset:], username)
offset += len(username)
// Expansion flags
binary.LittleEndian.PutUint16(data[offset:], config.ExpansionFlag)
offset += 2
// Additional flags
data[offset] = 0xFF
data[offset+1] = 0xFF
data[offset+2] = 0xFF
offset += 3
// Class access flag
data[offset] = 0xFF
offset++
// Enabled races
binary.LittleEndian.PutUint32(data[offset:], config.EnabledRaces)
offset += 4
// Cities flag
data[offset] = config.CitiesFlag
offset++
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpLoginReplyMsg,
Data: data[:offset],
}
lc.connection.SendPacket(packet)
}
// sendWorldList sends available world servers to client
func (lc *LoginClient) sendWorldList() {
worlds := lc.server.worldList.GetActiveWorlds()
// Build world list packet
data := make([]byte, 0, 1024)
// Number of worlds
worldCount := uint8(len(worlds))
data = append(data, worldCount)
for _, world := range worlds {
// World ID
worldID := make([]byte, 4)
binary.LittleEndian.PutUint32(worldID, uint32(world.ID))
data = append(data, worldID...)
// World name (16-bit string)
nameLen := make([]byte, 2)
binary.LittleEndian.PutUint16(nameLen, uint16(len(world.Name)))
data = append(data, nameLen...)
data = append(data, []byte(world.Name)...)
// World status flags
var flags uint8
if world.Online {
flags |= 0x01
}
if world.Locked {
flags |= 0x02
}
if world.Hidden {
flags |= 0x04
}
data = append(data, flags)
// Population (0-3, where 3 = full)
data = append(data, world.PopulationLevel)
}
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpWorldListMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}
// sendCharacterList sends character list to client
func (lc *LoginClient) sendCharacterList() {
if lc.account == nil {
return
}
data := make([]byte, 0, 2048)
// Number of characters
charCount := uint8(len(lc.account.Characters))
data = append(data, charCount)
// Character data
for _, char := range lc.account.Characters {
if char.Deleted {
continue
}
// Character ID
charID := make([]byte, 4)
binary.LittleEndian.PutUint32(charID, uint32(char.ID))
data = append(data, charID...)
// Server ID
serverID := make([]byte, 4)
binary.LittleEndian.PutUint32(serverID, uint32(char.ServerID))
data = append(data, serverID...)
// Character name (16-bit string)
nameLen := make([]byte, 2)
binary.LittleEndian.PutUint16(nameLen, uint16(len(char.Name)))
data = append(data, nameLen...)
data = append(data, []byte(char.Name)...)
// Character stats
data = append(data, byte(char.Race))
data = append(data, byte(char.Gender))
data = append(data, byte(char.Class))
data = append(data, byte(char.Level))
// Creation timestamp
timestamp := make([]byte, 4)
binary.LittleEndian.PutUint32(timestamp, uint32(char.CreatedDate.Unix()))
data = append(data, timestamp...)
}
// Account info
accountID := make([]byte, 4)
binary.LittleEndian.PutUint32(accountID, uint32(lc.account.ID))
data = append(data, accountID...)
// Max characters
data = append(data, 0xFF, 0xFF, 0xFF, 0xFF) // unknown1
data = append(data, 0x00, 0x00) // unknown2
data = append(data, 0x07, 0x00, 0x00, 0x00) // max chars (7)
data = append(data, 0x00) // unknown4
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpAllCharactersDescReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}
// handleCharacterCreateRequest processes character creation
func (lc *LoginClient) handleCharacterCreateRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
parsed, err := lc.parsePacketWithDefinition(opcodes.OpCreateCharacterRequestMsg, packet.Data)
if err != nil {
log.Printf("Failed to parse character create request: %v", err)
lc.sendCharacterCreateFailed(1)
return
}
serverID, ok := parsed["server_id"].(uint32)
if !ok {
lc.sendCharacterCreateFailed(1)
return
}
name, ok := parsed["character_name"].(string)
if !ok {
lc.sendCharacterCreateFailed(1)
return
}
// Validate character name
if len(name) < 3 || len(name) > 20 {
lc.sendCharacterCreateFailed(9) // Bad name length
return
}
// Check if name is taken
exists, err := lc.server.database.CharacterNameExists(name, int32(serverID))
if err != nil {
log.Printf("Error checking character name: %v", err)
lc.sendCharacterCreateFailed(1)
return
}
if exists {
lc.sendCharacterCreateFailed(12) // Name taken
return
}
// Create character in database
char := &Character{
AccountID: lc.account.ID,
ServerID: int32(serverID),
Name: name,
Level: 1,
Race: 1, // Would be parsed from packet
Gender: 1, // Would be parsed from packet
Class: 1, // Would be parsed from packet
CreatedDate: time.Now(),
}
charID, err := lc.server.database.CreateCharacter(char)
if err != nil {
log.Printf("Error creating character: %v", err)
lc.sendCharacterCreateFailed(1)
return
}
char.ID = charID
lc.account.Characters = append(lc.account.Characters, char)
lc.sendCharacterCreateSuccess(char)
lc.sendCharacterList() // Refresh character list
}
// sendCharacterCreateSuccess sends successful character creation response
func (lc *LoginClient) sendCharacterCreateSuccess(char *Character) {
data := make([]byte, 64)
offset := 0
// Account ID
binary.LittleEndian.PutUint32(data[offset:], uint32(lc.account.ID))
offset += 4
// Response code (0 = success)
binary.LittleEndian.PutUint32(data[offset:], 0)
offset += 4
// Character name
nameLen := uint16(len(char.Name))
binary.LittleEndian.PutUint16(data[offset:], nameLen)
offset += 2
copy(data[offset:], char.Name)
offset += int(nameLen)
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpCreateCharacterReplyMsg,
Data: data[:offset],
}
lc.connection.SendPacket(packet)
}
// sendCharacterCreateFailed sends character creation failure response
func (lc *LoginClient) sendCharacterCreateFailed(reason uint8) {
data := make([]byte, 16)
binary.LittleEndian.PutUint32(data[0:], uint32(lc.account.ID))
data[4] = reason
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpCreateCharacterReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}
// handleCharacterDeleteRequest processes character deletion
func (lc *LoginClient) handleCharacterDeleteRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
parsed, err := lc.parsePacketWithDefinition(opcodes.OpDeleteCharacterRequestMsg, packet.Data)
if err != nil {
log.Printf("Failed to parse character delete request: %v", err)
return
}
charID, ok := parsed["character_id"].(uint32)
if !ok {
return
}
serverID, ok := parsed["server_id"].(uint32)
if !ok {
return
}
// Verify character belongs to this account
var char *Character
for _, c := range lc.account.Characters {
if c.ID == int32(charID) && c.ServerID == int32(serverID) {
char = c
break
}
}
if char == nil {
log.Printf("Account %d attempted to delete character %d that doesn't belong to them", lc.account.ID, charID)
return
}
// Mark character as deleted
err = lc.server.database.DeleteCharacter(int32(charID), lc.account.ID)
if err != nil {
log.Printf("Error deleting character: %v", err)
return
}
char.Deleted = true
// Send deletion response
data := make([]byte, 24)
data[0] = 1 // Success
binary.LittleEndian.PutUint32(data[4:], serverID)
binary.LittleEndian.PutUint32(data[8:], charID)
binary.LittleEndian.PutUint32(data[12:], uint32(lc.account.ID))
responsePacket := &udp.ApplicationPacket{
Opcode: opcodes.OpDeleteCharacterReplyMsg,
Data: data,
}
lc.connection.SendPacket(responsePacket)
lc.sendCharacterList() // Refresh character list
}
// handlePlayCharacterRequest processes character selection for gameplay
func (lc *LoginClient) handlePlayCharacterRequest(packet *udp.ApplicationPacket) {
if !lc.authenticated {
lc.Disconnect()
return
}
parsed, err := lc.parsePacketWithDefinition(opcodes.OpPlayCharacterRequestMsg, packet.Data)
if err != nil {
log.Printf("Failed to parse play character request: %v", err)
lc.sendPlayFailed(1)
return
}
charID, ok := parsed["character_id"].(uint32)
if !ok {
lc.sendPlayFailed(1)
return
}
serverID, ok := parsed["server_id"].(uint32)
if !ok {
lc.sendPlayFailed(1)
return
}
// Find world server
world := lc.server.worldList.GetWorld(int32(serverID))
if world == nil || !world.Online {
lc.sendPlayFailed(2) // Server unavailable
return
}
// Verify character ownership
var char *Character
for _, c := range lc.account.Characters {
if c.ID == int32(charID) && c.ServerID == int32(serverID) {
char = c
break
}
}
if char == nil {
lc.sendPlayFailed(1) // Character not found
return
}
lc.pendingPlayCharID = int32(charID)
// Send play request to world server
err = lc.server.worldList.SendPlayRequest(world, lc.account.ID, int32(charID))
if err != nil {
log.Printf("Error sending play request to world server: %v", err)
lc.sendPlayFailed(1)
return
}
// World server will respond with connection details
log.Printf("Account %s requesting to play character %s on server %s",
lc.account.Username, char.Name, world.Name)
}
// sendPlayFailed sends play character failure response
func (lc *LoginClient) sendPlayFailed(reason uint8) {
data := make([]byte, 16)
data[0] = reason
binary.LittleEndian.PutUint32(data[4:], uint32(lc.account.ID))
packet := &udp.ApplicationPacket{
Opcode: opcodes.OpPlayCharacterReplyMsg,
Data: data,
}
lc.connection.SendPacket(packet)
}

View File

@ -1,212 +0,0 @@
package main
import (
"eq2emu/internal/packets"
"eq2emu/internal/udp"
"fmt"
"log"
"sync"
"time"
)
// LoginServer manages the main login server functionality
type LoginServer struct {
config *Config
server *udp.Server
database *Database
worldList *WorldList
webServer *WebServer
clients map[string]*LoginClient
clientMutex sync.RWMutex
// Statistics
stats struct {
ConnectionCount int32
LoginAttempts int32
SuccessfulLogins int32
StartTime time.Time
}
}
// NewLoginServer creates a new login server instance
func NewLoginServer(config *Config) (*LoginServer, error) {
ls := &LoginServer{
config: config,
clients: make(map[string]*LoginClient),
}
ls.stats.StartTime = time.Now()
// Initialize packet definitions
log.Printf("Loaded %d packet definitions", packets.GetPacketCount())
// Initialize database
db, err := NewDatabase(config.Database)
if err != nil {
return nil, fmt.Errorf("database initialization failed: %w", err)
}
ls.database = db
// Initialize world list
ls.worldList = NewWorldList(db)
// Create UDP server with login packet handler
udpConfig := udp.DefaultConfig()
udpConfig.MaxConnections = config.MaxConnections
udpConfig.Timeout = time.Duration(config.TimeoutSeconds) * time.Second
udpConfig.EnableCompression = config.EnableCompression
udpConfig.EnableEncryption = config.EnableEncryption
server, err := udp.NewServer(fmt.Sprintf(":%d", config.Port), ls.handlePacket, udpConfig)
if err != nil {
return nil, fmt.Errorf("UDP server creation failed: %w", err)
}
ls.server = server
// Initialize web server if configured
if config.WebServer.Enabled {
webServer, err := NewWebServer(config.WebServer, ls)
if err != nil {
log.Printf("Web server initialization failed: %v", err)
} else {
ls.webServer = webServer
}
}
return ls, nil
}
// Start begins accepting connections and processing packets
func (ls *LoginServer) Start() error {
log.Println("Starting login server components...")
// Start world list monitoring
go ls.worldList.Start()
// Start web server if configured
if ls.webServer != nil {
go ls.webServer.Start()
}
// Start UDP server
return ls.server.Start()
}
// Stop gracefully shuts down the server
func (ls *LoginServer) Stop() {
log.Println("Stopping login server...")
if ls.webServer != nil {
ls.webServer.Stop()
}
ls.worldList.Stop()
ls.server.Stop()
ls.database.Close()
}
// handlePacket processes incoming packets from clients
func (ls *LoginServer) handlePacket(conn *udp.Connection, packet *udp.ApplicationPacket) {
clientKey := conn.GetSessionID()
// Get or create client
ls.clientMutex.Lock()
client, exists := ls.clients[fmt.Sprintf("%d", clientKey)]
if !exists {
client = NewLoginClient(conn, ls)
ls.clients[fmt.Sprintf("%d", clientKey)] = client
}
ls.clientMutex.Unlock()
// Process packet
client.ProcessPacket(packet)
}
// RemoveClient removes a client from the active clients list
func (ls *LoginServer) RemoveClient(sessionID string) {
ls.clientMutex.Lock()
delete(ls.clients, sessionID)
ls.clientMutex.Unlock()
}
// UpdateStats updates server statistics
func (ls *LoginServer) UpdateStats() {
ls.clientMutex.RLock()
ls.stats.ConnectionCount = int32(len(ls.clients))
ls.clientMutex.RUnlock()
// Update world server statistics
ls.worldList.UpdateStats()
// Clean up old database entries
ls.database.CleanupOldEntries()
log.Printf("Stats: %d connections, %d login attempts, %d successful logins",
ls.stats.ConnectionCount, ls.stats.LoginAttempts, ls.stats.SuccessfulLogins)
}
// CleanupStaleConnections removes inactive connections
func (ls *LoginServer) CleanupStaleConnections() {
var staleClients []string
ls.clientMutex.RLock()
for sessionID, client := range ls.clients {
if client.IsStale() {
staleClients = append(staleClients, sessionID)
}
}
ls.clientMutex.RUnlock()
ls.clientMutex.Lock()
for _, sessionID := range staleClients {
if client, exists := ls.clients[sessionID]; exists {
client.Disconnect()
delete(ls.clients, sessionID)
}
}
ls.clientMutex.Unlock()
if len(staleClients) > 0 {
log.Printf("Cleaned up %d stale connections", len(staleClients))
}
}
// GetStats returns current server statistics
func (ls *LoginServer) GetStats() map[string]any {
ls.clientMutex.RLock()
connectionCount := len(ls.clients)
ls.clientMutex.RUnlock()
return map[string]any{
"connection_count": connectionCount,
"login_attempts": ls.stats.LoginAttempts,
"successful_logins": ls.stats.SuccessfulLogins,
"uptime_seconds": int(time.Since(ls.stats.StartTime).Seconds()),
"world_server_count": ls.worldList.GetActiveCount(),
}
}
// IncrementLoginAttempts atomically increments login attempt counter
func (ls *LoginServer) IncrementLoginAttempts() {
ls.stats.LoginAttempts++
}
// IncrementSuccessfulLogins atomically increments successful login counter
func (ls *LoginServer) IncrementSuccessfulLogins() {
ls.stats.SuccessfulLogins++
}
// GetDatabase returns the database instance
func (ls *LoginServer) GetDatabase() *Database {
return ls.database
}
// GetWorldList returns the world list instance
func (ls *LoginServer) GetWorldList() *WorldList {
return ls.worldList
}
// GetConfig returns the server configuration
func (ls *LoginServer) GetConfig() *Config {
return ls.config
}

View File

@ -1,74 +0,0 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
)
var (
loginServer *LoginServer
runLoops = true
)
func main() {
// Initialize logging
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("EQ2Emulator Login Server Starting...")
// Load configuration
config, err := LoadConfig("login_config.json")
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize login server
loginServer, err = NewLoginServer(config)
if err != nil {
log.Fatalf("Failed to create login server: %v", err)
}
// Setup signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start the server
go func() {
if err := loginServer.Start(); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Start maintenance routines
go startMaintenanceTimers()
log.Printf("Login server listening on port %d", config.Port)
// Wait for shutdown signal
<-sigChan
log.Println("Shutdown signal received, stopping server...")
runLoops = false
loginServer.Stop()
log.Println("Server stopped.")
}
// startMaintenanceTimers starts periodic maintenance tasks
func startMaintenanceTimers() {
statsTicker := time.NewTicker(60 * time.Second)
cleanupTicker := time.NewTicker(5 * time.Minute)
defer statsTicker.Stop()
defer cleanupTicker.Stop()
for runLoops {
select {
case <-statsTicker.C:
loginServer.UpdateStats()
case <-cleanupTicker.C:
loginServer.CleanupStaleConnections()
}
}
}

View File

@ -1,143 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// WebServer provides HTTP endpoints for monitoring
type WebServer struct {
config WebServerConfig
loginServer *LoginServer
server *http.Server
}
// NewWebServer creates a new web monitoring server
func NewWebServer(config WebServerConfig, loginServer *LoginServer) (*WebServer, error) {
ws := &WebServer{
config: config,
loginServer: loginServer,
}
mux := http.NewServeMux()
mux.HandleFunc("/status", ws.handleStatus)
mux.HandleFunc("/worlds", ws.handleWorlds)
mux.HandleFunc("/stats", ws.handleStats)
mux.HandleFunc("/health", ws.handleHealth)
ws.server = &http.Server{
Addr: fmt.Sprintf("%s:%d", config.Address, config.Port),
Handler: ws.basicAuth(mux),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
return ws, nil
}
// Start begins the web server
func (ws *WebServer) Start() {
log.Printf("Starting web server on %s", ws.server.Addr)
var err error
if ws.config.CertFile != "" && ws.config.KeyFile != "" {
err = ws.server.ListenAndServeTLS(ws.config.CertFile, ws.config.KeyFile)
} else {
err = ws.server.ListenAndServe()
}
if err != http.ErrServerClosed {
log.Printf("Web server error: %v", err)
}
}
// Stop shuts down the web server
func (ws *WebServer) Stop() {
if ws.server != nil {
ws.server.Close()
}
}
// basicAuth provides basic HTTP authentication
func (ws *WebServer) basicAuth(next http.Handler) http.Handler {
if ws.config.Username == "" {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != ws.config.Username || password != ws.config.Password {
w.Header().Set("WWW-Authenticate", `Basic realm="Login Server"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
return
}
next.ServeHTTP(w, r)
})
}
// handleStatus returns server status information
func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) {
stats := ws.loginServer.GetStats()
status := map[string]any{
"service": "eq2emu-login-server",
"version": "1.0.0",
"status": "running",
"timestamp": time.Now().UTC(),
"statistics": stats,
}
ws.writeJSON(w, status)
}
// handleWorlds returns world server information
func (ws *WebServer) handleWorlds(w http.ResponseWriter, r *http.Request) {
worlds := ws.loginServer.worldList.GetActiveWorlds()
worldStats := ws.loginServer.worldList.GetStats()
response := map[string]any{
"world_servers": worlds,
"statistics": worldStats,
}
ws.writeJSON(w, response)
}
// handleStats returns detailed server statistics
func (ws *WebServer) handleStats(w http.ResponseWriter, r *http.Request) {
serverStats := ws.loginServer.GetStats()
worldStats := ws.loginServer.worldList.GetStats()
stats := map[string]any{
"server": serverStats,
"worlds": worldStats,
"timestamp": time.Now().UTC(),
}
ws.writeJSON(w, stats)
}
// handleHealth returns basic health check
func (ws *WebServer) handleHealth(w http.ResponseWriter, r *http.Request) {
health := map[string]any{
"status": "healthy",
"timestamp": time.Now().UTC(),
}
ws.writeJSON(w, health)
}
// writeJSON writes JSON response with proper headers
func (ws *WebServer) writeJSON(w http.ResponseWriter, data any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache")
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("JSON encoding error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}

View File

@ -1,356 +0,0 @@
package main
import (
"fmt"
"log"
"sync"
"time"
)
// WorldServer represents a game world server
type WorldServer struct {
ID int32
Name string
Description string
IPAddress string
Port int
Status string
Population int32
PopulationLevel uint8
Locked bool
Hidden bool
Online bool
CreatedDate time.Time
LastUpdate time.Time
}
// WorldServerStats holds runtime statistics
type WorldServerStats struct {
Population int32
ZonesActive int32
PlayersOnline int32
UptimeSeconds int64
}
// WorldList manages all world servers
type WorldList struct {
servers map[int32]*WorldServer
mutex sync.RWMutex
database *Database
updateTicker *time.Ticker
stopChan chan struct{}
}
// NewWorldList creates a new world list manager
func NewWorldList(database *Database) *WorldList {
return &WorldList{
servers: make(map[int32]*WorldServer),
database: database,
stopChan: make(chan struct{}),
}
}
// Start begins world server monitoring
func (wl *WorldList) Start() {
log.Println("Starting world list monitoring...")
// Load world servers from database
if err := wl.LoadFromDatabase(); err != nil {
log.Printf("Failed to load world servers: %v", err)
}
// Start periodic updates
wl.updateTicker = time.NewTicker(30 * time.Second)
go func() {
for {
select {
case <-wl.updateTicker.C:
wl.UpdateStats()
case <-wl.stopChan:
return
}
}
}()
}
// Stop shuts down world server monitoring
func (wl *WorldList) Stop() {
log.Println("Stopping world list monitoring...")
if wl.updateTicker != nil {
wl.updateTicker.Stop()
}
close(wl.stopChan)
}
// LoadFromDatabase loads world servers from the database
func (wl *WorldList) LoadFromDatabase() error {
servers, err := wl.database.GetWorldServers()
if err != nil {
return fmt.Errorf("failed to load world servers: %w", err)
}
wl.mutex.Lock()
defer wl.mutex.Unlock()
// Clear existing servers
wl.servers = make(map[int32]*WorldServer)
// Add loaded servers
for _, server := range servers {
wl.servers[server.ID] = server
log.Printf("Loaded world server: %s (ID: %d)", server.Name, server.ID)
}
log.Printf("Loaded %d world servers", len(servers))
return nil
}
// GetActiveWorlds returns all online world servers
func (wl *WorldList) GetActiveWorlds() []*WorldServer {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
var active []*WorldServer
for _, server := range wl.servers {
if server.Online && !server.Hidden {
active = append(active, server)
}
}
return active
}
// GetWorld returns a specific world server by ID
func (wl *WorldList) GetWorld(id int32) *WorldServer {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
return wl.servers[id]
}
// GetActiveCount returns the number of online world servers
func (wl *WorldList) GetActiveCount() int {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
count := 0
for _, server := range wl.servers {
if server.Online {
count++
}
}
return count
}
// UpdateServerStatus updates a world server's status
func (wl *WorldList) UpdateServerStatus(id int32, online bool, population int32, locked bool) {
wl.mutex.Lock()
defer wl.mutex.Unlock()
server, exists := wl.servers[id]
if !exists {
log.Printf("Attempted to update unknown server ID: %d", id)
return
}
server.Online = online
server.Population = population
server.Locked = locked
server.LastUpdate = time.Now()
// Update population level
server.PopulationLevel = wl.calculatePopulationLevel(population)
if online {
server.Status = "online"
} else {
server.Status = "offline"
}
log.Printf("Updated server %s: online=%t, population=%d, locked=%t",
server.Name, online, population, locked)
}
// calculatePopulationLevel converts population to display level
func (wl *WorldList) calculatePopulationLevel(population int32) uint8 {
switch {
case population >= 1000:
return 3 // Full
case population >= 500:
return 2 // High
case population >= 100:
return 1 // Medium
default:
return 0 // Low
}
}
// UpdateStats updates world server statistics
func (wl *WorldList) UpdateStats() {
wl.mutex.RLock()
servers := make([]*WorldServer, 0, len(wl.servers))
for _, server := range wl.servers {
servers = append(servers, server)
}
wl.mutex.RUnlock()
// Update statistics for each server
for _, server := range servers {
if server.Online {
stats := &WorldServerStats{
Population: server.Population,
ZonesActive: 0, // Would be updated by world server
PlayersOnline: server.Population,
UptimeSeconds: int64(time.Since(server.LastUpdate).Seconds()),
}
if err := wl.database.UpdateWorldServerStats(server.ID, stats); err != nil {
log.Printf("Failed to update stats for server %d: %v", server.ID, err)
}
}
}
}
// SendPlayRequest sends a character play request to a world server
func (wl *WorldList) SendPlayRequest(world *WorldServer, accountID, charID int32) error {
// In a real implementation, this would establish communication with the world server
// and send the play request. For now, we'll simulate the response.
log.Printf("Sending play request to world server %s for account %d, character %d",
world.Name, accountID, charID)
// Simulate world server response after a short delay
go func() {
time.Sleep(100 * time.Millisecond)
// For demonstration, we'll always succeed
// In reality, the world server would validate the character and respond
accessKey := generateAccessKey()
// This would normally come from the world server's response
wl.HandlePlayResponse(world.ID, accountID, charID, true,
world.IPAddress, world.Port, accessKey)
}()
return nil
}
// HandlePlayResponse processes a play response from a world server
func (wl *WorldList) HandlePlayResponse(worldID, accountID, charID int32,
success bool, ipAddress string, port int, accessKey int32) {
// Find the client that requested this
// This is simplified - in reality you'd track pending requests
log.Printf("Play response from world %d: success=%t, access_key=%d",
worldID, success, accessKey)
// Send response to appropriate client
// This would need to be implemented with proper client tracking
}
// generateAccessKey generates a random access key for world server connections
func generateAccessKey() int32 {
return int32(time.Now().UnixNano() & 0x7FFFFFFF)
}
// AddServer adds a new world server (for dynamic registration)
func (wl *WorldList) AddServer(server *WorldServer) {
wl.mutex.Lock()
defer wl.mutex.Unlock()
wl.servers[server.ID] = server
log.Printf("Added world server: %s (ID: %d)", server.Name, server.ID)
}
// RemoveServer removes a world server
func (wl *WorldList) RemoveServer(id int32) {
wl.mutex.Lock()
defer wl.mutex.Unlock()
if server, exists := wl.servers[id]; exists {
delete(wl.servers, id)
log.Printf("Removed world server: %s (ID: %d)", server.Name, id)
}
}
// GetServerList returns a formatted server list for client packets
func (wl *WorldList) GetServerList() []byte {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
// Build server list packet data
data := make([]byte, 0, 1024)
// Count active servers
activeCount := 0
for _, server := range wl.servers {
if !server.Hidden {
activeCount++
}
}
// Add server count
data = append(data, byte(activeCount))
// Add server data
for _, server := range wl.servers {
if server.Hidden {
continue
}
// Server ID (4 bytes)
serverIDBytes := make([]byte, 4)
serverIDBytes[0] = byte(server.ID)
serverIDBytes[1] = byte(server.ID >> 8)
serverIDBytes[2] = byte(server.ID >> 16)
serverIDBytes[3] = byte(server.ID >> 24)
data = append(data, serverIDBytes...)
// Server name (null-terminated)
data = append(data, []byte(server.Name)...)
data = append(data, 0)
// Server flags
var flags byte
if server.Online {
flags |= 0x01
}
if server.Locked {
flags |= 0x02
}
data = append(data, flags)
// Population level
data = append(data, server.PopulationLevel)
}
return data
}
// GetStats returns world list statistics
func (wl *WorldList) GetStats() map[string]any {
wl.mutex.RLock()
defer wl.mutex.RUnlock()
totalServers := len(wl.servers)
onlineServers := 0
totalPopulation := int32(0)
for _, server := range wl.servers {
if server.Online {
onlineServers++
totalPopulation += server.Population
}
}
return map[string]any{
"total_servers": totalServers,
"online_servers": onlineServers,
"offline_servers": totalServers - onlineServers,
"total_population": totalPopulation,
}
}

1
cmd/world_server/TODO.md Normal file
View File

@ -0,0 +1 @@
Need to implement

View File

@ -1,137 +0,0 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
)
const (
ConfigFile = "world_config.json"
Version = "0.1.0"
)
// printHeader displays the EQ2Emu banner and copyright info
func printHeader() {
fmt.Println("EQ2Emulator World Server")
fmt.Printf("Version: %s\n", Version)
fmt.Println()
fmt.Println("Copyright (C) 2007-2026 EQ2Emulator Development Team")
fmt.Println("https://www.eq2emu.com")
fmt.Println()
fmt.Println("EQ2Emulator is free software licensed under the GNU GPL v3")
fmt.Println("See LICENSE file for details")
fmt.Println()
}
// loadConfig loads configuration from JSON file with command line overrides
func loadConfig() (*WorldConfig, error) {
// Default configuration
config := &WorldConfig{
ListenAddr: "0.0.0.0",
ListenPort: 9000,
MaxClients: 1000,
BufferSize: 8192,
WebAddr: "0.0.0.0",
WebPort: 8080,
DatabasePath: "world.db",
XPRate: 1.0,
TSXPRate: 1.0,
VitalityRate: 1.0,
LogLevel: "info",
ThreadedLoad: true,
}
// Load from config file if it exists
if data, err := os.ReadFile(ConfigFile); err == nil {
if err := json.Unmarshal(data, config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)
}
log.Printf("Loaded configuration from %s", ConfigFile)
} else {
log.Printf("Config file %s not found, using defaults", ConfigFile)
}
// Command line overrides
flag.StringVar(&config.ListenAddr, "listen-addr", config.ListenAddr, "UDP listen address")
flag.IntVar(&config.ListenPort, "listen-port", config.ListenPort, "UDP listen port")
flag.IntVar(&config.MaxClients, "max-clients", config.MaxClients, "Maximum client connections")
flag.StringVar(&config.WebAddr, "web-addr", config.WebAddr, "Web server address")
flag.IntVar(&config.WebPort, "web-port", config.WebPort, "Web server port")
flag.StringVar(&config.DatabasePath, "db-path", config.DatabasePath, "Database file path")
flag.StringVar(&config.LogLevel, "log-level", config.LogLevel, "Log level (debug, info, warn, error)")
flag.BoolVar(&config.ThreadedLoad, "threaded-load", config.ThreadedLoad, "Use threaded loading")
flag.Parse()
return config, nil
}
// saveConfig saves the current configuration to file
func saveConfig(config *WorldConfig) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(ConfigFile, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
// setupSignalHandlers sets up graceful shutdown on SIGINT/SIGTERM
func setupSignalHandlers(world *World) <-chan os.Signal {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
log.Printf("Received signal %v, initiating graceful shutdown...", sig)
world.Shutdown()
}()
return sigChan
}
func main() {
printHeader()
// Load configuration
config, err := loadConfig()
if err != nil {
log.Fatalf("Configuration error: %v", err)
}
// Save config file with any command line overrides
if err := saveConfig(config); err != nil {
log.Printf("Warning: failed to save config: %v", err)
}
// Create world server instance
world, err := NewWorld(config)
if err != nil {
log.Fatalf("Failed to create world server: %v", err)
}
// Initialize all components
log.Println("Initializing EQ2Emulator World Server...")
if err := world.Initialize(); err != nil {
log.Fatalf("Failed to initialize world server: %v", err)
}
// Setup signal handlers for graceful shutdown
setupSignalHandlers(world)
// Run the server
log.Println("Starting World Server...")
if err := world.Run(); err != nil {
log.Fatalf("World server error: %v", err)
}
log.Println("World Server stopped gracefully")
}

View File

@ -1,298 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// setupWebServer initializes the HTTP server for admin interface
func (w *World) setupWebServer() error {
if w.config.WebPort == 0 {
return nil // Web server disabled
}
mux := http.NewServeMux()
// API endpoints
mux.HandleFunc("/api/status", w.handleStatus)
mux.HandleFunc("/api/clients", w.handleClients)
mux.HandleFunc("/api/zones", w.handleZones)
mux.HandleFunc("/api/stats", w.handleStats)
mux.HandleFunc("/api/time", w.handleWorldTime)
mux.HandleFunc("/api/shutdown", w.handleShutdown)
// Administrative endpoints
mux.HandleFunc("/api/admin/reload", w.handleReload)
mux.HandleFunc("/api/admin/broadcast", w.handleBroadcast)
mux.HandleFunc("/api/admin/kick", w.handleKickClient)
// Peer management endpoints
mux.HandleFunc("/api/peers", w.handlePeers)
mux.HandleFunc("/api/peers/sync", w.handlePeerSync)
// Console command interface
mux.HandleFunc("/api/console", w.handleConsoleCommand)
// Static health check
mux.HandleFunc("/health", w.handleHealth)
// @TODO: Add authentication middleware
// @TODO: Add rate limiting middleware
// @TODO: Add CORS middleware for browser access
// @TODO: Add TLS support with cert/key files
addr := fmt.Sprintf("%s:%d", w.config.WebAddr, w.config.WebPort)
w.webServer = &http.Server{
Addr: addr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
return nil
}
// Core API handlers
// handleHealth provides a simple health check endpoint
func (w *World) handleHealth(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "ok"})
}
// handleStatus returns comprehensive server status information
func (w *World) handleStatus(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
status := map[string]any{
"status": "running",
"uptime": time.Since(w.stats.StartTime).Seconds(),
"version": Version,
"locked": w.config.WorldLocked,
"primary": w.config.IsPrimary,
"threaded": w.config.ThreadedLoad,
"data_loaded": w.isDataLoaded(),
"world_time": w.getWorldTime(),
}
json.NewEncoder(rw).Encode(status)
}
// handleClients returns list of connected clients
func (w *World) handleClients(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
w.clientsMux.RLock()
clients := make([]*ClientInfo, 0, len(w.clients))
for _, client := range w.clients {
clients = append(clients, client)
}
w.clientsMux.RUnlock()
json.NewEncoder(rw).Encode(map[string]any{
"count": len(clients),
"clients": clients,
})
}
// handleZones returns list of zone servers
func (w *World) handleZones(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
w.zonesMux.RLock()
zones := make([]*ZoneInfo, 0, len(w.zones))
for _, zone := range w.zones {
zones = append(zones, zone)
}
w.zonesMux.RUnlock()
json.NewEncoder(rw).Encode(map[string]any{
"count": len(zones),
"zones": zones,
})
}
// handleStats returns detailed server statistics
func (w *World) handleStats(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
w.statsMux.RLock()
stats := w.stats
w.statsMux.RUnlock()
// Add UDP server stats if available
if w.udpServer != nil {
serverStats := w.udpServer.GetStats()
stats.TotalConnections = int64(serverStats.ConnectionCount)
}
json.NewEncoder(rw).Encode(stats)
}
// handleWorldTime returns current game world time
func (w *World) handleWorldTime(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(w.getWorldTime())
}
// Administrative handlers
// handleShutdown initiates graceful server shutdown
func (w *World) handleShutdown(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Add confirmation parameter
// @TODO: Add delay parameter
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "shutdown initiated"})
go func() {
time.Sleep(time.Second) // Allow response to be sent
w.Shutdown()
}()
}
// handleReload reloads game data
func (w *World) handleReload(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Implement selective reloading (items, spells, quests, etc.)
// @TODO: Add progress reporting
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "reload not implemented"})
}
// handleBroadcast sends server-wide message
func (w *World) handleBroadcast(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Parse message from request body
// @TODO: Validate message content
// @TODO: Send to all connected clients
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "broadcast not implemented"})
}
// handleKickClient disconnects a specific client
func (w *World) handleKickClient(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Parse client ID from request
// @TODO: Find and disconnect client
// @TODO: Log kick action
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "kick not implemented"})
}
// Peer management handlers
// handlePeers returns list of peer servers
func (w *World) handlePeers(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Content-Type", "application/json")
peers := make([]map[string]any, 0)
for _, peer := range w.config.PeerServers {
peerInfo := map[string]any{
"address": peer.Address,
"port": peer.Port,
"status": "unknown", // @TODO: Implement peer status checking
}
peers = append(peers, peerInfo)
}
json.NewEncoder(rw).Encode(map[string]any{
"count": len(peers),
"peers": peers,
})
}
// handlePeerSync synchronizes data with peer servers
func (w *World) handlePeerSync(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Implement peer synchronization
// @TODO: Return sync status and results
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "peer sync not implemented"})
}
// Console command handler
// handleConsoleCommand executes administrative commands
func (w *World) handleConsoleCommand(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// @TODO: Add authentication check
// @TODO: Parse command from request body
// @TODO: Validate command permissions
// @TODO: Execute command and return results
// @TODO: Log command execution
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(map[string]string{"status": "console commands not implemented"})
}
// Helper methods for web handlers
// getWorldTime returns thread-safe copy of world time
func (w *World) getWorldTime() WorldTime {
w.worldTimeMux.RLock()
defer w.worldTimeMux.RUnlock()
return w.worldTime
}
// startWebServer starts the web server in a goroutine
func (w *World) startWebServer() {
if w.webServer == nil {
return
}
go func() {
if err := w.webServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("Web server error: %v\n", err)
}
}()
}
// stopWebServer gracefully stops the web server
func (w *World) stopWebServer() error {
if w.webServer == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return w.webServer.Shutdown(ctx)
}

View File

@ -1,830 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"sync"
"time"
"eq2emu/internal/database"
"eq2emu/internal/udp"
)
// WorldTime represents the in-game time
type WorldTime struct {
Year int32 `json:"year"`
Month int32 `json:"month"`
Day int32 `json:"day"`
Hour int32 `json:"hour"`
Minute int32 `json:"minute"`
}
// WorldConfig holds all world server configuration
type WorldConfig struct {
// Network settings
ListenAddr string `json:"listen_addr"`
ListenPort int `json:"listen_port"`
MaxClients int `json:"max_clients"`
BufferSize int `json:"buffer_size"`
// Web server settings
WebAddr string `json:"web_addr"`
WebPort int `json:"web_port"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
KeyPassword string `json:"key_password"`
WebUser string `json:"web_user"`
WebPassword string `json:"web_password"`
// Database settings
DatabasePath string `json:"database_path"`
// Game settings
XPRate float64 `json:"xp_rate"`
TSXPRate float64 `json:"ts_xp_rate"`
VitalityRate float64 `json:"vitality_rate"`
// Server settings
LogLevel string `json:"log_level"`
ThreadedLoad bool `json:"threaded_load"`
WorldLocked bool `json:"world_locked"`
IsPrimary bool `json:"is_primary"`
// Login server settings
LoginServers []LoginServerInfo `json:"login_servers"`
// Peer server settings
PeerServers []PeerServerInfo `json:"peer_servers"`
PeerPriority int `json:"peer_priority"`
}
// LoginServerInfo represents login server connection details
type LoginServerInfo struct {
Address string `json:"address"`
Port int `json:"port"`
Account string `json:"account"`
Password string `json:"password"`
}
// PeerServerInfo represents peer server connection details
type PeerServerInfo struct {
Address string `json:"address"`
Port int `json:"port"`
}
// ClientInfo represents a connected client
type ClientInfo struct {
ID int32 `json:"id"`
AccountID int32 `json:"account_id"`
CharacterID int32 `json:"character_id"`
Name string `json:"name"`
ZoneID int32 `json:"zone_id"`
ConnectedAt time.Time `json:"connected_at"`
LastActive time.Time `json:"last_active"`
IPAddress string `json:"ip_address"`
}
// ZoneInfo represents zone server information
type ZoneInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PlayerCount int32 `json:"player_count"`
MaxPlayers int32 `json:"max_players"`
IsShutdown bool `json:"is_shutdown"`
Address string `json:"address"`
Port int `json:"port"`
}
// ServerStats holds server statistics
type ServerStats struct {
StartTime time.Time `json:"start_time"`
ClientCount int32 `json:"client_count"`
ZoneCount int32 `json:"zone_count"`
TotalConnections int64 `json:"total_connections"`
PacketsProcessed int64 `json:"packets_processed"`
DataLoaded bool `json:"data_loaded"`
ItemsLoaded bool `json:"items_loaded"`
SpellsLoaded bool `json:"spells_loaded"`
QuestsLoaded bool `json:"quests_loaded"`
}
// World represents the main world server
type World struct {
config *WorldConfig
db *database.DB
// Network components
udpServer *udp.Server
webServer *http.Server
// Game state
worldTime WorldTime
worldTimeMux sync.RWMutex
// Client management
clients map[int32]*ClientInfo
clientsMux sync.RWMutex
// Zone management
zones map[int32]*ZoneInfo
zonesMux sync.RWMutex
// Statistics
stats ServerStats
statsMux sync.RWMutex
// Control
ctx context.Context
cancel context.CancelFunc
shutdownWg *sync.WaitGroup
// Timers
timeTickTimer *time.Ticker
saveTimer *time.Ticker
vitalityTimer *time.Ticker
statsTimer *time.Ticker
watchdogTimer *time.Ticker
loginCheckTimer *time.Ticker
// Loading state
loadingMux sync.RWMutex
itemsLoaded bool
spellsLoaded bool
questsLoaded bool
traitsLoaded bool
dataLoaded bool
}
// NewWorld creates a new world server instance
func NewWorld(config *WorldConfig) (*World, error) {
ctx, cancel := context.WithCancel(context.Background())
db, err := database.Open(config.DatabasePath)
if err != nil {
cancel()
return nil, fmt.Errorf("failed to open database: %w", err)
}
w := &World{
config: config,
db: db,
ctx: ctx,
cancel: cancel,
shutdownWg: &sync.WaitGroup{},
clients: make(map[int32]*ClientInfo),
zones: make(map[int32]*ZoneInfo),
stats: ServerStats{
StartTime: time.Now(),
},
}
// Initialize world time from database
if err := w.loadWorldTime(); err != nil {
log.Printf("Warning: failed to load world time: %v", err)
w.setDefaultWorldTime()
}
return w, nil
}
// Initialize sets up all world server components
func (w *World) Initialize() error {
log.Println("Loading System Data...")
// Initialize database schema
if err := w.initializeDatabase(); err != nil {
return fmt.Errorf("database initialization failed: %w", err)
}
// Load game data (threaded or sequential)
if w.config.ThreadedLoad {
log.Println("Using threaded loading of static data...")
if err := w.loadGameDataThreaded(); err != nil {
return fmt.Errorf("threaded game data loading failed: %w", err)
}
} else {
if err := w.loadGameData(); err != nil {
return fmt.Errorf("game data loading failed: %w", err)
}
}
// Setup UDP server for game connections
if err := w.setupUDPServer(); err != nil {
return fmt.Errorf("UDP server setup failed: %w", err)
}
// Setup web server for admin/API
if err := w.setupWebServer(); err != nil {
return fmt.Errorf("web server setup failed: %w", err)
}
// Initialize timers
w.initializeTimers()
log.Println("World Server initialization complete")
return nil
}
// Run starts the world server main loop
func (w *World) Run() error {
// Start background processes
w.shutdownWg.Add(6)
go w.processTimeUpdates()
go w.processSaveOperations()
go w.processVitalityUpdates()
go w.processStatsUpdates()
go w.processWatchdog()
go w.processLoginCheck()
// Start network servers
if w.udpServer != nil {
go func() {
if err := w.udpServer.Start(); err != nil {
log.Printf("UDP server error: %v", err)
}
}()
}
// Start web server
w.startWebServer()
log.Printf("World Server running on UDP %s:%d, Web %s:%d",
w.config.ListenAddr, w.config.ListenPort,
w.config.WebAddr, w.config.WebPort)
// Wait for shutdown signal
<-w.ctx.Done()
return w.shutdown()
}
// Shutdown gracefully stops the world server
func (w *World) Shutdown() {
log.Println("Initiating World Server shutdown...")
w.cancel()
}
// setupUDPServer initializes the UDP server for game client connections
func (w *World) setupUDPServer() error {
handler := func(conn *udp.Connection, packet *udp.ApplicationPacket) {
w.handleGamePacket(conn, packet)
}
config := udp.DefaultConfig()
config.MaxConnections = w.config.MaxClients
config.BufferSize = w.config.BufferSize
config.EnableCompression = true
config.EnableEncryption = true
addr := fmt.Sprintf("%s:%d", w.config.ListenAddr, w.config.ListenPort)
server, err := udp.NewServer(addr, handler, config)
if err != nil {
return err
}
w.udpServer = server
return nil
}
// initializeTimers sets up all periodic timers
func (w *World) initializeTimers() {
w.timeTickTimer = time.NewTicker(5 * time.Second) // Game time updates
w.saveTimer = time.NewTicker(5 * time.Minute) // Save operations
w.vitalityTimer = time.NewTicker(1 * time.Hour) // Vitality updates
w.statsTimer = time.NewTicker(1 * time.Minute) // Statistics updates
w.watchdogTimer = time.NewTicker(30 * time.Second) // Watchdog checks
w.loginCheckTimer = time.NewTicker(30 * time.Second) // Login server check
}
// Background processes
// processTimeUpdates handles game world time progression
func (w *World) processTimeUpdates() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.timeTickTimer.C:
w.updateWorldTime()
}
}
}
// processSaveOperations handles periodic save operations
func (w *World) processSaveOperations() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.saveTimer.C:
w.saveWorldState()
}
}
}
// processVitalityUpdates handles vitality system updates
func (w *World) processVitalityUpdates() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.vitalityTimer.C:
w.updateVitality()
}
}
}
// processStatsUpdates handles statistics collection
func (w *World) processStatsUpdates() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.statsTimer.C:
w.updateStatistics()
}
}
}
// processWatchdog handles connection timeouts and cleanup
func (w *World) processWatchdog() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.watchdogTimer.C:
w.cleanupInactiveClients()
w.cleanupTimeoutConnections()
}
}
}
// processLoginCheck handles login server connectivity
func (w *World) processLoginCheck() {
defer w.shutdownWg.Done()
for {
select {
case <-w.ctx.Done():
return
case <-w.loginCheckTimer.C:
w.checkLoginServers()
}
}
}
// Game packet handling
func (w *World) handleGamePacket(conn *udp.Connection, packet *udp.ApplicationPacket) {
// Update connection activity
w.updateConnectionActivity(conn)
// Route packet based on opcode
switch packet.Opcode {
case 0x2000: // Login request
w.handleLoginRequest(conn, packet)
case 0x0020: // Zone change request
w.handleZoneChange(conn, packet)
case 0x0080: // Client command
w.handleClientCommand(conn, packet)
case 0x01F0: // Chat message
w.handleChatMessage(conn, packet)
default:
// @TODO: Implement comprehensive packet routing
log.Printf("Unhandled packet opcode: 0x%04X, size: %d", packet.Opcode, len(packet.Data))
}
// Update packet statistics
w.statsMux.Lock()
w.stats.PacketsProcessed++
w.statsMux.Unlock()
}
// Game packet handlers
func (w *World) handleLoginRequest(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse login request packet
// @TODO: Validate credentials with login server
// @TODO: Create client session
// @TODO: Send login response
log.Printf("Login request from connection %d", conn.GetSessionID())
}
func (w *World) handleZoneChange(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse zone change request
// @TODO: Validate zone transfer
// @TODO: Coordinate with zone servers
// @TODO: Send zone change response
log.Printf("Zone change request from connection %d", conn.GetSessionID())
}
func (w *World) handleClientCommand(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse client command packet
// @TODO: Process administrative commands
// @TODO: Route to appropriate handlers
log.Printf("Client command from connection %d", conn.GetSessionID())
}
func (w *World) handleChatMessage(conn *udp.Connection, packet *udp.ApplicationPacket) {
// @TODO: Parse chat message packet
// @TODO: Handle channel routing
// @TODO: Apply filters and permissions
// @TODO: Broadcast to appropriate recipients
log.Printf("Chat message from connection %d", conn.GetSessionID())
}
// Game state management
func (w *World) updateWorldTime() {
w.worldTimeMux.Lock()
defer w.worldTimeMux.Unlock()
w.worldTime.Minute++
if w.worldTime.Minute >= 60 {
w.worldTime.Minute = 0
w.worldTime.Hour++
if w.worldTime.Hour >= 24 {
w.worldTime.Hour = 0
w.worldTime.Day++
if w.worldTime.Day >= 30 {
w.worldTime.Day = 0
w.worldTime.Month++
if w.worldTime.Month >= 12 {
w.worldTime.Month = 0
w.worldTime.Year++
}
}
}
}
// @TODO: Broadcast time update to all zones/clients
// @TODO: Save time to database periodically
}
func (w *World) saveWorldState() {
// @TODO: Save world time to database
// @TODO: Save player data
// @TODO: Save guild data
// @TODO: Save zone states
// @TODO: Save server statistics
log.Println("Saving world state...")
}
func (w *World) updateVitality() {
// @TODO: Update player vitality for offline/resting players
// @TODO: Broadcast vitality updates to zones
// @TODO: Apply vitality bonuses
log.Println("Updating vitality...")
}
func (w *World) updateStatistics() {
w.statsMux.Lock()
defer w.statsMux.Unlock()
// Update client count
w.clientsMux.RLock()
w.stats.ClientCount = int32(len(w.clients))
w.clientsMux.RUnlock()
// Update zone count
w.zonesMux.RLock()
w.stats.ZoneCount = int32(len(w.zones))
w.zonesMux.RUnlock()
// Update loading status
w.loadingMux.RLock()
w.stats.DataLoaded = w.dataLoaded
w.stats.ItemsLoaded = w.itemsLoaded
w.stats.SpellsLoaded = w.spellsLoaded
w.stats.QuestsLoaded = w.questsLoaded
w.loadingMux.RUnlock()
}
func (w *World) cleanupInactiveClients() {
w.clientsMux.Lock()
defer w.clientsMux.Unlock()
timeout := time.Now().Add(-5 * time.Minute)
for id, client := range w.clients {
if client.LastActive.Before(timeout) {
log.Printf("Removing inactive client %d (%s)", id, client.Name)
delete(w.clients, id)
}
}
}
func (w *World) cleanupTimeoutConnections() {
// @TODO: Clean up timed out UDP connections
// @TODO: Update connection statistics
}
func (w *World) checkLoginServers() {
// @TODO: Check connectivity to login servers
// @TODO: Attempt reconnection if disconnected
// @TODO: Update server status
}
func (w *World) updateConnectionActivity(conn *udp.Connection) {
sessionID := conn.GetSessionID()
w.clientsMux.Lock()
if client, exists := w.clients[int32(sessionID)]; exists {
client.LastActive = time.Now()
}
w.clientsMux.Unlock()
}
// Database operations
func (w *World) initializeDatabase() error {
// @TODO: Create/update database schema tables
// @TODO: Initialize character tables
// @TODO: Initialize guild tables
// @TODO: Initialize item tables
// @TODO: Initialize zone tables
log.Println("Database schema initialized")
return nil
}
func (w *World) loadGameData() error {
log.Println("Loading game data sequentially...")
// Load items
log.Println("Loading items...")
if err := w.loadItems(); err != nil {
return fmt.Errorf("failed to load items: %w", err)
}
// Load spells
log.Println("Loading spells...")
if err := w.loadSpells(); err != nil {
return fmt.Errorf("failed to load spells: %w", err)
}
// Load quests
log.Println("Loading quests...")
if err := w.loadQuests(); err != nil {
return fmt.Errorf("failed to load quests: %w", err)
}
// Load additional data
if err := w.loadTraits(); err != nil {
return fmt.Errorf("failed to load traits: %w", err)
}
if err := w.loadNPCs(); err != nil {
return fmt.Errorf("failed to load NPCs: %w", err)
}
if err := w.loadZones(); err != nil {
return fmt.Errorf("failed to load zones: %w", err)
}
w.loadingMux.Lock()
w.dataLoaded = true
w.loadingMux.Unlock()
log.Println("Game data loading complete")
return nil
}
func (w *World) loadGameDataThreaded() error {
log.Println("Loading game data with threads...")
var wg sync.WaitGroup
errChan := make(chan error, 10)
// Load items in thread
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Loading items...")
if err := w.loadItems(); err != nil {
errChan <- fmt.Errorf("failed to load items: %w", err)
return
}
w.loadingMux.Lock()
w.itemsLoaded = true
w.loadingMux.Unlock()
log.Println("Items loaded")
}()
// Load spells in thread
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Loading spells...")
if err := w.loadSpells(); err != nil {
errChan <- fmt.Errorf("failed to load spells: %w", err)
return
}
w.loadingMux.Lock()
w.spellsLoaded = true
w.loadingMux.Unlock()
log.Println("Spells loaded")
}()
// Load quests in thread
wg.Add(1)
go func() {
defer wg.Done()
log.Println("Loading quests...")
if err := w.loadQuests(); err != nil {
errChan <- fmt.Errorf("failed to load quests: %w", err)
return
}
w.loadingMux.Lock()
w.questsLoaded = true
w.loadingMux.Unlock()
log.Println("Quests loaded")
}()
// Wait for completion
go func() {
wg.Wait()
close(errChan)
}()
// Check for errors
for err := range errChan {
if err != nil {
return err
}
}
// Load additional data sequentially
if err := w.loadTraits(); err != nil {
return fmt.Errorf("failed to load traits: %w", err)
}
if err := w.loadNPCs(); err != nil {
return fmt.Errorf("failed to load NPCs: %w", err)
}
if err := w.loadZones(); err != nil {
return fmt.Errorf("failed to load zones: %w", err)
}
// Wait for threaded loads to complete
for !w.isDataLoaded() {
time.Sleep(100 * time.Millisecond)
}
w.loadingMux.Lock()
w.dataLoaded = true
w.loadingMux.Unlock()
log.Println("Threaded game data loading complete")
return nil
}
// Data loading functions
func (w *World) loadItems() error {
// @TODO: Load items from database
// @TODO: Build item lookup tables
// @TODO: Load item templates
// @TODO: Initialize item factories
return nil
}
func (w *World) loadSpells() error {
// @TODO: Load spells from database
// @TODO: Build spell lookup tables
// @TODO: Load spell effects
// @TODO: Initialize spell system
return nil
}
func (w *World) loadQuests() error {
// @TODO: Load quests from database
// @TODO: Build quest lookup tables
// @TODO: Load quest rewards
// @TODO: Initialize quest system
return nil
}
func (w *World) loadTraits() error {
// @TODO: Load traits from database
// @TODO: Build trait trees
// @TODO: Initialize trait system
return nil
}
func (w *World) loadNPCs() error {
// @TODO: Load NPCs from database
// @TODO: Load NPC templates
// @TODO: Load NPC spawn data
return nil
}
func (w *World) loadZones() error {
// @TODO: Load zone definitions
// @TODO: Load zone spawn points
// @TODO: Initialize zone management
return nil
}
func (w *World) loadWorldTime() error {
// @TODO: Load world time from database
w.worldTime = WorldTime{
Year: 3800,
Month: 0,
Day: 0,
Hour: 8,
Minute: 30,
}
return nil
}
func (w *World) setDefaultWorldTime() {
w.worldTimeMux.Lock()
defer w.worldTimeMux.Unlock()
w.worldTime = WorldTime{
Year: 3800,
Month: 0,
Day: 0,
Hour: 8,
Minute: 30,
}
}
func (w *World) isDataLoaded() bool {
w.loadingMux.RLock()
defer w.loadingMux.RUnlock()
if w.config.ThreadedLoad {
return w.itemsLoaded && w.spellsLoaded && w.questsLoaded && w.traitsLoaded
}
return w.dataLoaded
}
// Cleanup and shutdown
func (w *World) shutdown() error {
log.Println("Shutting down World Server...")
// Stop timers
if w.timeTickTimer != nil {
w.timeTickTimer.Stop()
}
if w.saveTimer != nil {
w.saveTimer.Stop()
}
if w.vitalityTimer != nil {
w.vitalityTimer.Stop()
}
if w.statsTimer != nil {
w.statsTimer.Stop()
}
if w.watchdogTimer != nil {
w.watchdogTimer.Stop()
}
if w.loginCheckTimer != nil {
w.loginCheckTimer.Stop()
}
// Stop network servers
if err := w.stopWebServer(); err != nil {
log.Printf("Error stopping web server: %v", err)
}
if w.udpServer != nil {
w.udpServer.Stop()
}
// Wait for background processes
w.shutdownWg.Wait()
// Save final state
w.saveWorldState()
// Close database
if w.db != nil {
w.db.Close()
}
log.Println("World Server shutdown complete")
return nil
}