eq2go/internal/zone/zone_manager.go
2025-08-06 14:39:39 -05:00

599 lines
16 KiB
Go

package zone
import (
"fmt"
"log"
"sync"
"time"
"zombiezen.com/go/sqlite"
)
// ZoneManager manages all active zones in the server
type ZoneManager struct {
zones map[int32]*ZoneServer
zonesByName map[string]*ZoneServer
instanceZones map[int32]*ZoneServer
db *sqlite.Conn
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 *sqlite.Conn) *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)
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)
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"`
}