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 }