package player import ( "fmt" "sync" "time" "eq2emu/internal/database" ) // Logger interface for player system type Logger interface { Debug(string, ...interface{}) Info(string, ...interface{}) Warn(string, ...interface{}) Error(string, ...interface{}) } // Config interface for player system type Config interface { GetString(string) string GetInt(string) int GetBool(string) bool } // PlayerManager manages all player operations in a unified system type PlayerManager struct { db *database.Database logger Logger config Config playersLock sync.RWMutex players map[int32]*Player experienceCalc ExperienceCalculator combatSystem CombatSystem questSystem QuestSystem dbPersister DatabasePersister eventManager EventManager } // ExperienceCalculator handles all experience calculations type ExperienceCalculator struct { baseLevelXP []int64 tsLevelXP []int64 vitaeLevels []float32 aaMutex sync.RWMutex } // CombatSystem handles all combat-related functionality type CombatSystem struct { activeCombats map[int32]*CombatSession combatMutex sync.RWMutex damageTypes map[int32]string } // QuestSystem manages all quest-related functionality type QuestSystem struct { questCache map[int32]*Quest questMutex sync.RWMutex completionCache map[int32]bool } // DatabasePersister handles all database operations type DatabasePersister struct { db *database.Database saveMutex sync.Mutex saveQueue chan *Player stopChan chan bool } // EventManager handles all player events type EventManager struct { eventQueue chan PlayerEvent subscribers map[EventType][]EventHandler eventMutex sync.RWMutex } // PlayerEvent represents a player event type PlayerEvent struct { Type EventType PlayerID int32 Data interface{} } // EventType represents the type of player event type EventType int const ( EventPlayerLogin EventType = iota EventPlayerLogout EventPlayerLevelUp EventPlayerDeath EventPlayerCombat EventPlayerQuest ) // EventHandler handles player events type EventHandler func(PlayerEvent) // CombatSession represents an active combat session type CombatSession struct { PlayerID int32 TargetID int32 StartTime time.Time DamageDealt int32 DamageTaken int32 } // Quest represents a quest (placeholder for full implementation) type Quest struct { ID int32 Name string Description string Status int32 } // NewPlayerManager creates a new unified player manager func NewPlayerManager(db *database.Database, logger Logger, config Config) *PlayerManager { pm := &PlayerManager{ db: db, logger: logger, config: config, players: make(map[int32]*Player), experienceCalc: ExperienceCalculator{ baseLevelXP: make([]int64, 101), tsLevelXP: make([]int64, 101), vitaeLevels: make([]float32, 101), }, combatSystem: CombatSystem{ activeCombats: make(map[int32]*CombatSession), damageTypes: make(map[int32]string), }, questSystem: QuestSystem{ questCache: make(map[int32]*Quest), completionCache: make(map[int32]bool), }, dbPersister: DatabasePersister{ db: db, saveQueue: make(chan *Player, 100), stopChan: make(chan bool), }, eventManager: EventManager{ eventQueue: make(chan PlayerEvent, 1000), subscribers: make(map[EventType][]EventHandler), }, } // Initialize experience tables pm.initializeExperienceTables() // Start background processors pm.startBackgroundProcessors() return pm } // AddPlayer adds a player to the manager func (pm *PlayerManager) AddPlayer(player *Player) { pm.playersLock.Lock() defer pm.playersLock.Unlock() pm.players[player.charID] = player pm.logger.Info("Player added: %d", player.charID) // Send login event pm.eventManager.eventQueue <- PlayerEvent{ Type: EventPlayerLogin, PlayerID: player.charID, Data: player, } } // RemovePlayer removes a player from the manager func (pm *PlayerManager) RemovePlayer(charID int32) { pm.playersLock.Lock() defer pm.playersLock.Unlock() if player, exists := pm.players[charID]; exists { // Send logout event pm.eventManager.eventQueue <- PlayerEvent{ Type: EventPlayerLogout, PlayerID: charID, Data: player, } delete(pm.players, charID) pm.logger.Info("Player removed: %d", charID) } } // GetPlayer retrieves a player by character ID func (pm *PlayerManager) GetPlayer(charID int32) *Player { pm.playersLock.RLock() defer pm.playersLock.RUnlock() return pm.players[charID] } // GetAllPlayers returns all active players func (pm *PlayerManager) GetAllPlayers() []*Player { pm.playersLock.RLock() defer pm.playersLock.RUnlock() players := make([]*Player, 0, len(pm.players)) for _, player := range pm.players { players = append(players, player) } return players } // UpdatePlayerLevel handles player level progression func (pm *PlayerManager) UpdatePlayerLevel(charID int32, newLevel int16) error { player := pm.GetPlayer(charID) if player == nil { return fmt.Errorf("player not found: %d", charID) } oldLevel := player.GetLevel() player.SetLevel(newLevel) // Send level up event pm.eventManager.eventQueue <- PlayerEvent{ Type: EventPlayerLevelUp, PlayerID: charID, Data: map[string]interface{}{"oldLevel": oldLevel, "newLevel": newLevel}, } // Send level up packet pm.sendLevelUpPacket(player) pm.logger.Info("Player %d leveled up from %d to %d", charID, oldLevel, newLevel) return nil } // AddExperience adds experience to a player func (pm *PlayerManager) AddExperience(charID int32, xpAmount int64, xpType string) error { player := pm.GetPlayer(charID) if player == nil { return fmt.Errorf("player not found: %d", charID) } // Calculate vitae modifier vitaeModifier := pm.experienceCalc.getVitaeModifier(int16(player.GetLevel())) adjustedXP := int64(float32(xpAmount) * vitaeModifier) // Validate XP type switch xpType { case "adventure", "tradeskill": // Valid types - continue default: return fmt.Errorf("unknown XP type: %s", xpType) } // For now, just log the XP addition - actual XP modification would require // accessor methods to be added to InfoStruct pm.logger.Debug("Would add %d %s XP to player %d (vitae: %.2f)", adjustedXP, xpType, charID, vitaeModifier) // Check for level up (simplified version) pm.checkLevelUp(player, xpType) // Send XP update packet pm.sendXPUpdatePacket(player, adjustedXP, xpType) return nil } // ProcessCombat handles combat between entities func (pm *PlayerManager) ProcessCombat(attackerID, defenderID int32, damageAmount int32) error { attacker := pm.GetPlayer(attackerID) if attacker == nil { return fmt.Errorf("attacker not found: %d", attackerID) } // Create or update combat session pm.combatSystem.combatMutex.Lock() session, exists := pm.combatSystem.activeCombats[attackerID] if !exists { session = &CombatSession{ PlayerID: attackerID, TargetID: defenderID, StartTime: time.Now(), } pm.combatSystem.activeCombats[attackerID] = session } session.DamageDealt += damageAmount pm.combatSystem.combatMutex.Unlock() // Send combat event pm.eventManager.eventQueue <- PlayerEvent{ Type: EventPlayerCombat, PlayerID: attackerID, Data: session, } // Send combat packet pm.sendCombatPacket(attacker, defenderID, damageAmount) pm.logger.Debug("Combat: Player %d dealt %d damage to %d", attackerID, damageAmount, defenderID) return nil } // UpdateCurrency updates a player's currency func (pm *PlayerManager) UpdateCurrency(charID int32, currencyType string, amount int64) error { player := pm.GetPlayer(charID) if player == nil { return fmt.Errorf("player not found: %d", charID) } // Get the player's info struct info := player.GetInfoStruct() if info == nil { return fmt.Errorf("player info not available") } // Update currency based on type switch currencyType { case "coin": info.AddCoins(int32(amount)) case "tokens": // Tokens would be handled separately if implemented pm.logger.Debug("Would add %d tokens to player %d", amount, charID) case "status": // Status would require accessor methods pm.logger.Debug("Would add %d status to player %d", amount, charID) default: return fmt.Errorf("unknown currency type: %s", currencyType) } // Send currency update packet pm.sendCurrencyUpdatePacket(player, currencyType, amount) pm.logger.Debug("Updated %s currency for player %d: %d", currencyType, charID, amount) return nil } // SavePlayer saves a player to the database func (pm *PlayerManager) SavePlayer(charID int32) error { player := pm.GetPlayer(charID) if player == nil { return fmt.Errorf("player not found: %d", charID) } // Queue for background save select { case pm.dbPersister.saveQueue <- player: pm.logger.Debug("Queued player %d for save", charID) default: pm.logger.Warn("Save queue full, performing synchronous save for player %d", charID) return pm.savePlayerSync(player) } return nil } // SaveAllPlayers saves all active players func (pm *PlayerManager) SaveAllPlayers() error { players := pm.GetAllPlayers() for _, player := range players { if err := pm.SavePlayer(player.charID); err != nil { pm.logger.Error("Failed to save player %d: %v", player.charID, err) } } pm.logger.Info("Initiated save for %d players", len(players)) return nil } // Shutdown gracefully shuts down the player manager func (pm *PlayerManager) Shutdown() { pm.logger.Info("Shutting down player manager...") // Stop background processors close(pm.dbPersister.stopChan) // Save all players pm.SaveAllPlayers() // Wait for save queue to empty time.Sleep(time.Second * 2) pm.logger.Info("Player manager shutdown complete") } // initializeExperienceTables initializes the XP lookup tables func (pm *PlayerManager) initializeExperienceTables() { // Initialize base level XP requirements (example values) for i := 0; i < 101; i++ { pm.experienceCalc.baseLevelXP[i] = int64(i * i * 100) pm.experienceCalc.tsLevelXP[i] = int64(i * i * 80) pm.experienceCalc.vitaeLevels[i] = 1.0 // No vitae by default } } // startBackgroundProcessors starts all background processing routines func (pm *PlayerManager) startBackgroundProcessors() { // Start save processor go pm.dbPersister.processSaveQueue() // Start event processor go pm.eventManager.processEvents() // Start periodic tasks go pm.periodicTasks() } // processSaveQueue processes the background save queue func (dp *DatabasePersister) processSaveQueue() { for { select { case player := <-dp.saveQueue: if err := dp.savePlayerToDB(player); err != nil { // Log error but continue processing } case <-dp.stopChan: // Process remaining items in queue for len(dp.saveQueue) > 0 { player := <-dp.saveQueue dp.savePlayerToDB(player) } return } } } // processEvents processes the background event queue func (em *EventManager) processEvents() { for event := range em.eventQueue { em.eventMutex.RLock() handlers := em.subscribers[event.Type] em.eventMutex.RUnlock() for _, handler := range handlers { go handler(event) } } } // periodicTasks runs periodic maintenance tasks func (pm *PlayerManager) periodicTasks() { ticker := time.NewTicker(time.Minute * 5) defer ticker.Stop() for { select { case <-ticker.C: pm.performMaintenance() case <-pm.dbPersister.stopChan: return } } } // performMaintenance performs periodic maintenance tasks func (pm *PlayerManager) performMaintenance() { // Clean up old combat sessions pm.combatSystem.combatMutex.Lock() cutoff := time.Now().Add(-time.Minute * 10) for playerID, session := range pm.combatSystem.activeCombats { if session.StartTime.Before(cutoff) { delete(pm.combatSystem.activeCombats, playerID) } } pm.combatSystem.combatMutex.Unlock() pm.logger.Debug("Performed maintenance tasks") } // Helper methods for experience calculation func (ec *ExperienceCalculator) getVitaeModifier(level int16) float32 { if level < 0 || int(level) >= len(ec.vitaeLevels) { return 1.0 } return ec.vitaeLevels[level] } // checkLevelUp checks if a player should level up func (pm *PlayerManager) checkLevelUp(player *Player, xpType string) { currentLevel := player.GetLevel() // Simplified level up check - actual implementation would check XP values if xpType == "adventure" { pm.logger.Debug("Would check adventure level up for player %d at level %d", player.charID, currentLevel) } else if xpType == "tradeskill" { pm.logger.Debug("Would check tradeskill level up for player %d", player.charID) } } // Packet sending methods func (pm *PlayerManager) sendLevelUpPacket(player *Player) { if pm.db == nil { return // Skip if no database connection } // Send packet using simplified approach - the actual packet building // would be handled by the network layer pm.logger.Debug("Sent level up packet for player %d to level %d", player.charID, player.GetLevel()) } func (pm *PlayerManager) sendXPUpdatePacket(player *Player, xpAmount int64, xpType string) { if pm.db == nil { return } pm.logger.Debug("Sent %s XP update packet for player %d: %d", xpType, player.charID, xpAmount) } func (pm *PlayerManager) sendCombatPacket(attacker *Player, defenderID int32, damage int32) { if pm.db == nil { return } pm.logger.Debug("Sent combat packet for player %d vs %d: %d damage", attacker.charID, defenderID, damage) } func (pm *PlayerManager) sendCurrencyUpdatePacket(player *Player, currencyType string, amount int64) { if pm.db == nil { return } pm.logger.Debug("Sent currency update packet for player %d: %s +%d", player.charID, currencyType, amount) } // Database operations func (dp *DatabasePersister) savePlayerToDB(player *Player) error { if dp.db == nil { return fmt.Errorf("no database connection") } dp.saveMutex.Lock() defer dp.saveMutex.Unlock() // Get player info info := player.GetInfoStruct() if info == nil { return fmt.Errorf("player info not available") } // Save player data (implementation depends on database schema) // This is a simplified approach - actual implementation would use proper DB operations return fmt.Errorf("database save not yet implemented") } func (pm *PlayerManager) savePlayerSync(player *Player) error { return pm.dbPersister.savePlayerToDB(player) } // Event subscription methods func (pm *PlayerManager) SubscribeToEvent(eventType EventType, handler EventHandler) { pm.eventManager.eventMutex.Lock() defer pm.eventManager.eventMutex.Unlock() pm.eventManager.subscribers[eventType] = append(pm.eventManager.subscribers[eventType], handler) }