update claude, add luajit wrapper source as reference
This commit is contained in:
parent
1987d48a77
commit
789729a07e
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,3 +17,6 @@
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# LuaJIT wrapper source
|
||||
luawrapper
|
53
CLAUDE.md
53
CLAUDE.md
@ -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
1
cmd/login_server/TODO.md
Normal file
@ -0,0 +1 @@
|
||||
need to implement
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
1
cmd/world_server/TODO.md
Normal file
@ -0,0 +1 @@
|
||||
Need to implement
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user