package world import ( "context" "fmt" "sync" "time" "eq2emu/internal/commands" "eq2emu/internal/database" "eq2emu/internal/packets" "eq2emu/internal/rules" ) // World represents the main world server instance type World struct { // Core components db *database.Database commandManager *commands.CommandManager rulesManager *rules.RuleManager // Server configuration config *WorldConfig startTime time.Time shutdownTime *time.Time shutdownReason string // World time management worldTime *WorldTime worldTimeTicker *time.Ticker // Zones management zones *ZoneList // Client management clients *ClientList // Achievement system achievementMgr *AchievementManager // Title system titleMgr *TitleManager // NPC system npcMgr *NPCManager // Master lists (singletons) masterSpells interface{} // TODO: implement spell manager masterItems interface{} // TODO: implement item manager masterQuests interface{} // TODO: implement quest manager masterSkills interface{} // TODO: implement skill manager masterFactions interface{} // TODO: implement faction manager // Server statistics stats *ServerStatistics // Synchronization mutex sync.RWMutex ctx context.Context cancel context.CancelFunc wg sync.WaitGroup } // WorldConfig holds world server configuration type WorldConfig struct { // Network settings ListenAddr string `json:"listen_addr"` ListenPort int `json:"listen_port"` MaxClients int `json:"max_clients"` // Web interface settings WebAddr string `json:"web_addr"` WebPort int `json:"web_port"` WebCertFile string `json:"web_cert_file"` WebKeyFile string `json:"web_key_file"` WebKeyPassword string `json:"web_key_password"` // Database settings DatabasePath string `json:"database_path"` // Server settings ServerName string `json:"server_name"` ServerMOTD string `json:"server_motd"` LogLevel string `json:"log_level"` // Game settings XPRate float32 `json:"xp_rate"` TSXPRate float32 `json:"ts_xp_rate"` CoinRate float32 `json:"coin_rate"` LootRate float32 `json:"loot_rate"` // Login server settings LoginServerAddr string `json:"login_server_addr"` LoginServerPort int `json:"login_server_port"` LoginServerKey string `json:"login_server_key"` } // WorldTime represents in-game time type WorldTime struct { Year int32 Month int32 Day int32 Hour int32 Minute int32 mutex sync.RWMutex } // ServerStatistics tracks server metrics type ServerStatistics struct { // Server info ServerCreated time.Time ServerStartTime time.Time // Connection stats TotalConnections int64 CurrentConnections int32 MaxConnections int32 // Character stats TotalAccounts int32 TotalCharacters int32 AverageCharLevel float32 // Zone stats ActiveZones int32 ActiveInstances int32 // Performance stats CPUUsage float32 MemoryUsage int64 PeakMemoryUsage int64 mutex sync.RWMutex } // NewWorld creates a new world server instance func NewWorld(config *WorldConfig) (*World, error) { // Initialize database db, err := database.New(config.DatabasePath) if err != nil { return nil, fmt.Errorf("failed to initialize database: %w", err) } // Initialize command manager cmdManager, err := commands.InitializeCommands() if err != nil { return nil, fmt.Errorf("failed to initialize commands: %w", err) } // Initialize rules manager rulesManager := rules.NewRuleManager() // Initialize achievement manager achievementMgr := NewAchievementManager(db) // Initialize title manager titleMgr := NewTitleManager(db) // Initialize NPC manager npcMgr := NewNPCManager(db) // Create context ctx, cancel := context.WithCancel(context.Background()) w := &World{ db: db, commandManager: cmdManager, rulesManager: rulesManager, achievementMgr: achievementMgr, titleMgr: titleMgr, npcMgr: npcMgr, config: config, startTime: time.Now(), worldTime: &WorldTime{Year: 3721, Month: 1, Day: 1, Hour: 12, Minute: 0}, zones: NewZoneList(), clients: NewClientList(), stats: &ServerStatistics{ ServerStartTime: time.Now(), }, ctx: ctx, cancel: cancel, } // Set world references for cross-system communication achievementMgr.SetWorld(w) npcMgr.SetWorld(w) // Load server data from database if err := w.loadServerData(); err != nil { cancel() return nil, fmt.Errorf("failed to load server data: %w", err) } return w, nil } // Start begins the world server operation func (w *World) Start() error { w.mutex.Lock() defer w.mutex.Unlock() fmt.Printf("Starting EQ2Go World Server '%s'...\n", w.config.ServerName) fmt.Printf("Listen Address: %s:%d\n", w.config.ListenAddr, w.config.ListenPort) // Register packet handlers w.RegisterPacketHandlers() // Load sample opcode mappings (TODO: Load from configuration files) w.loadSampleOpcodeMappings() // Start world time ticker w.worldTimeTicker = time.NewTicker(3 * time.Second) // EQ2 time tick w.wg.Add(1) go w.worldTimeTick() // Start statistics updater w.wg.Add(1) go w.updateStatistics() // Start zone watchdog w.wg.Add(1) go w.zoneWatchdog() // Start client handler w.wg.Add(1) go w.clientHandler() fmt.Println("World server started successfully!") return nil } // Stop gracefully shuts down the world server func (w *World) Stop() error { w.mutex.Lock() defer w.mutex.Unlock() fmt.Println("Shutting down world server...") // Cancel context to signal shutdown w.cancel() // Stop world time ticker if w.worldTimeTicker != nil { w.worldTimeTicker.Stop() } // Disconnect all clients w.clients.DisconnectAll("Server shutting down") // Shutdown all zones w.zones.ShutdownAll() // Wait for all goroutines to finish w.wg.Wait() // Shutdown achievement manager if w.achievementMgr != nil { w.achievementMgr.Shutdown() } // Shutdown title manager if w.titleMgr != nil { w.titleMgr.Shutdown() } // Shutdown NPC manager if w.npcMgr != nil { w.npcMgr.Shutdown() } // Close database if w.db != nil { w.db.Close() } fmt.Println("World server shutdown complete.") return nil } // Process handles the main world server loop func (w *World) Process() { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case <-w.ctx.Done(): return case <-ticker.C: w.processFrame() } } } // processFrame handles one frame of world processing func (w *World) processFrame() { // Process zones w.zones.ProcessAll() // Process clients w.clients.ProcessAll() // Process NPCs w.npcMgr.ProcessNPCs() // Check for scheduled shutdown w.checkShutdown() // Update vitality w.updateVitality() } // worldTimeTick advances the in-game time func (w *World) worldTimeTick() { defer w.wg.Done() for { select { case <-w.ctx.Done(): return case <-w.worldTimeTicker.C: w.worldTime.mutex.Lock() // Advance time (3 seconds = 1 game minute) 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 { // Simplified calendar w.worldTime.Day = 1 w.worldTime.Month++ if w.worldTime.Month > 12 { w.worldTime.Month = 1 w.worldTime.Year++ } } } } w.worldTime.mutex.Unlock() // Send time update to all zones w.zones.SendTimeUpdate(w.worldTime) } } } // updateStatistics updates server statistics periodically func (w *World) updateStatistics() { defer w.wg.Done() ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-w.ctx.Done(): return case <-ticker.C: w.stats.mutex.Lock() // Update current stats w.stats.CurrentConnections = w.clients.Count() w.stats.ActiveZones = w.zones.Count() w.stats.ActiveInstances = w.zones.CountInstances() // TODO: Update other statistics w.stats.mutex.Unlock() } } } // zoneWatchdog monitors zone health func (w *World) zoneWatchdog() { defer w.wg.Done() ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-w.ctx.Done(): return case <-ticker.C: // Check zone health w.zones.CheckHealth() // Clean up dead zones w.zones.CleanupDead() } } } // clientHandler handles incoming client connections func (w *World) clientHandler() { defer w.wg.Done() // TODO: Implement UDP listener and client connection handling // This will create a UDP server that listens for incoming connections // and creates Client instances for each connection fmt.Printf("Client handler ready - waiting for UDP server integration\n") fmt.Printf("When UDP integration is complete, this will:\n") fmt.Printf(" - Listen on %s:%d for client connections\n", w.config.ListenAddr, w.config.ListenPort) fmt.Printf(" - Create Client instances for new connections\n") fmt.Printf(" - Process incoming packets through the opcode system\n") fmt.Printf(" - Handle client authentication and zone entry\n") // For now, just wait for shutdown <-w.ctx.Done() } // loadServerData loads initial data from database func (w *World) loadServerData() error { fmt.Println("Loading server data from database...") // Load achievements if err := w.achievementMgr.LoadAchievements(); err != nil { fmt.Printf("Warning: Failed to load achievements: %v\n", err) // Don't fail startup if achievements don't load - server can still run } // Load titles if err := w.titleMgr.LoadTitles(); err != nil { fmt.Printf("Warning: Failed to load titles: %v\n", err) // Don't fail startup if titles don't load - server can still run } // Load NPCs if err := w.npcMgr.LoadNPCs(); err != nil { fmt.Printf("Warning: Failed to load NPCs: %v\n", err) // Don't fail startup if NPCs don't load - server can still run } // Setup title and achievement integration w.setupTitleAchievementIntegration() // Load rules (TODO: implement when rules database integration is ready) // if err := w.rulesManager.LoadRules(); err != nil { // return fmt.Errorf("failed to load rules: %w", err) // } // TODO: Load other server data // - Master spell list // - Master item list // - Master quest list // - Master skill list // - Master faction list // - Starting skills/spells // - Merchant data // - Transport data fmt.Println("Server data loaded successfully.") return nil } // checkShutdown checks if a scheduled shutdown should occur func (w *World) checkShutdown() { w.mutex.RLock() shutdownTime := w.shutdownTime w.mutex.RUnlock() if shutdownTime != nil && time.Now().After(*shutdownTime) { fmt.Printf("Scheduled shutdown: %s\n", w.shutdownReason) go w.Stop() } } // updateVitality updates player vitality func (w *World) updateVitality() { // TODO: Implement vitality system } // ScheduleShutdown schedules a server shutdown func (w *World) ScheduleShutdown(minutes int, reason string) { w.mutex.Lock() defer w.mutex.Unlock() shutdownTime := time.Now().Add(time.Duration(minutes) * time.Minute) w.shutdownTime = &shutdownTime w.shutdownReason = reason // Announce to all clients message := fmt.Sprintf("Server shutdown scheduled in %d minutes: %s", minutes, reason) w.clients.BroadcastMessage(message) } // CancelShutdown cancels a scheduled shutdown func (w *World) CancelShutdown() { w.mutex.Lock() defer w.mutex.Unlock() if w.shutdownTime != nil { w.shutdownTime = nil w.shutdownReason = "" // Announce cancellation w.clients.BroadcastMessage("Scheduled shutdown has been cancelled.") } } // GetWorldTime returns the current in-game time func (w *World) GetWorldTime() WorldTime { w.worldTime.mutex.RLock() defer w.worldTime.mutex.RUnlock() return WorldTime{ Year: w.worldTime.Year, Month: w.worldTime.Month, Day: w.worldTime.Day, Hour: w.worldTime.Hour, Minute: w.worldTime.Minute, } } // GetConfig returns the world configuration func (w *World) GetConfig() *WorldConfig { return w.config } // GetDatabase returns the database connection func (w *World) GetDatabase() *database.Database { return w.db } // GetCommandManager returns the command manager func (w *World) GetCommandManager() *commands.CommandManager { return w.commandManager } // GetRulesManager returns the rules manager func (w *World) GetRulesManager() *rules.RuleManager { return w.rulesManager } // GetAchievementManager returns the achievement manager func (w *World) GetAchievementManager() *AchievementManager { return w.achievementMgr } // GetTitleManager returns the title manager func (w *World) GetTitleManager() *TitleManager { return w.titleMgr } // GetNPCManager returns the NPC manager func (w *World) GetNPCManager() *NPCManager { return w.npcMgr } // loadSampleOpcodeMappings loads sample opcode mappings for testing func (w *World) loadSampleOpcodeMappings() { fmt.Println("Loading sample opcode mappings...") // Sample opcode mappings for a common client version (60013) // These should eventually be loaded from configuration files sampleOpcodes := map[string]uint16{ "OP_Unknown": 0x0000, "OP_LoginReplyMsg": 0x0001, "OP_LoginByNumRequestMsg": 0x0002, "OP_WSLoginRequestMsg": 0x0003, "OP_ESInitMsg": 0x0010, "OP_ESReadyForClientsMsg": 0x0011, "OP_CreateZoneInstanceMsg": 0x0012, "OP_ZoneInstanceCreateReplyMsg": 0x0013, "OP_ZoneInstanceDestroyedMsg": 0x0014, "OP_ExpectClientAsCharacterRequest": 0x0015, "OP_ExpectClientAsCharacterReplyMs": 0x0016, "OP_ZoneInfoMsg": 0x0017, "OP_CreateCharacterRequestMsg": 0x0020, "OP_DoneLoadingZoneResourcesMsg": 0x0021, "OP_DoneSendingInitialEntitiesMsg": 0x0022, "OP_DoneLoadingEntityResourcesMsg": 0x0023, "OP_DoneLoadingUIResourcesMsg": 0x0024, "OP_PredictionUpdateMsg": 0x0030, "OP_RemoteCmdMsg": 0x0031, "OP_SetRemoteCmdsMsg": 0x0032, "OP_GameWorldTimeMsg": 0x0033, "OP_MOTDMsg": 0x0034, "OP_ZoneMOTDMsg": 0x0035, "OP_ClientCmdMsg": 0x0040, "OP_DispatchClientCmdMsg": 0x0041, "OP_DispatchESMsg": 0x0042, "OP_UpdateCharacterSheetMsg": 0x0050, "OP_UpdateSpellBookMsg": 0x0051, "OP_UpdateInventoryMsg": 0x0052, "OP_ChangeZoneMsg": 0x0060, "OP_ClientTeleportRequestMsg": 0x0061, "OP_TeleportWithinZoneMsg": 0x0062, "OP_ReadyToZoneMsg": 0x0063, "OP_ChatTellChannelMsg": 0x0070, "OP_ChatTellUserMsg": 0x0071, "OP_UpdatePositionMsg": 0x0080, "OP_AchievementUpdateMsg": 0x0090, "OP_CharacterAchievements": 0x0091, "OP_TitleUpdateMsg": 0x0092, "OP_CharacterTitles": 0x0093, "OP_SetActiveTitleMsg": 0x0094, "OP_NPCAttackMsg": 0x0095, "OP_NPCTargetMsg": 0x0096, "OP_NPCInfoMsg": 0x0097, "OP_NPCSpellCastMsg": 0x0098, "OP_NPCMovementMsg": 0x0099, "OP_EqHearChatCmd": 0x1000, "OP_EqDisplayTextCmd": 0x1001, "OP_EqCreateGhostCmd": 0x1002, "OP_EqCreateWidgetCmd": 0x1003, "OP_EqDestroyGhostCmd": 0x1004, "OP_EqUpdateGhostCmd": 0x1005, "OP_EqSetControlGhostCmd": 0x1006, "OP_EqSetPOVGhostCmd": 0x1007, } // Load opcodes for client version 60013 err := packets.LoadGlobalOpcodeMappings(60013, sampleOpcodes) if err != nil { fmt.Printf("Error loading opcode mappings: %v\n", err) } else { fmt.Printf("Loaded %d opcode mappings for client version 60013\n", len(sampleOpcodes)) } // TODO: Load additional client versions and their opcode mappings // This would typically be done from external configuration files } // setupTitleAchievementIntegration sets up integration between titles and achievements func (w *World) setupTitleAchievementIntegration() { fmt.Println("Setting up title and achievement integration...") if w.titleMgr == nil || w.achievementMgr == nil { fmt.Println("Warning: Cannot setup integration - title or achievement manager is nil") return } // Setup title manager's achievement integration w.titleMgr.SetupAchievementIntegration() fmt.Println("Title and achievement integration setup complete") }