787 lines
21 KiB
Go

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)
}
}