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

867 lines
21 KiB
Go

package zone
import (
"fmt"
"log"
"time"
"eq2emu/internal/spawn"
)
// ZoneServerConfig holds configuration for creating a zone server
type ZoneServerConfig struct {
ZoneName string
ZoneFile string
ZoneSkyFile string
ZoneDescription string
ZoneID int32
InstanceID int32
InstanceType InstanceType
DatabasePath string
MaxPlayers int32
MinLevel int16
MaxLevel int16
SafeX float32
SafeY float32
SafeZ float32
SafeHeading float32
LoadMaps bool
EnableWeather bool
EnablePathfinding bool
}
// Initialize initializes the zone server with all required systems
func (zs *ZoneServer) Initialize(config *ZoneServerConfig) error {
zs.masterZoneLock.Lock()
defer zs.masterZoneLock.Unlock()
if zs.isInitialized.Load() {
return fmt.Errorf("zone server already initialized")
}
log.Printf("%s Initializing zone server '%s' (ID: %d)", LogPrefixZone, config.ZoneName, config.ZoneID)
// Set basic configuration
zs.zoneName = config.ZoneName
zs.zoneFile = config.ZoneFile
zs.zoneSkyFile = config.ZoneSkyFile
zs.zoneDescription = config.ZoneDescription
zs.zoneID = config.ZoneID
zs.instanceID = config.InstanceID
zs.instanceType = config.InstanceType
zs.isInstance = config.InstanceType != InstanceTypeNone
// Set zone limits
zs.minimumLevel = config.MinLevel
zs.maximumLevel = config.MaxLevel
zs.maxPlayers = config.MaxPlayers
// Set safe coordinates
zs.safeX = config.SafeX
zs.safeY = config.SafeY
zs.safeZ = config.SafeZ
zs.safeHeading = config.SafeHeading
// Initialize timers
if err := zs.initializeTimers(); err != nil {
return fmt.Errorf("failed to initialize timers: %v", err)
}
// Load zone data from database
if err := zs.loadZoneData(); err != nil {
return fmt.Errorf("failed to load zone data: %v", err)
}
// Initialize pathfinding if enabled
if config.EnablePathfinding {
if err := zs.initializePathfinding(); err != nil {
log.Printf("%s Warning: failed to initialize pathfinding: %v", LogPrefixZone, err)
// Don't fail initialization, just log warning
}
}
// Load maps if enabled
if config.LoadMaps {
if err := zs.loadZoneMaps(); err != nil {
log.Printf("%s Warning: failed to load zone maps: %v", LogPrefixZone, err)
// Don't fail initialization, just log warning
}
}
// Initialize weather if enabled
if config.EnableWeather {
zs.initializeWeather()
}
// Initialize movement manager
zs.movementMgr = NewMobMovementManager(zs)
// Start processing threads
zs.startProcessingThreads()
zs.loadingData = false
zs.isInitialized.Store(true)
log.Printf("%s Zone server '%s' initialized successfully", LogPrefixZone, zs.zoneName)
return nil
}
// Process performs the main zone processing loop
func (zs *ZoneServer) Process() error {
if zs.zoneShuttingDown.Load() {
return fmt.Errorf("zone is shutting down")
}
if !zs.isInitialized.Load() {
return fmt.Errorf("zone not initialized")
}
// Update watchdog timestamp
zs.watchdogTimestamp = int32(time.Now().Unix())
// Process clients
zs.processClients()
// Process spawns
zs.processSpawns()
// Process timers
zs.processTimers()
// Process movement
if zs.movementMgr != nil {
zs.movementMgr.Process()
}
// Process spell effects
if zs.spellProcess != nil {
zs.spellProcess.ProcessSpellEffects()
}
// Process pending spawn changes
zs.processSpawnChanges()
// Process proximity checks
zs.processProximityChecks()
// Process weather updates
zs.processWeather()
// Clean up expired data
zs.cleanupExpiredData()
return nil
}
// SpawnProcess performs spawn-specific processing
func (zs *ZoneServer) SpawnProcess() error {
if zs.zoneShuttingDown.Load() {
return fmt.Errorf("zone is shutting down")
}
// Process spawn locations for respawns
zs.processSpawnLocations()
// Process spawn movement
zs.processSpawnMovement()
// Process NPC AI
zs.processNPCAI()
// Process combat
zs.processCombat()
// Check for dead spawns to remove
zs.checkDeadSpawnRemoval()
// Process spawn script timers
zs.processSpawnScriptTimers()
return nil
}
// AddClient adds a client to the zone
func (zs *ZoneServer) AddClient(client Client) error {
if zs.zoneShuttingDown.Load() {
return fmt.Errorf("zone is shutting down")
}
zs.clientListLock.Lock()
defer zs.clientListLock.Unlock()
// Check zone capacity
if len(zs.clients) >= int(zs.GetMaxPlayers()) {
return fmt.Errorf("zone is full")
}
// Check client requirements
if !zs.canClientEnter(client) {
return fmt.Errorf("client does not meet zone requirements")
}
// Add client to list
zs.clients = append(zs.clients, client)
zs.numPlayers = int32(len(zs.clients))
zs.lifetimeClientCount++
log.Printf("%s Client %s entered zone '%s' (%d/%d players)",
LogPrefixZone, client.GetPlayerName(), zs.zoneName, zs.numPlayers, zs.GetMaxPlayers())
// Initialize client in zone
go zs.initializeClientInZone(client)
return nil
}
// RemoveClient removes a client from the zone
func (zs *ZoneServer) RemoveClient(client Client) {
zs.clientListLock.Lock()
defer zs.clientListLock.Unlock()
// Find and remove client
for i, c := range zs.clients {
if c.GetID() == client.GetID() {
// Remove from slice
zs.clients = append(zs.clients[:i], zs.clients[i+1:]...)
zs.numPlayers = int32(len(zs.clients))
log.Printf("%s Client %s left zone '%s' (%d/%d players)",
LogPrefixZone, client.GetPlayerName(), zs.zoneName, zs.numPlayers, zs.GetMaxPlayers())
// Clean up client-specific data
zs.cleanupClientData(client)
break
}
}
}
// AddSpawn adds a spawn to the zone
func (zs *ZoneServer) AddSpawn(spawn *spawn.Spawn) error {
if zs.zoneShuttingDown.Load() {
return fmt.Errorf("zone is shutting down")
}
zs.spawnListLock.Lock()
defer zs.spawnListLock.Unlock()
// Add to spawn list
zs.spawnList[spawn.GetID()] = spawn
// Add to appropriate grid
zs.addSpawnToGrid(spawn)
// Mark as changed for client updates
zs.markSpawnChanged(spawn.GetID())
log.Printf("%s Added spawn '%s' (ID: %d) to zone '%s'",
LogPrefixZone, spawn.GetName(), spawn.GetID(), zs.zoneName)
return nil
}
// RemoveSpawn removes a spawn from the zone
func (zs *ZoneServer) RemoveSpawn(spawnID int32, deleteSpawn bool) error {
zs.spawnListLock.Lock()
defer zs.spawnListLock.Unlock()
spawn, exists := zs.spawnList[spawnID]
if !exists {
return fmt.Errorf("spawn %d not found", spawnID)
}
// Remove from grids
zs.removeSpawnFromGrid(spawn)
// Clean up spawn data
zs.cleanupSpawnData(spawn)
// Remove from spawn list
delete(zs.spawnList, spawnID)
// Mark for client removal
zs.markSpawnForRemoval(spawnID)
log.Printf("%s Removed spawn '%s' (ID: %d) from zone '%s'",
LogPrefixZone, spawn.GetName(), spawn.GetID(), zs.zoneName)
return nil
}
// GetSpawn retrieves a spawn by ID
func (zs *ZoneServer) GetSpawn(spawnID int32) *spawn.Spawn {
zs.spawnListLock.RLock()
defer zs.spawnListLock.RUnlock()
return zs.spawnList[spawnID]
}
// GetSpawnsByRange retrieves all spawns within range of a position
func (zs *ZoneServer) GetSpawnsByRange(x, y, z, maxRange float32) []*spawn.Spawn {
zs.spawnListLock.RLock()
defer zs.spawnListLock.RUnlock()
var nearbySpawns []*spawn.Spawn
maxRangeSquared := maxRange * maxRange
for _, spawn := range zs.spawnList {
spawnX := spawn.GetX()
spawnY := spawn.GetY()
spawnZ := spawn.GetZ()
distSquared := Distance3DSquared(x, y, z, spawnX, spawnY, spawnZ)
if distSquared <= maxRangeSquared {
nearbySpawns = append(nearbySpawns, spawn)
}
}
return nearbySpawns
}
// GetGridsByLocation retrieves grid IDs that contain the specified location
func (zs *ZoneServer) GetGridsByLocation(x, y, z, distance float32) []int32 {
// Calculate grid boundaries
minGridX := int32((x - distance) / DefaultGridSize)
maxGridX := int32((x + distance) / DefaultGridSize)
minGridY := int32((y - distance) / DefaultGridSize)
maxGridY := int32((y + distance) / DefaultGridSize)
var gridIDs []int32
for gridX := minGridX; gridX <= maxGridX; gridX++ {
for gridY := minGridY; gridY <= maxGridY; gridY++ {
gridID := gridX*1000 + gridY // Simple grid ID calculation
if gridID >= 0 && gridID <= MaxGridID {
gridIDs = append(gridIDs, gridID)
}
}
}
return gridIDs
}
// SendZoneSpawns sends all visible spawns to a client
func (zs *ZoneServer) SendZoneSpawns(client Client) error {
playerX, playerY, playerZ, _, _ := client.GetPosition()
// Get spawns in range
spawns := zs.GetSpawnsByRange(playerX, playerY, playerZ, SendSpawnDistance)
log.Printf("%s Sending %d spawns to client %s", LogPrefixZone, len(spawns), client.GetPlayerName())
// Send each spawn
for _, spawn := range spawns {
if client.CanSeeSpawn(spawn) {
zs.sendSpawnToClient(client, spawn)
}
}
return nil
}
// ProcessWeather handles zone-wide weather changes
func (zs *ZoneServer) ProcessWeather() {
if !zs.weatherEnabled || !zs.weatherAllowed {
return
}
currentTime := int32(time.Now().Unix())
// Check if it's time for a weather change
if currentTime-zs.weatherLastChangedTime < zs.weatherFrequency {
return
}
// Roll for weather change
if zs.weatherChangeChance > 0 {
// Simple random roll (0-100)
roll := currentTime % 100
if int8(roll) > zs.weatherChangeChance {
return
}
}
// Calculate weather change
var change float32
switch zs.weatherType {
case WeatherTypeNormal:
change = zs.weatherChangeAmount
case WeatherTypeDynamic:
// Dynamic weather with random offset
change = zs.weatherChangeAmount + (float32(currentTime%100)/100.0-0.5)*zs.weatherDynamicOffset
case WeatherTypeRandom:
// Completely random change
change = (float32(currentTime%100)/100.0 - 0.5) * 2.0 * zs.weatherMaxSeverity
case WeatherTypeChaotic:
// Chaotic weather with large swings
change = (float32(currentTime%100)/100.0 - 0.5) * 4.0 * zs.weatherMaxSeverity
}
// Apply pattern
switch zs.weatherPattern {
case WeatherPatternDecreasing:
change = -abs(change)
case WeatherPatternIncreasing:
change = abs(change)
// WeatherPatternRandom uses calculated change as-is
}
// Update weather severity
newSeverity := zs.weatherCurrentSeverity + change
// Clamp to bounds
if newSeverity < zs.weatherMinSeverity {
newSeverity = zs.weatherMinSeverity
}
if newSeverity > zs.weatherMaxSeverity {
newSeverity = zs.weatherMaxSeverity
}
// Check if severity actually changed
if newSeverity != zs.weatherCurrentSeverity {
zs.weatherCurrentSeverity = newSeverity
zs.rain = newSeverity
// Send weather update to all clients
zs.sendWeatherUpdate()
log.Printf("%s Weather changed to %.2f in zone '%s'", LogPrefixWeather, newSeverity, zs.zoneName)
}
zs.weatherLastChangedTime = currentTime
}
// SetRain sets the rain level in the zone
func (zs *ZoneServer) SetRain(val float32) {
zs.rain = val
zs.weatherCurrentSeverity = val
zs.sendWeatherUpdate()
}
// GetZoneID returns the zone ID
func (zs *ZoneServer) GetZoneID() int32 {
return zs.zoneID
}
// GetZoneName returns the zone name
func (zs *ZoneServer) GetZoneName() string {
return zs.zoneName
}
// GetInstanceID returns the instance ID
func (zs *ZoneServer) GetInstanceID() int32 {
return zs.instanceID
}
// GetInstanceType returns the instance type
func (zs *ZoneServer) GetInstanceType() InstanceType {
return zs.instanceType
}
// IsInstanceZone returns whether this is an instance zone
func (zs *ZoneServer) IsInstanceZone() bool {
return zs.isInstance
}
// GetNumPlayers returns the current number of players
func (zs *ZoneServer) GetNumPlayers() int32 {
return zs.numPlayers
}
// GetMaxPlayers returns the maximum number of players allowed
func (zs *ZoneServer) GetMaxPlayers() int32 {
// Use configured max players if set, otherwise use instance/default values
if zs.maxPlayers > 0 {
return zs.maxPlayers
}
if zs.isInstance {
// Instance zones have different limits based on type
switch zs.instanceType {
case InstanceTypeGroupLockout, InstanceTypeGroupPersist:
return 6
case InstanceTypeRaidLockout, InstanceTypeRaidPersist:
return 24
case InstanceTypeSoloLockout, InstanceTypeSoloPersist:
return 1
case InstanceTypeTradeskill, InstanceTypePublic:
return DefaultMaxPlayers
case InstanceTypePersonalHouse:
return 10
case InstanceTypeGuildHouse:
return 50
case InstanceTypeQuest:
return 6
}
}
return DefaultMaxPlayers
}
// GetSafePosition returns the safe position coordinates
func (zs *ZoneServer) GetSafePosition() (x, y, z, heading float32) {
return zs.safeX, zs.safeY, zs.safeZ, zs.safeHeading
}
// SetSafePosition sets the safe position coordinates
func (zs *ZoneServer) SetSafePosition(x, y, z, heading float32) {
zs.safeX = x
zs.safeY = y
zs.safeZ = z
zs.safeHeading = heading
}
// IsLocked returns whether the zone is locked
func (zs *ZoneServer) IsLocked() bool {
return zs.locked
}
// SetLocked sets the zone lock state
func (zs *ZoneServer) SetLocked(locked bool) {
zs.locked = locked
if locked {
log.Printf("%s Zone '%s' has been locked", LogPrefixZone, zs.zoneName)
} else {
log.Printf("%s Zone '%s' has been unlocked", LogPrefixZone, zs.zoneName)
}
}
// GetWatchdogTime returns the last watchdog timestamp
func (zs *ZoneServer) GetWatchdogTime() int32 {
return zs.watchdogTimestamp
}
// Private helper methods
func (zs *ZoneServer) initializeTimers() error {
zs.aggroTimer = time.NewTimer(AggroCheckInterval)
zs.charsheetChanges = time.NewTimer(CharsheetUpdateInterval)
zs.clientSave = time.NewTimer(ClientSaveInterval)
zs.locationProxTimer = time.NewTimer(LocationProximityInterval)
zs.movementTimer = time.NewTimer(MovementUpdateInterval)
zs.regenTimer = time.NewTimer(DefaultTimerInterval)
zs.respawnTimer = time.NewTimer(RespawnCheckInterval)
zs.shutdownTimer = time.NewTimer(0) // Disabled by default
zs.spawnRangeTimer = time.NewTimer(SpawnRangeUpdateInterval)
zs.spawnUpdateTimer = time.NewTimer(DefaultTimerInterval)
zs.syncGameTimer = time.NewTimer(DefaultTimerInterval)
zs.trackingTimer = time.NewTimer(TrackingUpdateInterval)
zs.weatherTimer = time.NewTimer(WeatherUpdateInterval)
zs.widgetTimer = time.NewTimer(WidgetUpdateInterval)
return nil
}
func (zs *ZoneServer) loadZoneData() error {
// TODO: Load zone data from database
// This would include spawn locations, NPCs, objects, etc.
log.Printf("%s Loading zone data for '%s'", LogPrefixZone, zs.zoneName)
return nil
}
func (zs *ZoneServer) initializePathfinding() error {
// TODO: Initialize pathfinding system
log.Printf("%s Initializing pathfinding for zone '%s'", LogPrefixPathfind, zs.zoneName)
return nil
}
func (zs *ZoneServer) loadZoneMaps() error {
// TODO: Load zone maps and collision data
log.Printf("%s Loading maps for zone '%s'", LogPrefixMap, zs.zoneName)
return nil
}
func (zs *ZoneServer) initializeWeather() {
zs.weatherEnabled = true
zs.weatherType = WeatherTypeNormal
zs.weatherFrequency = DefaultWeatherFrequency
zs.weatherMinSeverity = DefaultWeatherMinSeverity
zs.weatherMaxSeverity = DefaultWeatherMaxSeverity
zs.weatherChangeAmount = DefaultWeatherChangeAmount
zs.weatherDynamicOffset = DefaultWeatherDynamicOffset
zs.weatherChangeChance = DefaultWeatherChangeChance
zs.weatherPattern = WeatherPatternRandom
zs.weatherCurrentSeverity = 0.0
zs.weatherLastChangedTime = int32(time.Now().Unix())
log.Printf("%s Weather system initialized for zone '%s'", LogPrefixWeather, zs.zoneName)
}
func (zs *ZoneServer) startProcessingThreads() {
zs.spawnThreadActive = true
zs.combatThreadActive = true
zs.clientThreadActive = true
log.Printf("%s Started processing threads for zone '%s'", LogPrefixZone, zs.zoneName)
}
func (zs *ZoneServer) canClientEnter(client Client) bool {
// Check level requirements
player := client.GetPlayer()
if player != nil {
level := player.GetLevel()
if zs.minimumLevel > 0 && level < zs.minimumLevel {
return false
}
if zs.maximumLevel > 0 && level > zs.maximumLevel {
return false
}
}
// Check client version
version := client.GetClientVersion()
if zs.minimumVersion > 0 && int16(version) < zs.minimumVersion {
return false
}
// Check if zone is locked
if zs.locked {
return false
}
return true
}
func (zs *ZoneServer) initializeClientInZone(client Client) {
// Send zone information
zs.sendZoneInfo(client)
// Send all visible spawns
zs.SendZoneSpawns(client)
// Send weather update
if zs.weatherEnabled {
zs.sendWeatherUpdateToClient(client)
}
// Send time update
zs.sendTimeUpdateToClient(client)
}
func (zs *ZoneServer) processClients() {
// Process each client
zs.clientListLock.RLock()
clients := make([]Client, len(zs.clients))
copy(clients, zs.clients)
zs.clientListLock.RUnlock()
for _, client := range clients {
if client.IsLoadingZone() {
continue
}
// Update spawn visibility
zs.updateClientSpawnVisibility(client)
}
}
func (zs *ZoneServer) processSpawns() {
// Process spawn updates and changes
zs.spawnListLock.RLock()
spawns := make([]*spawn.Spawn, 0, len(zs.spawnList))
for _, spawn := range zs.spawnList {
spawns = append(spawns, spawn)
}
zs.spawnListLock.RUnlock()
for _, spawn := range spawns {
// Process spawn logic here
_ = spawn // Placeholder
}
}
func (zs *ZoneServer) processTimers() {
// Check and process all timers using non-blocking selects
select {
case <-zs.aggroTimer.C:
zs.processAggroChecks()
zs.aggroTimer.Reset(AggroCheckInterval)
default:
}
select {
case <-zs.respawnTimer.C:
zs.processRespawns()
zs.respawnTimer.Reset(RespawnCheckInterval)
default:
}
select {
case <-zs.widgetTimer.C:
zs.processWidgets()
zs.widgetTimer.Reset(WidgetUpdateInterval)
default:
}
// Add other timer checks...
}
func (zs *ZoneServer) processSpawnChanges() {
zs.changedSpawnsLock.Lock()
defer zs.changedSpawnsLock.Unlock()
if len(zs.changedSpawns) == 0 {
return
}
// Send changes to all clients
zs.clientListLock.RLock()
clients := make([]Client, len(zs.clients))
copy(clients, zs.clients)
zs.clientListLock.RUnlock()
for spawnID := range zs.changedSpawns {
spawn := zs.GetSpawn(spawnID)
if spawn != nil {
for _, client := range clients {
if client.CanSeeSpawn(spawn) {
zs.sendSpawnUpdateToClient(client, spawn)
}
}
}
}
// Clear changed spawns
zs.changedSpawns = make(map[int32]bool)
}
func (zs *ZoneServer) processProximityChecks() {
// Process player and location proximity
select {
case <-zs.locationProxTimer.C:
zs.checkLocationProximity()
zs.checkPlayerProximity()
zs.locationProxTimer.Reset(LocationProximityInterval)
default:
}
}
func (zs *ZoneServer) processWeather() {
select {
case <-zs.weatherTimer.C:
zs.ProcessWeather()
zs.weatherTimer.Reset(WeatherUpdateInterval)
default:
}
}
func (zs *ZoneServer) cleanupExpiredData() {
// Clean up dead spawns, expired timers, etc.
zs.cleanupDeadSpawns()
zs.cleanupExpiredTimers()
}
// Helper functions for various processing tasks
func (zs *ZoneServer) processSpawnLocations() {
// TODO: Process spawn location respawns
}
func (zs *ZoneServer) processSpawnMovement() {
// TODO: Process NPC movement
}
func (zs *ZoneServer) processNPCAI() {
// TODO: Process NPC AI
}
func (zs *ZoneServer) processCombat() {
// TODO: Process combat
}
func (zs *ZoneServer) checkDeadSpawnRemoval() {
// TODO: Check for dead spawns to remove
}
func (zs *ZoneServer) processSpawnScriptTimers() {
// TODO: Process spawn script timers
}
func (zs *ZoneServer) processAggroChecks() {
// TODO: Process aggro checks
}
func (zs *ZoneServer) processRespawns() {
// TODO: Process respawns
}
func (zs *ZoneServer) processWidgets() {
// TODO: Process widget timers
}
func (zs *ZoneServer) checkLocationProximity() {
// TODO: Check location proximity
}
func (zs *ZoneServer) checkPlayerProximity() {
// TODO: Check player proximity
}
func (zs *ZoneServer) cleanupDeadSpawns() {
// TODO: Clean up dead spawns
}
func (zs *ZoneServer) cleanupExpiredTimers() {
// TODO: Clean up expired timers
}
func (zs *ZoneServer) cleanupClientData(client Client) {
// TODO: Clean up client-specific data
}
func (zs *ZoneServer) cleanupSpawnData(spawn *spawn.Spawn) {
// TODO: Clean up spawn-specific data
}
func (zs *ZoneServer) addSpawnToGrid(spawn *spawn.Spawn) {
// TODO: Add spawn to grid system
}
func (zs *ZoneServer) removeSpawnFromGrid(spawn *spawn.Spawn) {
// TODO: Remove spawn from grid system
}
func (zs *ZoneServer) markSpawnChanged(spawnID int32) {
zs.changedSpawnsLock.Lock()
defer zs.changedSpawnsLock.Unlock()
zs.changedSpawns[spawnID] = true
}
func (zs *ZoneServer) markSpawnForRemoval(spawnID int32) {
zs.pendingSpawnRemoveLock.Lock()
defer zs.pendingSpawnRemoveLock.Unlock()
zs.pendingSpawnRemove[spawnID] = true
}
func (zs *ZoneServer) sendSpawnToClient(client Client, spawn *spawn.Spawn) {
// TODO: Send spawn packet to client
}
func (zs *ZoneServer) sendSpawnUpdateToClient(client Client, spawn *spawn.Spawn) {
// TODO: Send spawn update packet to client
}
func (zs *ZoneServer) sendZoneInfo(client Client) {
// TODO: Send zone info packet to client
}
func (zs *ZoneServer) sendWeatherUpdate() {
// TODO: Send weather update to all clients
}
func (zs *ZoneServer) sendWeatherUpdateToClient(client Client) {
// TODO: Send weather update to specific client
}
func (zs *ZoneServer) sendTimeUpdateToClient(client Client) {
// TODO: Send time update to client
}
func (zs *ZoneServer) updateClientSpawnVisibility(client Client) {
// TODO: Update spawn visibility for client
}
func abs(x float32) float32 {
if x < 0 {
return -x
}
return x
}