643 lines
17 KiB
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")
|
|
} |