package zone import ( "fmt" "log" "sync" "time" "eq2emu/internal/database" ) // ZoneManager manages all active zones in the server type ZoneManager struct { zones map[int32]*ZoneServer zonesByName map[string]*ZoneServer instanceZones map[int32]*ZoneServer db *database.Database config *ZoneManagerConfig shutdownSignal chan struct{} isShuttingDown bool mutex sync.RWMutex processTimer *time.Ticker cleanupTimer *time.Ticker } // ZoneManagerConfig holds configuration for the zone manager type ZoneManagerConfig struct { MaxZones int32 MaxInstanceZones int32 ProcessInterval time.Duration CleanupInterval time.Duration DatabasePath string DefaultMapPath string EnableWeather bool EnablePathfinding bool EnableCombat bool EnableSpellProcess bool AutoSaveInterval time.Duration } // NewZoneManager creates a new zone manager func NewZoneManager(config *ZoneManagerConfig, db *database.Database) *ZoneManager { if config.ProcessInterval == 0 { config.ProcessInterval = time.Millisecond * 100 // 10 FPS default } if config.CleanupInterval == 0 { config.CleanupInterval = time.Minute * 5 // 5 minutes default } if config.MaxZones == 0 { config.MaxZones = 100 } if config.MaxInstanceZones == 0 { config.MaxInstanceZones = 1000 } zm := &ZoneManager{ zones: make(map[int32]*ZoneServer), zonesByName: make(map[string]*ZoneServer), instanceZones: make(map[int32]*ZoneServer), db: db, config: config, shutdownSignal: make(chan struct{}), } return zm } // Start starts the zone manager and its processing loops func (zm *ZoneManager) Start() error { zm.mutex.Lock() defer zm.mutex.Unlock() if zm.processTimer != nil { return fmt.Errorf("zone manager already started") } // Start processing timers zm.processTimer = time.NewTicker(zm.config.ProcessInterval) zm.cleanupTimer = time.NewTicker(zm.config.CleanupInterval) // Start processing goroutines go zm.processLoop() go zm.cleanupLoop() log.Printf("%s Zone manager started", LogPrefixZone) return nil } // Stop stops the zone manager and all zones func (zm *ZoneManager) Stop() error { zm.mutex.Lock() defer zm.mutex.Unlock() if zm.isShuttingDown { return fmt.Errorf("zone manager already shutting down") } zm.isShuttingDown = true close(zm.shutdownSignal) // Stop timers if zm.processTimer != nil { zm.processTimer.Stop() zm.processTimer = nil } if zm.cleanupTimer != nil { zm.cleanupTimer.Stop() zm.cleanupTimer = nil } // Shutdown all zones for _, zone := range zm.zones { zone.Shutdown() } for _, zone := range zm.instanceZones { zone.Shutdown() } log.Printf("%s Zone manager stopped", LogPrefixZone) return nil } // LoadZone loads a zone by ID func (zm *ZoneManager) LoadZone(zoneID int32) (*ZoneServer, error) { zm.mutex.Lock() defer zm.mutex.Unlock() // Check if zone is already loaded if zone, exists := zm.zones[zoneID]; exists { return zone, nil } // Check zone limit if int32(len(zm.zones)) >= zm.config.MaxZones { return nil, fmt.Errorf("maximum zones reached (%d)", zm.config.MaxZones) } // Load zone data from database zoneDB := NewZoneDatabase(zm.db.DB) zoneData, err := zoneDB.LoadZoneData(zoneID) if err != nil { return nil, fmt.Errorf("failed to load zone data: %v", err) } // Create zone server zoneServer := NewZoneServer(zoneData.Configuration.Name) // Configure zone server config := &ZoneServerConfig{ ZoneName: zoneData.Configuration.Name, ZoneFile: zoneData.Configuration.File, ZoneDescription: zoneData.Configuration.Description, ZoneID: zoneID, InstanceID: 0, // Not an instance InstanceType: InstanceTypeNone, DatabasePath: zm.config.DatabasePath, MaxPlayers: zoneData.Configuration.MaxPlayers, MinLevel: zoneData.Configuration.MinLevel, MaxLevel: zoneData.Configuration.MaxLevel, SafeX: zoneData.Configuration.SafeX, SafeY: zoneData.Configuration.SafeY, SafeZ: zoneData.Configuration.SafeZ, SafeHeading: zoneData.Configuration.SafeHeading, LoadMaps: true, EnableWeather: zm.config.EnableWeather && zoneData.Configuration.WeatherAllowed, EnablePathfinding: zm.config.EnablePathfinding, } // Initialize zone server if err := zoneServer.Initialize(config); err != nil { return nil, fmt.Errorf("failed to initialize zone server: %v", err) } // Add to manager zm.zones[zoneID] = zoneServer zm.zonesByName[zoneData.Configuration.Name] = zoneServer log.Printf("%s Loaded zone '%s' (ID: %d)", LogPrefixZone, zoneData.Configuration.Name, zoneID) return zoneServer, nil } // UnloadZone unloads a zone by ID func (zm *ZoneManager) UnloadZone(zoneID int32) error { zm.mutex.Lock() defer zm.mutex.Unlock() zone, exists := zm.zones[zoneID] if !exists { return fmt.Errorf("zone %d not found", zoneID) } // Check if zone has players if zone.GetNumPlayers() > 0 { return fmt.Errorf("cannot unload zone with active players") } // Shutdown zone zone.Shutdown() // Wait for shutdown to complete timeout := time.After(time.Second * 30) ticker := time.NewTicker(time.Millisecond * 100) defer ticker.Stop() for { select { case <-timeout: log.Printf("%s Warning: zone %d shutdown timed out", LogPrefixZone, zoneID) break case <-ticker.C: if zone.IsShuttingDown() { break } } break } // Remove from manager delete(zm.zones, zoneID) delete(zm.zonesByName, zone.GetZoneName()) log.Printf("%s Unloaded zone '%s' (ID: %d)", LogPrefixZone, zone.GetZoneName(), zoneID) return nil } // CreateInstance creates a new instance zone func (zm *ZoneManager) CreateInstance(baseZoneID int32, instanceType InstanceType, creatorID uint32) (*ZoneServer, error) { zm.mutex.Lock() defer zm.mutex.Unlock() // Check instance limit if int32(len(zm.instanceZones)) >= zm.config.MaxInstanceZones { return nil, fmt.Errorf("maximum instance zones reached (%d)", zm.config.MaxInstanceZones) } // Load base zone data zoneDB := NewZoneDatabase(zm.db.DB) zoneData, err := zoneDB.LoadZoneData(baseZoneID) if err != nil { return nil, fmt.Errorf("failed to load base zone data: %v", err) } // Generate instance ID instanceID := zm.generateInstanceID() // Create instance zone instanceName := fmt.Sprintf("%s_instance_%d", zoneData.Configuration.Name, instanceID) zoneServer := NewZoneServer(instanceName) // Configure instance zone config := &ZoneServerConfig{ ZoneName: instanceName, ZoneFile: zoneData.Configuration.File, ZoneDescription: zoneData.Configuration.Description, ZoneID: baseZoneID, InstanceID: instanceID, InstanceType: instanceType, DatabasePath: zm.config.DatabasePath, MaxPlayers: zm.getInstanceMaxPlayers(instanceType), MinLevel: zoneData.Configuration.MinLevel, MaxLevel: zoneData.Configuration.MaxLevel, SafeX: zoneData.Configuration.SafeX, SafeY: zoneData.Configuration.SafeY, SafeZ: zoneData.Configuration.SafeZ, SafeHeading: zoneData.Configuration.SafeHeading, LoadMaps: true, EnableWeather: zm.config.EnableWeather && zoneData.Configuration.WeatherAllowed, EnablePathfinding: zm.config.EnablePathfinding, } // Initialize instance zone if err := zoneServer.Initialize(config); err != nil { return nil, fmt.Errorf("failed to initialize instance zone: %v", err) } // Add to manager zm.instanceZones[instanceID] = zoneServer log.Printf("%s Created instance zone '%s' (ID: %d, Type: %s)", LogPrefixZone, instanceName, instanceID, instanceType.String()) return zoneServer, nil } // DestroyInstance destroys an instance zone func (zm *ZoneManager) DestroyInstance(instanceID int32) error { zm.mutex.Lock() defer zm.mutex.Unlock() zone, exists := zm.instanceZones[instanceID] if !exists { return fmt.Errorf("instance %d not found", instanceID) } // Shutdown instance zone.Shutdown() // Remove from manager delete(zm.instanceZones, instanceID) log.Printf("%s Destroyed instance zone '%s' (ID: %d)", LogPrefixZone, zone.GetZoneName(), instanceID) return nil } // GetZone retrieves a zone by ID func (zm *ZoneManager) GetZone(zoneID int32) *ZoneServer { zm.mutex.RLock() defer zm.mutex.RUnlock() return zm.zones[zoneID] } // GetZoneByName retrieves a zone by name func (zm *ZoneManager) GetZoneByName(name string) *ZoneServer { zm.mutex.RLock() defer zm.mutex.RUnlock() return zm.zonesByName[name] } // GetInstance retrieves an instance zone by ID func (zm *ZoneManager) GetInstance(instanceID int32) *ZoneServer { zm.mutex.RLock() defer zm.mutex.RUnlock() return zm.instanceZones[instanceID] } // GetAllZones returns a list of all active zones func (zm *ZoneManager) GetAllZones() []*ZoneServer { zm.mutex.RLock() defer zm.mutex.RUnlock() zones := make([]*ZoneServer, 0, len(zm.zones)) for _, zone := range zm.zones { zones = append(zones, zone) } return zones } // GetAllInstances returns a list of all active instances func (zm *ZoneManager) GetAllInstances() []*ZoneServer { zm.mutex.RLock() defer zm.mutex.RUnlock() instances := make([]*ZoneServer, 0, len(zm.instanceZones)) for _, instance := range zm.instanceZones { instances = append(instances, instance) } return instances } // GetZoneCount returns the number of active zones func (zm *ZoneManager) GetZoneCount() int { zm.mutex.RLock() defer zm.mutex.RUnlock() return len(zm.zones) } // GetInstanceCount returns the number of active instances func (zm *ZoneManager) GetInstanceCount() int { zm.mutex.RLock() defer zm.mutex.RUnlock() return len(zm.instanceZones) } // GetTotalPlayerCount returns the total number of players across all zones func (zm *ZoneManager) GetTotalPlayerCount() int32 { zm.mutex.RLock() defer zm.mutex.RUnlock() var total int32 for _, zone := range zm.zones { total += zone.GetNumPlayers() } for _, instance := range zm.instanceZones { total += instance.GetNumPlayers() } return total } // BroadcastMessage sends a message to all players in all zones func (zm *ZoneManager) BroadcastMessage(message string) { zm.mutex.RLock() zones := make([]*ZoneServer, 0, len(zm.zones)+len(zm.instanceZones)) for _, zone := range zm.zones { zones = append(zones, zone) } for _, instance := range zm.instanceZones { zones = append(zones, instance) } zm.mutex.RUnlock() for _, zone := range zones { // TODO: Implement broadcast message to zone _ = zone _ = message } } // GetStatistics returns zone manager statistics func (zm *ZoneManager) GetStatistics() *ZoneManagerStatistics { zm.mutex.RLock() defer zm.mutex.RUnlock() stats := &ZoneManagerStatistics{ TotalZones: int32(len(zm.zones)), TotalInstances: int32(len(zm.instanceZones)), TotalPlayers: zm.GetTotalPlayerCount(), MaxZones: zm.config.MaxZones, MaxInstances: zm.config.MaxInstanceZones, ZoneDetails: make([]*ZoneStatistics, 0), InstanceDetails: make([]*ZoneStatistics, 0), } for _, zone := range zm.zones { zoneStats := &ZoneStatistics{ ZoneID: zone.GetZoneID(), ZoneName: zone.GetZoneName(), PlayerCount: zone.GetNumPlayers(), MaxPlayers: zone.GetMaxPlayers(), IsLocked: zone.IsLocked(), WatchdogTime: zone.GetWatchdogTime(), } stats.ZoneDetails = append(stats.ZoneDetails, zoneStats) } for _, instance := range zm.instanceZones { instanceStats := &ZoneStatistics{ ZoneID: instance.GetZoneID(), InstanceID: instance.GetInstanceID(), ZoneName: instance.GetZoneName(), PlayerCount: instance.GetNumPlayers(), MaxPlayers: instance.GetMaxPlayers(), IsLocked: instance.IsLocked(), WatchdogTime: instance.GetWatchdogTime(), } stats.InstanceDetails = append(stats.InstanceDetails, instanceStats) } return stats } // Private methods func (zm *ZoneManager) processLoop() { defer func() { if r := recover(); r != nil { log.Printf("%s Zone manager process loop panic: %v", LogPrefixZone, r) } }() for { select { case <-zm.shutdownSignal: return case <-zm.processTimer.C: zm.processAllZones() } } } func (zm *ZoneManager) cleanupLoop() { defer func() { if r := recover(); r != nil { log.Printf("%s Zone manager cleanup loop panic: %v", LogPrefixZone, r) } }() for { select { case <-zm.shutdownSignal: return case <-zm.cleanupTimer.C: zm.cleanupInactiveInstances() } } } func (zm *ZoneManager) processAllZones() { // Get all zones to process zm.mutex.RLock() zones := make([]*ZoneServer, 0, len(zm.zones)+len(zm.instanceZones)) for _, zone := range zm.zones { zones = append(zones, zone) } for _, instance := range zm.instanceZones { zones = append(zones, instance) } zm.mutex.RUnlock() // Process each zone for _, zone := range zones { if zone.IsShuttingDown() { continue } // Process main zone logic if err := zone.Process(); err != nil { log.Printf("%s Error processing zone '%s': %v", LogPrefixZone, zone.GetZoneName(), err) } // Process spawn logic if err := zone.SpawnProcess(); err != nil { log.Printf("%s Error processing spawns in zone '%s': %v", LogPrefixZone, zone.GetZoneName(), err) } } } func (zm *ZoneManager) cleanupInactiveInstances() { zm.mutex.Lock() defer zm.mutex.Unlock() instancesToRemove := make([]int32, 0) for instanceID, instance := range zm.instanceZones { // Remove instances with no players that have been inactive if instance.GetNumPlayers() == 0 { // Check how long instance has been empty // TODO: Track instance creation/last activity time instancesToRemove = append(instancesToRemove, instanceID) } } // Remove inactive instances for _, instanceID := range instancesToRemove { if instance, exists := zm.instanceZones[instanceID]; exists { instance.Shutdown() delete(zm.instanceZones, instanceID) log.Printf("%s Cleaned up inactive instance %d", LogPrefixZone, instanceID) } } } func (zm *ZoneManager) generateInstanceID() int32 { // Simple instance ID generation - start from 1000 and increment // In production, this should be more sophisticated instanceID := int32(1000) for { if _, exists := zm.instanceZones[instanceID]; !exists { return instanceID } instanceID++ if instanceID > 999999 { // Wrap around to prevent overflow instanceID = 1000 } } } func (zm *ZoneManager) getInstanceMaxPlayers(instanceType InstanceType) int32 { switch instanceType { case InstanceTypeGroupLockout, InstanceTypeGroupPersist: return 6 case InstanceTypeRaidLockout, InstanceTypeRaidPersist: return 24 case InstanceTypeSoloLockout, InstanceTypeSoloPersist: return 1 case InstanceTypeTradeskill, InstanceTypePublic: return 50 case InstanceTypePersonalHouse: return 10 case InstanceTypeGuildHouse: return 50 case InstanceTypeQuest: return 6 default: return 6 } } // ZoneManagerStatistics holds statistics about the zone manager type ZoneManagerStatistics struct { TotalZones int32 `json:"total_zones"` TotalInstances int32 `json:"total_instances"` TotalPlayers int32 `json:"total_players"` MaxZones int32 `json:"max_zones"` MaxInstances int32 `json:"max_instances"` ZoneDetails []*ZoneStatistics `json:"zone_details"` InstanceDetails []*ZoneStatistics `json:"instance_details"` } // ZoneStatistics holds statistics about a specific zone type ZoneStatistics struct { ZoneID int32 `json:"zone_id"` InstanceID int32 `json:"instance_id,omitempty"` ZoneName string `json:"zone_name"` PlayerCount int32 `json:"player_count"` MaxPlayers int32 `json:"max_players"` IsLocked bool `json:"is_locked"` WatchdogTime int32 `json:"watchdog_time"` }