615 lines
14 KiB
Go
615 lines
14 KiB
Go
package player
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"eq2emu/internal/entity"
|
|
)
|
|
|
|
// Manager handles player management operations
|
|
type Manager struct {
|
|
// Players indexed by various keys
|
|
playersLock sync.RWMutex
|
|
players map[int32]*Player // playerID -> Player
|
|
playersByName map[string]*Player // name -> Player (case insensitive)
|
|
playersByCharID map[int32]*Player // characterID -> Player
|
|
playersByZone map[int32][]*Player // zoneID -> []*Player
|
|
|
|
// Player statistics
|
|
stats PlayerStats
|
|
statsLock sync.RWMutex
|
|
|
|
// Event handlers
|
|
eventHandlers []PlayerEventHandler
|
|
eventLock sync.RWMutex
|
|
|
|
// Validators
|
|
validators []PlayerValidator
|
|
|
|
// Database interface
|
|
database PlayerDatabase
|
|
|
|
// Packet handler
|
|
packetHandler PlayerPacketHandler
|
|
|
|
// Notifier
|
|
notifier PlayerNotifier
|
|
|
|
// Statistics tracker
|
|
statistics PlayerStatistics
|
|
|
|
// Configuration
|
|
config ManagerConfig
|
|
|
|
// Shutdown channel
|
|
shutdown chan struct{}
|
|
|
|
// Background goroutines
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// PlayerStats holds various player statistics
|
|
type PlayerStats struct {
|
|
TotalPlayers int64
|
|
ActivePlayers int64
|
|
PlayersLoggedIn int64
|
|
PlayersLoggedOut int64
|
|
AverageLevel float64
|
|
MaxLevel int8
|
|
TotalPlayTime time.Duration
|
|
}
|
|
|
|
// ManagerConfig holds configuration for the player manager
|
|
type ManagerConfig struct {
|
|
// Maximum number of players
|
|
MaxPlayers int32
|
|
|
|
// Player save interval
|
|
SaveInterval time.Duration
|
|
|
|
// Statistics update interval
|
|
StatsInterval time.Duration
|
|
|
|
// Enable player validation
|
|
EnableValidation bool
|
|
|
|
// Enable event handling
|
|
EnableEvents bool
|
|
|
|
// Enable statistics tracking
|
|
EnableStatistics bool
|
|
}
|
|
|
|
// NewManager creates a new player manager
|
|
func NewManager(config ManagerConfig) *Manager {
|
|
return &Manager{
|
|
players: make(map[int32]*Player),
|
|
playersByName: make(map[string]*Player),
|
|
playersByCharID: make(map[int32]*Player),
|
|
playersByZone: make(map[int32][]*Player),
|
|
eventHandlers: make([]PlayerEventHandler, 0),
|
|
validators: make([]PlayerValidator, 0),
|
|
config: config,
|
|
shutdown: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Start starts the player manager
|
|
func (m *Manager) Start() error {
|
|
// Start background processes
|
|
if m.config.SaveInterval > 0 {
|
|
m.wg.Add(1)
|
|
go m.savePlayersLoop()
|
|
}
|
|
|
|
if m.config.StatsInterval > 0 {
|
|
m.wg.Add(1)
|
|
go m.updateStatsLoop()
|
|
}
|
|
|
|
m.wg.Add(1)
|
|
go m.processPlayersLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the player manager
|
|
func (m *Manager) Stop() error {
|
|
close(m.shutdown)
|
|
m.wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
// AddPlayer adds a player to management
|
|
func (m *Manager) AddPlayer(player *Player) error {
|
|
if player == nil {
|
|
return fmt.Errorf("player cannot be nil")
|
|
}
|
|
|
|
m.playersLock.Lock()
|
|
defer m.playersLock.Unlock()
|
|
|
|
// Check if we're at capacity
|
|
if m.config.MaxPlayers > 0 && int32(len(m.players)) >= m.config.MaxPlayers {
|
|
return fmt.Errorf("server at maximum player capacity")
|
|
}
|
|
|
|
playerID := player.GetSpawnID()
|
|
characterID := player.GetCharacterID()
|
|
name := player.GetName()
|
|
zoneID := player.GetZone()
|
|
|
|
// Check for duplicates
|
|
if _, exists := m.players[playerID]; exists {
|
|
return fmt.Errorf("player with ID %d already exists", playerID)
|
|
}
|
|
|
|
if _, exists := m.playersByCharID[characterID]; exists {
|
|
return fmt.Errorf("player with character ID %d already exists", characterID)
|
|
}
|
|
|
|
if _, exists := m.playersByName[name]; exists {
|
|
return fmt.Errorf("player with name %s already exists", name)
|
|
}
|
|
|
|
// Add to maps
|
|
m.players[playerID] = player
|
|
m.playersByCharID[characterID] = player
|
|
m.playersByName[name] = player
|
|
|
|
// Add to zone map
|
|
if m.playersByZone[zoneID] == nil {
|
|
m.playersByZone[zoneID] = make([]*Player, 0)
|
|
}
|
|
m.playersByZone[zoneID] = append(m.playersByZone[zoneID], player)
|
|
|
|
// Update statistics
|
|
m.updateStatsForAdd()
|
|
|
|
// Fire event
|
|
if m.config.EnableEvents {
|
|
m.firePlayerLoginEvent(player)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemovePlayer removes a player from management
|
|
func (m *Manager) RemovePlayer(playerID int32) error {
|
|
m.playersLock.Lock()
|
|
defer m.playersLock.Unlock()
|
|
|
|
player, exists := m.players[playerID]
|
|
if !exists {
|
|
return fmt.Errorf("player with ID %d not found", playerID)
|
|
}
|
|
|
|
// Remove from maps
|
|
delete(m.players, playerID)
|
|
delete(m.playersByCharID, player.GetCharacterID())
|
|
delete(m.playersByName, player.GetName())
|
|
|
|
// Remove from zone map
|
|
zoneID := player.GetZone()
|
|
if zonePlayers, exists := m.playersByZone[zoneID]; exists {
|
|
for i, p := range zonePlayers {
|
|
if p == player {
|
|
m.playersByZone[zoneID] = append(zonePlayers[:i], zonePlayers[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
// Clean up empty zone lists
|
|
if len(m.playersByZone[zoneID]) == 0 {
|
|
delete(m.playersByZone, zoneID)
|
|
}
|
|
}
|
|
|
|
// Update statistics
|
|
m.updateStatsForRemove()
|
|
|
|
// Fire event
|
|
if m.config.EnableEvents {
|
|
m.firePlayerLogoutEvent(player)
|
|
}
|
|
|
|
// Save player data before removal
|
|
if m.database != nil {
|
|
m.database.SavePlayer(player)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPlayer returns a player by spawn ID
|
|
func (m *Manager) GetPlayer(playerID int32) *Player {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
return m.players[playerID]
|
|
}
|
|
|
|
// GetPlayerByName returns a player by name
|
|
func (m *Manager) GetPlayerByName(name string) *Player {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
return m.playersByName[name]
|
|
}
|
|
|
|
// GetPlayerByCharacterID returns a player by character ID
|
|
func (m *Manager) GetPlayerByCharacterID(characterID int32) *Player {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
return m.playersByCharID[characterID]
|
|
}
|
|
|
|
// GetAllPlayers returns all managed players
|
|
func (m *Manager) GetAllPlayers() []*Player {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
players := make([]*Player, 0, len(m.players))
|
|
for _, player := range m.players {
|
|
players = append(players, player)
|
|
}
|
|
return players
|
|
}
|
|
|
|
// GetPlayersInZone returns all players in a zone
|
|
func (m *Manager) GetPlayersInZone(zoneID int32) []*Player {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
if zonePlayers, exists := m.playersByZone[zoneID]; exists {
|
|
// Return a copy to avoid race conditions
|
|
players := make([]*Player, len(zonePlayers))
|
|
copy(players, zonePlayers)
|
|
return players
|
|
}
|
|
|
|
return []*Player{}
|
|
}
|
|
|
|
// SendToAll sends a message to all players
|
|
func (m *Manager) SendToAll(message any) error {
|
|
if m.packetHandler == nil {
|
|
return fmt.Errorf("no packet handler configured")
|
|
}
|
|
|
|
players := m.GetAllPlayers()
|
|
return m.packetHandler.BroadcastPacket(players, message)
|
|
}
|
|
|
|
// SendToZone sends a message to all players in a zone
|
|
func (m *Manager) SendToZone(zoneID int32, message any) error {
|
|
if m.packetHandler == nil {
|
|
return fmt.Errorf("no packet handler configured")
|
|
}
|
|
|
|
players := m.GetPlayersInZone(zoneID)
|
|
return m.packetHandler.BroadcastPacket(players, message)
|
|
}
|
|
|
|
// MovePlayerToZone moves a player to a different zone
|
|
func (m *Manager) MovePlayerToZone(playerID, newZoneID int32) error {
|
|
m.playersLock.Lock()
|
|
defer m.playersLock.Unlock()
|
|
|
|
player, exists := m.players[playerID]
|
|
if !exists {
|
|
return fmt.Errorf("player with ID %d not found", playerID)
|
|
}
|
|
|
|
oldZoneID := player.GetZone()
|
|
if oldZoneID == newZoneID {
|
|
return nil // Already in the zone
|
|
}
|
|
|
|
// Remove from old zone
|
|
if zonePlayers, exists := m.playersByZone[oldZoneID]; exists {
|
|
for i, p := range zonePlayers {
|
|
if p == player {
|
|
m.playersByZone[oldZoneID] = append(zonePlayers[:i], zonePlayers[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(m.playersByZone[oldZoneID]) == 0 {
|
|
delete(m.playersByZone, oldZoneID)
|
|
}
|
|
}
|
|
|
|
// Add to new zone
|
|
if m.playersByZone[newZoneID] == nil {
|
|
m.playersByZone[newZoneID] = make([]*Player, 0)
|
|
}
|
|
m.playersByZone[newZoneID] = append(m.playersByZone[newZoneID], player)
|
|
|
|
// Update player's zone
|
|
player.SetZone(newZoneID)
|
|
|
|
// Fire event
|
|
if m.config.EnableEvents {
|
|
m.firePlayerZoneChangeEvent(player, oldZoneID, newZoneID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPlayerCount returns the current number of players
|
|
func (m *Manager) GetPlayerCount() int32 {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
return int32(len(m.players))
|
|
}
|
|
|
|
// GetZonePlayerCount returns the number of players in a zone
|
|
func (m *Manager) GetZonePlayerCount(zoneID int32) int32 {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
if zonePlayers, exists := m.playersByZone[zoneID]; exists {
|
|
return int32(len(zonePlayers))
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// GetPlayerStats returns current player statistics
|
|
func (m *Manager) GetPlayerStats() PlayerStats {
|
|
m.statsLock.RLock()
|
|
defer m.statsLock.RUnlock()
|
|
|
|
return m.stats
|
|
}
|
|
|
|
// AddEventHandler adds an event handler
|
|
func (m *Manager) AddEventHandler(handler PlayerEventHandler) {
|
|
m.eventLock.Lock()
|
|
defer m.eventLock.Unlock()
|
|
|
|
m.eventHandlers = append(m.eventHandlers, handler)
|
|
}
|
|
|
|
// AddValidator adds a validator
|
|
func (m *Manager) AddValidator(validator PlayerValidator) {
|
|
m.validators = append(m.validators, validator)
|
|
}
|
|
|
|
// SetDatabase sets the database interface
|
|
func (m *Manager) SetDatabase(db PlayerDatabase) {
|
|
m.database = db
|
|
}
|
|
|
|
// SetPacketHandler sets the packet handler
|
|
func (m *Manager) SetPacketHandler(handler PlayerPacketHandler) {
|
|
m.packetHandler = handler
|
|
}
|
|
|
|
// SetNotifier sets the notifier
|
|
func (m *Manager) SetNotifier(notifier PlayerNotifier) {
|
|
m.notifier = notifier
|
|
}
|
|
|
|
// SetStatistics sets the statistics tracker
|
|
func (m *Manager) SetStatistics(stats PlayerStatistics) {
|
|
m.statistics = stats
|
|
}
|
|
|
|
// ValidatePlayer validates a player using all validators
|
|
func (m *Manager) ValidatePlayer(player *Player) error {
|
|
if !m.config.EnableValidation {
|
|
return nil
|
|
}
|
|
|
|
for _, validator := range m.validators {
|
|
if err := validator.ValidateLogin(player); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// savePlayersLoop periodically saves all players
|
|
func (m *Manager) savePlayersLoop() {
|
|
defer m.wg.Done()
|
|
|
|
ticker := time.NewTicker(m.config.SaveInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
m.saveAllPlayers()
|
|
case <-m.shutdown:
|
|
// Final save before shutdown
|
|
m.saveAllPlayers()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateStatsLoop periodically updates statistics
|
|
func (m *Manager) updateStatsLoop() {
|
|
defer m.wg.Done()
|
|
|
|
ticker := time.NewTicker(m.config.StatsInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
m.updatePlayerStats()
|
|
case <-m.shutdown:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// processPlayersLoop processes player updates
|
|
func (m *Manager) processPlayersLoop() {
|
|
defer m.wg.Done()
|
|
|
|
ticker := time.NewTicker(100 * time.Millisecond) // 10Hz
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
m.processAllPlayers()
|
|
case <-m.shutdown:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// saveAllPlayers saves all players to database
|
|
func (m *Manager) saveAllPlayers() {
|
|
if m.database == nil {
|
|
return
|
|
}
|
|
|
|
players := m.GetAllPlayers()
|
|
for _, player := range players {
|
|
m.database.SavePlayer(player)
|
|
}
|
|
}
|
|
|
|
// updatePlayerStats updates player statistics
|
|
func (m *Manager) updatePlayerStats() {
|
|
m.playersLock.RLock()
|
|
defer m.playersLock.RUnlock()
|
|
|
|
m.statsLock.Lock()
|
|
defer m.statsLock.Unlock()
|
|
|
|
m.stats.ActivePlayers = int64(len(m.players))
|
|
|
|
var totalLevel int64
|
|
var maxLevel int8
|
|
|
|
for _, player := range m.players {
|
|
level := player.GetLevel()
|
|
totalLevel += int64(level)
|
|
if level > maxLevel {
|
|
maxLevel = level
|
|
}
|
|
}
|
|
|
|
if len(m.players) > 0 {
|
|
m.stats.AverageLevel = float64(totalLevel) / float64(len(m.players))
|
|
}
|
|
m.stats.MaxLevel = maxLevel
|
|
}
|
|
|
|
// processAllPlayers processes updates for all players
|
|
func (m *Manager) processAllPlayers() {
|
|
players := m.GetAllPlayers()
|
|
|
|
for _, player := range players {
|
|
// Process spawn state queue
|
|
player.CheckSpawnStateQueue()
|
|
|
|
// Process combat
|
|
player.ProcessCombat()
|
|
|
|
// Process range updates
|
|
player.ProcessSpawnRangeUpdates()
|
|
|
|
// TODO: Add other periodic processing
|
|
}
|
|
}
|
|
|
|
// updateStatsForAdd updates stats when a player is added
|
|
func (m *Manager) updateStatsForAdd() {
|
|
m.statsLock.Lock()
|
|
defer m.statsLock.Unlock()
|
|
|
|
m.stats.TotalPlayers++
|
|
m.stats.PlayersLoggedIn++
|
|
}
|
|
|
|
// updateStatsForRemove updates stats when a player is removed
|
|
func (m *Manager) updateStatsForRemove() {
|
|
m.statsLock.Lock()
|
|
defer m.statsLock.Unlock()
|
|
|
|
m.stats.PlayersLoggedOut++
|
|
}
|
|
|
|
// Event firing methods
|
|
func (m *Manager) firePlayerLoginEvent(player *Player) {
|
|
m.eventLock.RLock()
|
|
defer m.eventLock.RUnlock()
|
|
|
|
for _, handler := range m.eventHandlers {
|
|
handler.OnPlayerLogin(player)
|
|
}
|
|
|
|
if m.statistics != nil {
|
|
m.statistics.RecordPlayerLogin(player)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) firePlayerLogoutEvent(player *Player) {
|
|
m.eventLock.RLock()
|
|
defer m.eventLock.RUnlock()
|
|
|
|
for _, handler := range m.eventHandlers {
|
|
handler.OnPlayerLogout(player)
|
|
}
|
|
|
|
if m.statistics != nil {
|
|
m.statistics.RecordPlayerLogout(player)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) firePlayerZoneChangeEvent(player *Player, fromZoneID, toZoneID int32) {
|
|
m.eventLock.RLock()
|
|
defer m.eventLock.RUnlock()
|
|
|
|
for _, handler := range m.eventHandlers {
|
|
handler.OnPlayerZoneChange(player, fromZoneID, toZoneID)
|
|
}
|
|
}
|
|
|
|
// FirePlayerLevelUpEvent fires a level up event
|
|
func (m *Manager) FirePlayerLevelUpEvent(player *Player, newLevel int8) {
|
|
m.eventLock.RLock()
|
|
defer m.eventLock.RUnlock()
|
|
|
|
for _, handler := range m.eventHandlers {
|
|
handler.OnPlayerLevelUp(player, newLevel)
|
|
}
|
|
|
|
if m.notifier != nil {
|
|
m.notifier.NotifyLevelUp(player, newLevel)
|
|
}
|
|
}
|
|
|
|
// FirePlayerDeathEvent fires a death event
|
|
func (m *Manager) FirePlayerDeathEvent(player *Player, killer entity.Entity) {
|
|
m.eventLock.RLock()
|
|
defer m.eventLock.RUnlock()
|
|
|
|
for _, handler := range m.eventHandlers {
|
|
handler.OnPlayerDeath(player, killer)
|
|
}
|
|
|
|
if m.statistics != nil {
|
|
m.statistics.RecordPlayerDeath(player, killer)
|
|
}
|
|
}
|
|
|
|
// FirePlayerResurrectEvent fires a resurrect event
|
|
func (m *Manager) FirePlayerResurrectEvent(player *Player) {
|
|
m.eventLock.RLock()
|
|
defer m.eventLock.RUnlock()
|
|
|
|
for _, handler := range m.eventHandlers {
|
|
handler.OnPlayerResurrect(player)
|
|
}
|
|
}
|