eq2go/internal/world/world.go

643 lines
17 KiB
Go

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")
}