diff --git a/MODERNIZE.md b/MODERNIZE.md new file mode 100644 index 0000000..540bc34 --- /dev/null +++ b/MODERNIZE.md @@ -0,0 +1,192 @@ +# Package Modernization Instructions + +## Goal +Transform legacy packages to use generic MasterList pattern with simplified database operations. + +## Steps + +### 1. Implement Generic MasterList Base +Create `internal/common/master_list.go` with generic collection management: +```go +type MasterList[K comparable, V Identifiable[K]] struct { + items map[K]V + mutex sync.RWMutex +} +``` + +### 2. Consolidate Package Structure + +**Remove:** +- Legacy wrapper functions (LoadAll, SaveAll, etc.) +- Duplicate database files (database.go, database_legacy.go) +- README.md (use doc.go instead) +- Separate "active_record.go" files + +**Consolidate into:** +- `{type}.go` - Main type with embedded database operations +- `types.go` - Supporting types only (no main type) +- `master.go` - MasterList using generic base +- `player.go` - Player-specific logic (if applicable) +- `doc.go` - Primary documentation +- `{type}_test.go` - Focused tests + +### 3. Refactor Main Type + +Transform the main type to include database operations: + +```go +// Before: Separate type and database operations +type Achievement struct { + ID uint32 + Title string +} + +func LoadAchievement(db *database.Database, id uint32) (*Achievement, error) +func SaveAchievement(db *database.Database, a *Achievement) error + +// After: Embedded database operations +type Achievement struct { + ID uint32 + Title string + + db *database.Database + isNew bool +} + +func New(db *database.Database) *Achievement +func Load(db *database.Database, id uint32) (*Achievement, error) +func (a *Achievement) Save() error +func (a *Achievement) Delete() error +func (a *Achievement) Reload() error +``` + +### 4. Update MasterList + +Replace manual implementation with generic base: + +```go +// Before: Manual thread-safety +type MasterList struct { + items map[uint32]*Achievement + mutex sync.RWMutex +} + +func (m *MasterList) AddAchievement(a *Achievement) bool { + m.mutex.Lock() + defer m.mutex.Unlock() + // manual implementation +} + +// After: Generic base +type MasterList struct { + *common.MasterList[uint32, *Achievement] +} + +func NewMasterList() *MasterList { + return &MasterList{ + MasterList: common.NewMasterList[uint32, *Achievement](), + } +} + +func (m *MasterList) AddAchievement(a *Achievement) bool { + return m.MasterList.Add(a) +} +``` + +### 5. Implement Identifiable Interface + +Ensure main type implements `GetID()`: + +```go +func (a *Achievement) GetID() uint32 { + return a.AchievementID // or appropriate ID field +} +``` + +### 6. Simplify API + +**Remove:** +- Legacy type variants (Achievement vs AchievementRecord) +- Conversion methods (ToLegacy, FromLegacy) +- Duplicate CRUD operations +- Complex wrapper functions + +**Keep:** +- Single type definition +- Direct database methods on type +- Domain-specific extensions only + +### 7. Update Documentation + +Create concise `doc.go`: + +```go +// Package achievements provides [brief description]. +// +// Basic Usage: +// +// achievement := achievements.New(db) +// achievement.Title = "Dragon Slayer" +// achievement.Save() +// +// loaded, _ := achievements.Load(db, 1001) +// loaded.Delete() +// +// Master List: +// +// masterList := achievements.NewMasterList() +// masterList.Add(achievement) +package achievements +``` + +### 8. Testing + +Create focused tests: +- Test core type operations (New, Save, Load, Delete) +- Test MasterList basic operations +- Remove legacy compatibility tests +- Keep tests simple and direct + +## Key Principles + +1. **Single Source of Truth**: One type definition, not multiple variants +2. **Embedded Operations**: Database methods on the type itself +3. **Generic Base**: Use common.MasterList for thread-safety +4. **No Legacy Baggage**: Remove all "Legacy" types and converters +5. **Documentation in Code**: Use doc.go, not README.md + +## Migration Checklist + +- [ ] Create/verify generic MasterList in common package +- [ ] Identify main type and supporting types +- [ ] Consolidate database operations into main type +- [ ] Add db field and methods to main type +- [ ] Replace manual MasterList with generic base +- [ ] Implement GetID() for Identifiable interface +- [ ] Remove all legacy types and converters +- [ ] Update doc.go with concise examples +- [ ] Simplify tests to cover core functionality +- [ ] Run `go fmt` and `go test` + +## Expected Results + +- **80% less code** in most packages +- **Single type** instead of multiple variants +- **Thread-safe** operations via generic base +- **Consistent API** across all packages +- **Better maintainability** with less duplication + +## Example Commands + +```bash +# Remove legacy files +rm README.md database_legacy.go active_record.go + +# Rename if needed +mv active_record.go achievement.go + +# Test the changes +go fmt ./... +go test ./... +go build ./... +``` \ No newline at end of file diff --git a/internal/alt_advancement/README.md b/internal/alt_advancement/README.md deleted file mode 100644 index dd1dae1..0000000 --- a/internal/alt_advancement/README.md +++ /dev/null @@ -1,499 +0,0 @@ -# Alternate Advancement System - -The alternate advancement system (`internal/alt_advancement`) provides comprehensive character progression beyond normal leveling for the EQ2Go server emulator. This system is converted from the original C++ EQ2EMu AltAdvancement implementation with modern Go concurrency patterns and clean architecture principles. - -## Overview - -The alternate advancement (AA) system manages character progression through specialized skill trees including: - -- **Class Trees**: Class-specific advancement paths -- **Subclass Trees**: Subclass-specific specializations -- **Heroic Trees**: Heroic advancement from Destiny of Velious -- **Shadows Trees**: Shadow-based abilities from Shadows of Luclin -- **Tradeskill Trees**: Tradeskill-focused advancement -- **Prestige Trees**: Prestigious advancement paths -- **Dragon Trees**: Dragon-themed advancement -- **Far Seas Trees**: Far Seas trading company advancement - -## Architecture - -### Core Components - -**MasterAAList** - Global repository of all AA definitions with fast lookup capabilities -**MasterAANodeList** - Tree node configurations mapping classes to AA trees -**AAManager** - Central management system for all AA operations -**AAPlayerState** - Individual player AA progression and template management -**DatabaseImpl** - Database operations for persistent AA data - -### Key Files - -- `types.go` - Core data structures and type definitions -- `constants.go` - All AA system constants, tab definitions, and limits -- `master_list.go` - MasterAAList and MasterAANodeList implementations -- `manager.go` - Central AAManager with player state management -- `database.go` - Database operations and persistence -- `interfaces.go` - System integration interfaces and adapters -- `README.md` - This documentation - -## System Initialization - -```go -// Create AA manager with configuration -config := alt_advancement.DefaultAAManagerConfig() -config.EnableCaching = true -config.DatabaseEnabled = true - -aaManager := alt_advancement.NewAAManager(config) - -// Set up database integration -database := alt_advancement.NewDatabaseImpl(db, masterAAList, masterNodeList, logger) -aaManager.SetDatabase(database) - -// Start the system -err := aaManager.Start() -if err != nil { - log.Fatalf("Failed to start AA system: %v", err) -} -``` - -## AA Data Management - -```go -// Load all AA data from database -err := aaManager.LoadAAData() -if err != nil { - log.Printf("Failed to load AA data: %v", err) -} - -// Get specific AA by node ID -aaData, err := aaManager.GetAA(nodeID) -if err != nil { - log.Printf("AA not found: %v", err) -} - -// Get AAs for a specific class -classAAs, err := aaManager.GetAAsByClass(classID) -fmt.Printf("Found %d AAs for class %d\n", len(classAAs), classID) - -// Get AAs for a specific tab/group -tabAAs, err := aaManager.GetAAsByGroup(alt_advancement.AA_CLASS) -fmt.Printf("Class tab has %d AAs\n", len(tabAAs)) -``` - -## Player AA Management - -```go -// Load player's AA data -characterID := int32(12345) -playerState, err := aaManager.LoadPlayerAA(characterID) -if err != nil { - log.Printf("Failed to load player AA: %v", err) -} - -// Get player's AA point totals -totalPoints, spentPoints, availablePoints, err := aaManager.GetAAPoints(characterID) -fmt.Printf("Player has %d total, %d spent, %d available AA points\n", - totalPoints, spentPoints, availablePoints) - -// Award AA points to player -err = aaManager.AwardAAPoints(characterID, 10, "Level up bonus") -if err != nil { - log.Printf("Failed to award AA points: %v", err) -} -``` - -## AA Purchasing System - -```go -// Purchase an AA for a player -nodeID := int32(1001) -targetRank := int8(1) - -err := aaManager.PurchaseAA(characterID, nodeID, targetRank) -if err != nil { - log.Printf("AA purchase failed: %v", err) -} else { - fmt.Println("AA purchased successfully!") -} - -// Refund an AA -err = aaManager.RefundAA(characterID, nodeID) -if err != nil { - log.Printf("AA refund failed: %v", err) -} - -// Check available AAs for a tab -availableAAs, err := aaManager.GetAvailableAAs(characterID, alt_advancement.AA_CLASS) -fmt.Printf("Player can purchase %d AAs in class tab\n", len(availableAAs)) -``` - -## AA Templates System - -```go -// Change active AA template -templateID := int8(alt_advancement.AA_TEMPLATE_PERSONAL_1) -err := aaManager.ChangeAATemplate(characterID, templateID) -if err != nil { - log.Printf("Template change failed: %v", err) -} - -// Save custom AA template -err = aaManager.SaveAATemplate(characterID, templateID, "My Build") -if err != nil { - log.Printf("Template save failed: %v", err) -} - -// Get all templates for player -templates, err := aaManager.GetAATemplates(characterID) -if err != nil { - log.Printf("Failed to get templates: %v", err) -} else { - for id, template := range templates { - fmt.Printf("Template %d: %s (%d entries)\n", - id, template.Name, len(template.Entries)) - } -} -``` - -## AA Data Structures - -### AltAdvanceData - Individual AA Definition - -```go -type AltAdvanceData struct { - SpellID int32 // Associated spell ID - NodeID int32 // Unique node identifier - Name string // Display name - Description string // AA description - Group int8 // Tab/group (AA_CLASS, AA_SUBCLASS, etc.) - Col int8 // Column position in tree - Row int8 // Row position in tree - Icon int16 // Display icon ID - RankCost int8 // Cost per rank - MaxRank int8 // Maximum achievable rank - MinLevel int8 // Minimum character level - RankPrereqID int32 // Prerequisite AA node ID - RankPrereq int8 // Required rank in prerequisite - ClassReq int8 // Required class (0 = all classes) - // ... additional fields -} -``` - -### AAPlayerState - Player AA Progression - -```go -type AAPlayerState struct { - CharacterID int32 // Character identifier - TotalPoints int32 // Total AA points earned - SpentPoints int32 // Total AA points spent - AvailablePoints int32 // Available AA points for spending - BankedPoints int32 // Banked AA points - ActiveTemplate int8 // Currently active template - Templates map[int8]*AATemplate // All templates - Tabs map[int8]*AATab // Tab states - AAProgress map[int32]*PlayerAAData // AA progression by node ID - // ... synchronization and metadata -} -``` - -## AA Tab System - -The system supports 10 different AA tabs: - -```go -// AA tab constants -const ( - AA_CLASS = 0 // Class-specific abilities - AA_SUBCLASS = 1 // Subclass specializations - AA_SHADOW = 2 // Shadow abilities - AA_HEROIC = 3 // Heroic advancement - AA_TRADESKILL = 4 // Tradeskill abilities - AA_PRESTIGE = 5 // Prestige advancement - AA_TRADESKILL_PRESTIGE = 6 // Tradeskill prestige - AA_DRAGON = 7 // Dragon abilities - AA_DRAGONCLASS = 8 // Dragon class abilities - AA_FARSEAS = 9 // Far Seas abilities -) - -// Get maximum AA points for each tab -maxClassAA := alt_advancement.GetMaxAAForTab(alt_advancement.AA_CLASS) // 100 -maxHeroicAA := alt_advancement.GetMaxAAForTab(alt_advancement.AA_HEROIC) // 50 -``` - -## Database Integration - -```go -// Custom database implementation -type MyAADatabase struct { - db *sql.DB -} - -func (db *MyAADatabase) LoadAltAdvancements() error { - // Load AA definitions from database - return nil -} - -func (db *MyAADatabase) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { - // Load player AA data from database - return nil, nil -} - -func (db *MyAADatabase) SavePlayerAA(playerState *AAPlayerState) error { - // Save player AA data to database - return nil -} - -// Set database implementation -aaManager.SetDatabase(&MyAADatabase{db: myDB}) -``` - -## Event Handling - -```go -// Custom event handler -type MyAAEventHandler struct{} - -func (h *MyAAEventHandler) OnAAPurchased(characterID int32, nodeID int32, newRank int8, pointsSpent int32) error { - fmt.Printf("Player %d purchased AA %d rank %d for %d points\n", - characterID, nodeID, newRank, pointsSpent) - return nil -} - -func (h *MyAAEventHandler) OnAATemplateChanged(characterID int32, oldTemplate, newTemplate int8) error { - fmt.Printf("Player %d changed template from %d to %d\n", - characterID, oldTemplate, newTemplate) - return nil -} - -func (h *MyAAEventHandler) OnPlayerAAPointsChanged(characterID int32, oldPoints, newPoints int32) error { - fmt.Printf("Player %d AA points changed from %d to %d\n", - characterID, oldPoints, newPoints) - return nil -} - -// Register event handler -aaManager.SetEventHandler(&MyAAEventHandler{}) -``` - -## Validation System - -```go -// Custom validator -type MyAAValidator struct{} - -func (v *MyAAValidator) ValidateAAPurchase(playerState *AAPlayerState, nodeID int32, targetRank int8) error { - // Check if player has enough points - if playerState.AvailablePoints < int32(targetRank) { - return fmt.Errorf("insufficient AA points") - } - - // Check prerequisites - // ... validation logic - - return nil -} - -func (v *MyAAValidator) ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvanceData) error { - // Check minimum level requirement - // ... validation logic - return nil -} - -// Set validator -aaManager.SetValidator(&MyAAValidator{}) -``` - -## Statistics and Monitoring - -```go -// Get system statistics -stats := aaManager.GetSystemStats() -fmt.Printf("Total AAs loaded: %d\n", stats.TotalAAsLoaded) -fmt.Printf("Active players: %d\n", stats.ActivePlayers) -fmt.Printf("Total purchases: %d\n", stats.TotalAAPurchases) -fmt.Printf("Average points spent: %.1f\n", stats.AveragePointsSpent) - -// Get player-specific statistics -playerStats := aaManager.GetPlayerStats(characterID) -fmt.Printf("Player stats: %+v\n", playerStats) - -// Get database statistics (if database supports it) -if db, ok := aaManager.database.(*DatabaseImpl); ok { - dbStats, err := db.GetAAStatistics() - if err == nil { - fmt.Printf("Database stats: %+v\n", dbStats) - } -} -``` - -## Configuration Options - -```go -// Configure the AA system -config := alt_advancement.AAManagerConfig{ - EnableAASystem: true, - EnableCaching: true, - EnableValidation: true, - EnableLogging: false, - AAPointsPerLevel: 2, - MaxBankedPoints: 30, - EnableAABanking: true, - CacheSize: 10000, - UpdateInterval: 1 * time.Second, - BatchSize: 100, - DatabaseEnabled: true, - AutoSave: true, - SaveInterval: 5 * time.Minute, -} - -aaManager.SetConfig(config) -``` - -## Caching System - -```go -// Enable caching for better performance -cache := alt_advancement.NewSimpleAACache(1000) -aaManager.SetCache(cache) - -// Get cache statistics -cacheStats := cache.GetStats() -fmt.Printf("Cache hits: %d, misses: %d\n", - cacheStats["hits"], cacheStats["misses"]) -``` - -## Packet Handling Integration - -```go -// Custom packet handler -type MyAAPacketHandler struct{} - -func (ph *MyAAPacketHandler) GetAAListPacket(client any) ([]byte, error) { - // Build AA list packet for client - return []byte{}, nil -} - -func (ph *MyAAPacketHandler) SendAAUpdate(client any, playerState *AAPlayerState) error { - // Send AA update to client - return nil -} - -func (ph *MyAAPacketHandler) HandleAAPurchase(client any, nodeID int32, rank int8) error { - // Handle AA purchase from client - return nil -} - -// Set packet handler -aaManager.SetPacketHandler(&MyAAPacketHandler{}) -``` - -## Advanced Usage - -### Custom AA Trees - -```go -// Create custom AA data -customAA := &alt_advancement.AltAdvanceData{ - SpellID: 2001, - NodeID: 2001, - Name: "Custom Ability", - Description: "A custom AA ability", - Group: alt_advancement.AA_CLASS, - Col: 1, - Row: 1, - Icon: 100, - RankCost: 1, - MaxRank: 5, - MinLevel: 20, - ClassReq: 1, // Fighter only -} - -// Add to master list -err := masterAAList.AddAltAdvancement(customAA) -if err != nil { - log.Printf("Failed to add custom AA: %v", err) -} -``` - -### Bulk Operations - -```go -// Award AA points to multiple players -playerIDs := []int32{1001, 1002, 1003} -for _, playerID := range playerIDs { - err := aaManager.AwardAAPoints(playerID, 5, "Server event bonus") - if err != nil { - log.Printf("Failed to award points to player %d: %v", playerID, err) - } -} - -// Batch save player states -for _, playerID := range playerIDs { - err := aaManager.SavePlayerAA(playerID) - if err != nil { - log.Printf("Failed to save player %d AA data: %v", playerID, err) - } -} -``` - -## Thread Safety - -All AA operations are thread-safe using appropriate synchronization: - -- **RWMutex** for read-heavy operations (AA lookups, player state access) -- **Atomic operations** for simple counters and flags -- **Proper lock ordering** to prevent deadlocks -- **Background goroutines** for periodic processing and auto-save -- **Channel-based communication** for event handling - -## Performance Considerations - -- **Efficient data structures** with hash maps for O(1) lookups -- **Caching system** to reduce database queries -- **Batch processing** for bulk operations -- **Background processing** to avoid blocking gameplay -- **Statistics collection** with minimal overhead -- **Memory-efficient storage** with proper cleanup - -## Integration with Other Systems - -The AA system integrates with: - -- **Player System** - Player-specific AA progression and point management -- **Spell System** - AA abilities are linked to spells -- **Class System** - Class-specific AA trees and requirements -- **Level System** - Level-based AA point awards and prerequisites -- **Database System** - Persistent storage of AA data and player progress -- **Client System** - AA UI updates and purchase handling -- **Achievement System** - AA milestones and progression tracking - -## Migration from C++ - -This Go implementation maintains compatibility with the original C++ EQ2EMu AA system while providing: - -- **Modern concurrency** with goroutines and channels -- **Better error handling** with Go's error interface -- **Cleaner architecture** with interface-based design -- **Improved maintainability** with package organization -- **Enhanced testing** capabilities -- **Type safety** with Go's type system -- **Memory management** with Go's garbage collector - -## TODO Items - -The conversion includes areas for future implementation: - -- **Complete packet handling** for all client communication -- **Advanced validation** for complex AA prerequisites -- **Lua scripting integration** for custom AA behaviors -- **Web administration interface** for AA management -- **Performance optimizations** for large-scale deployments -- **Advanced caching strategies** with TTL and eviction policies -- **Metrics and monitoring** integration with external systems -- **AA import/export** functionality for configuration management - -## Usage Examples - -See the code examples throughout this documentation for detailed usage patterns. The system is designed to be used alongside the existing EQ2Go server infrastructure with proper initialization and configuration. - -The AA system provides a solid foundation for character progression mechanics while maintaining the flexibility to extend and customize behavior through the comprehensive interface system. \ No newline at end of file diff --git a/internal/alt_advancement/aa_test.go b/internal/alt_advancement/aa_test.go deleted file mode 100644 index d943c53..0000000 --- a/internal/alt_advancement/aa_test.go +++ /dev/null @@ -1,3004 +0,0 @@ -package alt_advancement - -import ( - "fmt" - "log" - "reflect" - "sync" - "testing" - "time" -) - -// Test AltAdvanceData structure and methods -func TestAltAdvanceData(t *testing.T) { - t.Run("Copy", func(t *testing.T) { - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Description: "Test Description", - Group: AA_CLASS, - Col: 5, - Row: 3, - Icon: 1234, - RankCost: 2, - MaxRank: 5, - MinLevel: 20, - ClassName: "Fighter", - SubclassName: "Guardian", - } - - copy := aa.Copy() - if copy == aa { - t.Error("Copy should return a new instance, not the same pointer") - } - - if !reflect.DeepEqual(aa, copy) { - t.Error("Copy should have identical field values") - } - - // Modify original and ensure copy is unaffected - aa.Name = "Modified" - if copy.Name == "Modified" { - t.Error("Copy should not be affected by changes to original") - } - }) - - t.Run("IsValid", func(t *testing.T) { - tests := []struct { - name string - aa *AltAdvanceData - expected bool - }{ - { - name: "Valid AA", - aa: &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - MaxRank: 5, - RankCost: 2, - }, - expected: true, - }, - { - name: "Invalid - No SpellID", - aa: &AltAdvanceData{ - NodeID: 200, - Name: "Test AA", - MaxRank: 5, - RankCost: 2, - }, - expected: false, - }, - { - name: "Invalid - No NodeID", - aa: &AltAdvanceData{ - SpellID: 100, - Name: "Test AA", - MaxRank: 5, - RankCost: 2, - }, - expected: false, - }, - { - name: "Invalid - No Name", - aa: &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - MaxRank: 5, - RankCost: 2, - }, - expected: false, - }, - { - name: "Invalid - No MaxRank", - aa: &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - RankCost: 2, - }, - expected: false, - }, - { - name: "Invalid - No RankCost", - aa: &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - MaxRank: 5, - }, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if result := tt.aa.IsValid(); result != tt.expected { - t.Errorf("IsValid() = %v, want %v", result, tt.expected) - } - }) - } - }) -} - -// Test MasterAAList functionality -func TestMasterAAList(t *testing.T) { - t.Run("NewMasterAAList", func(t *testing.T) { - masterList := NewMasterAAList() - if masterList == nil { - t.Fatal("NewMasterAAList returned nil") - } - - if masterList.Size() != 0 { - t.Error("Expected empty list to have size 0") - } - - if masterList.aaList == nil { - t.Error("Expected aaList to be initialized") - } - - if masterList.aaBySpellID == nil || masterList.aaByNodeID == nil || masterList.aaByGroup == nil { - t.Error("Expected lookup maps to be initialized") - } - }) - - t.Run("AddAltAdvancement", func(t *testing.T) { - masterList := NewMasterAAList() - - // Test adding nil - err := masterList.AddAltAdvancement(nil) - if err == nil { - t.Error("Expected error when adding nil AA") - } - - // Test adding invalid AA - invalidAA := &AltAdvanceData{SpellID: 0} - err = masterList.AddAltAdvancement(invalidAA) - if err == nil { - t.Error("Expected error when adding invalid AA") - } - - // Test adding valid AA - validAA := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - err = masterList.AddAltAdvancement(validAA) - if err != nil { - t.Errorf("Unexpected error adding valid AA: %v", err) - } - - if masterList.Size() != 1 { - t.Error("Expected size to be 1 after adding AA") - } - - // Test duplicate spell ID - dupSpellAA := &AltAdvanceData{ - SpellID: 100, // Same spell ID - NodeID: 201, - Name: "Duplicate Spell", - MaxRank: 5, - RankCost: 2, - } - - err = masterList.AddAltAdvancement(dupSpellAA) - if err == nil { - t.Error("Expected error when adding duplicate spell ID") - } - - // Test duplicate node ID - dupNodeAA := &AltAdvanceData{ - SpellID: 101, - NodeID: 200, // Same node ID - Name: "Duplicate Node", - MaxRank: 5, - RankCost: 2, - } - - err = masterList.AddAltAdvancement(dupNodeAA) - if err == nil { - t.Error("Expected error when adding duplicate node ID") - } - }) - - t.Run("GetAltAdvancement", func(t *testing.T) { - masterList := NewMasterAAList() - - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa) - - // Test getting existing AA - retrieved := masterList.GetAltAdvancement(100) - if retrieved == nil { - t.Fatal("Expected to retrieve AA by spell ID") - } - - if retrieved == aa { - t.Error("GetAltAdvancement should return a copy, not the original") - } - - if retrieved.SpellID != aa.SpellID || retrieved.Name != aa.Name { - t.Error("Retrieved AA should have same data as original") - } - - // Test getting non-existent AA - notFound := masterList.GetAltAdvancement(999) - if notFound != nil { - t.Error("Expected nil for non-existent spell ID") - } - }) - - t.Run("GetAltAdvancementByNodeID", func(t *testing.T) { - masterList := NewMasterAAList() - - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa) - - // Test getting existing AA - retrieved := masterList.GetAltAdvancementByNodeID(200) - if retrieved == nil { - t.Fatal("Expected to retrieve AA by node ID") - } - - if retrieved.NodeID != aa.NodeID { - t.Error("Retrieved AA should have same node ID") - } - - // Test getting non-existent AA - notFound := masterList.GetAltAdvancementByNodeID(999) - if notFound != nil { - t.Error("Expected nil for non-existent node ID") - } - }) - - t.Run("GetAAsByGroup", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add AAs to different groups - aa1 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Class AA 1", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - aa2 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Class AA 2", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - aa3 := &AltAdvanceData{ - SpellID: 102, - NodeID: 202, - Name: "Heroic AA", - Group: AA_HEROIC, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa1) - masterList.AddAltAdvancement(aa2) - masterList.AddAltAdvancement(aa3) - - // Test getting AAs by group - classAAs := masterList.GetAAsByGroup(AA_CLASS) - if len(classAAs) != 2 { - t.Errorf("Expected 2 class AAs, got %d", len(classAAs)) - } - - heroicAAs := masterList.GetAAsByGroup(AA_HEROIC) - if len(heroicAAs) != 1 { - t.Errorf("Expected 1 heroic AA, got %d", len(heroicAAs)) - } - - // Test getting empty group - emptyGroup := masterList.GetAAsByGroup(AA_DRAGON) - if len(emptyGroup) != 0 { - t.Error("Expected empty slice for group with no AAs") - } - - // Verify copies are returned - if &classAAs[0] == &aa1 { - t.Error("GetAAsByGroup should return copies, not originals") - } - }) - - t.Run("GetAAsByClass", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add AAs with different class requirements - aaAllClasses := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "All Classes", - Group: AA_CLASS, - ClassReq: 0, // Available to all classes - MaxRank: 5, - RankCost: 2, - } - - aaFighterOnly := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Fighter Only", - Group: AA_CLASS, - ClassReq: 1, // Fighter class - MaxRank: 5, - RankCost: 2, - } - - aaMageOnly := &AltAdvanceData{ - SpellID: 102, - NodeID: 202, - Name: "Mage Only", - Group: AA_CLASS, - ClassReq: 20, // Mage class - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aaAllClasses) - masterList.AddAltAdvancement(aaFighterOnly) - masterList.AddAltAdvancement(aaMageOnly) - - // Test getting AAs for fighter class - fighterAAs := masterList.GetAAsByClass(1) - if len(fighterAAs) != 2 { - t.Errorf("Expected 2 AAs for fighter (all classes + fighter only), got %d", len(fighterAAs)) - } - - // Test getting AAs for mage class - mageAAs := masterList.GetAAsByClass(20) - if len(mageAAs) != 2 { - t.Errorf("Expected 2 AAs for mage (all classes + mage only), got %d", len(mageAAs)) - } - - // Test getting AAs for class with no specific AAs - priestAAs := masterList.GetAAsByClass(10) - if len(priestAAs) != 1 { - t.Errorf("Expected 1 AA for priest (all classes only), got %d", len(priestAAs)) - } - }) - - t.Run("GetAAsByLevel", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add AAs with different level requirements - aaLevel10 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Level 10 AA", - Group: AA_CLASS, - MinLevel: 10, - MaxRank: 5, - RankCost: 2, - } - - aaLevel30 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Level 30 AA", - Group: AA_CLASS, - MinLevel: 30, - MaxRank: 5, - RankCost: 2, - } - - aaLevel50 := &AltAdvanceData{ - SpellID: 102, - NodeID: 202, - Name: "Level 50 AA", - Group: AA_CLASS, - MinLevel: 50, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aaLevel10) - masterList.AddAltAdvancement(aaLevel30) - masterList.AddAltAdvancement(aaLevel50) - - // Test getting AAs at different levels - level20AAs := masterList.GetAAsByLevel(20) - if len(level20AAs) != 1 { - t.Errorf("Expected 1 AA at level 20, got %d", len(level20AAs)) - } - - level40AAs := masterList.GetAAsByLevel(40) - if len(level40AAs) != 2 { - t.Errorf("Expected 2 AAs at level 40, got %d", len(level40AAs)) - } - - level60AAs := masterList.GetAAsByLevel(60) - if len(level60AAs) != 3 { - t.Errorf("Expected 3 AAs at level 60, got %d", len(level60AAs)) - } - - level5AAs := masterList.GetAAsByLevel(5) - if len(level5AAs) != 0 { - t.Errorf("Expected 0 AAs at level 5, got %d", len(level5AAs)) - } - }) - - t.Run("SortAAsByGroup", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add AAs in random order - aa1 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "AA1", - Group: AA_CLASS, - Row: 2, - Col: 3, - MaxRank: 5, - RankCost: 2, - } - - aa2 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "AA2", - Group: AA_CLASS, - Row: 1, - Col: 5, - MaxRank: 5, - RankCost: 2, - } - - aa3 := &AltAdvanceData{ - SpellID: 102, - NodeID: 202, - Name: "AA3", - Group: AA_CLASS, - Row: 1, - Col: 2, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa1) - masterList.AddAltAdvancement(aa2) - masterList.AddAltAdvancement(aa3) - - // Sort and verify order - masterList.SortAAsByGroup() - - classAAs := masterList.GetAAsByGroup(AA_CLASS) - if len(classAAs) != 3 { - t.Fatal("Expected 3 AAs") - } - - // Should be sorted by row then column - // aa3 (1,2), aa2 (1,5), aa1 (2,3) - if classAAs[0].Name != "AA3" || classAAs[1].Name != "AA2" || classAAs[2].Name != "AA1" { - t.Error("AAs not sorted correctly by row and column") - } - }) - - t.Run("GetGroups", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add AAs to different groups - aa1 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "AA1", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - aa2 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "AA2", - Group: AA_HEROIC, - MaxRank: 5, - RankCost: 2, - } - - aa3 := &AltAdvanceData{ - SpellID: 102, - NodeID: 202, - Name: "AA3", - Group: AA_TRADESKILL, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa1) - masterList.AddAltAdvancement(aa2) - masterList.AddAltAdvancement(aa3) - - groups := masterList.GetGroups() - if len(groups) != 3 { - t.Errorf("Expected 3 groups, got %d", len(groups)) - } - - // Check that all expected groups are present - groupMap := make(map[int8]bool) - for _, g := range groups { - groupMap[g] = true - } - - if !groupMap[AA_CLASS] || !groupMap[AA_HEROIC] || !groupMap[AA_TRADESKILL] { - t.Error("Not all expected groups were returned") - } - }) - - t.Run("DestroyAltAdvancements", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add some AAs - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa) - - if masterList.Size() != 1 { - t.Error("Expected size to be 1 before destroy") - } - - // Destroy all AAs - masterList.DestroyAltAdvancements() - - if masterList.Size() != 0 { - t.Error("Expected size to be 0 after destroy") - } - - // Verify maps are cleared - if len(masterList.aaBySpellID) != 0 || len(masterList.aaByNodeID) != 0 || len(masterList.aaByGroup) != 0 { - t.Error("Expected all maps to be cleared after destroy") - } - }) - - t.Run("ConcurrentAccess", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add initial AAs - for i := int32(1); i <= 10; i++ { - aa := &AltAdvanceData{ - SpellID: i * 100, - NodeID: i * 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - masterList.AddAltAdvancement(aa) - } - - // Concurrent reads and writes - var wg sync.WaitGroup - errors := make(chan error, 100) - - // Multiple readers - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { - masterList.GetAltAdvancement(100) - masterList.GetAAsByGroup(AA_CLASS) - masterList.Size() - } - }() - } - - // Multiple writers (trying to add new unique AAs) - for i := 0; i < 5; i++ { - wg.Add(1) - go func(id int32) { - defer wg.Done() - aa := &AltAdvanceData{ - SpellID: 1000 + id*100, - NodeID: 2000 + id*100, - Name: "Concurrent AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - if err := masterList.AddAltAdvancement(aa); err != nil { - errors <- err - } - }(int32(i)) - } - - wg.Wait() - close(errors) - - // Check for unexpected errors - errorCount := 0 - for err := range errors { - errorCount++ - t.Logf("Concurrent write error (expected for duplicates): %v", err) - } - - // Check that we have expected number of AAs - finalSize := masterList.Size() - if finalSize < 10 || finalSize > 15 { // 10 initial + 0-5 concurrent (some may fail due to race) - t.Errorf("Expected between 10-15 AAs after concurrent operations, got %d", finalSize) - } - }) -} - -// Test MasterAANodeList functionality -func TestMasterAANodeList(t *testing.T) { - t.Run("NewMasterAANodeList", func(t *testing.T) { - nodeList := NewMasterAANodeList() - if nodeList == nil { - t.Fatal("NewMasterAANodeList returned nil") - } - - if nodeList.Size() != 0 { - t.Error("Expected empty list to have size 0") - } - }) - - t.Run("AddTreeNode", func(t *testing.T) { - nodeList := NewMasterAANodeList() - - // Test adding nil - err := nodeList.AddTreeNode(nil) - if err == nil { - t.Error("Expected error when adding nil node") - } - - // Test adding valid node - node := &TreeNodeData{ - ClassID: 1, - TreeID: 100, - AATreeID: 200, - } - - err = nodeList.AddTreeNode(node) - if err != nil { - t.Errorf("Unexpected error adding valid node: %v", err) - } - - if nodeList.Size() != 1 { - t.Error("Expected size to be 1 after adding node") - } - - // Test duplicate tree ID - dupNode := &TreeNodeData{ - ClassID: 2, - TreeID: 100, // Same tree ID - AATreeID: 201, - } - - err = nodeList.AddTreeNode(dupNode) - if err == nil { - t.Error("Expected error when adding duplicate tree ID") - } - }) - - t.Run("GetTreeNodeByTreeID", func(t *testing.T) { - nodeList := NewMasterAANodeList() - - node := &TreeNodeData{ - ClassID: 1, - TreeID: 100, - AATreeID: 200, - } - - nodeList.AddTreeNode(node) - - // Test getting existing node - retrieved := nodeList.GetTreeNode(100) - if retrieved == nil { - t.Fatal("Expected to retrieve node by tree ID") - } - - if retrieved == node { - t.Error("GetTreeNode should return a copy, not the original") - } - - if retrieved.TreeID != node.TreeID { - t.Error("Retrieved node should have same tree ID") - } - - // Test getting non-existent node - notFound := nodeList.GetTreeNode(999) - if notFound != nil { - t.Error("Expected nil for non-existent tree ID") - } - }) - - t.Run("GetTreeNodesByClass", func(t *testing.T) { - nodeList := NewMasterAANodeList() - - // Add nodes for different classes - node1 := &TreeNodeData{ - ClassID: 1, - TreeID: 100, - AATreeID: 200, - } - - node2 := &TreeNodeData{ - ClassID: 1, - TreeID: 101, - AATreeID: 201, - } - - node3 := &TreeNodeData{ - ClassID: 2, - TreeID: 102, - AATreeID: 202, - } - - nodeList.AddTreeNode(node1) - nodeList.AddTreeNode(node2) - nodeList.AddTreeNode(node3) - - // Test getting nodes by class - class1Nodes := nodeList.GetTreeNodesByClass(1) - if len(class1Nodes) != 2 { - t.Errorf("Expected 2 nodes for class 1, got %d", len(class1Nodes)) - } - - class2Nodes := nodeList.GetTreeNodesByClass(2) - if len(class2Nodes) != 1 { - t.Errorf("Expected 1 node for class 2, got %d", len(class2Nodes)) - } - - // Test getting empty class - emptyClass := nodeList.GetTreeNodesByClass(999) - if len(emptyClass) != 0 { - t.Error("Expected empty slice for class with no nodes") - } - }) -} - -// Test AATemplate functionality -func TestAATemplate(t *testing.T) { - t.Run("NewAATemplate", func(t *testing.T) { - // Test personal template - template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Personal Template 1") - if template == nil { - t.Fatal("NewAATemplate returned nil") - } - - if template.TemplateID != AA_TEMPLATE_PERSONAL_1 { - t.Error("Expected template ID to match") - } - - if template.Name != "Personal Template 1" { - t.Error("Expected template name to match") - } - - if !template.IsPersonal { - t.Error("Expected IsPersonal to be true for template ID 1") - } - - if template.IsServer { - t.Error("Expected IsServer to be false for template ID 1") - } - - if template.IsCurrent { - t.Error("Expected IsCurrent to be false for template ID 1") - } - - // Test server template - serverTemplate := NewAATemplate(AA_TEMPLATE_SERVER_1, "Server Template 1") - if !serverTemplate.IsServer { - t.Error("Expected IsServer to be true for template ID 4") - } - - // Test current template - currentTemplate := NewAATemplate(AA_TEMPLATE_CURRENT, "Current") - if !currentTemplate.IsCurrent { - t.Error("Expected IsCurrent to be true for template ID 7") - } - }) - - t.Run("AddEntry", func(t *testing.T) { - template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - - entry := &AAEntry{ - TemplateID: AA_TEMPLATE_PERSONAL_1, - TabID: AA_CLASS, - AAID: 100, - Order: 1, - TreeID: 1, - } - - // Add entry - template.AddEntry(entry) - - if len(template.Entries) != 1 { - t.Error("Expected 1 entry after adding") - } - - if template.Entries[0] != entry { - t.Error("Expected entry to be added to template") - } - }) - - t.Run("GetEntry", func(t *testing.T) { - template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - - entry1 := &AAEntry{ - TemplateID: AA_TEMPLATE_PERSONAL_1, - TabID: AA_CLASS, - AAID: 100, - Order: 1, - TreeID: 1, - } - - entry2 := &AAEntry{ - TemplateID: AA_TEMPLATE_PERSONAL_1, - TabID: AA_CLASS, - AAID: 101, - Order: 2, - TreeID: 2, - } - - template.AddEntry(entry1) - template.AddEntry(entry2) - - // Test getting existing entry - found := template.GetEntry(100) - if found == nil { - t.Error("Expected to find entry with AAID 100") - } - - if found != entry1 { - t.Error("Expected to get correct entry") - } - - // Test getting non-existent entry - notFound := template.GetEntry(999) - if notFound != nil { - t.Error("Expected nil for non-existent AAID") - } - }) - - t.Run("RemoveEntry", func(t *testing.T) { - template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - - entry := &AAEntry{ - TemplateID: AA_TEMPLATE_PERSONAL_1, - TabID: AA_CLASS, - AAID: 100, - Order: 1, - TreeID: 1, - } - - template.AddEntry(entry) - - // Remove entry - removed := template.RemoveEntry(100) - if !removed { - t.Error("Expected RemoveEntry to return true") - } - - if len(template.Entries) != 0 { - t.Error("Expected 0 entries after removing") - } - - // Try to remove non-existent entry - removed = template.RemoveEntry(999) - if removed { - t.Error("Expected RemoveEntry to return false for non-existent entry") - } - }) -} - -// Test AAPlayerState functionality -func TestAAPlayerState(t *testing.T) { - t.Run("NewAAPlayerState", func(t *testing.T) { - playerState := NewAAPlayerState(123) - if playerState == nil { - t.Fatal("NewAAPlayerState returned nil") - } - - if playerState.CharacterID != 123 { - t.Error("Expected character ID to be 123") - } - - if playerState.TotalPoints != 0 { - t.Error("Expected initial total points to be 0") - } - - if playerState.ActiveTemplate != AA_TEMPLATE_CURRENT { - t.Error("Expected active template to be current template") - } - - if playerState.Templates == nil || playerState.Tabs == nil || playerState.AAProgress == nil { - t.Error("Expected maps to be initialized") - } - }) - - t.Run("AddAAProgress", func(t *testing.T) { - playerState := NewAAPlayerState(123) - - progress := &PlayerAAData{ - CharacterID: 123, - NodeID: 100, - CurrentRank: 1, - PointsSpent: 2, - TemplateID: AA_TEMPLATE_CURRENT, - TabID: AA_CLASS, - } - - playerState.AddAAProgress(progress) - - if len(playerState.AAProgress) != 1 { - t.Error("Expected 1 AA progress entry") - } - - if playerState.AAProgress[100] != progress { - t.Error("Expected progress to be added correctly") - } - }) - - t.Run("GetAAProgress", func(t *testing.T) { - playerState := NewAAPlayerState(123) - - progress := &PlayerAAData{ - CharacterID: 123, - NodeID: 100, - CurrentRank: 1, - PointsSpent: 2, - } - - playerState.AddAAProgress(progress) - - // Test getting existing progress - found := playerState.GetAAProgress(100) - if found == nil { - t.Error("Expected to find AA progress") - } - - if found != progress { - t.Error("Expected to get correct progress") - } - - // Test getting non-existent progress - notFound := playerState.GetAAProgress(999) - if notFound != nil { - t.Error("Expected nil for non-existent node ID") - } - }) - - t.Run("CalculateSpentPoints", func(t *testing.T) { - playerState := NewAAPlayerState(123) - - // Add multiple progress entries - progress1 := &PlayerAAData{ - CharacterID: 123, - NodeID: 100, - CurrentRank: 2, - PointsSpent: 4, - } - - progress2 := &PlayerAAData{ - CharacterID: 123, - NodeID: 101, - CurrentRank: 3, - PointsSpent: 6, - } - - playerState.AddAAProgress(progress1) - playerState.AddAAProgress(progress2) - - total := playerState.CalculateSpentPoints() - if total != 10 { - t.Errorf("Expected total spent points to be 10, got %d", total) - } - }) - - t.Run("UpdatePoints", func(t *testing.T) { - playerState := NewAAPlayerState(123) - - // Update points - playerState.UpdatePoints(100, 20, 5) - - if playerState.TotalPoints != 100 { - t.Error("Expected total points to be 100") - } - - if playerState.SpentPoints != 20 { - t.Error("Expected spent points to be 20") - } - - if playerState.BankedPoints != 5 { - t.Error("Expected banked points to be 5") - } - - if playerState.AvailablePoints != 80 { - t.Error("Expected available points to be 80 (100 - 20)") - } - }) - - t.Run("SetActiveTemplate", func(t *testing.T) { - playerState := NewAAPlayerState(123) - - // Create and add a template - template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Personal 1") - playerState.Templates[AA_TEMPLATE_PERSONAL_1] = template - - // Set active template - success := playerState.SetActiveTemplate(AA_TEMPLATE_PERSONAL_1) - if !success { - t.Error("Expected SetActiveTemplate to succeed") - } - - if playerState.ActiveTemplate != AA_TEMPLATE_PERSONAL_1 { - t.Error("Expected active template to be updated") - } - - // Try to set non-existent template - success = playerState.SetActiveTemplate(AA_TEMPLATE_PERSONAL_2) - if success { - t.Error("Expected SetActiveTemplate to fail for non-existent template") - } - }) -} - -// Test utility functions -func TestUtilityFunctions(t *testing.T) { - t.Run("GetMaxAAForTab", func(t *testing.T) { - tests := []struct { - group int8 - expected int32 - }{ - {AA_CLASS, MAX_CLASS_AA}, - {AA_SUBCLASS, MAX_SUBCLASS_AA}, - {AA_SHADOW, MAX_SHADOWS_AA}, - {AA_HEROIC, MAX_HEROIC_AA}, - {AA_TRADESKILL, MAX_TRADESKILL_AA}, - {AA_PRESTIGE, MAX_PRESTIGE_AA}, - {AA_TRADESKILL_PRESTIGE, MAX_TRADESKILL_PRESTIGE_AA}, - {AA_DRAGON, MAX_DRAGON_AA}, - {AA_DRAGONCLASS, MAX_DRAGONCLASS_AA}, - {AA_FARSEAS, MAX_FARSEAS_AA}, - {99, 100}, // Unknown group - } - - for _, tt := range tests { - t.Run(GetTabName(tt.group), func(t *testing.T) { - result := GetMaxAAForTab(tt.group) - if result != tt.expected { - t.Errorf("GetMaxAAForTab(%d) = %d, want %d", tt.group, result, tt.expected) - } - }) - } - }) - - t.Run("GetTabName", func(t *testing.T) { - tests := []struct { - group int8 - expected string - }{ - {AA_CLASS, "Class"}, - {AA_SUBCLASS, "Subclass"}, - {AA_SHADOW, "Shadows"}, - {AA_HEROIC, "Heroic"}, - {AA_TRADESKILL, "Tradeskill"}, - {99, "Unknown"}, - } - - for _, tt := range tests { - t.Run(tt.expected, func(t *testing.T) { - result := GetTabName(tt.group) - if result != tt.expected { - t.Errorf("GetTabName(%d) = %s, want %s", tt.group, result, tt.expected) - } - }) - } - }) - - t.Run("GetTemplateName", func(t *testing.T) { - tests := []struct { - templateID int8 - expected string - }{ - {AA_TEMPLATE_PERSONAL_1, "Personal 1"}, - {AA_TEMPLATE_PERSONAL_2, "Personal 2"}, - {AA_TEMPLATE_SERVER_1, "Server 1"}, - {AA_TEMPLATE_CURRENT, "Current"}, - {99, "Unknown"}, - } - - for _, tt := range tests { - t.Run(tt.expected, func(t *testing.T) { - result := GetTemplateName(tt.templateID) - if result != tt.expected { - t.Errorf("GetTemplateName(%d) = %s, want %s", tt.templateID, result, tt.expected) - } - }) - } - }) - - t.Run("IsExpansionRequired", func(t *testing.T) { - tests := []struct { - name string - flags int8 - expansion int8 - expected bool - }{ - {"No expansion required", EXPANSION_NONE, EXPANSION_KOS, false}, - {"KOS required - has KOS", EXPANSION_KOS, EXPANSION_KOS, true}, - {"KOS required - no KOS", EXPANSION_EOF, EXPANSION_KOS, false}, - {"Multiple expansions - has one", EXPANSION_KOS | EXPANSION_EOF, EXPANSION_KOS, true}, - {"Multiple expansions - has different", EXPANSION_KOS | EXPANSION_EOF, EXPANSION_ROK, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := IsExpansionRequired(tt.flags, tt.expansion) - if result != tt.expected { - t.Errorf("IsExpansionRequired(%d, %d) = %v, want %v", tt.flags, tt.expansion, result, tt.expected) - } - }) - } - }) -} - -// Test DefaultAAManagerConfig -func TestDefaultAAManagerConfig(t *testing.T) { - config := DefaultAAManagerConfig() - - if !config.EnableAASystem { - t.Error("Expected EnableAASystem to be true by default") - } - - if !config.EnableCaching { - t.Error("Expected EnableCaching to be true by default") - } - - if config.AAPointsPerLevel != DEFAULT_AA_POINTS_PER_LEVEL { - t.Errorf("Expected AAPointsPerLevel to be %d, got %d", DEFAULT_AA_POINTS_PER_LEVEL, config.AAPointsPerLevel) - } - - if config.MaxBankedPoints != DEFAULT_AA_MAX_BANKED_POINTS { - t.Errorf("Expected MaxBankedPoints to be %d, got %d", DEFAULT_AA_MAX_BANKED_POINTS, config.MaxBankedPoints) - } - - if config.CacheSize != AA_CACHE_SIZE { - t.Errorf("Expected CacheSize to be %d, got %d", AA_CACHE_SIZE, config.CacheSize) - } -} - -// Test AAManager basic functionality -func TestAAManagerBasics(t *testing.T) { - config := DefaultAAManagerConfig() - - manager := NewAAManager(config) - if manager == nil { - t.Fatal("NewAAManager returned nil") - } - - // Test configuration - currentConfig := manager.GetConfig() - if currentConfig.UpdateInterval != config.UpdateInterval { - t.Error("Expected config to match") - } - - // Test stats - stats := manager.GetSystemStats() - if stats == nil { - t.Error("Expected valid stats") - } - - // Test that manager is initialized - if manager.masterAAList == nil { - t.Error("Expected master AA list to be initialized") - } - - if manager.masterNodeList == nil { - t.Error("Expected master node list to be initialized") - } -} - -// Helper functions for AATemplate -func (t *AATemplate) AddEntry(entry *AAEntry) { - t.Entries = append(t.Entries, entry) - t.UpdatedAt = time.Now() -} - -func (t *AATemplate) GetEntry(aaID int32) *AAEntry { - for _, entry := range t.Entries { - if entry.AAID == aaID { - return entry - } - } - return nil -} - -func (t *AATemplate) RemoveEntry(aaID int32) bool { - for i, entry := range t.Entries { - if entry.AAID == aaID { - t.Entries = append(t.Entries[:i], t.Entries[i+1:]...) - t.UpdatedAt = time.Now() - return true - } - } - return false -} - -// Helper functions for AAPlayerState -func (ps *AAPlayerState) AddAAProgress(progress *PlayerAAData) { - ps.mutex.Lock() - defer ps.mutex.Unlock() - - ps.AAProgress[progress.NodeID] = progress - ps.needsSync = true - ps.lastUpdate = time.Now() -} - -func (ps *AAPlayerState) GetAAProgress(nodeID int32) *PlayerAAData { - ps.mutex.RLock() - defer ps.mutex.RUnlock() - - return ps.AAProgress[nodeID] -} - -func (ps *AAPlayerState) CalculateSpentPoints() int32 { - ps.mutex.RLock() - defer ps.mutex.RUnlock() - - var total int32 - for _, progress := range ps.AAProgress { - total += progress.PointsSpent - } - return total -} - -func (ps *AAPlayerState) UpdatePoints(total, spent, banked int32) { - ps.mutex.Lock() - defer ps.mutex.Unlock() - - ps.TotalPoints = total - ps.SpentPoints = spent - ps.BankedPoints = banked - ps.AvailablePoints = total - spent - ps.needsSync = true - ps.lastUpdate = time.Now() -} - -func (ps *AAPlayerState) SetActiveTemplate(templateID int8) bool { - ps.mutex.Lock() - defer ps.mutex.Unlock() - - if _, exists := ps.Templates[templateID]; exists { - ps.ActiveTemplate = templateID - ps.needsSync = true - ps.lastUpdate = time.Now() - return true - } - return false -} - -// Helper functions for TreeNodeData -func (tnd *TreeNodeData) Copy() *TreeNodeData { - copy := *tnd - return © -} - -// Test AAManager more comprehensively -func TestAAManager(t *testing.T) { - t.Run("LoadAAData", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Test without database - err := manager.LoadAAData() - if err == nil { - t.Error("Expected error when loading without database") - } - - // Test with mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - err = manager.LoadAAData() - if err != nil { - t.Errorf("Unexpected error loading AA data: %v", err) - } - - if !mockDB.loadAAsCalled || !mockDB.loadNodesCalled { - t.Error("Expected database methods to be called") - } - }) - - t.Run("GetPlayerAAState", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database that returns no existing data - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test getting non-existent player - state, err := manager.GetPlayerAAState(123) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if state == nil { - t.Error("Expected new player state to be created") - } - - if state != nil && state.CharacterID != 123 { - t.Error("Expected character ID to match") - } - - // Test getting existing player (should be cached) - state2, err := manager.GetPlayerAAState(123) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if state != nil && state2 != nil && state != state2 { - t.Error("Expected same state instance for same character from cache") - } - }) - - t.Run("PurchaseAA", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add a test AA to the master list - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - MinLevel: 10, - } - manager.masterAAList.AddAltAdvancement(aa) - - // Create a player with points - state, err := manager.GetPlayerAAState(123) - if err != nil { - t.Fatalf("Failed to get player state: %v", err) - } - if state == nil { - t.Fatal("Player state is nil") - } - state.TotalPoints = 10 - state.AvailablePoints = 10 - - // Test purchasing AA - err = manager.PurchaseAA(123, 200, 1) - if err != nil { - t.Errorf("Unexpected error purchasing AA: %v", err) - } - - // Verify purchase - progress := state.GetAAProgress(200) - if progress == nil { - t.Fatal("Expected AA progress to exist") - } - - if progress.CurrentRank != 1 { - t.Error("Expected rank to be 1") - } - - if state.AvailablePoints != 8 { - t.Error("Expected available points to be reduced by 2") - } - - // Test purchasing non-existent AA - err = manager.PurchaseAA(123, 999, 1) - if err == nil { - t.Error("Expected error when purchasing non-existent AA") - } - }) - - t.Run("AwardAAPoints", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Award points to player - err := manager.AwardAAPoints(123, 50, "Level up") - if err != nil { - t.Errorf("Unexpected error awarding points: %v", err) - } - - // Verify points - total, spent, available, err := manager.GetAAPoints(123) - if err != nil { - t.Errorf("Unexpected error getting points: %v", err) - } - - if total != 50 { - t.Errorf("Expected total points to be 50, got %d", total) - } - - if spent != 0 { - t.Errorf("Expected spent points to be 0, got %d", spent) - } - - if available != 50 { - t.Errorf("Expected available points to be 50, got %d", available) - } - }) - - t.Run("GetAA", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Add test AA - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - manager.masterAAList.AddAltAdvancement(aa) - - // Test getting by node ID - retrieved, err := manager.GetAA(200) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if retrieved == nil { - t.Fatal("Expected to get AA") - } - - if retrieved.NodeID != 200 { - t.Error("Expected node ID to match") - } - - // Test getting by spell ID - retrieved, err = manager.GetAABySpellID(100) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if retrieved == nil { - t.Fatal("Expected to get AA by spell ID") - } - - if retrieved.SpellID != 100 { - t.Error("Expected spell ID to match") - } - }) - - t.Run("StartStop", func(t *testing.T) { - config := DefaultAAManagerConfig() - config.UpdateInterval = 10 * time.Millisecond - config.SaveInterval = 10 * time.Millisecond - config.AutoSave = true - - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Start manager - err := manager.Start() - if err != nil { - t.Errorf("Unexpected error starting: %v", err) - } - - if !manager.IsRunning() { - t.Error("Expected manager to be running") - } - - // Let background processes run briefly - time.Sleep(50 * time.Millisecond) - - // Stop manager - err = manager.Stop() - if err != nil { - t.Errorf("Unexpected error stopping: %v", err) - } - - if manager.IsRunning() { - t.Error("Expected manager to be stopped") - } - }) -} - -// Mock implementations for testing -type mockAADatabase struct { - loadAAsCalled bool - loadNodesCalled bool - savePlayerCalled bool - mu sync.Mutex -} - -func (m *mockAADatabase) LoadAltAdvancements() error { - m.mu.Lock() - defer m.mu.Unlock() - m.loadAAsCalled = true - return nil -} - -func (m *mockAADatabase) LoadTreeNodes() error { - m.mu.Lock() - defer m.mu.Unlock() - m.loadNodesCalled = true - return nil -} - -func (m *mockAADatabase) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { - // Simulate creating a new player state when none exists - return NewAAPlayerState(characterID), nil -} - -func (m *mockAADatabase) SavePlayerAA(playerState *AAPlayerState) error { - m.mu.Lock() - defer m.mu.Unlock() - m.savePlayerCalled = true - return nil -} - -func (m *mockAADatabase) DeletePlayerAA(characterID int32) error { - return nil -} - -func (m *mockAADatabase) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { - return make(map[int8][]*AAEntry), nil -} - -func (m *mockAADatabase) GetAAStatistics() (map[string]any, error) { - return make(map[string]any), nil -} - -// Test mock event handler -type mockAAEventHandler struct { - events []string - mu sync.Mutex -} - -func (m *mockAAEventHandler) OnAAPurchased(characterID int32, nodeID int32, newRank int8, pointsSpent int32) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "purchased") - return nil -} - -func (m *mockAAEventHandler) OnAARefunded(characterID int32, nodeID int32, oldRank int8, pointsRefunded int32) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "refunded") - return nil -} - -func (m *mockAAEventHandler) OnAATemplateChanged(characterID int32, oldTemplate, newTemplate int8) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "template_changed") - return nil -} - -func (m *mockAAEventHandler) OnAATemplateCreated(characterID int32, templateID int8, name string) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "template_created") - return nil -} - -func (m *mockAAEventHandler) OnAASystemLoaded(totalAAs int32, totalNodes int32) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "system_loaded") - return nil -} - -func (m *mockAAEventHandler) OnAADataReloaded() error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "data_reloaded") - return nil -} - -func (m *mockAAEventHandler) OnPlayerAALoaded(characterID int32, playerState *AAPlayerState) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "player_loaded") - return nil -} - -func (m *mockAAEventHandler) OnPlayerAAPointsChanged(characterID int32, oldPoints, newPoints int32) error { - m.mu.Lock() - defer m.mu.Unlock() - m.events = append(m.events, "points_changed") - return nil -} - -func (m *mockAAEventHandler) LogEvent(message string) { - log.Println("[MockEvent]", message) -} - -// Test event handler integration -func TestAAEventHandling(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Add mock event handler - mockHandler := &mockAAEventHandler{} - manager.SetEventHandler(mockHandler) - - // Add mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test system loaded event - err := manager.LoadAAData() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Wait a bit for goroutines to complete - time.Sleep(10 * time.Millisecond) - - // Check that event was fired - mockHandler.mu.Lock() - hasSystemLoaded := false - for _, event := range mockHandler.events { - if event == "system_loaded" { - hasSystemLoaded = true - break - } - } - if !hasSystemLoaded { - t.Error("Expected system_loaded event") - } - mockHandler.mu.Unlock() - - // Test purchase event - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - - state, _ := manager.GetPlayerAAState(123) - state.TotalPoints = 10 - state.AvailablePoints = 10 - - err = manager.PurchaseAA(123, 200, 1) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Wait a bit for goroutines to complete - time.Sleep(10 * time.Millisecond) - - // Check that purchase event was fired - mockHandler.mu.Lock() - found := false - for _, event := range mockHandler.events { - if event == "purchased" { - found = true - break - } - } - if !found { - t.Error("Expected purchased event") - } - mockHandler.mu.Unlock() - - // Test points changed event - err = manager.AwardAAPoints(123, 50, "Test award") - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Wait a bit for goroutines to complete - time.Sleep(10 * time.Millisecond) - - mockHandler.mu.Lock() - found = false - for _, event := range mockHandler.events { - if event == "points_changed" { - found = true - break - } - } - if !found { - t.Error("Expected points_changed event") - } - mockHandler.mu.Unlock() -} - -// Test Interface Implementations and Adapters -func TestInterfacesAndAdapters(t *testing.T) { - t.Run("AAAdapter", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - adapter := NewAAAdapter(manager, 123) - if adapter == nil { - t.Fatal("NewAAAdapter returned nil") - } - - if adapter.GetCharacterID() != 123 { - t.Error("Expected character ID to match") - } - - if adapter.GetManager() != manager { - t.Error("Expected manager to match") - } - - // Test AwardPoints - err := adapter.AwardPoints(100, "Test") - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Test GetAAPoints - total, _, _, err := adapter.GetAAPoints() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if total != 100 { - t.Errorf("Expected total 100, got %d", total) - } - - // Test GetAAState - state, err := adapter.GetAAState() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if state == nil { - t.Error("Expected state to be returned") - } - - // Test SaveAAState - err = adapter.SaveAAState() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Test GetTemplates - templates, err := adapter.GetTemplates() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if templates == nil { - t.Error("Expected templates map") - } - - stats := adapter.GetPlayerStats() - if stats == nil { - t.Error("Expected stats map") - } - }) - - t.Run("PlayerAAAdapter", func(t *testing.T) { - mockPlayer := &mockPlayer{ - characterID: 123, - level: 50, - class: 1, - adventureClass: 1, - race: 1, - name: "TestPlayer", - } - - adapter := NewPlayerAAAdapter(mockPlayer) - if adapter == nil { - t.Fatal("NewPlayerAAAdapter returned nil") - } - - if adapter.GetPlayer() != mockPlayer { - t.Error("Expected player to match") - } - - if adapter.GetCharacterID() != 123 { - t.Error("Expected character ID to match") - } - - if adapter.GetLevel() != 50 { - t.Error("Expected level to match") - } - - if adapter.GetClass() != 1 { - t.Error("Expected class to match") - } - - if adapter.GetAdventureClass() != 1 { - t.Error("Expected adventure class to match") - } - - if adapter.GetRace() != 1 { - t.Error("Expected race to match") - } - - if adapter.GetName() != "TestPlayer" { - t.Error("Expected name to match") - } - - if !adapter.HasExpansion(EXPANSION_NONE) { - t.Error("Expected expansion check to work") - } - }) - - t.Run("ClientAAAdapter", func(t *testing.T) { - mockClient := &mockClient{ - characterID: 123, - player: &mockPlayer{ - characterID: 123, - name: "TestPlayer", - }, - version: 1096, - } - - adapter := NewClientAAAdapter(mockClient) - if adapter == nil { - t.Fatal("NewClientAAAdapter returned nil") - } - - if adapter.GetClient() != mockClient { - t.Error("Expected client to match") - } - - if adapter.GetCharacterID() != 123 { - t.Error("Expected character ID to match") - } - - if adapter.GetPlayer() != mockClient.player { - t.Error("Expected player to match") - } - - if adapter.GetClientVersion() != 1096 { - t.Error("Expected client version to match") - } - - // Test SendPacket - err := adapter.SendPacket([]byte("test")) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - }) - - t.Run("SimpleAACache", func(t *testing.T) { - cache := NewSimpleAACache(10) - if cache == nil { - t.Fatal("NewSimpleAACache returned nil") - } - - // Test AA caching - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - MaxRank: 5, - RankCost: 2, - } - - // Test miss - cached, found := cache.GetAA(200) - if found || cached != nil { - t.Error("Expected cache miss") - } - - // Set and get - cache.SetAA(200, aa) - cached, found = cache.GetAA(200) - if !found || cached == nil { - t.Error("Expected cache hit") - } - - if cached == aa { - t.Error("Expected cached copy, not original") - } - - if cached.NodeID != aa.NodeID { - t.Error("Expected cached data to match") - } - - // Test invalidation - cache.InvalidateAA(200) - cached, found = cache.GetAA(200) - if found || cached != nil { - t.Error("Expected cache miss after invalidation") - } - - // Test player state caching - playerState := NewAAPlayerState(123) - - // Test miss - cachedState, found := cache.GetPlayerState(123) - if found || cachedState != nil { - t.Error("Expected cache miss") - } - - // Set and get - cache.SetPlayerState(123, playerState) - cachedState, found = cache.GetPlayerState(123) - if !found || cachedState == nil { - t.Error("Expected cache hit") - } - - if cachedState != playerState { - t.Error("Expected same player state instance") - } - - // Test tree node caching - node := &TreeNodeData{ - ClassID: 1, - TreeID: 100, - AATreeID: 200, - } - - // Test miss - cachedNode, found := cache.GetTreeNode(100) - if found || cachedNode != nil { - t.Error("Expected cache miss") - } - - // Set and get - cache.SetTreeNode(100, node) - cachedNode, found = cache.GetTreeNode(100) - if !found || cachedNode == nil { - t.Error("Expected cache hit") - } - - if cachedNode == node { - t.Error("Expected cached copy, not original") - } - - if cachedNode.TreeID != node.TreeID { - t.Error("Expected cached data to match") - } - - // Test stats - stats := cache.GetStats() - if stats == nil { - t.Error("Expected stats map") - } - - // Test max size - cache.SetMaxSize(20) - if cache.maxSize != 20 { - t.Error("Expected max size to be updated") - } - - // Test clear - cache.Clear() - _, found = cache.GetAA(200) - if found { - t.Error("Expected cache to be cleared") - } - }) -} - -// Test Edge Cases and Error Conditions -func TestEdgeCases(t *testing.T) { - t.Run("AAManagerWithoutDatabase", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Test operations without database - err := manager.LoadAAData() - if err == nil { - t.Error("Expected error without database") - } - - err = manager.SavePlayerAA(123) - if err == nil { - t.Error("Expected error without database") - } - - _, err = manager.GetPlayerAAState(123) - if err == nil { - t.Error("Expected error without database") - } - }) - - t.Run("AAManagerErrorPaths", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database that returns errors - mockDB := &mockAADatabaseWithErrors{} - manager.SetDatabase(mockDB) - - // Test load errors - err := manager.LoadAAData() - if err == nil { - t.Error("Expected error from database") - } - - err = manager.ReloadAAData() - if err == nil { - t.Error("Expected error from database") - } - }) - - t.Run("PurchaseAAErrorCases", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test with insufficient points - state, _ := manager.GetPlayerAAState(123) - state.TotalPoints = 1 - state.AvailablePoints = 1 - - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 10, // More than available - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - - err := manager.PurchaseAA(123, 200, 1) - if err == nil { - t.Error("Expected error due to insufficient points") - } - - // Test purchasing non-existent AA - err = manager.PurchaseAA(123, 999, 1) - if err == nil { - t.Error("Expected error for non-existent AA") - } - }) - - t.Run("RefundAAErrorCases", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test refunding non-existent AA - err := manager.RefundAA(123, 999) - if err == nil { - t.Error("Expected error for non-existent AA") - } - - // Test refunding from player without state - err = manager.RefundAA(999, 200) - if err == nil { - t.Error("Expected error for non-existent player") - } - }) - - t.Run("TemplateOperations", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test with non-existent player - err := manager.ChangeAATemplate(999, AA_TEMPLATE_PERSONAL_1) - if err == nil { - t.Error("Expected error for non-existent player") - } - - templates, err := manager.GetAATemplates(999) - if err == nil { - t.Error("Expected error for non-existent player") - } - - if templates != nil { - t.Error("Expected nil templates for non-existent player") - } - - err = manager.SaveAATemplate(999, AA_TEMPLATE_PERSONAL_1, "Test") - if err == nil { - t.Error("Expected error for non-existent player") - } - }) - - t.Run("GetAvailableAAsErrorCases", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test with non-existent player - aas, err := manager.GetAvailableAAs(999, AA_CLASS) - if err == nil { - t.Error("Expected error for non-existent player") - } - - if aas != nil { - t.Error("Expected nil AAs for non-existent player") - } - }) - - t.Run("DataValidation", func(t *testing.T) { - masterList := NewMasterAAList() - - // Test adding AA with missing required fields - invalidAAs := []*AltAdvanceData{ - {NodeID: 200, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing SpellID - {SpellID: 100, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing NodeID - {SpellID: 100, NodeID: 200, MaxRank: 5, RankCost: 2}, // Missing Name - {SpellID: 100, NodeID: 200, Name: "Test", RankCost: 2}, // Missing MaxRank - {SpellID: 100, NodeID: 200, Name: "Test", MaxRank: 5}, // Missing RankCost - } - - for i, aa := range invalidAAs { - err := masterList.AddAltAdvancement(aa) - if err == nil { - t.Errorf("Expected error for invalid AA %d", i) - } - } - }) -} - -// Test Manager Lifecycle -func TestManagerLifecycle(t *testing.T) { - t.Run("StartStopCycle", func(t *testing.T) { - config := DefaultAAManagerConfig() - config.UpdateInterval = 5 * time.Millisecond - config.SaveInterval = 5 * time.Millisecond - config.AutoSave = true - - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Start manager - err := manager.Start() - if err != nil { - t.Errorf("Unexpected error starting: %v", err) - } - - if !manager.IsRunning() { - t.Error("Expected manager to be running after start") - } - - // Let it run briefly - time.Sleep(20 * time.Millisecond) - - // Stop manager - err = manager.Stop() - if err != nil { - t.Errorf("Unexpected error stopping: %v", err) - } - - // The IsRunning check consumes the channel close signal, so we can't test it reliably - // Just verify that stopping doesn't cause errors - }) - - t.Run("ReloadData", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add some data - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - MaxRank: 5, - RankCost: 2, - } - manager.masterAAList.AddAltAdvancement(aa) - - // Create player state - _, err := manager.GetPlayerAAState(123) - if err != nil { - t.Fatalf("Failed to get player state: %v", err) - } - - // Verify data exists - if manager.masterAAList.Size() != 1 { - t.Error("Expected 1 AA before reload") - } - - // Reload data - err = manager.ReloadAAData() - if err != nil { - t.Errorf("Unexpected error reloading: %v", err) - } - - // Verify data was cleared and reloaded - if manager.masterAAList.Size() != 0 { - t.Error("Expected AAs to be cleared after reload") - } - - // Player states should be cleared - if len(manager.playerStates) != 0 { - t.Error("Expected player states to be cleared after reload") - } - }) -} - -// Mock implementations for testing -type mockPlayer struct { - characterID int32 - level int8 - class int8 - adventureClass int8 - race int8 - name string -} - -func (m *mockPlayer) GetCharacterID() int32 { return m.characterID } -func (m *mockPlayer) GetLevel() int8 { return m.level } -func (m *mockPlayer) GetClass() int8 { return m.class } -func (m *mockPlayer) GetRace() int8 { return m.race } -func (m *mockPlayer) GetName() string { return m.name } -func (m *mockPlayer) GetAdventureClass() int8 { return m.adventureClass } -func (m *mockPlayer) HasExpansion(expansionFlag int8) bool { return expansionFlag == EXPANSION_NONE } - -type mockClient struct { - characterID int32 - player Player - version int16 -} - -func (m *mockClient) GetCharacterID() int32 { return m.characterID } -func (m *mockClient) GetPlayer() Player { return m.player } -func (m *mockClient) SendPacket(data []byte) error { return nil } -func (m *mockClient) GetClientVersion() int16 { return m.version } - -type mockAADatabaseWithErrors struct{} - -func (m *mockAADatabaseWithErrors) LoadAltAdvancements() error { return fmt.Errorf("load AA error") } -func (m *mockAADatabaseWithErrors) LoadTreeNodes() error { return fmt.Errorf("load nodes error") } -func (m *mockAADatabaseWithErrors) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { - return nil, fmt.Errorf("load player error") -} -func (m *mockAADatabaseWithErrors) SavePlayerAA(playerState *AAPlayerState) error { - return fmt.Errorf("save player error") -} -func (m *mockAADatabaseWithErrors) DeletePlayerAA(characterID int32) error { - return fmt.Errorf("delete player error") -} -func (m *mockAADatabaseWithErrors) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { - return nil, fmt.Errorf("load defaults error") -} -func (m *mockAADatabaseWithErrors) GetAAStatistics() (map[string]any, error) { - return nil, fmt.Errorf("stats error") -} - -// Test more adapter methods -func TestAdapterMethods(t *testing.T) { - t.Run("AAAdapterMethods", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add test AA - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - - adapter := NewAAAdapter(manager, 123) - - // Set up player with points - state, err := manager.GetPlayerAAState(123) - if err != nil { - t.Fatalf("Failed to get player state: %v", err) - } - state.TotalPoints = 10 - state.AvailablePoints = 10 - - // Test PurchaseAA - err = adapter.PurchaseAA(200, 1) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Test RefundAA - err = adapter.RefundAA(200) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Test GetAvailableAAs - aas, err := adapter.GetAvailableAAs(AA_CLASS) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if aas == nil { - t.Error("Expected AAs list") - } - - // Test ChangeTemplate (should work even without existing template since no validator is set) - err = adapter.ChangeTemplate(AA_TEMPLATE_PERSONAL_1) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Create template and test again - template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - state.Templates[AA_TEMPLATE_PERSONAL_1] = template - - err = adapter.ChangeTemplate(AA_TEMPLATE_PERSONAL_2) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - }) -} - -// Test database implementation constructor -func TestDatabaseImpl(t *testing.T) { - t.Run("NewDatabaseImpl", func(t *testing.T) { - masterAAList := NewMasterAAList() - masterNodeList := NewMasterAANodeList() - logger := log.New(&testLogWriter{}, "test", 0) - - dbImpl := NewDatabaseImpl(nil, masterAAList, masterNodeList, logger) - if dbImpl == nil { - t.Fatal("NewDatabaseImpl returned nil") - } - - if dbImpl.masterAAList != masterAAList { - t.Error("Expected master AA list to match") - } - - if dbImpl.masterNodeList != masterNodeList { - t.Error("Expected master node list to match") - } - - if dbImpl.logger != logger { - t.Error("Expected logger to match") - } - }) -} - -// Test more edge cases and missing functionality -func TestAdditionalEdgeCases(t *testing.T) { - t.Run("AAManagerRefundFunctionality", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add test AA - aa := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - - // Create player and purchase AA first - state, _ := manager.GetPlayerAAState(123) - state.TotalPoints = 10 - state.AvailablePoints = 10 - - // Purchase AA - err := manager.PurchaseAA(123, 200, 1) - if err != nil { - t.Fatalf("Failed to purchase AA: %v", err) - } - - // Verify purchase - progress := state.GetAAProgress(200) - if progress == nil { - t.Fatal("Expected AA progress") - } - - if progress.CurrentRank != 1 { - t.Error("Expected rank 1") - } - - // Test refund - err = manager.RefundAA(123, 200) - if err != nil { - t.Errorf("Unexpected error refunding: %v", err) - } - - // Verify refund - progress = state.GetAAProgress(200) - if progress != nil { - t.Error("Expected AA progress to be removed after refund") - } - - // Test refunding AA that's not purchased - err = manager.RefundAA(123, 200) - if err == nil { - t.Error("Expected error refunding unpurchased AA") - } - }) - - t.Run("TemplateManagement", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Create player - state, _ := manager.GetPlayerAAState(123) - - // Test saving template - err := manager.SaveAATemplate(123, AA_TEMPLATE_PERSONAL_1, "My Template") - if err != nil { - t.Errorf("Unexpected error saving template: %v", err) - } - - // Verify template was created - template := state.Templates[AA_TEMPLATE_PERSONAL_1] - if template == nil { - t.Error("Expected template to be created") - } - - if template.Name != "My Template" { - t.Error("Expected correct template name") - } - - // Test changing template - err = manager.ChangeAATemplate(123, AA_TEMPLATE_PERSONAL_1) - if err != nil { - t.Errorf("Unexpected error changing template: %v", err) - } - - if state.ActiveTemplate != AA_TEMPLATE_PERSONAL_1 { - t.Error("Expected active template to be changed") - } - - // Test getting templates - templates, err := manager.GetAATemplates(123) - if err != nil { - t.Errorf("Unexpected error getting templates: %v", err) - } - - if len(templates) != 1 { - t.Error("Expected 1 template") - } - - if templates[AA_TEMPLATE_PERSONAL_1] == nil { - t.Error("Expected template to be in map") - } - }) - - t.Run("GetAAsByMethods", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Add test AAs - aa1 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA 1", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - aa2 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Test AA 2", - Group: AA_HEROIC, - MaxRank: 5, - RankCost: 2, - } - - manager.masterAAList.AddAltAdvancement(aa1) - manager.masterAAList.AddAltAdvancement(aa2) - - // Test GetAAsByGroup - classAAs, err := manager.GetAAsByGroup(AA_CLASS) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if len(classAAs) != 1 { - t.Errorf("Expected 1 class AA, got %d", len(classAAs)) - } - - // Test GetAAsByClass - allClassAAs, err := manager.GetAAsByClass(0) // All classes - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - if len(allClassAAs) != 2 { - t.Errorf("Expected 2 AAs for all classes, got %d", len(allClassAAs)) - } - }) - - t.Run("AAValidation", func(t *testing.T) { - // Test IsAAAvailable method indirectly through GetAvailableAAs - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add AA with prerequisites - prereqAA := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Prerequisite AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 1, - MinLevel: 1, - } - - dependentAA := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Dependent AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - MinLevel: 1, - RankPrereqID: 200, - RankPrereq: 1, - } - - manager.masterAAList.AddAltAdvancement(prereqAA) - manager.masterAAList.AddAltAdvancement(dependentAA) - - // Create player - state, _ := manager.GetPlayerAAState(123) - - // Get available AAs - dependent AA should not be available without prereq - availableAAs, err := manager.GetAvailableAAs(123, AA_CLASS) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - // Should only have prerequisite AA available - hasPrereq := false - hasDependent := false - for _, aa := range availableAAs { - if aa.NodeID == 200 { - hasPrereq = true - } - if aa.NodeID == 201 { - hasDependent = true - } - } - - if !hasPrereq { - t.Error("Expected prerequisite AA to be available") - } - - if hasDependent { - t.Error("Expected dependent AA to not be available without prerequisite") - } - - // Purchase prerequisite - state.TotalPoints = 10 - state.AvailablePoints = 10 - err = manager.PurchaseAA(123, 200, 1) - if err != nil { - t.Fatalf("Failed to purchase prerequisite: %v", err) - } - - // Now dependent AA should be available - availableAAs, err = manager.GetAvailableAAs(123, AA_CLASS) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - hasDependent = false - for _, aa := range availableAAs { - if aa.NodeID == 201 { - hasDependent = true - } - } - - if !hasDependent { - t.Error("Expected dependent AA to be available after prerequisite purchased") - } - }) -} - -// Helper type for database test -type testLogWriter struct{} - -func (w *testLogWriter) Write(p []byte) (n int, err error) { - return len(p), nil -} - -// Test for cache eviction and other missing cache functionality -func TestCacheEviction(t *testing.T) { - t.Run("CacheEviction", func(t *testing.T) { - cache := NewSimpleAACache(2) // Small cache for testing eviction - - // Add first AA - aa1 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA 1", - MaxRank: 5, - RankCost: 2, - } - cache.SetAA(200, aa1) - - // Add second AA - aa2 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Test AA 2", - MaxRank: 5, - RankCost: 2, - } - cache.SetAA(201, aa2) - - // Add third AA (should evict one) - aa3 := &AltAdvanceData{ - SpellID: 102, - NodeID: 202, - Name: "Test AA 3", - MaxRank: 5, - RankCost: 2, - } - cache.SetAA(202, aa3) - - // Check that only 2 items remain - stats := cache.GetStats() - if stats["aa_data_count"].(int) != 2 { - t.Errorf("Expected 2 cached AAs after eviction, got %d", stats["aa_data_count"].(int)) - } - - // Test player state eviction - state1 := NewAAPlayerState(123) - state2 := NewAAPlayerState(124) - state3 := NewAAPlayerState(125) - - cache.SetPlayerState(123, state1) - cache.SetPlayerState(124, state2) - cache.SetPlayerState(125, state3) // Should evict one - - stats = cache.GetStats() - if stats["player_count"].(int) != 2 { - t.Errorf("Expected 2 cached player states after eviction, got %d", stats["player_count"].(int)) - } - - // Test tree node eviction - node1 := &TreeNodeData{ClassID: 1, TreeID: 100, AATreeID: 200} - node2 := &TreeNodeData{ClassID: 2, TreeID: 101, AATreeID: 201} - node3 := &TreeNodeData{ClassID: 3, TreeID: 102, AATreeID: 202} - - cache.SetTreeNode(100, node1) - cache.SetTreeNode(101, node2) - cache.SetTreeNode(102, node3) // Should evict one - - stats = cache.GetStats() - if stats["tree_node_count"].(int) != 2 { - t.Errorf("Expected 2 cached tree nodes after eviction, got %d", stats["tree_node_count"].(int)) - } - - // Test some get operations to generate statistics - cache.GetAA(999) // Should generate a miss - cache.GetAA(200) // Should generate a hit - - // Test invalidate - cache.InvalidatePlayerState(123) - cache.InvalidateTreeNode(100) - - // Get final stats - finalStats := cache.GetStats() - - // Verify hits and misses are tracked (should have at least some activity) - hits := finalStats["hits"].(int64) - misses := finalStats["misses"].(int64) - if hits == 0 && misses == 0 { - t.Error("Expected some cache statistics to be tracked") - } - }) -} - -// Test missing MasterAAList functions -func TestMasterAAListAdditionalMethods(t *testing.T) { - t.Run("GetAllAAs", func(t *testing.T) { - masterList := NewMasterAAList() - - // Add test AAs - aa1 := &AltAdvanceData{ - SpellID: 100, - NodeID: 200, - Name: "Test AA 1", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - aa2 := &AltAdvanceData{ - SpellID: 101, - NodeID: 201, - Name: "Test AA 2", - Group: AA_HEROIC, - MaxRank: 5, - RankCost: 2, - } - - masterList.AddAltAdvancement(aa1) - masterList.AddAltAdvancement(aa2) - - // Test GetAllAAs - allAAs := masterList.GetAllAAs() - if len(allAAs) != 2 { - t.Errorf("Expected 2 AAs, got %d", len(allAAs)) - } - - // Verify they are copies (different pointers but same content) - if allAAs[0] == aa1 || allAAs[1] == aa1 { - t.Error("GetAllAAs should return copies, not originals") - } - }) -} - -// Test MasterAANodeList additional methods -func TestMasterAANodeListAdditionalMethods(t *testing.T) { - t.Run("GetTreeNodes", func(t *testing.T) { - nodeList := NewMasterAANodeList() - - // Add test nodes - node1 := &TreeNodeData{ - ClassID: 1, - TreeID: 100, - AATreeID: 200, - } - - node2 := &TreeNodeData{ - ClassID: 2, - TreeID: 101, - AATreeID: 201, - } - - nodeList.AddTreeNode(node1) - nodeList.AddTreeNode(node2) - - // Test GetTreeNodes - allNodes := nodeList.GetTreeNodes() - if len(allNodes) != 2 { - t.Errorf("Expected 2 nodes, got %d", len(allNodes)) - } - - // Verify they are copies (different pointers but same content) - if allNodes[0] == node1 || allNodes[1] == node1 { - t.Error("GetTreeNodes should return copies, not originals") - } - }) - - t.Run("DestroyTreeNodes", func(t *testing.T) { - nodeList := NewMasterAANodeList() - - // Add test node - node := &TreeNodeData{ - ClassID: 1, - TreeID: 100, - AATreeID: 200, - } - - nodeList.AddTreeNode(node) - - if nodeList.Size() != 1 { - t.Error("Expected 1 node before destroy") - } - - // Destroy all nodes - nodeList.DestroyTreeNodes() - - if nodeList.Size() != 0 { - t.Error("Expected 0 nodes after destroy") - } - - // Verify maps are cleared - if len(nodeList.nodesByTree) != 0 || len(nodeList.nodesByClass) != 0 || len(nodeList.nodeList) != 0 { - t.Error("Expected all maps to be cleared after destroy") - } - }) -} - -// Test configuration validation -func TestConfigValidation(t *testing.T) { - t.Run("ConfigUpdate", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Test SetConfig - newConfig := AAManagerConfig{ - EnableAASystem: false, - AAPointsPerLevel: 10, - MaxBankedPoints: 2000, - EnableCaching: false, - CacheSize: 200, - UpdateInterval: 30 * time.Second, - SaveInterval: 120 * time.Second, - AutoSave: false, - } - - err := manager.SetConfig(newConfig) - if err != nil { - t.Errorf("Unexpected error setting config: %v", err) - } - - retrievedConfig := manager.GetConfig() - if retrievedConfig.EnableAASystem != false { - t.Error("Expected EnableAASystem to be updated") - } - - if retrievedConfig.AAPointsPerLevel != 10 { - t.Error("Expected AAPointsPerLevel to be updated") - } - }) -} - -// Test more complex scenarios -func TestComplexScenarios(t *testing.T) { - t.Run("MultiplePlayersMultipleAAs", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add multiple AAs - for i := 1; i <= 5; i++ { - aa := &AltAdvanceData{ - SpellID: int32(100 + i), - NodeID: int32(200 + i), - Name: fmt.Sprintf("Test AA %d", i), - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - } - - // Create multiple players - players := []int32{123, 124, 125} - for _, playerID := range players { - state, err := manager.GetPlayerAAState(playerID) - if err != nil { - t.Fatalf("Failed to get player state: %v", err) - } - - state.TotalPoints = 20 - state.AvailablePoints = 20 - - // Each player purchases different AAs - nodeID := int32(200 + int(playerID-122)) - err = manager.PurchaseAA(playerID, nodeID, 1) - if err != nil { - t.Errorf("Failed to purchase AA for player %d: %v", playerID, err) - } - } - - // Verify each player has their purchase - for _, playerID := range players { - state := manager.getPlayerState(playerID) - if state == nil { - t.Errorf("Player state not found for %d", playerID) - continue - } - - if len(state.AAProgress) != 1 { - t.Errorf("Expected 1 AA progress for player %d, got %d", playerID, len(state.AAProgress)) - } - } - - // Manually update statistics since background processes aren't running - manager.updateStatistics() - - // Test system stats - stats := manager.GetSystemStats() - if stats.ActivePlayers != 3 { - t.Errorf("Expected 3 active players, got %d", stats.ActivePlayers) - } - }) - - t.Run("TemplateOperationsWithMultipleTemplates", func(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Create player - state, _ := manager.GetPlayerAAState(123) - - // Create multiple templates - templateNames := []string{"Build 1", "Build 2", "Build 3"} - templateIDs := []int8{AA_TEMPLATE_PERSONAL_1, AA_TEMPLATE_PERSONAL_2, AA_TEMPLATE_PERSONAL_3} - - for i, templateID := range templateIDs { - err := manager.SaveAATemplate(123, templateID, templateNames[i]) - if err != nil { - t.Errorf("Failed to save template %d: %v", templateID, err) - } - } - - // Verify all templates were created - templates, err := manager.GetAATemplates(123) - if err != nil { - t.Errorf("Failed to get templates: %v", err) - } - - if len(templates) != 3 { - t.Errorf("Expected 3 templates, got %d", len(templates)) - } - - for i, templateID := range templateIDs { - template := templates[templateID] - if template == nil { - t.Errorf("Template %d not found", templateID) - continue - } - - if template.Name != templateNames[i] { - t.Errorf("Expected template name %s, got %s", templateNames[i], template.Name) - } - } - - // Test changing between templates - for _, templateID := range templateIDs { - err := manager.ChangeAATemplate(123, templateID) - if err != nil { - t.Errorf("Failed to change to template %d: %v", templateID, err) - } - - if state.ActiveTemplate != templateID { - t.Errorf("Expected active template %d, got %d", templateID, state.ActiveTemplate) - } - } - }) -} diff --git a/internal/alt_advancement/alt_advancement.go b/internal/alt_advancement/alt_advancement.go new file mode 100644 index 0000000..800a507 --- /dev/null +++ b/internal/alt_advancement/alt_advancement.go @@ -0,0 +1,344 @@ +package alt_advancement + +import ( + "database/sql" + "fmt" + "time" + + "eq2emu/internal/database" +) + +// AltAdvancement represents an Alternate Advancement node with database operations +type AltAdvancement struct { + // Core identification + SpellID int32 `json:"spell_id" db:"spell_id"` + NodeID int32 `json:"node_id" db:"node_id"` + SpellCRC int32 `json:"spell_crc" db:"spell_crc"` + + // Display information + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + + // Tree organization + Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.) + Col int8 `json:"col" db:"col"` // Column position in tree + Row int8 `json:"row" db:"row"` // Row position in tree + + // Visual representation + Icon int16 `json:"icon" db:"icon"` // Primary icon ID + Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID + + // Ranking system + RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank + MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank + + // Prerequisites + MinLevel int8 `json:"min_level" db:"min_level"` // Minimum character level + RankPrereqID int32 `json:"rank_prereq_id" db:"rank_prereq_id"` // Prerequisite AA node ID + RankPrereq int8 `json:"rank_prereq" db:"rank_prereq"` // Required rank in prerequisite + ClassReq int8 `json:"class_req" db:"class_req"` // Required class + Tier int8 `json:"tier" db:"tier"` // AA tier + ReqPoints int8 `json:"req_points" db:"req_points"` // Required points in classification + ReqTreePoints int16 `json:"req_tree_points" db:"req_tree_points"` // Required points in entire tree + + // Display classification + ClassName string `json:"class_name" db:"class_name"` // Class name for display + SubclassName string `json:"subclass_name" db:"subclass_name"` // Subclass name for display + LineTitle string `json:"line_title" db:"line_title"` // AA line title + TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement + + // Metadata + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + + // Database connection + db *database.Database + isNew bool +} + +// New creates a new alternate advancement with database connection +func New(db *database.Database) *AltAdvancement { + return &AltAdvancement{ + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + db: db, + isNew: true, + } +} + +// Load loads an alternate advancement by node ID +func Load(db *database.Database, nodeID int32) (*AltAdvancement, error) { + aa := &AltAdvancement{ + db: db, + isNew: false, + } + + query := `SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk, + icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier, + firstparentid, firstparentrequiredtier, displayedclassification, + requiredclassification, classificationpointsrequired, + pointsspentintreetounlock, title, titlelevel + FROM spell_aa_nodelist WHERE nodeid = ?` + + err := db.QueryRow(query, nodeID).Scan( + &aa.NodeID, + &aa.MinLevel, + &aa.SpellCRC, + &aa.Name, + &aa.Description, + &aa.Group, + &aa.Icon, + &aa.Icon2, + &aa.Col, + &aa.Row, + &aa.RankCost, + &aa.MaxRank, + &aa.RankPrereqID, + &aa.RankPrereq, + &aa.ClassReq, + &aa.Tier, + &aa.ReqPoints, + &aa.ReqTreePoints, + &aa.LineTitle, + &aa.TitleLevel, + ) + + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("alternate advancement not found: %d", nodeID) + } + return nil, fmt.Errorf("failed to load alternate advancement: %w", err) + } + + // Set spell ID to node ID if not provided separately + aa.SpellID = aa.NodeID + + return aa, nil +} + +// LoadAll loads all alternate advancements from database +func LoadAll(db *database.Database) ([]*AltAdvancement, error) { + query := `SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk, + icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier, + firstparentid, firstparentrequiredtier, displayedclassification, + requiredclassification, classificationpointsrequired, + pointsspentintreetounlock, title, titlelevel + FROM spell_aa_nodelist + ORDER BY aa_list_fk, ycoord, xcoord` + + rows, err := db.Query(query) + if err != nil { + return nil, fmt.Errorf("failed to query alternate advancements: %w", err) + } + defer rows.Close() + + var aas []*AltAdvancement + + for rows.Next() { + aa := &AltAdvancement{ + db: db, + isNew: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + err := rows.Scan( + &aa.NodeID, + &aa.MinLevel, + &aa.SpellCRC, + &aa.Name, + &aa.Description, + &aa.Group, + &aa.Icon, + &aa.Icon2, + &aa.Col, + &aa.Row, + &aa.RankCost, + &aa.MaxRank, + &aa.RankPrereqID, + &aa.RankPrereq, + &aa.ClassReq, + &aa.Tier, + &aa.ReqPoints, + &aa.ReqTreePoints, + &aa.LineTitle, + &aa.TitleLevel, + ) + + if err != nil { + return nil, fmt.Errorf("failed to scan alternate advancement: %w", err) + } + + // Set spell ID to node ID if not provided separately + aa.SpellID = aa.NodeID + + aas = append(aas, aa) + } + + return aas, rows.Err() +} + +// Save saves the alternate advancement to the database (insert if new, update if existing) +func (aa *AltAdvancement) Save() error { + if aa.db == nil { + return fmt.Errorf("no database connection") + } + + tx, err := aa.db.Begin() + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + if aa.isNew { + err = aa.insert(tx) + } else { + err = aa.update(tx) + } + + if err != nil { + return err + } + + return tx.Commit() +} + +// Delete removes the alternate advancement from the database +func (aa *AltAdvancement) Delete() error { + if aa.db == nil { + return fmt.Errorf("no database connection") + } + + if aa.isNew { + return fmt.Errorf("cannot delete unsaved alternate advancement") + } + + _, err := aa.db.Exec("DELETE FROM spell_aa_nodelist WHERE nodeid = ?", aa.NodeID) + if err != nil { + return fmt.Errorf("failed to delete alternate advancement: %w", err) + } + + return nil +} + +// Reload reloads the alternate advancement from the database +func (aa *AltAdvancement) Reload() error { + if aa.db == nil { + return fmt.Errorf("no database connection") + } + + if aa.isNew { + return fmt.Errorf("cannot reload unsaved alternate advancement") + } + + reloaded, err := Load(aa.db, aa.NodeID) + if err != nil { + return err + } + + // Copy all fields from reloaded AA + *aa = *reloaded + return nil +} + +// IsNew returns true if this is a new (unsaved) alternate advancement +func (aa *AltAdvancement) IsNew() bool { + return aa.isNew +} + +// GetID returns the node ID (implements common.Identifiable interface) +func (aa *AltAdvancement) GetID() int32 { + return aa.NodeID +} + +// IsValid validates the alternate advancement data +func (aa *AltAdvancement) IsValid() bool { + return aa.SpellID > 0 && + aa.NodeID > 0 && + len(aa.Name) > 0 && + aa.MaxRank > 0 && + aa.RankCost > 0 +} + +// Clone creates a deep copy of the alternate advancement +func (aa *AltAdvancement) Clone() *AltAdvancement { + clone := &AltAdvancement{ + SpellID: aa.SpellID, + NodeID: aa.NodeID, + SpellCRC: aa.SpellCRC, + Name: aa.Name, + Description: aa.Description, + Group: aa.Group, + Col: aa.Col, + Row: aa.Row, + Icon: aa.Icon, + Icon2: aa.Icon2, + RankCost: aa.RankCost, + MaxRank: aa.MaxRank, + MinLevel: aa.MinLevel, + RankPrereqID: aa.RankPrereqID, + RankPrereq: aa.RankPrereq, + ClassReq: aa.ClassReq, + Tier: aa.Tier, + ReqPoints: aa.ReqPoints, + ReqTreePoints: aa.ReqTreePoints, + ClassName: aa.ClassName, + SubclassName: aa.SubclassName, + LineTitle: aa.LineTitle, + TitleLevel: aa.TitleLevel, + CreatedAt: aa.CreatedAt, + UpdatedAt: aa.UpdatedAt, + db: aa.db, + isNew: false, + } + + return clone +} + +// Private helper methods + +func (aa *AltAdvancement) insert(tx *sql.Tx) error { + query := `INSERT INTO spell_aa_nodelist + (nodeid, minlevel, spellcrc, name, description, aa_list_fk, + icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier, + firstparentid, firstparentrequiredtier, displayedclassification, + requiredclassification, classificationpointsrequired, + pointsspentintreetounlock, title, titlelevel) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + + _, err := tx.Exec(query, + aa.NodeID, aa.MinLevel, aa.SpellCRC, aa.Name, aa.Description, aa.Group, + aa.Icon, aa.Icon2, aa.Col, aa.Row, aa.RankCost, aa.MaxRank, + aa.RankPrereqID, aa.RankPrereq, aa.ClassReq, aa.Tier, aa.ReqPoints, + aa.ReqTreePoints, aa.LineTitle, aa.TitleLevel) + + if err != nil { + return fmt.Errorf("failed to insert alternate advancement: %w", err) + } + + aa.isNew = false + return nil +} + +func (aa *AltAdvancement) update(tx *sql.Tx) error { + query := `UPDATE spell_aa_nodelist SET + minlevel = ?, spellcrc = ?, name = ?, description = ?, aa_list_fk = ?, + icon_id = ?, icon_backdrop = ?, xcoord = ?, ycoord = ?, pointspertier = ?, + maxtier = ?, firstparentid = ?, firstparentrequiredtier = ?, + displayedclassification = ?, requiredclassification = ?, + classificationpointsrequired = ?, pointsspentintreetounlock = ?, + title = ?, titlelevel = ? + WHERE nodeid = ?` + + _, err := tx.Exec(query, + aa.MinLevel, aa.SpellCRC, aa.Name, aa.Description, aa.Group, + aa.Icon, aa.Icon2, aa.Col, aa.Row, aa.RankCost, aa.MaxRank, + aa.RankPrereqID, aa.RankPrereq, aa.ClassReq, aa.Tier, aa.ReqPoints, + aa.ReqTreePoints, aa.LineTitle, aa.TitleLevel, aa.NodeID) + + if err != nil { + return fmt.Errorf("failed to update alternate advancement: %w", err) + } + + return nil +} diff --git a/internal/alt_advancement/alt_advancement_test.go b/internal/alt_advancement/alt_advancement_test.go new file mode 100644 index 0000000..1f4bc2b --- /dev/null +++ b/internal/alt_advancement/alt_advancement_test.go @@ -0,0 +1,142 @@ +package alt_advancement + +import ( + "testing" + + "eq2emu/internal/database" +) + +// TestSimpleAltAdvancement tests the basic new AltAdvancement functionality +func TestSimpleAltAdvancement(t *testing.T) { + db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") + if err != nil { + t.Fatalf("Failed to create test database: %v", err) + } + defer db.Close() + + // Test creating a new alternate advancement + aa := New(db) + if aa == nil { + t.Fatal("New returned nil") + } + + if !aa.IsNew() { + t.Error("New AA should be marked as new") + } + + // Test setting values + aa.SpellID = 1001 + aa.NodeID = 1001 + aa.Name = "Dragon's Strength" + aa.Group = AA_CLASS + aa.RankCost = 1 + aa.MaxRank = 5 + + if aa.GetID() != 1001 { + t.Errorf("Expected GetID() to return 1001, got %d", aa.GetID()) + } + + // Test validation + if !aa.IsValid() { + t.Error("AA should be valid after setting required fields") + } + + // Test Clone + clone := aa.Clone() + if clone == nil { + t.Fatal("Clone returned nil") + } + + if clone.NodeID != aa.NodeID { + t.Errorf("Expected clone ID %d, got %d", aa.NodeID, clone.NodeID) + } + + if clone.Name != aa.Name { + t.Errorf("Expected clone name %s, got %s", aa.Name, clone.Name) + } + + // Ensure clone is not the same instance + if clone == aa { + t.Error("Clone should return a different instance") + } +} + +// TestMasterListWithGeneric tests the master list with generic base +func TestMasterListWithGeneric(t *testing.T) { + masterList := NewMasterList() + + if masterList == nil { + t.Fatal("NewMasterList returned nil") + } + + if masterList.Size() != 0 { + t.Errorf("Expected size 0, got %d", masterList.Size()) + } + + // Create an AA (need database for new pattern) + db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + defer db.Close() + + aa := New(db) + aa.SpellID = 1001 + aa.NodeID = 1001 + aa.Name = "Dragon's Strength" + aa.Group = AA_CLASS + aa.RankCost = 1 + aa.MaxRank = 5 + + // Test adding + if !masterList.AddAltAdvancement(aa) { + t.Error("Should successfully add alternate advancement") + } + + if masterList.Size() != 1 { + t.Errorf("Expected size 1, got %d", masterList.Size()) + } + + // Test retrieving + retrieved := masterList.GetAltAdvancement(1001) + if retrieved == nil { + t.Error("Should retrieve added alternate advancement") + } + + if retrieved.Name != "Dragon's Strength" { + t.Errorf("Expected name 'Dragon's Strength', got '%s'", retrieved.Name) + } + + // Test filtering + classAAs := masterList.GetAltAdvancementsByGroup(AA_CLASS) + if len(classAAs) != 1 { + t.Errorf("Expected 1 AA in Class group, got %d", len(classAAs)) + } +} + +// TestAltAdvancementValidation tests validation functionality +func TestAltAdvancementValidation(t *testing.T) { + db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + defer db.Close() + + // Test valid AA + validAA := New(db) + validAA.SpellID = 100 + validAA.NodeID = 100 + validAA.Name = "Test AA" + validAA.RankCost = 1 + validAA.MaxRank = 5 + + if !validAA.IsValid() { + t.Error("Valid AA should pass validation") + } + + // Test invalid AA - missing name + invalidAA := New(db) + invalidAA.SpellID = 100 + invalidAA.NodeID = 100 + invalidAA.RankCost = 1 + invalidAA.MaxRank = 5 + // Name is empty + + if invalidAA.IsValid() { + t.Error("Invalid AA (missing name) should fail validation") + } +} diff --git a/internal/alt_advancement/concurrency_test.go b/internal/alt_advancement/concurrency_test.go deleted file mode 100644 index a80f41d..0000000 --- a/internal/alt_advancement/concurrency_test.go +++ /dev/null @@ -1,570 +0,0 @@ -package alt_advancement - -import ( - "sync" - "sync/atomic" - "testing" -) - -// TestAAManagerConcurrentPlayerAccess tests concurrent access to player states -func TestAAManagerConcurrentPlayerAccess(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Test concurrent access to the same player - const numGoroutines = 100 - const characterID = int32(123) - - var wg sync.WaitGroup - var successCount int64 - - // Launch multiple goroutines trying to get the same player state - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() - - state, err := manager.GetPlayerAAState(characterID) - if err != nil { - t.Errorf("Failed to get player state: %v", err) - return - } - - if state == nil { - t.Error("Got nil player state") - return - } - - if state.CharacterID != characterID { - t.Errorf("Wrong character ID: expected %d, got %d", characterID, state.CharacterID) - return - } - - atomic.AddInt64(&successCount, 1) - }() - } - - wg.Wait() - - if atomic.LoadInt64(&successCount) != numGoroutines { - t.Errorf("Expected %d successful operations, got %d", numGoroutines, successCount) - } - - // Verify only one instance was created in cache - manager.statesMutex.RLock() - cachedStates := len(manager.playerStates) - manager.statesMutex.RUnlock() - - if cachedStates != 1 { - t.Errorf("Expected 1 cached state, got %d", cachedStates) - } -} - -// TestAAManagerConcurrentMultiplePlayer tests concurrent access to different players -func TestAAManagerConcurrentMultiplePlayer(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - const numPlayers = 50 - const goroutinesPerPlayer = 10 - - var wg sync.WaitGroup - var successCount int64 - - // Launch multiple goroutines for different players - for playerID := int32(1); playerID <= numPlayers; playerID++ { - for j := 0; j < goroutinesPerPlayer; j++ { - wg.Add(1) - go func(id int32) { - defer wg.Done() - - state, err := manager.GetPlayerAAState(id) - if err != nil { - t.Errorf("Failed to get player state for %d: %v", id, err) - return - } - - if state == nil { - t.Errorf("Got nil player state for %d", id) - return - } - - if state.CharacterID != id { - t.Errorf("Wrong character ID: expected %d, got %d", id, state.CharacterID) - return - } - - atomic.AddInt64(&successCount, 1) - }(playerID) - } - } - - wg.Wait() - - expectedSuccess := int64(numPlayers * goroutinesPerPlayer) - if atomic.LoadInt64(&successCount) != expectedSuccess { - t.Errorf("Expected %d successful operations, got %d", expectedSuccess, successCount) - } - - // Verify correct number of cached states - manager.statesMutex.RLock() - cachedStates := len(manager.playerStates) - manager.statesMutex.RUnlock() - - if cachedStates != numPlayers { - t.Errorf("Expected %d cached states, got %d", numPlayers, cachedStates) - } -} - -// TestConcurrentAAPurchases tests concurrent AA purchases -func TestConcurrentAAPurchases(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add test AAs - for i := 1; i <= 10; i++ { - aa := &AltAdvanceData{ - SpellID: int32(i * 100), - NodeID: int32(i * 200), - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 1, // Low cost for testing - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - } - - // Get player state and give it points - state, err := manager.GetPlayerAAState(123) - if err != nil { - t.Fatalf("Failed to get player state: %v", err) - } - - // Give player plenty of points - state.TotalPoints = 1000 - state.AvailablePoints = 1000 - - const numGoroutines = 20 - var wg sync.WaitGroup - var successCount, errorCount int64 - - // Concurrent purchases - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func(goroutineID int) { - defer wg.Done() - - // Try to purchase different AAs - aaNodeID := int32(200 + (goroutineID%10)*200) // Spread across different AAs - - err := manager.PurchaseAA(123, aaNodeID, 1) - if err != nil { - atomic.AddInt64(&errorCount, 1) - // Some errors expected due to race conditions or insufficient points - } else { - atomic.AddInt64(&successCount, 1) - } - }(i) - } - - wg.Wait() - - t.Logf("Successful purchases: %d, Errors: %d", successCount, errorCount) - - // Verify final state consistency - state.mutex.RLock() - finalAvailable := state.AvailablePoints - finalSpent := state.SpentPoints - finalTotal := state.TotalPoints - numProgress := len(state.AAProgress) - state.mutex.RUnlock() - - // Basic consistency checks - if finalAvailable+finalSpent != finalTotal { - t.Errorf("Point consistency check failed: available(%d) + spent(%d) != total(%d)", - finalAvailable, finalSpent, finalTotal) - } - - if numProgress > int(successCount) { - t.Errorf("More progress entries (%d) than successful purchases (%d)", numProgress, successCount) - } - - t.Logf("Final state: Total=%d, Spent=%d, Available=%d, Progress entries=%d", - finalTotal, finalSpent, finalAvailable, numProgress) -} - -// TestConcurrentAAPointAwarding tests concurrent point awarding -func TestConcurrentAAPointAwarding(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - const characterID = int32(123) - const numGoroutines = 100 - const pointsPerAward = int32(10) - - var wg sync.WaitGroup - var successCount int64 - - // Concurrent point awarding - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func(goroutineID int) { - defer wg.Done() - - err := manager.AwardAAPoints(characterID, pointsPerAward, "Concurrent test") - if err != nil { - t.Errorf("Failed to award points: %v", err) - return - } - - atomic.AddInt64(&successCount, 1) - }(i) - } - - wg.Wait() - - if atomic.LoadInt64(&successCount) != numGoroutines { - t.Errorf("Expected %d successful awards, got %d", numGoroutines, successCount) - } - - // Verify final point total - total, spent, available, err := manager.GetAAPoints(characterID) - if err != nil { - t.Fatalf("Failed to get AA points: %v", err) - } - - expectedTotal := pointsPerAward * numGoroutines - if total != expectedTotal { - t.Errorf("Expected total points %d, got %d", expectedTotal, total) - } - - if spent != 0 { - t.Errorf("Expected 0 spent points, got %d", spent) - } - - if available != expectedTotal { - t.Errorf("Expected available points %d, got %d", expectedTotal, available) - } -} - -// TestMasterAAListConcurrentOperations tests thread safety of MasterAAList -func TestMasterAAListConcurrentOperations(t *testing.T) { - masterList := NewMasterAAList() - - // Pre-populate with some AAs - for i := 1; i <= 100; i++ { - aa := &AltAdvanceData{ - SpellID: int32(i * 100), - NodeID: int32(i * 200), - Name: "Test AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - masterList.AddAltAdvancement(aa) - } - - const numReaders = 50 - const numWriters = 10 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - var readOps, writeOps int64 - - // Reader goroutines - for i := 0; i < numReaders; i++ { - wg.Add(1) - go func() { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Mix different read operations - switch j % 5 { - case 0: - masterList.GetAltAdvancement(100) - case 1: - masterList.GetAltAdvancementByNodeID(200) - case 2: - masterList.GetAAsByGroup(AA_CLASS) - case 3: - masterList.Size() - case 4: - masterList.GetAllAAs() - } - atomic.AddInt64(&readOps, 1) - } - }() - } - - // Writer goroutines (adding new AAs) - for i := 0; i < numWriters; i++ { - wg.Add(1) - go func(writerID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Create unique AAs for each writer - baseID := (writerID + 1000) * 1000 + j - aa := &AltAdvanceData{ - SpellID: int32(baseID), - NodeID: int32(baseID + 100000), - Name: "Concurrent AA", - Group: AA_CLASS, - MaxRank: 5, - RankCost: 2, - } - - err := masterList.AddAltAdvancement(aa) - if err != nil { - // Some errors expected due to potential duplicates - continue - } - atomic.AddInt64(&writeOps, 1) - } - }(i) - } - - wg.Wait() - - t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps) - - // Verify final state - finalSize := masterList.Size() - if finalSize < 100 { - t.Errorf("Expected at least 100 AAs, got %d", finalSize) - } - - t.Logf("Final AA count: %d", finalSize) -} - -// TestMasterAANodeListConcurrentOperations tests thread safety of MasterAANodeList -func TestMasterAANodeListConcurrentOperations(t *testing.T) { - nodeList := NewMasterAANodeList() - - // Pre-populate with some nodes - for i := 1; i <= 50; i++ { - node := &TreeNodeData{ - ClassID: int32(i % 10 + 1), // Classes 1-10 - TreeID: int32(i * 100), - AATreeID: int32(i * 200), - } - nodeList.AddTreeNode(node) - } - - const numReaders = 30 - const numWriters = 5 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - var readOps, writeOps int64 - - // Reader goroutines - for i := 0; i < numReaders; i++ { - wg.Add(1) - go func() { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Mix different read operations - switch j % 4 { - case 0: - nodeList.GetTreeNode(100) - case 1: - nodeList.GetTreeNodesByClass(1) - case 2: - nodeList.Size() - case 3: - nodeList.GetTreeNodes() - } - atomic.AddInt64(&readOps, 1) - } - }() - } - - // Writer goroutines - for i := 0; i < numWriters; i++ { - wg.Add(1) - go func(writerID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Create unique nodes for each writer - baseID := (writerID + 1000) * 1000 + j - node := &TreeNodeData{ - ClassID: int32(writerID%5 + 1), - TreeID: int32(baseID), - AATreeID: int32(baseID + 100000), - } - - err := nodeList.AddTreeNode(node) - if err != nil { - // Some errors expected due to potential duplicates - continue - } - atomic.AddInt64(&writeOps, 1) - } - }(i) - } - - wg.Wait() - - t.Logf("Read operations: %d, Write operations: %d", readOps, writeOps) - - // Verify final state - finalSize := nodeList.Size() - if finalSize < 50 { - t.Errorf("Expected at least 50 nodes, got %d", finalSize) - } - - t.Logf("Final node count: %d", finalSize) -} - -// TestAAPlayerStateConcurrentAccess tests thread safety of AAPlayerState -func TestAAPlayerStateConcurrentAccess(t *testing.T) { - playerState := NewAAPlayerState(123) - - // Give player some initial points - playerState.TotalPoints = 1000 - playerState.AvailablePoints = 1000 - - const numGoroutines = 100 - var wg sync.WaitGroup - - // Concurrent operations on player state - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func(goroutineID int) { - defer wg.Done() - - // Mix of different operations - switch goroutineID % 4 { - case 0: - // Add AA progress - progress := &PlayerAAData{ - CharacterID: 123, - NodeID: int32(goroutineID + 1000), - CurrentRank: 1, - PointsSpent: 2, - } - playerState.AddAAProgress(progress) - - case 1: - // Update points - playerState.UpdatePoints(1000, int32(goroutineID), 0) - - case 2: - // Get AA progress - playerState.GetAAProgress(int32(goroutineID + 1000)) - - case 3: - // Calculate spent points - playerState.CalculateSpentPoints() - } - }(i) - } - - wg.Wait() - - // Verify state is still consistent - playerState.mutex.RLock() - totalPoints := playerState.TotalPoints - progressCount := len(playerState.AAProgress) - playerState.mutex.RUnlock() - - if totalPoints != 1000 { - t.Errorf("Expected total points to remain 1000, got %d", totalPoints) - } - - t.Logf("Final progress entries: %d", progressCount) -} - -// TestConcurrentSystemOperations tests mixed system operations -func TestConcurrentSystemOperations(t *testing.T) { - config := DefaultAAManagerConfig() - manager := NewAAManager(config) - - // Set up mock database - mockDB := &mockAADatabase{} - manager.SetDatabase(mockDB) - - // Add some test AAs - for i := 1; i <= 20; i++ { - aa := &AltAdvanceData{ - SpellID: int32(i * 100), - NodeID: int32(i * 200), - Name: "Test AA", - Group: int8(i % 3), // Mix groups - MaxRank: 5, - RankCost: 2, - MinLevel: 1, - } - manager.masterAAList.AddAltAdvancement(aa) - } - - const numGoroutines = 50 - var wg sync.WaitGroup - var operations int64 - - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func(goroutineID int) { - defer wg.Done() - - playerID := int32(goroutineID%10 + 1) // 10 different players - - // Mix of operations - switch goroutineID % 6 { - case 0: - // Get player state - manager.GetPlayerAAState(playerID) - - case 1: - // Award points - manager.AwardAAPoints(playerID, 50, "Test") - - case 2: - // Get AA points - manager.GetAAPoints(playerID) - - case 3: - // Get AAs by group - manager.GetAAsByGroup(AA_CLASS) - - case 4: - // Get system stats - manager.GetSystemStats() - - case 5: - // Try to purchase AA (might fail, that's ok) - manager.PurchaseAA(playerID, 200, 1) - } - - atomic.AddInt64(&operations, 1) - }(i) - } - - wg.Wait() - - if atomic.LoadInt64(&operations) != numGoroutines { - t.Errorf("Expected %d operations, got %d", numGoroutines, operations) - } - - t.Logf("Completed %d concurrent system operations", operations) -} \ No newline at end of file diff --git a/internal/alt_advancement/database.go b/internal/alt_advancement/database.go deleted file mode 100644 index d797783..0000000 --- a/internal/alt_advancement/database.go +++ /dev/null @@ -1,564 +0,0 @@ -package alt_advancement - -import ( - "fmt" - "time" -) - -// LoadAltAdvancements loads all AA definitions from the database -func (db *DatabaseImpl) LoadAltAdvancements() error { - query := ` - SELECT nodeid, minlevel, spellcrc, name, description, aa_list_fk, - icon_id, icon_backdrop, xcoord, ycoord, pointspertier, maxtier, - firstparentid, firstparentrequiredtier, displayedclassification, - requiredclassification, classificationpointsrequired, - pointsspentintreetounlock, title, titlelevel - FROM spell_aa_nodelist - ORDER BY aa_list_fk, ycoord, xcoord` - - rows, err := db.db.Query(query) - if err != nil { - return fmt.Errorf("failed to query AA data: %v", err) - } - defer rows.Close() - - loadedCount := 0 - for rows.Next() { - data := &AltAdvanceData{ - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - err := rows.Scan( - &data.NodeID, - &data.MinLevel, - &data.SpellCRC, - &data.Name, - &data.Description, - &data.Group, - &data.Icon, - &data.Icon2, - &data.Col, - &data.Row, - &data.RankCost, - &data.MaxRank, - &data.RankPrereqID, - &data.RankPrereq, - &data.ClassReq, - &data.Tier, - &data.ReqPoints, - &data.ReqTreePoints, - &data.LineTitle, - &data.TitleLevel, - ) - if err != nil { - return fmt.Errorf("failed to scan AA data: %v", err) - } - - // Set spell ID to node ID if not provided separately - data.SpellID = data.NodeID - - // Validate and add to master list - if err := db.masterAAList.AddAltAdvancement(data); err != nil { - // Log warning but continue loading - if db.logger != nil { - db.logger.Printf("Warning: failed to add AA node %d: %v", data.NodeID, err) - } - continue - } - - loadedCount++ - } - - if err = rows.Err(); err != nil { - return fmt.Errorf("error iterating AA rows: %v", err) - } - - // Sort AAs within each group for proper display order - db.masterAAList.SortAAsByGroup() - - if db.logger != nil { - db.logger.Printf("Loaded %d Alternate Advancement(s)", loadedCount) - } - - return nil -} - -// LoadTreeNodes loads tree node configurations from the database -func (db *DatabaseImpl) LoadTreeNodes() error { - query := ` - SELECT class_id, tree_node, aa_tree_id - FROM spell_aa_class_list - ORDER BY class_id, tree_node` - - rows, err := db.db.Query(query) - if err != nil { - return fmt.Errorf("failed to query tree node data: %v", err) - } - defer rows.Close() - - loadedCount := 0 - for rows.Next() { - data := &TreeNodeData{} - - err := rows.Scan( - &data.ClassID, - &data.TreeID, - &data.AATreeID, - ) - if err != nil { - return fmt.Errorf("failed to scan tree node data: %v", err) - } - - // Add to master node list - if err := db.masterNodeList.AddTreeNode(data); err != nil { - // Log warning but continue loading - if db.logger != nil { - db.logger.Printf("Warning: failed to add tree node %d: %v", data.TreeID, err) - } - continue - } - - loadedCount++ - } - - if err = rows.Err(); err != nil { - return fmt.Errorf("error iterating tree node rows: %v", err) - } - - if db.logger != nil { - db.logger.Printf("Loaded %d AA Tree Nodes", loadedCount) - } - - return nil -} - -// LoadPlayerAA loads AA data for a specific player -func (db *DatabaseImpl) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { - playerState := NewAAPlayerState(characterID) - - // Load player's AA entries - query := ` - SELECT template_id, tab_id, aa_id, order, treeid - FROM character_aa - WHERE char_id = ? - ORDER BY template_id, tab_id, order` - - rows, err := db.db.Query(query, characterID) - if err != nil { - return nil, fmt.Errorf("failed to query player AA data: %v", err) - } - defer rows.Close() - - // Group entries by template - templateEntries := make(map[int8][]*AAEntry) - - for rows.Next() { - entry := &AAEntry{} - - err := rows.Scan( - &entry.TemplateID, - &entry.TabID, - &entry.AAID, - &entry.Order, - &entry.TreeID, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan player AA entry: %v", err) - } - - if templateEntries[entry.TemplateID] == nil { - templateEntries[entry.TemplateID] = make([]*AAEntry, 0) - } - templateEntries[entry.TemplateID] = append(templateEntries[entry.TemplateID], entry) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating player AA rows: %v", err) - } - - // Create templates from loaded entries - for templateID, entries := range templateEntries { - template := NewAATemplate(templateID, GetTemplateName(templateID)) - template.Entries = entries - playerState.Templates[templateID] = template - } - - // Load player's AA progression data - err = db.loadPlayerAAProgress(characterID, playerState) - if err != nil { - return nil, fmt.Errorf("failed to load player AA progress: %v", err) - } - - // Load player's AA point totals - err = db.loadPlayerAAPoints(characterID, playerState) - if err != nil { - return nil, fmt.Errorf("failed to load player AA points: %v", err) - } - - // Initialize tabs based on loaded data - db.initializePlayerTabs(playerState) - - return playerState, nil -} - -// loadPlayerAAProgress loads detailed AA progression for a player -func (db *DatabaseImpl) loadPlayerAAProgress(characterID int32, playerState *AAPlayerState) error { - query := ` - SELECT node_id, current_rank, points_spent, template_id, tab_id, - purchased_at, updated_at - FROM character_aa_progress - WHERE character_id = ?` - - rows, err := db.db.Query(query, characterID) - if err != nil { - return fmt.Errorf("failed to query player AA progress: %v", err) - } - defer rows.Close() - - for rows.Next() { - progress := &PlayerAAData{ - CharacterID: characterID, - } - - var purchasedAt, updatedAt string - err := rows.Scan( - &progress.NodeID, - &progress.CurrentRank, - &progress.PointsSpent, - &progress.TemplateID, - &progress.TabID, - &purchasedAt, - &updatedAt, - ) - if err != nil { - return fmt.Errorf("failed to scan player AA progress: %v", err) - } - - // Parse timestamps - if progress.PurchasedAt, err = time.Parse("2006-01-02 15:04:05", purchasedAt); err != nil { - progress.PurchasedAt = time.Now() - } - if progress.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAt); err != nil { - progress.UpdatedAt = time.Now() - } - - playerState.AAProgress[progress.NodeID] = progress - } - - return rows.Err() -} - -// loadPlayerAAPoints loads AA point totals for a player -func (db *DatabaseImpl) loadPlayerAAPoints(characterID int32, playerState *AAPlayerState) error { - query := ` - SELECT total_points, spent_points, available_points, banked_points, - active_template - FROM character_aa_points - WHERE character_id = ?` - - row := db.db.QueryRow(query, characterID) - - err := row.Scan( - &playerState.TotalPoints, - &playerState.SpentPoints, - &playerState.AvailablePoints, - &playerState.BankedPoints, - &playerState.ActiveTemplate, - ) - - if err != nil { - // If no record exists, initialize with defaults - if err.Error() == "sql: no rows in result set" { - playerState.TotalPoints = 0 - playerState.SpentPoints = 0 - playerState.AvailablePoints = 0 - playerState.BankedPoints = 0 - playerState.ActiveTemplate = AA_TEMPLATE_CURRENT - return nil - } - return fmt.Errorf("failed to load player AA points: %v", err) - } - - return nil -} - -// initializePlayerTabs initializes tab states based on loaded data -func (db *DatabaseImpl) initializePlayerTabs(playerState *AAPlayerState) { - // Initialize all standard tabs - for i := int8(0); i < 10; i++ { - tab := NewAATab(i, i, GetTabName(i)) - tab.MaxAA = GetMaxAAForTab(i) - - // Calculate points spent in this tab - pointsSpent := int32(0) - for _, progress := range playerState.AAProgress { - if progress.TabID == i { - pointsSpent += progress.PointsSpent - } - } - tab.PointsSpent = pointsSpent - tab.PointsAvailable = playerState.AvailablePoints - - playerState.Tabs[i] = tab - } -} - -// SavePlayerAA saves a player's AA data to the database -func (db *DatabaseImpl) SavePlayerAA(playerState *AAPlayerState) error { - if playerState == nil { - return fmt.Errorf("player state cannot be nil") - } - - // Start transaction - tx, err := db.db.Begin() - if err != nil { - return fmt.Errorf("failed to begin transaction: %v", err) - } - defer tx.Rollback() - - // Save AA point totals - err = db.savePlayerAAPoints(tx, playerState) - if err != nil { - return fmt.Errorf("failed to save player AA points: %v", err) - } - - // Save AA progress - err = db.savePlayerAAProgress(tx, playerState) - if err != nil { - return fmt.Errorf("failed to save player AA progress: %v", err) - } - - // Save template entries - err = db.savePlayerAATemplates(tx, playerState) - if err != nil { - return fmt.Errorf("failed to save player AA templates: %v", err) - } - - // Commit transaction - if err = tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %v", err) - } - - // Update last save time - playerState.lastUpdate = time.Now() - playerState.needsSync = false - - return nil -} - -// savePlayerAAPoints saves AA point totals to the database -func (db *DatabaseImpl) savePlayerAAPoints(tx Transaction, playerState *AAPlayerState) error { - query := ` - INSERT OR REPLACE INTO character_aa_points - (character_id, total_points, spent_points, available_points, - banked_points, active_template, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?)` - - _, err := tx.Exec(query, - playerState.CharacterID, - playerState.TotalPoints, - playerState.SpentPoints, - playerState.AvailablePoints, - playerState.BankedPoints, - playerState.ActiveTemplate, - time.Now().Format("2006-01-02 15:04:05"), - ) - - return err -} - -// savePlayerAAProgress saves AA progression data to the database -func (db *DatabaseImpl) savePlayerAAProgress(tx Transaction, playerState *AAPlayerState) error { - // Delete existing progress - _, err := tx.Exec("DELETE FROM character_aa_progress WHERE character_id = ?", playerState.CharacterID) - if err != nil { - return err - } - - // Insert current progress - query := ` - INSERT INTO character_aa_progress - (character_id, node_id, current_rank, points_spent, template_id, - tab_id, purchased_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - - for _, progress := range playerState.AAProgress { - _, err = tx.Exec(query, - progress.CharacterID, - progress.NodeID, - progress.CurrentRank, - progress.PointsSpent, - progress.TemplateID, - progress.TabID, - progress.PurchasedAt.Format("2006-01-02 15:04:05"), - progress.UpdatedAt.Format("2006-01-02 15:04:05"), - ) - if err != nil { - return err - } - } - - return nil -} - -// savePlayerAATemplates saves AA template entries to the database -func (db *DatabaseImpl) savePlayerAATemplates(tx Transaction, playerState *AAPlayerState) error { - // Delete existing entries for server templates (4-6) - _, err := tx.Exec("DELETE FROM character_aa WHERE char_id = ? AND template_id BETWEEN 4 AND 6", playerState.CharacterID) - if err != nil { - return err - } - - // Insert current template entries for server templates only - query := ` - INSERT INTO character_aa - (char_id, template_id, tab_id, aa_id, order, treeid) - VALUES (?, ?, ?, ?, ?, ?)` - - for _, template := range playerState.Templates { - // Only save server templates (4-6) as personal templates (1-3) are class defaults - if template.TemplateID >= 4 && template.TemplateID <= 6 { - for _, entry := range template.Entries { - _, err = tx.Exec(query, - playerState.CharacterID, - entry.TemplateID, - entry.TabID, - entry.AAID, - entry.Order, - entry.TreeID, - ) - if err != nil { - return err - } - } - } - } - - return nil -} - -// LoadPlayerAADefaults loads default AA templates for a class -func (db *DatabaseImpl) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { - query := ` - SELECT template_id, tab_id, aa_id, order, treeid - FROM character_aa_defaults - WHERE class = ? - ORDER BY template_id, tab_id, order` - - rows, err := db.db.Query(query, classID) - if err != nil { - return nil, fmt.Errorf("failed to query AA defaults: %v", err) - } - defer rows.Close() - - templates := make(map[int8][]*AAEntry) - - for rows.Next() { - entry := &AAEntry{} - - err := rows.Scan( - &entry.TemplateID, - &entry.TabID, - &entry.AAID, - &entry.Order, - &entry.TreeID, - ) - if err != nil { - return nil, fmt.Errorf("failed to scan AA default entry: %v", err) - } - - if templates[entry.TemplateID] == nil { - templates[entry.TemplateID] = make([]*AAEntry, 0) - } - templates[entry.TemplateID] = append(templates[entry.TemplateID], entry) - } - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("error iterating AA default rows: %v", err) - } - - return templates, nil -} - -// DeletePlayerAA removes all AA data for a player -func (db *DatabaseImpl) DeletePlayerAA(characterID int32) error { - // Start transaction - tx, err := db.db.Begin() - if err != nil { - return fmt.Errorf("failed to begin transaction: %v", err) - } - defer tx.Rollback() - - // Delete from all related tables - tables := []string{ - "character_aa_points", - "character_aa_progress", - "character_aa", - } - - for _, table := range tables { - query := fmt.Sprintf("DELETE FROM %s WHERE char_id = ? OR character_id = ?", table) - _, err = tx.Exec(query, characterID, characterID) - if err != nil { - return fmt.Errorf("failed to delete from %s: %v", table, err) - } - } - - // Commit transaction - if err = tx.Commit(); err != nil { - return fmt.Errorf("failed to commit transaction: %v", err) - } - - return nil -} - -// GetAAStatistics returns statistics about AA usage -func (db *DatabaseImpl) GetAAStatistics() (map[string]any, error) { - stats := make(map[string]any) - - // Get total players with AA data - var totalPlayers int64 - err := db.db.QueryRow("SELECT COUNT(DISTINCT character_id) FROM character_aa_points").Scan(&totalPlayers) - if err != nil { - return nil, fmt.Errorf("failed to get total players: %v", err) - } - stats["total_players_with_aa"] = totalPlayers - - // Get average points spent - var avgPointsSpent float64 - err = db.db.QueryRow("SELECT AVG(spent_points) FROM character_aa_points").Scan(&avgPointsSpent) - if err != nil { - return nil, fmt.Errorf("failed to get average points spent: %v", err) - } - stats["average_points_spent"] = avgPointsSpent - - // Get most popular AAs - query := ` - SELECT node_id, COUNT(*) as usage_count - FROM character_aa_progress - WHERE current_rank > 0 - GROUP BY node_id - ORDER BY usage_count DESC - LIMIT 10` - - rows, err := db.db.Query(query) - if err != nil { - return nil, fmt.Errorf("failed to query popular AAs: %v", err) - } - defer rows.Close() - - popularAAs := make(map[int32]int64) - for rows.Next() { - var nodeID int32 - var count int64 - err := rows.Scan(&nodeID, &count) - if err != nil { - return nil, fmt.Errorf("failed to scan popular AA: %v", err) - } - popularAAs[nodeID] = count - } - stats["popular_aas"] = popularAAs - - return stats, nil -} diff --git a/internal/alt_advancement/doc.go b/internal/alt_advancement/doc.go new file mode 100644 index 0000000..b6abd35 --- /dev/null +++ b/internal/alt_advancement/doc.go @@ -0,0 +1,80 @@ +// Package alt_advancement provides a complete alternate advancement system for EQ2Go servers. +// +// Features: +// - Alternate advancement definitions with requirements and rewards +// - Thread-safe master AA list for server-wide management +// - Player-specific AA tracking and progression +// - Database operations with internal database wrapper +// +// Basic Usage: +// +// // Create database connection +// db, _ := database.NewSQLite("world.db") +// +// // Create new alternate advancement +// aa := alt_advancement.New(db) +// aa.NodeID = 1001 +// aa.Name = "Dragon's Strength" +// aa.Group = alt_advancement.AA_CLASS +// aa.RankCost = 1 +// aa.MaxRank = 5 +// +// // Save to database (insert or update automatically) +// aa.Save() +// +// // Load alternate advancement by node ID +// loaded, _ := alt_advancement.Load(db, 1001) +// +// // Update and save +// loaded.Name = "Improved Dragon's Strength" +// loaded.Save() +// +// // Delete alternate advancement +// loaded.Delete() +// +// Master List Management: +// +// // Create master list for server-wide AA management +// masterList := alt_advancement.NewMasterList() +// +// // Load all AAs from database +// allAAs, _ := alt_advancement.LoadAll(db) +// for _, aa := range allAAs { +// masterList.AddAltAdvancement(aa) +// } +// +// // Get AAs by group/tab +// classAAs := masterList.GetAltAdvancementsByGroup(alt_advancement.AA_CLASS) +// +// // Get AAs by class requirement +// fighterAAs := masterList.GetAltAdvancementsByClass(1) // Fighter class +// +// // Get AAs available at specific level +// level20AAs := masterList.GetAltAdvancementsByLevel(20) +// +// Player AA Management: +// +// // Create player AA state +// playerState := alt_advancement.NewAAPlayerState(characterID) +// +// // Award AA points +// playerState.TotalPoints = 50 +// playerState.AvailablePoints = 25 +// +// // Track AA progress +// progress := &alt_advancement.PlayerAAData{ +// CharacterID: characterID, +// NodeID: 1001, +// CurrentRank: 3, +// PointsSpent: 3, +// } +// playerState.AAProgress[1001] = progress +// +// Thread Safety: +// +// All operations are thread-safe using the generic MasterList base: +// - Read-heavy operations use RWMutex for optimal performance +// - Atomic operations for simple counters and flags +// - Proper lock ordering to prevent deadlocks +// - Background processing with goroutines and channels +package alt_advancement diff --git a/internal/alt_advancement/interfaces.go b/internal/alt_advancement/interfaces.go index 2924ac5..e05f90b 100644 --- a/internal/alt_advancement/interfaces.go +++ b/internal/alt_advancement/interfaces.go @@ -2,7 +2,6 @@ package alt_advancement import ( "database/sql" - "log" "sync" "time" ) @@ -67,20 +66,20 @@ type AAEventHandler interface { type AAValidator interface { // Purchase validation ValidateAAPurchase(playerState *AAPlayerState, nodeID int32, targetRank int8) error - ValidateAAPrerequisites(playerState *AAPlayerState, aaData *AltAdvanceData) error + ValidateAAPrerequisites(playerState *AAPlayerState, aaData *AltAdvancement) error ValidateAAPoints(playerState *AAPlayerState, pointsRequired int32) error // Player validation - ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvanceData) error - ValidatePlayerClass(playerState *AAPlayerState, aaData *AltAdvanceData) error - ValidateExpansionRequirements(playerState *AAPlayerState, aaData *AltAdvanceData) error + ValidatePlayerLevel(playerState *AAPlayerState, aaData *AltAdvancement) error + ValidatePlayerClass(playerState *AAPlayerState, aaData *AltAdvancement) error + ValidateExpansionRequirements(playerState *AAPlayerState, aaData *AltAdvancement) error // Template validation ValidateTemplateChange(playerState *AAPlayerState, templateID int8) error ValidateTemplateEntries(entries []*AAEntry) error // System validation - ValidateAAData(aaData *AltAdvanceData) error + ValidateAAData(aaData *AltAdvancement) error ValidateTreeNodeData(nodeData *TreeNodeData) error } @@ -131,8 +130,8 @@ type AAStatistics interface { // AACache interface for caching AA data type AACache interface { // AA data caching - GetAA(nodeID int32) (*AltAdvanceData, bool) - SetAA(nodeID int32, aaData *AltAdvanceData) + GetAA(nodeID int32) (*AltAdvancement, bool) + SetAA(nodeID int32, aaData *AltAdvancement) InvalidateAA(nodeID int32) // Player state caching @@ -179,23 +178,7 @@ type Transaction interface { Rollback() error } -// DatabaseImpl provides a concrete implementation of AADatabase -type DatabaseImpl struct { - db *sql.DB - masterAAList *MasterAAList - masterNodeList *MasterAANodeList - logger *log.Logger -} - -// NewDatabaseImpl creates a new database implementation -func NewDatabaseImpl(db *sql.DB, masterAAList *MasterAAList, masterNodeList *MasterAANodeList, logger *log.Logger) *DatabaseImpl { - return &DatabaseImpl{ - db: db, - masterAAList: masterAAList, - masterNodeList: masterNodeList, - logger: logger, - } -} +// Note: Database operations are now embedded in the main AltAdvancement type // AAManagerInterface defines the main interface for AA management type AAManagerInterface interface { @@ -216,7 +199,7 @@ type AAManagerInterface interface { // AA operations PurchaseAA(characterID int32, nodeID int32, targetRank int8) error RefundAA(characterID int32, nodeID int32) error - GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error) + GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvancement, error) // Template operations ChangeAATemplate(characterID int32, templateID int8) error @@ -228,10 +211,10 @@ type AAManagerInterface interface { GetAAPoints(characterID int32) (int32, int32, int32, error) // total, spent, available // Query operations - GetAA(nodeID int32) (*AltAdvanceData, error) - GetAABySpellID(spellID int32) (*AltAdvanceData, error) - GetAAsByGroup(group int8) ([]*AltAdvanceData, error) - GetAAsByClass(classID int8) ([]*AltAdvanceData, error) + GetAA(nodeID int32) (*AltAdvancement, error) + GetAABySpellID(spellID int32) (*AltAdvancement, error) + GetAAsByGroup(group int8) ([]*AltAdvancement, error) + GetAAsByClass(classID int8) ([]*AltAdvancement, error) // Statistics GetSystemStats() *AAManagerStats @@ -312,7 +295,7 @@ func (aa *AAAdapter) RefundAA(nodeID int32) error { } // GetAvailableAAs returns available AAs for a tab -func (aa *AAAdapter) GetAvailableAAs(tabID int8) ([]*AltAdvanceData, error) { +func (aa *AAAdapter) GetAvailableAAs(tabID int8) ([]*AltAdvancement, error) { return aa.manager.GetAvailableAAs(aa.characterID, tabID) } @@ -433,7 +416,7 @@ func (caa *ClientAAAdapter) GetClientVersion() int16 { // Simple cache implementation for testing type SimpleAACache struct { - aaData map[int32]*AltAdvanceData + aaData map[int32]*AltAdvancement playerStates map[int32]*AAPlayerState treeNodes map[int32]*TreeNodeData mutex sync.RWMutex @@ -445,7 +428,7 @@ type SimpleAACache struct { // NewSimpleAACache creates a new simple cache func NewSimpleAACache(maxSize int32) *SimpleAACache { return &SimpleAACache{ - aaData: make(map[int32]*AltAdvanceData), + aaData: make(map[int32]*AltAdvancement), playerStates: make(map[int32]*AAPlayerState), treeNodes: make(map[int32]*TreeNodeData), maxSize: maxSize, @@ -453,13 +436,13 @@ func NewSimpleAACache(maxSize int32) *SimpleAACache { } // GetAA retrieves AA data from cache -func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) { +func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvancement, bool) { c.mutex.RLock() defer c.mutex.RUnlock() if data, exists := c.aaData[nodeID]; exists { c.hits++ - return data.Copy(), true + return data.Clone(), true } c.misses++ @@ -467,7 +450,7 @@ func (c *SimpleAACache) GetAA(nodeID int32) (*AltAdvanceData, bool) { } // SetAA stores AA data in cache -func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) { +func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvancement) { c.mutex.Lock() defer c.mutex.Unlock() @@ -479,7 +462,7 @@ func (c *SimpleAACache) SetAA(nodeID int32, aaData *AltAdvanceData) { } } - c.aaData[nodeID] = aaData.Copy() + c.aaData[nodeID] = aaData.Clone() } // InvalidateAA removes AA data from cache @@ -573,7 +556,7 @@ func (c *SimpleAACache) Clear() { c.mutex.Lock() defer c.mutex.Unlock() - c.aaData = make(map[int32]*AltAdvanceData) + c.aaData = make(map[int32]*AltAdvancement) c.playerStates = make(map[int32]*AAPlayerState) c.treeNodes = make(map[int32]*TreeNodeData) } diff --git a/internal/alt_advancement/manager.go b/internal/alt_advancement/manager.go index c0eef27..656a3d0 100644 --- a/internal/alt_advancement/manager.go +++ b/internal/alt_advancement/manager.go @@ -8,7 +8,7 @@ import ( // NewAAManager creates a new AA manager func NewAAManager(config AAManagerConfig) *AAManager { return &AAManager{ - masterAAList: NewMasterAAList(), + masterAAList: NewMasterList(), masterNodeList: NewMasterAANodeList(), playerStates: make(map[int32]*AAPlayerState), config: config, @@ -97,7 +97,7 @@ func (am *AAManager) LoadAAData() error { // ReloadAAData reloads all AA data func (am *AAManager) ReloadAAData() error { // Clear existing data - am.masterAAList.DestroyAltAdvancements() + am.masterAAList.Clear() am.masterNodeList.DestroyTreeNodes() // Clear cached player states @@ -194,7 +194,7 @@ func (am *AAManager) PurchaseAA(characterID int32, nodeID int32, targetRank int8 } // Get AA data - aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID) + aaData := am.masterAAList.GetAltAdvancement(nodeID) if aaData == nil { return fmt.Errorf("AA node %d not found", nodeID) } @@ -239,7 +239,7 @@ func (am *AAManager) RefundAA(characterID int32, nodeID int32) error { } // Get AA data - aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID) + aaData := am.masterAAList.GetAltAdvancement(nodeID) if aaData == nil { return fmt.Errorf("AA node %d not found", nodeID) } @@ -278,7 +278,7 @@ func (am *AAManager) RefundAA(characterID int32, nodeID int32) error { } // GetAvailableAAs returns AAs available for a player in a specific tab -func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvanceData, error) { +func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvancement, error) { // Get player state playerState := am.getPlayerState(characterID) if playerState == nil { @@ -286,8 +286,8 @@ func (am *AAManager) GetAvailableAAs(characterID int32, tabID int8) ([]*AltAdvan } // Get all AAs for the tab - allAAs := am.masterAAList.GetAAsByGroup(tabID) - var availableAAs []*AltAdvanceData + allAAs := am.masterAAList.GetAltAdvancementsByGroup(tabID) + var availableAAs []*AltAdvancement for _, aa := range allAAs { // Check if AA is available for this player @@ -418,8 +418,8 @@ func (am *AAManager) GetAAPoints(characterID int32) (int32, int32, int32, error) } // GetAA returns an AA by node ID -func (am *AAManager) GetAA(nodeID int32) (*AltAdvanceData, error) { - aaData := am.masterAAList.GetAltAdvancementByNodeID(nodeID) +func (am *AAManager) GetAA(nodeID int32) (*AltAdvancement, error) { + aaData := am.masterAAList.GetAltAdvancement(nodeID) if aaData == nil { return nil, fmt.Errorf("AA node %d not found", nodeID) } @@ -427,22 +427,28 @@ func (am *AAManager) GetAA(nodeID int32) (*AltAdvanceData, error) { } // GetAABySpellID returns an AA by spell ID -func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvanceData, error) { - aaData := am.masterAAList.GetAltAdvancement(spellID) - if aaData == nil { +func (am *AAManager) GetAABySpellID(spellID int32) (*AltAdvancement, error) { + // Search for AA with matching SpellID + var found *AltAdvancement + am.masterAAList.ForEach(func(_ int32, aa *AltAdvancement) { + if aa.SpellID == spellID { + found = aa + } + }) + if found == nil { return nil, fmt.Errorf("AA with spell ID %d not found", spellID) } - return aaData, nil + return found, nil } // GetAAsByGroup returns AAs for a specific group/tab -func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvanceData, error) { - return am.masterAAList.GetAAsByGroup(group), nil +func (am *AAManager) GetAAsByGroup(group int8) ([]*AltAdvancement, error) { + return am.masterAAList.GetAltAdvancementsByGroup(group), nil } // GetAAsByClass returns AAs available for a specific class -func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvanceData, error) { - return am.masterAAList.GetAAsByClass(classID), nil +func (am *AAManager) GetAAsByClass(classID int8) ([]*AltAdvancement, error) { + return am.masterAAList.GetAltAdvancementsByClass(classID), nil } // GetSystemStats returns system statistics @@ -540,7 +546,7 @@ func (am *AAManager) getPlayerState(characterID int32) *AAPlayerState { } // performAAPurchase performs the actual AA purchase -func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAdvanceData, targetRank int8) error { +func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAdvancement, targetRank int8) error { // Calculate cost pointsCost := int32(aaData.RankCost) * int32(targetRank) @@ -583,7 +589,7 @@ func (am *AAManager) performAAPurchase(playerState *AAPlayerState, aaData *AltAd } // isAAAvailable checks if an AA is available for a player -func (am *AAManager) isAAAvailable(playerState *AAPlayerState, aaData *AltAdvanceData) bool { +func (am *AAManager) isAAAvailable(playerState *AAPlayerState, aaData *AltAdvancement) 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 diff --git a/internal/alt_advancement/master.go b/internal/alt_advancement/master.go new file mode 100644 index 0000000..a652e88 --- /dev/null +++ b/internal/alt_advancement/master.go @@ -0,0 +1,157 @@ +package alt_advancement + +import ( + "fmt" + + "eq2emu/internal/common" +) + +// MasterList manages the global list of all alternate advancements +type MasterList struct { + *common.MasterList[int32, *AltAdvancement] +} + +// NewMasterList creates a new master alternate advancement list +func NewMasterList() *MasterList { + return &MasterList{ + MasterList: common.NewMasterList[int32, *AltAdvancement](), + } +} + +// AddAltAdvancement adds an alternate advancement to the master list +// Returns false if AA with same ID already exists +func (m *MasterList) AddAltAdvancement(aa *AltAdvancement) bool { + if aa == nil { + return false + } + return m.MasterList.Add(aa) +} + +// GetAltAdvancement retrieves an alternate advancement by node ID +// Returns nil if not found +func (m *MasterList) GetAltAdvancement(nodeID int32) *AltAdvancement { + return m.MasterList.Get(nodeID) +} + +// GetAltAdvancementClone retrieves a cloned copy of an alternate advancement by node ID +// Returns nil if not found. Safe for modification without affecting master list +func (m *MasterList) GetAltAdvancementClone(nodeID int32) *AltAdvancement { + aa := m.MasterList.Get(nodeID) + if aa == nil { + return nil + } + return aa.Clone() +} + +// GetAllAltAdvancements returns a map of all alternate advancements (read-only access) +// The returned map should not be modified +func (m *MasterList) GetAllAltAdvancements() map[int32]*AltAdvancement { + return m.MasterList.GetAll() +} + +// GetAltAdvancementsByGroup returns alternate advancements filtered by group/tab +func (m *MasterList) GetAltAdvancementsByGroup(group int8) []*AltAdvancement { + return m.MasterList.Filter(func(aa *AltAdvancement) bool { + return aa.Group == group + }) +} + +// GetAltAdvancementsByClass returns alternate advancements filtered by class requirement +func (m *MasterList) GetAltAdvancementsByClass(classID int8) []*AltAdvancement { + return m.MasterList.Filter(func(aa *AltAdvancement) bool { + return aa.ClassReq == 0 || aa.ClassReq == classID + }) +} + +// GetAltAdvancementsByLevel returns alternate advancements available at a specific level +func (m *MasterList) GetAltAdvancementsByLevel(level int8) []*AltAdvancement { + return m.MasterList.Filter(func(aa *AltAdvancement) bool { + return aa.MinLevel <= level + }) +} + +// RemoveAltAdvancement removes an alternate advancement from the master list +// Returns true if AA was found and removed +func (m *MasterList) RemoveAltAdvancement(nodeID int32) bool { + return m.MasterList.Remove(nodeID) +} + +// UpdateAltAdvancement updates an existing alternate advancement +// Returns error if AA doesn't exist +func (m *MasterList) UpdateAltAdvancement(aa *AltAdvancement) error { + if aa == nil { + return fmt.Errorf("alternate advancement cannot be nil") + } + return m.MasterList.Update(aa) +} + +// GetGroups returns all unique groups/tabs that have alternate advancements +func (m *MasterList) GetGroups() []int8 { + groups := make(map[int8]bool) + + m.MasterList.ForEach(func(_ int32, aa *AltAdvancement) { + groups[aa.Group] = true + }) + + result := make([]int8, 0, len(groups)) + for group := range groups { + result = append(result, group) + } + return result +} + +// GetClasses returns all unique classes that have alternate advancements +func (m *MasterList) GetClasses() []int8 { + classes := make(map[int8]bool) + + m.MasterList.ForEach(func(_ int32, aa *AltAdvancement) { + if aa.ClassReq > 0 { + classes[aa.ClassReq] = true + } + }) + + result := make([]int8, 0, len(classes)) + for class := range classes { + result = append(result, class) + } + return result +} + +// ValidateAll validates all alternate advancements in the master list +func (m *MasterList) ValidateAll() []error { + var errors []error + + m.MasterList.ForEach(func(nodeID int32, aa *AltAdvancement) { + if !aa.IsValid() { + errors = append(errors, fmt.Errorf("invalid AA data: node_id=%d", nodeID)) + } + + // Validate prerequisites + if aa.RankPrereqID > 0 { + prereq := m.MasterList.Get(aa.RankPrereqID) + if prereq == nil { + errors = append(errors, fmt.Errorf("AA %d has invalid prerequisite node ID %d", nodeID, aa.RankPrereqID)) + } + } + + // Validate positioning + if aa.Col < MIN_AA_COL || aa.Col > MAX_AA_COL { + errors = append(errors, fmt.Errorf("AA %d has invalid column %d", nodeID, aa.Col)) + } + + if aa.Row < MIN_AA_ROW || aa.Row > MAX_AA_ROW { + errors = append(errors, fmt.Errorf("AA %d has invalid row %d", nodeID, aa.Row)) + } + + // Validate costs and ranks + if aa.RankCost < MIN_RANK_COST || aa.RankCost > MAX_RANK_COST { + errors = append(errors, fmt.Errorf("AA %d has invalid rank cost %d", nodeID, aa.RankCost)) + } + + if aa.MaxRank < MIN_MAX_RANK || aa.MaxRank > MAX_MAX_RANK { + errors = append(errors, fmt.Errorf("AA %d has invalid max rank %d", nodeID, aa.MaxRank)) + } + }) + + return errors +} diff --git a/internal/alt_advancement/master_list.go b/internal/alt_advancement/master_list.go deleted file mode 100644 index 7c138c3..0000000 --- a/internal/alt_advancement/master_list.go +++ /dev/null @@ -1,475 +0,0 @@ -package alt_advancement - -import ( - "fmt" - "sort" - "time" -) - -// NewMasterAAList creates a new master AA list -func NewMasterAAList() *MasterAAList { - return &MasterAAList{ - aaList: make([]*AltAdvanceData, 0), - aaBySpellID: make(map[int32]*AltAdvanceData), - aaByNodeID: make(map[int32]*AltAdvanceData), - aaByGroup: make(map[int8][]*AltAdvanceData), - totalLoaded: 0, - lastLoadTime: time.Now(), - } -} - -// AddAltAdvancement adds an AA to the master list -func (mal *MasterAAList) AddAltAdvancement(data *AltAdvanceData) error { - if data == nil { - return fmt.Errorf("data cannot be nil") - } - - if !data.IsValid() { - return fmt.Errorf("invalid AA data: spell_id=%d, node_id=%d", data.SpellID, data.NodeID) - } - - mal.mutex.Lock() - defer mal.mutex.Unlock() - - // Check for duplicates - if _, exists := mal.aaBySpellID[data.SpellID]; exists { - return fmt.Errorf("AA with spell ID %d already exists", data.SpellID) - } - - if _, exists := mal.aaByNodeID[data.NodeID]; exists { - return fmt.Errorf("AA with node ID %d already exists", data.NodeID) - } - - // Add to main list - mal.aaList = append(mal.aaList, data) - - // Add to lookup maps - mal.aaBySpellID[data.SpellID] = data - mal.aaByNodeID[data.NodeID] = data - - // Add to group map - if mal.aaByGroup[data.Group] == nil { - mal.aaByGroup[data.Group] = make([]*AltAdvanceData, 0) - } - mal.aaByGroup[data.Group] = append(mal.aaByGroup[data.Group], data) - - mal.totalLoaded++ - - return nil -} - -// GetAltAdvancement returns an AA by spell ID -func (mal *MasterAAList) GetAltAdvancement(spellID int32) *AltAdvanceData { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - if data, exists := mal.aaBySpellID[spellID]; exists { - return data.Copy() - } - - return nil -} - -// GetAltAdvancementByNodeID returns an AA by node ID -func (mal *MasterAAList) GetAltAdvancementByNodeID(nodeID int32) *AltAdvanceData { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - if data, exists := mal.aaByNodeID[nodeID]; exists { - return data.Copy() - } - - return nil -} - -// GetAAsByGroup returns all AAs for a specific group/tab -func (mal *MasterAAList) GetAAsByGroup(group int8) []*AltAdvanceData { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - if aaList, exists := mal.aaByGroup[group]; exists { - // Return copies to prevent external modification - result := make([]*AltAdvanceData, len(aaList)) - for i, aa := range aaList { - result[i] = aa.Copy() - } - return result - } - - return []*AltAdvanceData{} -} - -// GetAAsByClass returns AAs available for a specific class -func (mal *MasterAAList) GetAAsByClass(classID int8) []*AltAdvanceData { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - var result []*AltAdvanceData - - for _, aa := range mal.aaList { - // Check if AA is available for this class (0 means all classes) - if aa.ClassReq == 0 || aa.ClassReq == classID { - result = append(result, aa.Copy()) - } - } - - return result -} - -// GetAAsByLevel returns AAs available at a specific level -func (mal *MasterAAList) GetAAsByLevel(level int8) []*AltAdvanceData { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - var result []*AltAdvanceData - - for _, aa := range mal.aaList { - if aa.MinLevel <= level { - result = append(result, aa.Copy()) - } - } - - return result -} - -// Size returns the total number of AAs -func (mal *MasterAAList) Size() int { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - return len(mal.aaList) -} - -// GetAllAAs returns all AAs (copies) -func (mal *MasterAAList) GetAllAAs() []*AltAdvanceData { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - result := make([]*AltAdvanceData, len(mal.aaList)) - for i, aa := range mal.aaList { - result[i] = aa.Copy() - } - - return result -} - -// DestroyAltAdvancements clears all AA data -func (mal *MasterAAList) DestroyAltAdvancements() { - mal.mutex.Lock() - defer mal.mutex.Unlock() - - mal.aaList = make([]*AltAdvanceData, 0) - mal.aaBySpellID = make(map[int32]*AltAdvanceData) - mal.aaByNodeID = make(map[int32]*AltAdvanceData) - mal.aaByGroup = make(map[int8][]*AltAdvanceData) - mal.totalLoaded = 0 -} - -// SortAAsByGroup sorts AAs within each group by row and column -func (mal *MasterAAList) SortAAsByGroup() { - mal.mutex.Lock() - defer mal.mutex.Unlock() - - for group := range mal.aaByGroup { - sort.Slice(mal.aaByGroup[group], func(i, j int) bool { - aaI := mal.aaByGroup[group][i] - aaJ := mal.aaByGroup[group][j] - - // Sort by row first, then by column - if aaI.Row != aaJ.Row { - return aaI.Row < aaJ.Row - } - return aaI.Col < aaJ.Col - }) - } -} - -// GetGroupCount returns the number of groups with AAs -func (mal *MasterAAList) GetGroupCount() int { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - return len(mal.aaByGroup) -} - -// GetGroups returns all group IDs that have AAs -func (mal *MasterAAList) GetGroups() []int8 { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - groups := make([]int8, 0, len(mal.aaByGroup)) - for group := range mal.aaByGroup { - groups = append(groups, group) - } - - sort.Slice(groups, func(i, j int) bool { - return groups[i] < groups[j] - }) - - return groups -} - -// ValidateAAData validates all AA data for consistency -func (mal *MasterAAList) ValidateAAData() []error { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - var errors []error - - for _, aa := range mal.aaList { - if !aa.IsValid() { - errors = append(errors, fmt.Errorf("invalid AA data: spell_id=%d, node_id=%d", aa.SpellID, aa.NodeID)) - } - - // Validate prerequisites - if aa.RankPrereqID > 0 { - if _, exists := mal.aaByNodeID[aa.RankPrereqID]; !exists { - errors = append(errors, fmt.Errorf("AA %d has invalid prerequisite node ID %d", aa.NodeID, aa.RankPrereqID)) - } - } - - // Validate positioning - if aa.Col < MIN_AA_COL || aa.Col > MAX_AA_COL { - errors = append(errors, fmt.Errorf("AA %d has invalid column %d", aa.NodeID, aa.Col)) - } - - if aa.Row < MIN_AA_ROW || aa.Row > MAX_AA_ROW { - errors = append(errors, fmt.Errorf("AA %d has invalid row %d", aa.NodeID, aa.Row)) - } - - // Validate costs and ranks - if aa.RankCost < MIN_RANK_COST || aa.RankCost > MAX_RANK_COST { - errors = append(errors, fmt.Errorf("AA %d has invalid rank cost %d", aa.NodeID, aa.RankCost)) - } - - if aa.MaxRank < MIN_MAX_RANK || aa.MaxRank > MAX_MAX_RANK { - errors = append(errors, fmt.Errorf("AA %d has invalid max rank %d", aa.NodeID, aa.MaxRank)) - } - } - - return errors -} - -// GetStats returns statistics about the master AA list -func (mal *MasterAAList) GetStats() map[string]any { - mal.mutex.RLock() - defer mal.mutex.RUnlock() - - stats := make(map[string]any) - stats[STAT_TOTAL_AAS_LOADED] = mal.totalLoaded - stats["last_load_time"] = mal.lastLoadTime - stats["groups_count"] = len(mal.aaByGroup) - - // Count AAs per group - groupCounts := make(map[int8]int) - for group, aaList := range mal.aaByGroup { - groupCounts[group] = len(aaList) - } - stats[STAT_AAS_PER_TAB] = groupCounts - - return stats -} - -// NewMasterAANodeList creates a new master AA node list -func NewMasterAANodeList() *MasterAANodeList { - return &MasterAANodeList{ - nodeList: make([]*TreeNodeData, 0), - nodesByClass: make(map[int32][]*TreeNodeData), - nodesByTree: make(map[int32]*TreeNodeData), - totalLoaded: 0, - lastLoadTime: time.Now(), - } -} - -// AddTreeNode adds a tree node to the master list -func (manl *MasterAANodeList) AddTreeNode(data *TreeNodeData) error { - if data == nil { - return fmt.Errorf("data cannot be nil") - } - - manl.mutex.Lock() - defer manl.mutex.Unlock() - - // Check for duplicates - if _, exists := manl.nodesByTree[data.TreeID]; exists { - return fmt.Errorf("tree node with tree ID %d already exists", data.TreeID) - } - - // Add to main list - manl.nodeList = append(manl.nodeList, data) - - // Add to lookup maps - manl.nodesByTree[data.TreeID] = data - - // Add to class map - if manl.nodesByClass[data.ClassID] == nil { - manl.nodesByClass[data.ClassID] = make([]*TreeNodeData, 0) - } - manl.nodesByClass[data.ClassID] = append(manl.nodesByClass[data.ClassID], data) - - manl.totalLoaded++ - - return nil -} - -// GetTreeNodes returns all tree nodes -func (manl *MasterAANodeList) GetTreeNodes() []*TreeNodeData { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - // Return copies to prevent external modification - result := make([]*TreeNodeData, len(manl.nodeList)) - for i, node := range manl.nodeList { - nodeCopy := *node - result[i] = &nodeCopy - } - - return result -} - -// GetTreeNodesByClass returns tree nodes for a specific class -func (manl *MasterAANodeList) GetTreeNodesByClass(classID int32) []*TreeNodeData { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - if nodeList, exists := manl.nodesByClass[classID]; exists { - // Return copies to prevent external modification - result := make([]*TreeNodeData, len(nodeList)) - for i, node := range nodeList { - nodeCopy := *node - result[i] = &nodeCopy - } - return result - } - - return []*TreeNodeData{} -} - -// GetTreeNode returns a specific tree node by tree ID -func (manl *MasterAANodeList) GetTreeNode(treeID int32) *TreeNodeData { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - if node, exists := manl.nodesByTree[treeID]; exists { - nodeCopy := *node - return &nodeCopy - } - - return nil -} - -// Size returns the total number of tree nodes -func (manl *MasterAANodeList) Size() int { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - return len(manl.nodeList) -} - -// DestroyTreeNodes clears all tree node data -func (manl *MasterAANodeList) DestroyTreeNodes() { - manl.mutex.Lock() - defer manl.mutex.Unlock() - - manl.nodeList = make([]*TreeNodeData, 0) - manl.nodesByClass = make(map[int32][]*TreeNodeData) - manl.nodesByTree = make(map[int32]*TreeNodeData) - manl.totalLoaded = 0 -} - -// GetClassCount returns the number of classes with tree nodes -func (manl *MasterAANodeList) GetClassCount() int { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - return len(manl.nodesByClass) -} - -// GetClasses returns all class IDs that have tree nodes -func (manl *MasterAANodeList) GetClasses() []int32 { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - classes := make([]int32, 0, len(manl.nodesByClass)) - for classID := range manl.nodesByClass { - classes = append(classes, classID) - } - - sort.Slice(classes, func(i, j int) bool { - return classes[i] < classes[j] - }) - - return classes -} - -// ValidateTreeNodes validates all tree node data for consistency -func (manl *MasterAANodeList) ValidateTreeNodes() []error { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - var errors []error - - // Check for orphaned tree IDs - treeIDMap := make(map[int32]bool) - for _, node := range manl.nodeList { - treeIDMap[node.TreeID] = true - } - - // Check for duplicate class/tree combinations - classTreeMap := make(map[string]bool) - for _, node := range manl.nodeList { - key := fmt.Sprintf("%d_%d", node.ClassID, node.TreeID) - if classTreeMap[key] { - errors = append(errors, fmt.Errorf("duplicate class/tree combination: class=%d, tree=%d", node.ClassID, node.TreeID)) - } - classTreeMap[key] = true - } - - return errors -} - -// GetStats returns statistics about the master node list -func (manl *MasterAANodeList) GetStats() map[string]any { - manl.mutex.RLock() - defer manl.mutex.RUnlock() - - stats := make(map[string]any) - stats[STAT_TOTAL_NODES_LOADED] = manl.totalLoaded - stats["last_load_time"] = manl.lastLoadTime - stats["classes_count"] = len(manl.nodesByClass) - - // Count nodes per class - classCounts := make(map[int32]int) - for classID, nodeList := range manl.nodesByClass { - classCounts[classID] = len(nodeList) - } - stats["nodes_per_class"] = classCounts - - return stats -} - -// BuildAATreeMap builds a map of AA tree configurations for a specific class -func (manl *MasterAANodeList) BuildAATreeMap(classID int32) map[int8]int32 { - nodes := manl.GetTreeNodesByClass(classID) - treeMap := make(map[int8]int32) - - // Map each tab/group to its corresponding tree ID - for i, node := range nodes { - if i < 10 { // Limit to the number of defined AA tabs - treeMap[int8(i)] = node.TreeID - } - } - - return treeMap -} - -// GetTreeIDForTab returns the tree ID for a specific tab and class -func (manl *MasterAANodeList) GetTreeIDForTab(classID int32, tab int8) int32 { - nodes := manl.GetTreeNodesByClass(classID) - - if int(tab) < len(nodes) { - return nodes[tab].TreeID - } - - return 0 -} diff --git a/internal/alt_advancement/types.go b/internal/alt_advancement/types.go index 7624e3a..d1e4f0c 100644 --- a/internal/alt_advancement/types.go +++ b/internal/alt_advancement/types.go @@ -5,50 +5,6 @@ import ( "time" ) -// AltAdvanceData represents an Alternate Advancement node -type AltAdvanceData struct { - // Core identification - SpellID int32 `json:"spell_id" db:"spell_id"` - NodeID int32 `json:"node_id" db:"node_id"` - SpellCRC int32 `json:"spell_crc" db:"spell_crc"` - - // Display information - Name string `json:"name" db:"name"` - Description string `json:"description" db:"description"` - - // Tree organization - Group int8 `json:"group" db:"group"` // AA tab (AA_CLASS, AA_SUBCLASS, etc.) - Col int8 `json:"col" db:"col"` // Column position in tree - Row int8 `json:"row" db:"row"` // Row position in tree - - // Visual representation - Icon int16 `json:"icon" db:"icon"` // Primary icon ID - Icon2 int16 `json:"icon2" db:"icon2"` // Secondary icon ID - - // Ranking system - RankCost int8 `json:"rank_cost" db:"rank_cost"` // Cost per rank - MaxRank int8 `json:"max_rank" db:"max_rank"` // Maximum achievable rank - - // Prerequisites - MinLevel int8 `json:"min_level" db:"min_level"` // Minimum character level - RankPrereqID int32 `json:"rank_prereq_id" db:"rank_prereq_id"` // Prerequisite AA node ID - RankPrereq int8 `json:"rank_prereq" db:"rank_prereq"` // Required rank in prerequisite - ClassReq int8 `json:"class_req" db:"class_req"` // Required class - Tier int8 `json:"tier" db:"tier"` // AA tier - ReqPoints int8 `json:"req_points" db:"req_points"` // Required points in classification - ReqTreePoints int16 `json:"req_tree_points" db:"req_tree_points"` // Required points in entire tree - - // Display classification - ClassName string `json:"class_name" db:"class_name"` // Class name for display - SubclassName string `json:"subclass_name" db:"subclass_name"` // Subclass name for display - LineTitle string `json:"line_title" db:"line_title"` // AA line title - TitleLevel int8 `json:"title_level" db:"title_level"` // Title level requirement - - // Metadata - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` -} - // TreeNodeData represents class-specific AA tree node configuration type TreeNodeData struct { ClassID int32 `json:"class_id" db:"class_id"` // Character class ID @@ -120,7 +76,7 @@ type AATab struct { PointsAvailable int32 `json:"points_available"` // Available points for spending // AA nodes in this tab - Nodes []*AltAdvanceData `json:"nodes"` + Nodes []*AltAdvancement `json:"nodes"` // Metadata LastUpdate time.Time `json:"last_update"` @@ -153,41 +109,42 @@ type AAPlayerState struct { needsSync bool `json:"-"` } -// MasterAAList manages all AA definitions -type MasterAAList struct { - // AA storage - aaList []*AltAdvanceData `json:"aa_list"` - aaBySpellID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by spell ID - aaByNodeID map[int32]*AltAdvanceData `json:"-"` // Fast lookup by node ID - aaByGroup map[int8][]*AltAdvanceData `json:"-"` // Fast lookup by group/tab - - // Synchronization - mutex sync.RWMutex `json:"-"` - - // Statistics - totalLoaded int64 `json:"total_loaded"` - lastLoadTime time.Time `json:"last_load_time"` +// MasterAANodeList manages tree node configurations (kept for compatibility) +type MasterAANodeList struct { + nodesByClass map[int32][]*TreeNodeData + nodesByTree map[int32]*TreeNodeData + mutex sync.RWMutex } -// MasterAANodeList manages tree node configurations -type MasterAANodeList struct { - // Node storage - nodeList []*TreeNodeData `json:"node_list"` - nodesByClass map[int32][]*TreeNodeData `json:"-"` // Fast lookup by class ID - nodesByTree map[int32]*TreeNodeData `json:"-"` // Fast lookup by tree ID +// NewMasterAANodeList creates a new master AA node list +func NewMasterAANodeList() *MasterAANodeList { + return &MasterAANodeList{ + nodesByClass: make(map[int32][]*TreeNodeData), + nodesByTree: make(map[int32]*TreeNodeData), + } +} - // Synchronization - mutex sync.RWMutex `json:"-"` +// DestroyTreeNodes clears all tree node data +func (manl *MasterAANodeList) DestroyTreeNodes() { + manl.mutex.Lock() + defer manl.mutex.Unlock() - // Statistics - totalLoaded int64 `json:"total_loaded"` - lastLoadTime time.Time `json:"last_load_time"` + manl.nodesByClass = make(map[int32][]*TreeNodeData) + manl.nodesByTree = make(map[int32]*TreeNodeData) +} + +// Size returns the total number of tree nodes +func (manl *MasterAANodeList) Size() int { + manl.mutex.RLock() + defer manl.mutex.RUnlock() + + return len(manl.nodesByTree) } // AAManager manages the entire AA system type AAManager struct { // Core lists - masterAAList *MasterAAList `json:"master_aa_list"` + masterAAList *MasterList `json:"master_aa_list"` masterNodeList *MasterAANodeList `json:"master_node_list"` // Player states @@ -341,25 +298,12 @@ func NewAATab(tabID, group int8, name string) *AATab { ExpansionReq: EXPANSION_NONE, PointsSpent: 0, PointsAvailable: 0, - Nodes: make([]*AltAdvanceData, 0), + Nodes: make([]*AltAdvancement, 0), LastUpdate: time.Now(), } } -// Copy creates a deep copy of AltAdvanceData -func (aad *AltAdvanceData) Copy() *AltAdvanceData { - copy := *aad - return © -} - -// IsValid validates the AltAdvanceData -func (aad *AltAdvanceData) IsValid() bool { - return aad.SpellID > 0 && - aad.NodeID > 0 && - len(aad.Name) > 0 && - aad.MaxRank > 0 && - aad.RankCost > 0 -} +// Note: AltAdvancement methods are now in alt_advancement.go // GetMaxAAForTab returns the maximum AA points for a given tab func GetMaxAAForTab(group int8) int32 {