package alt_advancement import ( "fmt" "time" ) // NewAAManager creates a new AA manager func NewAAManager(config AAManagerConfig) *AAManager { return &AAManager{ masterAAList: NewMasterAAList(), masterNodeList: NewMasterAANodeList(), playerStates: make(map[int32]*AAPlayerState), config: config, eventHandlers: make([]AAEventHandler, 0), stats: AAManagerStats{}, stopChan: make(chan struct{}), } } // Start starts the AA manager func (am *AAManager) Start() error { // Load AA data if err := am.LoadAAData(); err != nil { return fmt.Errorf("failed to load AA data: %v", err) } // Start background processes if am.config.UpdateInterval > 0 { am.wg.Add(1) go am.updateStatsLoop() } if am.config.AutoSave && am.config.SaveInterval > 0 { am.wg.Add(1) go am.autoSaveLoop() } return nil } // Stop stops the AA manager func (am *AAManager) Stop() error { close(am.stopChan) am.wg.Wait() // Save all player states if auto-save is enabled if am.config.AutoSave { am.saveAllPlayerStates() } return nil } // IsRunning returns true if the manager is running func (am *AAManager) IsRunning() bool { select { case <-am.stopChan: return false default: return true } } // LoadAAData loads all AA data from the database func (am *AAManager) LoadAAData() error { if am.database == nil { return fmt.Errorf("database not configured") } startTime := time.Now() // Load AA definitions if err := am.database.LoadAltAdvancements(); err != nil { return fmt.Errorf("failed to load AAs: %v", err) } // Load tree nodes if err := am.database.LoadTreeNodes(); err != nil { return fmt.Errorf("failed to load tree nodes: %v", err) } // Update statistics am.statsMutex.Lock() am.stats.TotalAAsLoaded = int64(am.masterAAList.Size()) am.stats.TotalNodesLoaded = int64(am.masterNodeList.Size()) am.stats.LastLoadTime = startTime am.stats.LoadDuration = time.Since(startTime) am.statsMutex.Unlock() // Fire load event am.fireSystemLoadedEvent(int32(am.stats.TotalAAsLoaded), int32(am.stats.TotalNodesLoaded)) return nil } // ReloadAAData reloads all AA data func (am *AAManager) ReloadAAData() error { // Clear existing data am.masterAAList.DestroyAltAdvancements() am.masterNodeList.DestroyTreeNodes() // Clear cached player states am.statesMutex.Lock() am.playerStates = make(map[int32]*AAPlayerState) am.statesMutex.Unlock() // Reload data err := am.LoadAAData() if err == nil { am.fireDataReloadedEvent() } return err } // LoadPlayerAA loads AA data for a specific player func (am *AAManager) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { // For backwards compatibility, delegate to GetPlayerAAState // Note: GetPlayerAAState already handles thread safety return am.GetPlayerAAState(characterID) } // loadPlayerAAFromDatabase loads player data directly from database (internal helper) func (am *AAManager) loadPlayerAAFromDatabase(characterID int32) (*AAPlayerState, error) { if am.database == nil { return nil, fmt.Errorf("database not configured") } playerState, err := am.database.LoadPlayerAA(characterID) if err != nil { return nil, fmt.Errorf("failed to load player AA: %v", err) } return playerState, nil } // SavePlayerAA saves AA data for a specific player func (am *AAManager) SavePlayerAA(characterID int32) error { if am.database == nil { return fmt.Errorf("database not configured") } // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return fmt.Errorf("player state not found for character %d", characterID) } // Save to database return am.database.SavePlayerAA(playerState) } // GetPlayerAAState returns the AA state for a player func (am *AAManager) GetPlayerAAState(characterID int32) (*AAPlayerState, error) { // Try to get from cache first (read lock) am.statesMutex.RLock() if playerState, exists := am.playerStates[characterID]; exists { am.statesMutex.RUnlock() return playerState, nil } am.statesMutex.RUnlock() // Need to load from database, use write lock to prevent race condition am.statesMutex.Lock() defer am.statesMutex.Unlock() // Double-check pattern: another goroutine might have loaded it while we waited if playerState, exists := am.playerStates[characterID]; exists { return playerState, nil } // Load from database if not cached playerState, err := am.loadPlayerAAFromDatabase(characterID) if err != nil { return nil, err } // Cache the player state (already have write lock) am.playerStates[characterID] = playerState // Fire load event am.firePlayerAALoadedEvent(characterID, playerState) return playerState, nil } // PurchaseAA purchases an AA for a player func (am *AAManager) PurchaseAA(characterID int32, nodeID int32, targetRank int8) error { // Get player state - this handles loading if needed playerState, err := am.GetPlayerAAState(characterID) if err != nil { return fmt.Errorf("failed to get player state: %v", err) } // Get AA data aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID) if aaData == nil { return fmt.Errorf("AA node %d not found", nodeID) } // Validate purchase if am.validator != nil { if err := am.validator.ValidateAAPurchase(playerState, nodeID, targetRank); err != nil { am.updateErrorStats("validation_errors") return fmt.Errorf("validation failed: %v", err) } } // Perform purchase err = am.performAAPurchase(playerState, aaData, targetRank) if err != nil { return err } // Fire purchase event pointsSpent := int32(aaData.RankCost) * int32(targetRank) am.fireAAPurchasedEvent(characterID, nodeID, targetRank, pointsSpent) // Send notification if am.notifier != nil { am.notifier.NotifyAAPurchaseSuccess(characterID, aaData.Name, targetRank) } // Update statistics if am.statistics != nil { am.statistics.RecordAAPurchase(characterID, nodeID, pointsSpent) } return nil } // RefundAA refunds an AA for a player func (am *AAManager) RefundAA(characterID int32, nodeID int32) error { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return fmt.Errorf("player state not found") } // Get AA data aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID) if aaData == nil { return fmt.Errorf("AA node %d not found", nodeID) } // Get current progress progress, exists := playerState.AAProgress[nodeID] if !exists || progress.CurrentRank == 0 { return fmt.Errorf("AA not purchased or already at rank 0") } // Calculate refund amount pointsRefunded := progress.PointsSpent // Perform refund playerState.mutex.Lock() delete(playerState.AAProgress, nodeID) playerState.SpentPoints -= pointsRefunded playerState.AvailablePoints += pointsRefunded playerState.needsSync = true playerState.mutex.Unlock() // Fire refund event am.fireAARefundedEvent(characterID, nodeID, progress.CurrentRank, pointsRefunded) // Send notification if am.notifier != nil { am.notifier.NotifyAARefund(characterID, aaData.Name, pointsRefunded) } // Update statistics if am.statistics != nil { am.statistics.RecordAARefund(characterID, nodeID, pointsRefunded) } return nil } // GetAvailableAAs returns AAs available for a player in a specific tab func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error) { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return nil, fmt.Errorf("player state not found") } // Get all AAs for the tab allAAs := am.masterAAList.GetAAsByGroup(tabID) var availableAAs []*AltAdvanceData for _, aa := range allAAs { // Check if AA is available for this player if am.isAAAvailable(playerState, aa) { availableAAs = append(availableAAs, aa) } } return availableAAs, nil } // ChangeAATemplate changes the active AA template for a player func (am *AAManager) ChangeAATemplate(characterID int32, templateID int8) error { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return fmt.Errorf("player state not found") } // Validate template change if am.validator != nil { if err := am.validator.ValidateTemplateChange(playerState, templateID); err != nil { return fmt.Errorf("template change validation failed: %v", err) } } // Change template oldTemplate := playerState.ActiveTemplate playerState.mutex.Lock() playerState.ActiveTemplate = templateID playerState.needsSync = true playerState.mutex.Unlock() // Fire template change event am.fireTemplateChangedEvent(characterID, oldTemplate, templateID) return nil } // SaveAATemplate saves an AA template for a player func (am *AAManager) SaveAATemplate(characterID int32, templateID int8, name string) error { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return fmt.Errorf("player state not found") } // Create or update template template := playerState.Templates[templateID] if template == nil { template = NewAATemplate(templateID, name) playerState.Templates[templateID] = template } else { template.Name = name template.UpdatedAt = time.Now() } playerState.mutex.Lock() playerState.needsSync = true playerState.mutex.Unlock() // Fire template created event am.fireTemplateCreatedEvent(characterID, templateID, name) return nil } // GetAATemplates returns all AA templates for a player func (am *AAManager) GetAATemplates(characterID int32) (map[int8]*AATemplate, error) { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return nil, fmt.Errorf("player state not found") } // Return copy of templates templates := make(map[int8]*AATemplate) playerState.mutex.RLock() for id, template := range playerState.Templates { templates[id] = template } playerState.mutex.RUnlock() return templates, nil } // AwardAAPoints awards AA points to a player func (am *AAManager) AwardAAPoints(characterID int32, points int32, reason string) error { // Get player state - this handles loading if needed playerState, err := am.GetPlayerAAState(characterID) if err != nil { return fmt.Errorf("failed to get player state: %v", err) } // Award points and capture values for events var oldTotal, newTotal int32 playerState.mutex.Lock() oldTotal = playerState.TotalPoints playerState.TotalPoints += points playerState.AvailablePoints += points newTotal = playerState.TotalPoints playerState.needsSync = true playerState.mutex.Unlock() // Send notification if am.notifier != nil { am.notifier.NotifyAAPointsAwarded(characterID, points, reason) } // Fire points changed event with captured values am.firePlayerAAPointsChangedEvent(characterID, oldTotal, newTotal) return nil } // GetAAPoints returns AA point totals for a player func (am *AAManager) GetAAPoints(characterID int32) (int32, int32, int32, error) { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { return 0, 0, 0, fmt.Errorf("player state not found") } playerState.mutex.RLock() defer playerState.mutex.RUnlock() return playerState.TotalPoints, playerState.SpentPoints, playerState.AvailablePoints, nil } // GetAA returns an AA by node ID func (am *AAManager) GetAA(nodeID int32) (*AltAdvanceData, error) { aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID) if aaData == nil { return nil, fmt.Errorf("AA node %d not found", nodeID) } return aaData, nil } // GetAABySpellID returns an AA by spell ID func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvanceData, error) { aaData := am.masterAAList.GetAltAdvancement(spellID) if aaData == nil { return nil, fmt.Errorf("AA with spell ID %d not found", spellID) } return aaData, nil } // GetAAsByGroup returns AAs for a specific group/tab func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvanceData, error) { return am.masterAAList.GetAAsByGroup(group), nil } // GetAAsByClass returns AAs available for a specific class func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvanceData, error) { return am.masterAAList.GetAAsByClass(classID), nil } // GetSystemStats returns system statistics func (am *AAManager) GetSystemStats() *AAManagerStats { am.statsMutex.RLock() defer am.statsMutex.RUnlock() // Return copy of stats stats := am.stats return &stats } // GetPlayerStats returns player-specific statistics func (am *AAManager) GetPlayerStats(characterID int32) map[string]interface{} { playerState := am.getPlayerState(characterID) if playerState == nil { return map[string]interface{}{"error": "player not found"} } playerState.mutex.RLock() defer playerState.mutex.RUnlock() return map[string]interface{}{ "character_id": characterID, "total_points": playerState.TotalPoints, "spent_points": playerState.SpentPoints, "available_points": playerState.AvailablePoints, "banked_points": playerState.BankedPoints, "active_template": playerState.ActiveTemplate, "aa_count": len(playerState.AAProgress), "template_count": len(playerState.Templates), "last_update": playerState.lastUpdate, } } // SetConfig updates the manager configuration func (am *AAManager) SetConfig(config AAManagerConfig) error { am.config = config return nil } // GetConfig returns the current configuration func (am *AAManager) GetConfig() AAManagerConfig { return am.config } // Integration methods // SetDatabase sets the database interface func (am *AAManager) SetDatabase(db AADatabase) { am.database = db } // SetPacketHandler sets the packet handler interface func (am *AAManager) SetPacketHandler(handler AAPacketHandler) { am.packetHandler = handler } // SetEventHandler adds an event handler func (am *AAManager) SetEventHandler(handler AAEventHandler) { am.eventMutex.Lock() defer am.eventMutex.Unlock() am.eventHandlers = append(am.eventHandlers, handler) } // SetValidator sets the validator interface func (am *AAManager) SetValidator(validator AAValidator) { am.validator = validator } // SetNotifier sets the notifier interface func (am *AAManager) SetNotifier(notifier AANotifier) { am.notifier = notifier } // SetStatistics sets the statistics interface func (am *AAManager) SetStatistics(stats AAStatistics) { am.statistics = stats } // SetCache sets the cache interface func (am *AAManager) SetCache(cache AACache) { am.cache = cache } // Helper methods // getPlayerState gets a player state from cache func (am *AAManager) getPlayerState(characterID int32) *AAPlayerState { am.statesMutex.RLock() defer am.statesMutex.RUnlock() return am.playerStates[characterID] } // performAAPurchase performs the actual AA purchase func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAdvanceData, targetRank int8) error { // Calculate cost pointsCost := int32(aaData.RankCost) * int32(targetRank) // Update player state - MUST acquire lock BEFORE checking points to prevent TOCTOU race playerState.mutex.Lock() defer playerState.mutex.Unlock() // Check if player has enough points (inside the lock to prevent race condition) if playerState.AvailablePoints < pointsCost { return fmt.Errorf("insufficient AA points: need %d, have %d", pointsCost, playerState.AvailablePoints) } // Create or update progress progress := playerState.AAProgress[aaData.NodeID] if progress == nil { progress = &PlayerAAData{ CharacterID: playerState.CharacterID, NodeID: aaData.NodeID, CurrentRank: 0, PointsSpent: 0, TemplateID: playerState.ActiveTemplate, TabID: aaData.Group, PurchasedAt: time.Now(), UpdatedAt: time.Now(), } playerState.AAProgress[aaData.NodeID] = progress } // Update progress progress.CurrentRank = targetRank progress.PointsSpent = pointsCost progress.UpdatedAt = time.Now() // Update point totals playerState.SpentPoints += pointsCost playerState.AvailablePoints -= pointsCost playerState.needsSync = true return nil } // isAAAvailable checks if an AA is available for a player func (am *AAManager) isAAAvailable(playerState *AAPlayerState, aaData *AltAdvanceData) bool { // Check if player meets minimum level requirement // Note: This would normally get player level from the actual player object // For now, we'll assume level requirements are met // Check class requirements // Note: This would normally check the player's actual class // For now, we'll assume class requirements are met // Check prerequisites if aaData.RankPrereqID > 0 { prereqProgress, exists := playerState.AAProgress[aaData.RankPrereqID] if !exists || prereqProgress.CurrentRank < aaData.RankPrereq { return false } } return true } // Background processing loops // updateStatsLoop periodically updates statistics func (am *AAManager) updateStatsLoop() { defer am.wg.Done() ticker := time.NewTicker(am.config.UpdateInterval) defer ticker.Stop() for { select { case <-ticker.C: am.updateStatistics() case <-am.stopChan: return } } } // autoSaveLoop periodically saves player states func (am *AAManager) autoSaveLoop() { defer am.wg.Done() ticker := time.NewTicker(am.config.SaveInterval) defer ticker.Stop() for { select { case <-ticker.C: am.saveAllPlayerStates() case <-am.stopChan: return } } } // updateStatistics updates system statistics func (am *AAManager) updateStatistics() { am.statsMutex.Lock() defer am.statsMutex.Unlock() am.statesMutex.RLock() am.stats.ActivePlayers = int64(len(am.playerStates)) var totalPointsSpent int64 var totalPurchases int64 for _, playerState := range am.playerStates { playerState.mutex.RLock() totalPointsSpent += int64(playerState.SpentPoints) totalPurchases += int64(len(playerState.AAProgress)) playerState.mutex.RUnlock() } am.statesMutex.RUnlock() am.stats.TotalPointsSpent = totalPointsSpent am.stats.TotalAAPurchases = totalPurchases if am.stats.ActivePlayers > 0 { am.stats.AveragePointsSpent = float64(totalPointsSpent) / float64(am.stats.ActivePlayers) } am.stats.LastStatsUpdate = time.Now() } // saveAllPlayerStates saves all cached player states func (am *AAManager) saveAllPlayerStates() { if am.database == nil { return } am.statesMutex.RLock() defer am.statesMutex.RUnlock() for _, playerState := range am.playerStates { if playerState.needsSync { if err := am.database.SavePlayerAA(playerState); err != nil { am.updateErrorStats("database_errors") continue } playerState.needsSync = false } } } // updateErrorStats updates error statistics func (am *AAManager) updateErrorStats(errorType string) { am.statsMutex.Lock() defer am.statsMutex.Unlock() switch errorType { case "validation_errors": am.stats.ValidationErrors++ case "database_errors": am.stats.DatabaseErrors++ case "packet_errors": am.stats.PacketErrors++ } } // Event firing methods // fireSystemLoadedEvent fires a system loaded event func (am *AAManager) fireSystemLoadedEvent(totalAAs, totalNodes int32) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnAASystemLoaded(totalAAs, totalNodes) } } // fireDataReloadedEvent fires a data reloaded event func (am *AAManager) fireDataReloadedEvent() { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnAADataReloaded() } } // firePlayerAALoadedEvent fires a player AA loaded event func (am *AAManager) firePlayerAALoadedEvent(characterID int32, playerState *AAPlayerState) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnPlayerAALoaded(characterID, playerState) } } // fireAAPurchasedEvent fires an AA purchased event func (am *AAManager) fireAAPurchasedEvent(characterID int32, nodeID int32, newRank int8, pointsSpent int32) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnAAPurchased(characterID, nodeID, newRank, pointsSpent) } } // fireAARefundedEvent fires an AA refunded event func (am *AAManager) fireAARefundedEvent(characterID int32, nodeID int32, oldRank int8, pointsRefunded int32) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnAARefunded(characterID, nodeID, oldRank, pointsRefunded) } } // fireTemplateChangedEvent fires a template changed event func (am *AAManager) fireTemplateChangedEvent(characterID int32, oldTemplate, newTemplate int8) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnAATemplateChanged(characterID, oldTemplate, newTemplate) } } // fireTemplateCreatedEvent fires a template created event func (am *AAManager) fireTemplateCreatedEvent(characterID int32, templateID int8, name string) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnAATemplateCreated(characterID, templateID, name) } } // firePlayerAAPointsChangedEvent fires a player AA points changed event func (am *AAManager) firePlayerAAPointsChangedEvent(characterID int32, oldPoints, newPoints int32) { am.eventMutex.RLock() defer am.eventMutex.RUnlock() for _, handler := range am.eventHandlers { go handler.OnPlayerAAPointsChanged(characterID, oldPoints, newPoints) } }