599 lines
16 KiB
Go
599 lines
16 KiB
Go
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"`
|
|
}
|