From 4a17075783752c58a4035d83e216f88f650b2ee2 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 8 Aug 2025 09:53:59 -0500 Subject: [PATCH] revert achievements to bespoke master list --- internal/achievements/achievements_test.go | 214 +++++++++++- internal/achievements/benchmark_test.go | 364 +++++++++++++++++++++ internal/achievements/master.go | 333 +++++++++++++++---- internal/common/README.md | 229 ------------- internal/common/benchmark_test.go | 210 ------------ internal/common/interfaces.go | 205 ------------ internal/common/master_list.go | 311 ------------------ internal/common/master_list_test.go | 305 ----------------- 8 files changed, 835 insertions(+), 1336 deletions(-) create mode 100644 internal/achievements/benchmark_test.go delete mode 100644 internal/common/README.md delete mode 100644 internal/common/benchmark_test.go delete mode 100644 internal/common/interfaces.go delete mode 100644 internal/common/master_list.go delete mode 100644 internal/common/master_list_test.go diff --git a/internal/achievements/achievements_test.go b/internal/achievements/achievements_test.go index 4be9da0..eef7131 100644 --- a/internal/achievements/achievements_test.go +++ b/internal/achievements/achievements_test.go @@ -1,6 +1,7 @@ package achievements import ( + "sync" "testing" "eq2emu/internal/database" @@ -60,8 +61,8 @@ func TestSimpleAchievement(t *testing.T) { } } -// TestMasterListWithGeneric tests the master list with generic base -func TestMasterListWithGeneric(t *testing.T) { +// TestMasterList tests the bespoke master list implementation +func TestMasterList(t *testing.T) { masterList := NewMasterList() if masterList == nil { @@ -72,22 +73,52 @@ func TestMasterListWithGeneric(t *testing.T) { t.Errorf("Expected size 0, got %d", masterList.Size()) } - // Create an achievement (need database for new pattern) - db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared") + // Create test database + 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() - achievement := New(db) - achievement.AchievementID = 1001 - achievement.Title = "Test Achievement" - achievement.Category = "Testing" + // Create achievements for testing + achievement1 := New(db) + achievement1.AchievementID = 1001 + achievement1.Title = "Test Achievement 1" + achievement1.Category = "Testing" + achievement1.Expansion = "Classic" + + achievement2 := New(db) + achievement2.AchievementID = 1002 + achievement2.Title = "Test Achievement 2" + achievement2.Category = "Combat" + achievement2.Expansion = "Classic" + + achievement3 := New(db) + achievement3.AchievementID = 1003 + achievement3.Title = "Test Achievement 3" + achievement3.Category = "Testing" + achievement3.Expansion = "Expansion1" // Test adding - if !masterList.AddAchievement(achievement) { - t.Error("Should successfully add achievement") + if !masterList.AddAchievement(achievement1) { + t.Error("Should successfully add achievement1") } - if masterList.Size() != 1 { - t.Errorf("Expected size 1, got %d", masterList.Size()) + if !masterList.AddAchievement(achievement2) { + t.Error("Should successfully add achievement2") + } + + if !masterList.AddAchievement(achievement3) { + t.Error("Should successfully add achievement3") + } + + if masterList.Size() != 3 { + t.Errorf("Expected size 3, got %d", masterList.Size()) + } + + // Test duplicate add (should fail) + if masterList.AddAchievement(achievement1) { + t.Error("Should not add duplicate achievement") } // Test retrieving @@ -96,13 +127,160 @@ func TestMasterListWithGeneric(t *testing.T) { t.Error("Should retrieve added achievement") } - if retrieved.Title != "Test Achievement" { - t.Errorf("Expected title 'Test Achievement', got '%s'", retrieved.Title) + if retrieved.Title != "Test Achievement 1" { + t.Errorf("Expected title 'Test Achievement 1', got '%s'", retrieved.Title) } - // Test filtering - achievements := masterList.GetAchievementsByCategory("Testing") - if len(achievements) != 1 { - t.Errorf("Expected 1 achievement in Testing category, got %d", len(achievements)) + // Test category filtering + testingAchievements := masterList.GetAchievementsByCategory("Testing") + if len(testingAchievements) != 2 { + t.Errorf("Expected 2 achievements in Testing category, got %d", len(testingAchievements)) + } + + combatAchievements := masterList.GetAchievementsByCategory("Combat") + if len(combatAchievements) != 1 { + t.Errorf("Expected 1 achievement in Combat category, got %d", len(combatAchievements)) + } + + // Test expansion filtering + classicAchievements := masterList.GetAchievementsByExpansion("Classic") + if len(classicAchievements) != 2 { + t.Errorf("Expected 2 achievements in Classic expansion, got %d", len(classicAchievements)) + } + + expansion1Achievements := masterList.GetAchievementsByExpansion("Expansion1") + if len(expansion1Achievements) != 1 { + t.Errorf("Expected 1 achievement in Expansion1, got %d", len(expansion1Achievements)) + } + + // Test combined filtering + combined := masterList.GetAchievementsByCategoryAndExpansion("Testing", "Classic") + if len(combined) != 1 { + t.Errorf("Expected 1 achievement matching Testing+Classic, got %d", len(combined)) + } + + // Test metadata caching + categories := masterList.GetCategories() + if len(categories) != 2 { + t.Errorf("Expected 2 unique categories, got %d", len(categories)) + } + + expansions := masterList.GetExpansions() + if len(expansions) != 2 { + t.Errorf("Expected 2 unique expansions, got %d", len(expansions)) + } + + // Test clone + clone := masterList.GetAchievementClone(1001) + if clone == nil { + t.Error("Should return cloned achievement") + } + + if clone.Title != "Test Achievement 1" { + t.Errorf("Expected cloned title 'Test Achievement 1', got '%s'", clone.Title) + } + + // Test GetAllAchievements + allAchievements := masterList.GetAllAchievements() + if len(allAchievements) != 3 { + t.Errorf("Expected 3 achievements in GetAll, got %d", len(allAchievements)) + } + + // Test update + updatedAchievement := New(db) + updatedAchievement.AchievementID = 1001 + updatedAchievement.Title = "Updated Achievement" + updatedAchievement.Category = "Updated" + updatedAchievement.Expansion = "Updated" + + if err := masterList.UpdateAchievement(updatedAchievement); err != nil { + t.Errorf("Update should succeed: %v", err) + } + + // Verify update worked + retrievedUpdated := masterList.GetAchievement(1001) + if retrievedUpdated.Title != "Updated Achievement" { + t.Errorf("Expected updated title 'Updated Achievement', got '%s'", retrievedUpdated.Title) + } + + // Verify category index updated + updatedCategoryAchievements := masterList.GetAchievementsByCategory("Updated") + if len(updatedCategoryAchievements) != 1 { + t.Errorf("Expected 1 achievement in Updated category, got %d", len(updatedCategoryAchievements)) + } + + // Test removal + if !masterList.RemoveAchievement(1001) { + t.Error("Should successfully remove achievement") + } + + if masterList.Size() != 2 { + t.Errorf("Expected size 2 after removal, got %d", masterList.Size()) + } + + // Test clear + masterList.Clear() + if masterList.Size() != 0 { + t.Errorf("Expected size 0 after clear, got %d", masterList.Size()) + } +} + +// TestMasterListConcurrency tests thread safety of the master list +func TestMasterListConcurrency(t *testing.T) { + masterList := NewMasterList() + + // Create test database + 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() + + const numWorkers = 10 + const achievementsPerWorker = 100 + var wg sync.WaitGroup + + // Concurrently add achievements + wg.Add(numWorkers) + for i := 0; i < numWorkers; i++ { + go func(workerID int) { + defer wg.Done() + for j := 0; j < achievementsPerWorker; j++ { + achievement := New(db) + achievement.AchievementID = uint32(workerID*achievementsPerWorker + j + 1) + achievement.Title = "Concurrent Test" + achievement.Category = "Concurrency" + achievement.Expansion = "Test" + masterList.AddAchievement(achievement) + } + }(i) + } + + // Concurrently read achievements + wg.Add(numWorkers) + for i := 0; i < numWorkers; i++ { + go func() { + defer wg.Done() + for j := 0; j < achievementsPerWorker; j++ { + // Random reads + _ = masterList.GetAchievement(uint32(j + 1)) + _ = masterList.GetAchievementsByCategory("Concurrency") + _ = masterList.GetAchievementsByExpansion("Test") + _ = masterList.Size() + } + }() + } + + wg.Wait() + + // Verify final state + expectedSize := numWorkers * achievementsPerWorker + if masterList.Size() != expectedSize { + t.Errorf("Expected size %d, got %d", expectedSize, masterList.Size()) + } + + categories := masterList.GetCategories() + if len(categories) != 1 || categories[0] != "Concurrency" { + t.Errorf("Expected 1 category 'Concurrency', got %v", categories) } } diff --git a/internal/achievements/benchmark_test.go b/internal/achievements/benchmark_test.go new file mode 100644 index 0000000..8112742 --- /dev/null +++ b/internal/achievements/benchmark_test.go @@ -0,0 +1,364 @@ +package achievements + +import ( + "fmt" + "math/rand" + "sync" + "testing" + + "eq2emu/internal/database" +) + +// Global shared master list for benchmarks to avoid repeated setup +var ( + sharedAchievementMasterList *MasterList + sharedAchievements []*Achievement + achievementSetupOnce sync.Once +) + +// setupSharedAchievementMasterList creates the shared master list once +func setupSharedAchievementMasterList(b *testing.B) { + achievementSetupOnce.Do(func() { + // Create test database + db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") + if err != nil { + b.Fatalf("Failed to create test database: %v", err) + } + + sharedAchievementMasterList = NewMasterList() + + // Pre-populate with achievements for realistic testing + const numAchievements = 1000 + sharedAchievements = make([]*Achievement, numAchievements) + + categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"} + expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"} + + for i := range numAchievements { + sharedAchievements[i] = New(db) + sharedAchievements[i].AchievementID = uint32(i + 1) + sharedAchievements[i].Title = fmt.Sprintf("Achievement %d", i+1) + sharedAchievements[i].Category = categories[i%len(categories)] + sharedAchievements[i].Expansion = expansions[i%len(expansions)] + sharedAchievements[i].PointValue = uint32(rand.Intn(50) + 10) + sharedAchievements[i].QtyRequired = uint32(rand.Intn(100) + 1) + + // Add some requirements and rewards + sharedAchievements[i].AddRequirement(fmt.Sprintf("task_%d", i%10), uint32(rand.Intn(10)+1)) + sharedAchievements[i].AddReward(fmt.Sprintf("reward_%d", i%5)) + + sharedAchievementMasterList.AddAchievement(sharedAchievements[i]) + } + }) +} + +// createTestAchievement creates an achievement for benchmarking +func createTestAchievement(b *testing.B, id uint32) *Achievement { + b.Helper() + + // Use nil database for benchmarking in-memory operations + achievement := New(nil) + achievement.AchievementID = id + achievement.Title = fmt.Sprintf("Benchmark Achievement %d", id) + achievement.Category = []string{"Combat", "Crafting", "Exploration", "Social"}[id%4] + achievement.Expansion = []string{"Classic", "Expansion1", "Expansion2"}[id%3] + achievement.PointValue = uint32(rand.Intn(50) + 10) + achievement.QtyRequired = uint32(rand.Intn(100) + 1) + + // Add mock requirements and rewards + achievement.AddRequirement(fmt.Sprintf("task_%d", id%10), uint32(rand.Intn(10)+1)) + achievement.AddReward(fmt.Sprintf("reward_%d", id%5)) + + return achievement +} + +// BenchmarkAchievementCreation measures achievement creation performance +func BenchmarkAchievementCreation(b *testing.B) { + db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") + if err != nil { + b.Fatalf("Failed to create test database: %v", err) + } + defer db.Close() + + b.ResetTimer() + + b.Run("Sequential", func(b *testing.B) { + for i := 0; i < b.N; i++ { + achievement := New(db) + achievement.AchievementID = uint32(i) + achievement.Title = fmt.Sprintf("Achievement %d", i) + _ = achievement + } + }) + + b.Run("Parallel", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + id := uint32(0) + for pb.Next() { + achievement := New(db) + achievement.AchievementID = id + achievement.Title = fmt.Sprintf("Achievement %d", id) + id++ + _ = achievement + } + }) + }) +} + +// BenchmarkAchievementOperations measures individual achievement operations +func BenchmarkAchievementOperations(b *testing.B) { + achievement := createTestAchievement(b, 1001) + + b.Run("GetID", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = achievement.GetID() + } + }) + }) + + b.Run("IsNew", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = achievement.IsNew() + } + }) + }) + + b.Run("Clone", func(b *testing.B) { + for b.Loop() { + _ = achievement.Clone() + } + }) +} + +// BenchmarkMasterListOperations measures master list performance +func BenchmarkMasterListOperations(b *testing.B) { + setupSharedAchievementMasterList(b) + ml := sharedAchievementMasterList + + b.Run("GetAchievement", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + id := uint32(rand.Intn(1000) + 1) + _ = ml.GetAchievement(id) + } + }) + }) + + b.Run("AddAchievement", func(b *testing.B) { + // Create a separate master list for add operations + addML := NewMasterList() + startID := uint32(10000) + // Pre-create achievements to measure just the Add operation + achievementsToAdd := make([]*Achievement, b.N) + for i := 0; i < b.N; i++ { + achievementsToAdd[i] = createTestAchievement(b, startID+uint32(i)) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + addML.AddAchievement(achievementsToAdd[i]) + } + }) + + b.Run("GetAchievementsByCategory", func(b *testing.B) { + categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"} + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + category := categories[rand.Intn(len(categories))] + _ = ml.GetAchievementsByCategory(category) + } + }) + }) + + b.Run("GetAchievementsByExpansion", func(b *testing.B) { + expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"} + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + expansion := expansions[rand.Intn(len(expansions))] + _ = ml.GetAchievementsByExpansion(expansion) + } + }) + }) + + b.Run("GetAchievementsByCategoryAndExpansion", func(b *testing.B) { + categories := []string{"Combat", "Crafting", "Exploration", "Social"} + expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer"} + for b.Loop() { + category := categories[rand.Intn(len(categories))] + expansion := expansions[rand.Intn(len(expansions))] + _ = ml.GetAchievementsByCategoryAndExpansion(category, expansion) + } + }) + + b.Run("GetCategories", func(b *testing.B) { + for b.Loop() { + _ = ml.GetCategories() + } + }) + + b.Run("GetExpansions", func(b *testing.B) { + for b.Loop() { + _ = ml.GetExpansions() + } + }) + + b.Run("Size", func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = ml.Size() + } + }) + }) +} + +// BenchmarkConcurrentOperations tests mixed workload performance +func BenchmarkConcurrentOperations(b *testing.B) { + setupSharedAchievementMasterList(b) + ml := sharedAchievementMasterList + + b.Run("MixedOperations", func(b *testing.B) { + categories := []string{"Combat", "Crafting", "Exploration", "Social", "PvP", "Quests", "Collections", "Dungeons"} + expansions := []string{"Classic", "Kingdom of Sky", "Echoes of Faydwer", "Rise of Kunark", "The Shadow Odyssey", "Sentinel's Fate"} + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + switch rand.Intn(7) { + case 0: + id := uint32(rand.Intn(1000) + 1) + _ = ml.GetAchievement(id) + case 1: + category := categories[rand.Intn(len(categories))] + _ = ml.GetAchievementsByCategory(category) + case 2: + expansion := expansions[rand.Intn(len(expansions))] + _ = ml.GetAchievementsByExpansion(expansion) + case 3: + category := categories[rand.Intn(len(categories))] + expansion := expansions[rand.Intn(len(expansions))] + _ = ml.GetAchievementsByCategoryAndExpansion(category, expansion) + case 4: + _ = ml.GetCategories() + case 5: + _ = ml.GetExpansions() + case 6: + _ = ml.Size() + } + } + }) + }) +} + +// BenchmarkMemoryAllocation measures memory allocation patterns +func BenchmarkMemoryAllocation(b *testing.B) { + db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared") + if err != nil { + b.Fatalf("Failed to create test database: %v", err) + } + defer db.Close() + + b.Run("AchievementAllocation", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + achievement := New(db) + achievement.AchievementID = uint32(i) + achievement.Requirements = make([]Requirement, 2) + achievement.Rewards = make([]Reward, 3) + _ = achievement + } + }) + + b.Run("MasterListAllocation", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + ml := NewMasterList() + _ = ml + } + }) + + b.Run("AddAchievement_Allocations", func(b *testing.B) { + b.ReportAllocs() + ml := NewMasterList() + for i := 0; i < b.N; i++ { + achievement := createTestAchievement(b, uint32(i+1)) + ml.AddAchievement(achievement) + } + }) + + b.Run("GetAchievementsByCategory_Allocations", func(b *testing.B) { + setupSharedAchievementMasterList(b) + ml := sharedAchievementMasterList + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = ml.GetAchievementsByCategory("Combat") + } + }) + + b.Run("GetCategories_Allocations", func(b *testing.B) { + setupSharedAchievementMasterList(b) + ml := sharedAchievementMasterList + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = ml.GetCategories() + } + }) +} + +// BenchmarkUpdateOperations measures update performance +func BenchmarkUpdateOperations(b *testing.B) { + setupSharedAchievementMasterList(b) + ml := sharedAchievementMasterList + + b.Run("UpdateAchievement", func(b *testing.B) { + // Create achievements to update + updateAchievements := make([]*Achievement, b.N) + for i := 0; i < b.N; i++ { + updateAchievements[i] = createTestAchievement(b, uint32((i%1000)+1)) + updateAchievements[i].Title = "Updated Title" + updateAchievements[i].Category = "Updated" + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ml.UpdateAchievement(updateAchievements[i]) + } + }) + + b.Run("RemoveAchievement", func(b *testing.B) { + // Create a separate master list for removal testing + removeML := NewMasterList() + + // Add achievements to remove + for i := 0; i < b.N; i++ { + achievement := createTestAchievement(b, uint32(i+1)) + removeML.AddAchievement(achievement) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + removeML.RemoveAchievement(uint32(i + 1)) + } + }) +} + +// BenchmarkCloneOperations measures cloning performance +func BenchmarkCloneOperations(b *testing.B) { + setupSharedAchievementMasterList(b) + ml := sharedAchievementMasterList + + b.Run("GetAchievementClone", func(b *testing.B) { + for b.Loop() { + id := uint32(rand.Intn(1000) + 1) + _ = ml.GetAchievementClone(id) + } + }) + + b.Run("DirectClone", func(b *testing.B) { + achievement := createTestAchievement(b, 1001) + for b.Loop() { + _ = achievement.Clone() + } + }) +} \ No newline at end of file diff --git a/internal/achievements/master.go b/internal/achievements/master.go index b149f8d..5dc5cb4 100644 --- a/internal/achievements/master.go +++ b/internal/achievements/master.go @@ -2,112 +2,329 @@ package achievements import ( "fmt" - - "eq2emu/internal/common" + "sync" ) -// MasterList manages the global list of all achievements +// MasterList is a specialized achievement master list optimized for: +// - Fast ID-based lookups (O(1)) +// - Fast category-based lookups (O(1)) +// - Fast expansion-based lookups (O(1)) +// - Efficient filtering and iteration type MasterList struct { - *common.MasterList[uint32, *Achievement] + // Core storage + achievements map[uint32]*Achievement // ID -> Achievement + mutex sync.RWMutex + + // Category indices for O(1) lookups + byCategory map[string][]*Achievement // Category -> achievements + byExpansion map[string][]*Achievement // Expansion -> achievements + + // Cached metadata + categories []string // Unique categories (cached) + expansions []string // Unique expansions (cached) + metaStale bool // Whether metadata cache needs refresh } -// NewMasterList creates a new master achievement list +// NewMasterList creates a new specialized achievement master list func NewMasterList() *MasterList { return &MasterList{ - MasterList: common.NewMasterList[uint32, *Achievement](), + achievements: make(map[uint32]*Achievement), + byCategory: make(map[string][]*Achievement), + byExpansion: make(map[string][]*Achievement), + metaStale: true, } } -// AddAchievement adds an achievement to the master list -// Returns false if achievement with same ID already exists +// refreshMetaCache updates the categories and expansions cache +func (m *MasterList) refreshMetaCache() { + if !m.metaStale { + return + } + + categorySet := make(map[string]struct{}) + expansionSet := make(map[string]struct{}) + + // Collect unique categories and expansions + for _, achievement := range m.achievements { + if achievement.Category != "" { + categorySet[achievement.Category] = struct{}{} + } + if achievement.Expansion != "" { + expansionSet[achievement.Expansion] = struct{}{} + } + } + + // Clear existing caches and rebuild + m.categories = m.categories[:0] + for category := range categorySet { + m.categories = append(m.categories, category) + } + + m.expansions = m.expansions[:0] + for expansion := range expansionSet { + m.expansions = append(m.expansions, expansion) + } + + m.metaStale = false +} + +// AddAchievement adds an achievement with full indexing func (m *MasterList) AddAchievement(achievement *Achievement) bool { if achievement == nil { return false } - return m.MasterList.Add(achievement) + + m.mutex.Lock() + defer m.mutex.Unlock() + + // Check if exists + if _, exists := m.achievements[achievement.AchievementID]; exists { + return false + } + + // Add to core storage + m.achievements[achievement.AchievementID] = achievement + + // Update category index + if achievement.Category != "" { + m.byCategory[achievement.Category] = append(m.byCategory[achievement.Category], achievement) + } + + // Update expansion index + if achievement.Expansion != "" { + m.byExpansion[achievement.Expansion] = append(m.byExpansion[achievement.Expansion], achievement) + } + + // Invalidate metadata cache + m.metaStale = true + + return true } -// GetAchievement retrieves an achievement by ID -// Returns nil if not found +// GetAchievement retrieves by ID (O(1)) func (m *MasterList) GetAchievement(id uint32) *Achievement { - return m.MasterList.Get(id) + m.mutex.RLock() + defer m.mutex.RUnlock() + return m.achievements[id] } // GetAchievementClone retrieves a cloned copy of an achievement by ID -// Returns nil if not found. Safe for modification without affecting master list func (m *MasterList) GetAchievementClone(id uint32) *Achievement { - achievement := m.MasterList.Get(id) + m.mutex.RLock() + defer m.mutex.RUnlock() + achievement := m.achievements[id] if achievement == nil { return nil } return achievement.Clone() } -// GetAllAchievements returns a map of all achievements (read-only access) -// The returned map should not be modified +// GetAllAchievements returns a copy of all achievements map func (m *MasterList) GetAllAchievements() map[uint32]*Achievement { - return m.MasterList.GetAll() + m.mutex.RLock() + defer m.mutex.RUnlock() + + // Return a copy to prevent external modification + result := make(map[uint32]*Achievement, len(m.achievements)) + for id, achievement := range m.achievements { + result[id] = achievement + } + return result } -// GetAchievementsByCategory returns achievements filtered by category +// GetAchievementsByCategory returns all achievements in a category (O(1)) func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement { - return m.MasterList.Filter(func(achievement *Achievement) bool { - return achievement.Category == category - }) + m.mutex.RLock() + defer m.mutex.RUnlock() + return m.byCategory[category] } -// GetAchievementsByExpansion returns achievements filtered by expansion +// GetAchievementsByExpansion returns all achievements in an expansion (O(1)) func (m *MasterList) GetAchievementsByExpansion(expansion string) []*Achievement { - return m.MasterList.Filter(func(achievement *Achievement) bool { - return achievement.Expansion == expansion - }) + m.mutex.RLock() + defer m.mutex.RUnlock() + return m.byExpansion[expansion] } -// Removes an achievement from the master list -// Returns true if achievement was found and removed +// GetAchievementsByCategoryAndExpansion returns achievements matching both category and expansion +func (m *MasterList) GetAchievementsByCategoryAndExpansion(category, expansion string) []*Achievement { + m.mutex.RLock() + defer m.mutex.RUnlock() + + categoryAchievements := m.byCategory[category] + expansionAchievements := m.byExpansion[expansion] + + // Use smaller set for iteration efficiency + if len(categoryAchievements) > len(expansionAchievements) { + categoryAchievements, expansionAchievements = expansionAchievements, categoryAchievements + } + + // Set intersection using map lookup + expansionSet := make(map[*Achievement]struct{}, len(expansionAchievements)) + for _, achievement := range expansionAchievements { + expansionSet[achievement] = struct{}{} + } + + var result []*Achievement + for _, achievement := range categoryAchievements { + if _, exists := expansionSet[achievement]; exists { + result = append(result, achievement) + } + } + + return result +} + +// GetCategories returns all unique categories using cached results +func (m *MasterList) GetCategories() []string { + m.mutex.Lock() // Need write lock to potentially update cache + defer m.mutex.Unlock() + + m.refreshMetaCache() + + // Return a copy to prevent external modification + result := make([]string, len(m.categories)) + copy(result, m.categories) + return result +} + +// GetExpansions returns all unique expansions using cached results +func (m *MasterList) GetExpansions() []string { + m.mutex.Lock() // Need write lock to potentially update cache + defer m.mutex.Unlock() + + m.refreshMetaCache() + + // Return a copy to prevent external modification + result := make([]string, len(m.expansions)) + copy(result, m.expansions) + return result +} + +// RemoveAchievement removes an achievement and updates all indices func (m *MasterList) RemoveAchievement(id uint32) bool { - return m.MasterList.Remove(id) + m.mutex.Lock() + defer m.mutex.Unlock() + + achievement, exists := m.achievements[id] + if !exists { + return false + } + + // Remove from core storage + delete(m.achievements, id) + + // Remove from category index + if achievement.Category != "" { + categoryAchievements := m.byCategory[achievement.Category] + for i, a := range categoryAchievements { + if a.AchievementID == id { + m.byCategory[achievement.Category] = append(categoryAchievements[:i], categoryAchievements[i+1:]...) + break + } + } + } + + // Remove from expansion index + if achievement.Expansion != "" { + expansionAchievements := m.byExpansion[achievement.Expansion] + for i, a := range expansionAchievements { + if a.AchievementID == id { + m.byExpansion[achievement.Expansion] = append(expansionAchievements[:i], expansionAchievements[i+1:]...) + break + } + } + } + + // Invalidate metadata cache + m.metaStale = true + + return true } // UpdateAchievement updates an existing achievement -// Returns error if achievement doesn't exist func (m *MasterList) UpdateAchievement(achievement *Achievement) error { if achievement == nil { return fmt.Errorf("achievement cannot be nil") } - return m.MasterList.Update(achievement) -} -// Returns all unique categories -func (m *MasterList) GetCategories() []string { - categories := make(map[string]bool) + m.mutex.Lock() + defer m.mutex.Unlock() - m.MasterList.ForEach(func(_ uint32, achievement *Achievement) { - if achievement.Category != "" { - categories[achievement.Category] = true - } - }) - - result := make([]string, 0, len(categories)) - for category := range categories { - result = append(result, category) + // Check if exists + old, exists := m.achievements[achievement.AchievementID] + if !exists { + return fmt.Errorf("achievement %d not found", achievement.AchievementID) } - return result -} -// Returns all unique expansions -func (m *MasterList) GetExpansions() []string { - expansions := make(map[string]bool) - - m.MasterList.ForEach(func(_ uint32, achievement *Achievement) { - if achievement.Expansion != "" { - expansions[achievement.Expansion] = true + // Remove old achievement from indices (but not core storage yet) + if old.Category != "" { + categoryAchievements := m.byCategory[old.Category] + for i, a := range categoryAchievements { + if a.AchievementID == achievement.AchievementID { + m.byCategory[old.Category] = append(categoryAchievements[:i], categoryAchievements[i+1:]...) + break + } } - }) - - result := make([]string, 0, len(expansions)) - for expansion := range expansions { - result = append(result, expansion) } - return result + + if old.Expansion != "" { + expansionAchievements := m.byExpansion[old.Expansion] + for i, a := range expansionAchievements { + if a.AchievementID == achievement.AchievementID { + m.byExpansion[old.Expansion] = append(expansionAchievements[:i], expansionAchievements[i+1:]...) + break + } + } + } + + // Update core storage + m.achievements[achievement.AchievementID] = achievement + + // Add new achievement to indices + if achievement.Category != "" { + m.byCategory[achievement.Category] = append(m.byCategory[achievement.Category], achievement) + } + + if achievement.Expansion != "" { + m.byExpansion[achievement.Expansion] = append(m.byExpansion[achievement.Expansion], achievement) + } + + // Invalidate metadata cache + m.metaStale = true + + return nil +} + +// Size returns the total number of achievements +func (m *MasterList) Size() int { + m.mutex.RLock() + defer m.mutex.RUnlock() + return len(m.achievements) +} + +// Clear removes all achievements from the master list +func (m *MasterList) Clear() { + m.mutex.Lock() + defer m.mutex.Unlock() + + // Clear all maps + m.achievements = make(map[uint32]*Achievement) + m.byCategory = make(map[string][]*Achievement) + m.byExpansion = make(map[string][]*Achievement) + + // Clear cached metadata + m.categories = m.categories[:0] + m.expansions = m.expansions[:0] + m.metaStale = true +} + +// ForEach executes a function for each achievement +func (m *MasterList) ForEach(fn func(uint32, *Achievement)) { + m.mutex.RLock() + defer m.mutex.RUnlock() + + for id, achievement := range m.achievements { + fn(id, achievement) + } } diff --git a/internal/common/README.md b/internal/common/README.md deleted file mode 100644 index 09316cd..0000000 --- a/internal/common/README.md +++ /dev/null @@ -1,229 +0,0 @@ -# Common Package - -The common package provides shared utilities and patterns used across multiple EQ2Go game systems. - -## Generic Master List - -### Overview - -The generic `MasterList[K, V]` type provides a thread-safe, reusable collection management pattern that eliminates code duplication across the EQ2Go codebase. It implements the master list pattern used by 15+ game systems including achievements, items, spells, factions, skills, etc. - -### Key Features - -- **Generic Type Safety**: Full compile-time type checking with `MasterList[KeyType, ValueType]` -- **Thread Safety**: All operations use `sync.RWMutex` for concurrent access -- **Consistent API**: Standardized CRUD operations across all master lists -- **Performance Optimized**: Efficient filtering, searching, and bulk operations -- **Extension Support**: Compose with specialized interfaces for domain-specific features - -### Basic Usage - -```go -// Any type implementing Identifiable can be stored -type Achievement struct { - ID uint32 `json:"id"` - Title string `json:"title"` - // ... other fields -} - -func (a *Achievement) GetID() uint32 { - return a.ID -} - -// Create a master list -masterList := common.NewMasterList[uint32, *Achievement]() - -// Add items -achievement := &Achievement{ID: 1, Title: "Dragon Slayer"} -added := masterList.Add(achievement) - -// Retrieve items -retrieved := masterList.Get(1) -item, exists := masterList.GetSafe(1) - -// Check existence -if masterList.Exists(1) { - // Item exists -} - -// Update items -achievement.Title = "Master Dragon Slayer" -masterList.Update(achievement) // Returns error if not found -masterList.AddOrUpdate(achievement) // Always succeeds - -// Remove items -removed := masterList.Remove(1) - -// Bulk operations -allItems := masterList.GetAll() // Map copy -allSlice := masterList.GetAllSlice() // Slice copy -allIDs := masterList.GetAllIDs() // ID slice - -// Query operations -filtered := masterList.Filter(func(a *Achievement) bool { - return strings.Contains(a.Title, "Dragon") -}) - -found, exists := masterList.Find(func(a *Achievement) bool { - return a.Title == "Dragon Slayer" -}) - -count := masterList.Count(func(a *Achievement) bool { - return strings.HasPrefix(a.Title, "Master") -}) - -// Iteration -masterList.ForEach(func(id uint32, achievement *Achievement) { - fmt.Printf("Achievement %d: %s\n", id, achievement.Title) -}) -``` - -### Migration from Existing Master Lists - -#### Before (Manual Implementation) -```go -type MasterList struct { - achievements map[uint32]*Achievement - mutex sync.RWMutex -} - -func (m *MasterList) AddAchievement(achievement *Achievement) bool { - m.mutex.Lock() - defer m.mutex.Unlock() - - if _, exists := m.achievements[achievement.ID]; exists { - return false - } - - m.achievements[achievement.ID] = achievement - return true -} - -func (m *MasterList) GetAchievement(id uint32) *Achievement { - m.mutex.RLock() - defer m.mutex.RUnlock() - return m.achievements[id] -} - -// ... 15+ more methods with manual mutex handling -``` - -#### After (Generic Implementation) -```go -type MasterList struct { - *common.MasterList[uint32, *Achievement] -} - -func NewMasterList() *MasterList { - return &MasterList{ - MasterList: common.NewMasterList[uint32, *Achievement](), - } -} - -func (m *MasterList) AddAchievement(achievement *Achievement) bool { - if achievement == nil { - return false - } - return m.MasterList.Add(achievement) -} - -func (m *MasterList) GetAchievement(id uint32) *Achievement { - return m.MasterList.Get(id) -} - -// Domain-specific extensions -func (m *MasterList) GetAchievementsByCategory(category string) []*Achievement { - return m.MasterList.Filter(func(achievement *Achievement) bool { - return achievement.Category == category - }) -} -``` - -### Benefits - -1. **Code Reduction**: 80%+ reduction in boilerplate code per master list -2. **Consistency**: Identical behavior across all master lists -3. **Thread Safety**: Guaranteed concurrent access safety -4. **Performance**: Optimized operations with minimal overhead -5. **Type Safety**: Compile-time guarantees prevent runtime errors -6. **Extensibility**: Easy to add domain-specific functionality -7. **Testing**: Single well-tested implementation vs 15+ custom implementations -8. **Maintenance**: Changes benefit all master lists simultaneously - -### Advanced Features - -#### Thread-Safe Batch Operations -```go -// Complex read operation -masterList.WithReadLock(func(items map[uint32]*Achievement) { - // Direct access to internal map while holding read lock - for id, achievement := range items { - // Complex processing... - } -}) - -// Complex write operation -masterList.WithWriteLock(func(items map[uint32]*Achievement) { - // Direct access to internal map while holding write lock - // Atomic multi-item modifications -}) -``` - -#### Specialized Interface Implementations - -The package provides optional interfaces for advanced functionality: - -- **`DatabaseIntegrated`**: Load/save from database -- **`Validatable`**: Item validation and integrity checks -- **`Searchable`**: Advanced search capabilities -- **`Cacheable`**: Cache management -- **`Statistician`**: Usage statistics tracking -- **`Indexable`**: Multiple index support -- **`Categorizable`**: Category-based organization -- **`Versioned`**: Version compatibility filtering -- **`Relationship`**: Entity relationship management -- **`Hierarchical`**: Tree structure support -- **`Observable`**: Event notifications - -### Migration Steps - -1. **Add Identifiable Interface**: Ensure your type implements `GetID() KeyType` -2. **Embed Generic MasterList**: Replace custom struct with embedded generic -3. **Update Constructor**: Use `common.NewMasterList[K, V]()` -4. **Replace Manual Methods**: Use generic methods or create thin wrappers -5. **Update Domain Methods**: Convert filters to use `Filter()`, `Find()`, etc. -6. **Test**: Existing API should work unchanged with thin wrapper methods - -### Performance Comparison - -Based on benchmarks with 10,000 items: - -| Operation | Before (Manual) | After (Generic) | Improvement | -|-----------|-----------------|------------------|-------------| -| Get | 15ns | 15ns | Same | -| Add | 45ns | 45ns | Same | -| Filter | 125μs | 120μs | 4% faster | -| Memory | Various | Consistent | Predictable | - -The generic implementation maintains identical performance while providing consistency and type safety. - -### Compatibility - -The generic master list is fully backward compatible when used with thin wrapper methods. Existing code continues to work without modification while gaining: - -- Thread safety guarantees -- Performance optimizations -- Consistent behavior -- Type safety -- Reduced maintenance burden - -### Future Enhancements - -Planned additions to the generic master list: - -- **Persistence**: Automatic database synchronization -- **Metrics**: Built-in performance monitoring -- **Events**: Change notification system -- **Indexing**: Automatic secondary index management -- **Validation**: Built-in data integrity checks -- **Sharding**: Horizontal scaling support \ No newline at end of file diff --git a/internal/common/benchmark_test.go b/internal/common/benchmark_test.go deleted file mode 100644 index b1c4576..0000000 --- a/internal/common/benchmark_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package common - -import ( - "fmt" - "math/rand" - "testing" -) - -// testItem implements Identifiable for benchmarking -type testItem struct { - id int32 - name string - value int32 - flag bool -} - -func (t *testItem) GetID() int32 { return t.id } - -// BenchmarkMasterListOperations benchmarks the generic MasterList -func BenchmarkMasterListOperations(b *testing.B) { - // Create master list with test data - ml := NewMasterList[int32, *testItem]() - const numItems = 10000 - - // Pre-populate - b.StopTimer() - for i := 0; i < numItems; i++ { - item := &testItem{ - id: int32(i + 1), - name: fmt.Sprintf("Item %d", i+1), - value: int32(rand.Intn(100)), - flag: rand.Intn(2) == 1, - } - ml.Add(item) - } - b.StartTimer() - - b.Run("Get", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - id := int32(rand.Intn(numItems) + 1) - _ = ml.Get(id) - } - }) - }) - - b.Run("Filter_10Percent", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.Filter(func(item *testItem) bool { - return item.value < 10 // ~10% match - }) - } - }) - - b.Run("Filter_50Percent", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.Filter(func(item *testItem) bool { - return item.value < 50 // ~50% match - }) - } - }) - - b.Run("Filter_90Percent", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.Filter(func(item *testItem) bool { - return item.value < 90 // ~90% match - }) - } - }) - - b.Run("Count_10Percent", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.Count(func(item *testItem) bool { - return item.value < 10 - }) - } - }) - - b.Run("Count_50Percent", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.Count(func(item *testItem) bool { - return item.value < 50 - }) - } - }) - - b.Run("Find", func(b *testing.B) { - for i := 0; i < b.N; i++ { - targetValue := int32(rand.Intn(100)) - _, _ = ml.Find(func(item *testItem) bool { - return item.value == targetValue - }) - } - }) - - b.Run("ForEach", func(b *testing.B) { - for i := 0; i < b.N; i++ { - ml.ForEach(func(id int32, item *testItem) { - _ = item.value + 1 // Minimal work - }) - } - }) - - b.Run("WithReadLock", func(b *testing.B) { - for i := 0; i < b.N; i++ { - ml.WithReadLock(func(items map[int32]*testItem) { - count := 0 - for _, item := range items { - if item.value < 50 { - count++ - } - } - _ = count - }) - } - }) - - b.Run("FilterWithCapacity_Accurate", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.FilterWithCapacity(func(item *testItem) bool { - return item.value < 50 - }, 5000) // Accurate estimate: 50% of 10k = 5k - } - }) - - b.Run("FilterInto_Reuse", func(b *testing.B) { - var reusableSlice []*testItem - for i := 0; i < b.N; i++ { - reusableSlice = ml.FilterInto(func(item *testItem) bool { - return item.value < 50 - }, reusableSlice) - } - }) - - b.Run("CountAndFilter_Combined", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = ml.CountAndFilter(func(item *testItem) bool { - return item.value < 50 - }) - } - }) -} - -// BenchmarkMemoryAllocations tests allocation patterns -func BenchmarkMemoryAllocations(b *testing.B) { - ml := NewMasterList[int32, *testItem]() - const numItems = 1000 - - // Pre-populate - for i := 0; i < numItems; i++ { - item := &testItem{ - id: int32(i + 1), - name: fmt.Sprintf("Item %d", i+1), - value: int32(rand.Intn(100)), - flag: rand.Intn(2) == 1, - } - ml.Add(item) - } - - b.Run("Filter_Allocations", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = ml.Filter(func(item *testItem) bool { - return item.value < 50 - }) - } - }) - - b.Run("GetAll_Allocations", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = ml.GetAll() - } - }) - - b.Run("GetAllSlice_Allocations", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = ml.GetAllSlice() - } - }) - - b.Run("FilterWithCapacity_Allocations", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = ml.FilterWithCapacity(func(item *testItem) bool { - return item.value < 50 - }, 500) // Accurate capacity estimate - } - }) - - b.Run("FilterInto_Allocations", func(b *testing.B) { - b.ReportAllocs() - reusableSlice := make([]*testItem, 0, 600) // Pre-sized - for i := 0; i < b.N; i++ { - reusableSlice = ml.FilterInto(func(item *testItem) bool { - return item.value < 50 - }, reusableSlice) - } - }) - - b.Run("CountAndFilter_Allocations", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, _ = ml.CountAndFilter(func(item *testItem) bool { - return item.value < 50 - }) - } - }) -} \ No newline at end of file diff --git a/internal/common/interfaces.go b/internal/common/interfaces.go deleted file mode 100644 index a737589..0000000 --- a/internal/common/interfaces.go +++ /dev/null @@ -1,205 +0,0 @@ -package common - -import ( - "context" - - "eq2emu/internal/database" -) - -// DatabaseIntegrated defines the interface for master lists that can load from database -type DatabaseIntegrated interface { - // LoadFromDatabase loads all items from the database - LoadFromDatabase(db *database.Database) error - - // SaveToDatabase saves all items to the database (if supported) - SaveToDatabase(db *database.Database) error -} - -// ContextAware defines the interface for master lists that need context for initialization -type ContextAware interface { - // Initialize performs setup operations that may require external dependencies - Initialize(ctx context.Context) error -} - -// Validatable defines the interface for master lists that support validation -type Validatable interface { - // Validate checks the integrity of all items in the list - Validate() []error - - // ValidateItem checks the integrity of a specific item - ValidateItem(item interface{}) error -} - -// Searchable defines the interface for master lists that support advanced search -type Searchable[V any] interface { - // Search finds items matching the given criteria - Search(criteria SearchCriteria) []V - - // SearchByName finds items by name (case-insensitive) - SearchByName(name string) []V -} - -// SearchCriteria defines search parameters for advanced search operations -type SearchCriteria struct { - Name string // Name-based search (case-insensitive) - Category string // Category-based search - Filters map[string]interface{} // Custom filters - Limit int // Maximum results to return (0 = no limit) -} - -// Cacheable defines the interface for master lists that support caching -type Cacheable interface { - // ClearCache clears any cached data - ClearCache() - - // RefreshCache rebuilds cached data - RefreshCache() error - - // IsCacheValid returns true if cache is valid - IsCacheValid() bool -} - -// Statistician defines the interface for master lists that track statistics -type Statistician interface { - // GetStatistics returns usage statistics for the list - GetStatistics() Statistics - - // ResetStatistics resets all tracked statistics - ResetStatistics() -} - -// Statistics represents usage statistics for a master list -type Statistics struct { - TotalItems int `json:"total_items"` - AccessCount int64 `json:"access_count"` - HitRate float64 `json:"hit_rate"` - MissCount int64 `json:"miss_count"` - LastAccessed int64 `json:"last_accessed"` - MemoryUsage int64 `json:"memory_usage"` -} - -// Indexable defines the interface for master lists that support multiple indexes -type Indexable[K comparable, V any] interface { - // GetByIndex retrieves items using an alternate index - GetByIndex(indexName string, key interface{}) []V - - // GetIndexes returns the names of all available indexes - GetIndexes() []string - - // RebuildIndex rebuilds a specific index - RebuildIndex(indexName string) error - - // RebuildAllIndexes rebuilds all indexes - RebuildAllIndexes() error -} - -// Categorizable defines the interface for master lists that support categorization -type Categorizable[V any] interface { - // GetByCategory returns all items in a specific category - GetByCategory(category string) []V - - // GetCategories returns all available categories - GetCategories() []string - - // GetCategoryCount returns the number of items in a category - GetCategoryCount(category string) int -} - -// Versioned defines the interface for master lists that support version filtering -type Versioned[V any] interface { - // GetByVersion returns items compatible with a specific version - GetByVersion(version uint32) []V - - // GetByVersionRange returns items compatible within a version range - GetByVersionRange(minVersion, maxVersion uint32) []V -} - -// Relationship defines the interface for master lists that manage entity relationships -type Relationship[K comparable, V any] interface { - // GetRelated returns items related to the given item - GetRelated(id K, relationshipType string) []V - - // AddRelationship adds a relationship between two items - AddRelationship(fromID K, toID K, relationshipType string) error - - // RemoveRelationship removes a relationship between two items - RemoveRelationship(fromID K, toID K, relationshipType string) error - - // GetRelationshipTypes returns all supported relationship types - GetRelationshipTypes() []string -} - -// Grouped defines the interface for master lists that support grouping -type Grouped[K comparable, V any] interface { - // GetByGroup returns all items in a specific group - GetByGroup(groupID K) []V - - // GetGroups returns all available groups - GetGroups() []K - - // GetGroupSize returns the number of items in a group - GetGroupSize(groupID K) int -} - -// Hierarchical defines the interface for master lists that support tree structures -type Hierarchical[K comparable, V any] interface { - // GetChildren returns direct children of an item - GetChildren(parentID K) []V - - // GetDescendants returns all descendants of an item - GetDescendants(parentID K) []V - - // GetParent returns the parent of an item - GetParent(childID K) (V, bool) - - // GetRoot returns the root item(s) - GetRoot() []V - - // IsAncestor checks if one item is an ancestor of another - IsAncestor(ancestorID K, descendantID K) bool -} - -// Observable defines the interface for master lists that support event notifications -type Observable[K comparable, V any] interface { - // Subscribe adds a listener for list events - Subscribe(listener EventListener[K, V]) - - // Unsubscribe removes a listener - Unsubscribe(listener EventListener[K, V]) - - // NotifyEvent sends an event to all listeners - NotifyEvent(event Event[K, V]) -} - -// EventListener receives notifications about list changes -type EventListener[K comparable, V any] interface { - // OnItemAdded is called when an item is added - OnItemAdded(id K, item V) - - // OnItemRemoved is called when an item is removed - OnItemRemoved(id K, item V) - - // OnItemUpdated is called when an item is updated - OnItemUpdated(id K, oldItem V, newItem V) - - // OnListCleared is called when the list is cleared - OnListCleared() -} - -// Event represents a change event in a master list -type Event[K comparable, V any] struct { - Type EventType `json:"type"` - ItemID K `json:"item_id"` - Item V `json:"item,omitempty"` - OldItem V `json:"old_item,omitempty"` -} - -// EventType represents the type of event that occurred -type EventType int - -const ( - EventItemAdded EventType = iota - EventItemRemoved - EventItemUpdated - EventListCleared -) \ No newline at end of file diff --git a/internal/common/master_list.go b/internal/common/master_list.go deleted file mode 100644 index cf3cf3b..0000000 --- a/internal/common/master_list.go +++ /dev/null @@ -1,311 +0,0 @@ -// Package common provides shared utilities and patterns used across multiple game systems. -// -// The MasterList type provides a generic, thread-safe collection management pattern -// that is used extensively throughout the EQ2Go server implementation for managing -// game entities like items, spells, spawns, achievements, etc. -package common - -import ( - "fmt" - "maps" - "sync" -) - -// Identifiable represents any type that can be identified by a key -type Identifiable[K comparable] interface { - GetID() K -} - -// MasterList provides a generic, thread-safe collection for managing game entities. -// It implements the common pattern used across all EQ2Go master lists with consistent -// CRUD operations, bulk operations, and thread safety. -// -// K is the key type (typically int32, uint32, or string) -// V is the value type (must implement Identifiable[K]) -type MasterList[K comparable, V Identifiable[K]] struct { - items map[K]V - mutex sync.RWMutex -} - -// NewMasterList creates a new master list instance -func NewMasterList[K comparable, V Identifiable[K]]() *MasterList[K, V] { - return &MasterList[K, V]{ - items: make(map[K]V), - } -} - -// Add adds an item to the master list. Returns true if added, false if it already exists. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Add(item V) bool { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - id := item.GetID() - if _, exists := ml.items[id]; exists { - return false - } - - ml.items[id] = item - return true -} - -// AddOrUpdate adds an item to the master list or updates it if it already exists. -// Always returns true. Thread-safe for concurrent access. -func (ml *MasterList[K, V]) AddOrUpdate(item V) bool { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - id := item.GetID() - ml.items[id] = item - return true -} - -// Get retrieves an item by its ID. Returns the zero value of V if not found. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Get(id K) V { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - return ml.items[id] -} - -// GetSafe retrieves an item by its ID with existence check. -// Returns the item and true if found, zero value and false if not found. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) GetSafe(id K) (V, bool) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - item, exists := ml.items[id] - return item, exists -} - -// Exists checks if an item with the given ID exists in the list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Exists(id K) bool { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - _, exists := ml.items[id] - return exists -} - -// Remove removes an item by its ID. Returns true if removed, false if not found. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Remove(id K) bool { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - if _, exists := ml.items[id]; !exists { - return false - } - - delete(ml.items, id) - return true -} - -// Update updates an existing item. Returns error if the item doesn't exist. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Update(item V) error { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - id := item.GetID() - if _, exists := ml.items[id]; !exists { - return fmt.Errorf("item with ID %v not found", id) - } - - ml.items[id] = item - return nil -} - -// Size returns the number of items in the list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Size() int { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - return len(ml.items) -} - -// IsEmpty returns true if the list contains no items. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) IsEmpty() bool { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - return len(ml.items) == 0 -} - -// Clear removes all items from the list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Clear() { - ml.mutex.Lock() - defer ml.mutex.Unlock() - - // Create new map to ensure memory is freed - ml.items = make(map[K]V) -} - -// GetAll returns a copy of all items in the list. -// The returned map is safe to modify without affecting the master list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) GetAll() map[K]V { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make(map[K]V, len(ml.items)) - maps.Copy(result, ml.items) - return result -} - -// GetAllSlice returns a slice containing all items in the list. -// The returned slice is safe to modify without affecting the master list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) GetAllSlice() []V { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make([]V, 0, len(ml.items)) - for _, v := range ml.items { - result = append(result, v) - } - return result -} - -// GetAllIDs returns a slice containing all IDs in the list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) GetAllIDs() []K { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make([]K, 0, len(ml.items)) - for k := range ml.items { - result = append(result, k) - } - return result -} - -// ForEach executes a function for each item in the list. -// The function receives a copy of each item, so modifications won't affect the list. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) ForEach(fn func(K, V)) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - for k, v := range ml.items { - fn(k, v) - } -} - -// Filter returns a new slice containing items that match the predicate function. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Filter(predicate func(V) bool) []V { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - // Pre-allocate with estimated capacity to reduce allocations - result := make([]V, 0, len(ml.items)/4) // Assume ~25% match rate - for _, v := range ml.items { - if predicate(v) { - result = append(result, v) - } - } - return result -} - -// Find returns the first item that matches the predicate function. -// Returns zero value and false if no match is found. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Find(predicate func(V) bool) (V, bool) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - for _, v := range ml.items { - if predicate(v) { - return v, true - } - } - - var zero V - return zero, false -} - -// Count returns the number of items that match the predicate function. -// Thread-safe for concurrent access. -func (ml *MasterList[K, V]) Count(predicate func(V) bool) int { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - count := 0 - for _, v := range ml.items { - if predicate(v) { - count++ - } - } - return count -} - -// WithReadLock executes a function while holding a read lock on the list. -// Use this for complex operations that need consistent read access to multiple items. -func (ml *MasterList[K, V]) WithReadLock(fn func(map[K]V)) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - fn(ml.items) -} - -// WithWriteLock executes a function while holding a write lock on the list. -// Use this for complex operations that need to modify multiple items atomically. -func (ml *MasterList[K, V]) WithWriteLock(fn func(map[K]V)) { - ml.mutex.Lock() - defer ml.mutex.Unlock() - fn(ml.items) -} - -// FilterWithCapacity returns items matching predicate with pre-allocated capacity. -// Use when you have a good estimate of result size to optimize allocations. -func (ml *MasterList[K, V]) FilterWithCapacity(predicate func(V) bool, expectedSize int) []V { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - result := make([]V, 0, expectedSize) - for _, v := range ml.items { - if predicate(v) { - result = append(result, v) - } - } - return result -} - -// FilterInto appends matching items to the provided slice, avoiding new allocations. -// Returns the updated slice. Use this for repeated filtering to reuse memory. -func (ml *MasterList[K, V]) FilterInto(predicate func(V) bool, result []V) []V { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - // Clear the slice but keep capacity - result = result[:0] - for _, v := range ml.items { - if predicate(v) { - result = append(result, v) - } - } - return result -} - -// CountAndFilter performs both count and filter in a single pass. -// More efficient than calling Count() and Filter() separately. -func (ml *MasterList[K, V]) CountAndFilter(predicate func(V) bool) (int, []V) { - ml.mutex.RLock() - defer ml.mutex.RUnlock() - - count := 0 - result := make([]V, 0, len(ml.items)/4) - for _, v := range ml.items { - if predicate(v) { - count++ - result = append(result, v) - } - } - return count, result -} diff --git a/internal/common/master_list_test.go b/internal/common/master_list_test.go deleted file mode 100644 index b97021a..0000000 --- a/internal/common/master_list_test.go +++ /dev/null @@ -1,305 +0,0 @@ -package common - -import ( - "fmt" - "testing" -) - -// TestItem implements Identifiable for testing -type TestItem struct { - ID int32 `json:"id"` - Name string `json:"name"` - Category string `json:"category"` -} - -func (t *TestItem) GetID() int32 { - return t.ID -} - -// TestMasterList tests the basic functionality of the generic master list -func TestMasterList(t *testing.T) { - ml := NewMasterList[int32, *TestItem]() - - // Test initial state - if !ml.IsEmpty() { - t.Error("New master list should be empty") - } - - if ml.Size() != 0 { - t.Error("New master list should have size 0") - } - - // Test adding items - item1 := &TestItem{ID: 1, Name: "Item One", Category: "A"} - item2 := &TestItem{ID: 2, Name: "Item Two", Category: "B"} - item3 := &TestItem{ID: 3, Name: "Item Three", Category: "A"} - - if !ml.Add(item1) { - t.Error("Should successfully add item1") - } - - if !ml.Add(item2) { - t.Error("Should successfully add item2") - } - - if !ml.Add(item3) { - t.Error("Should successfully add item3") - } - - // Test duplicate addition - if ml.Add(item1) { - t.Error("Should not add duplicate item") - } - - // Test size - if ml.Size() != 3 { - t.Errorf("Expected size 3, got %d", ml.Size()) - } - - if ml.IsEmpty() { - t.Error("List should not be empty") - } - - // Test retrieval - retrieved := ml.Get(1) - if retrieved == nil || retrieved.Name != "Item One" { - t.Error("Failed to retrieve item1") - } - - // Test safe retrieval - retrievedSafe, exists := ml.GetSafe(1) - if !exists || retrievedSafe.Name != "Item One" { - t.Error("Failed to safely retrieve item1") - } - - _, exists = ml.GetSafe(999) - if exists { - t.Error("Should not find non-existent item") - } - - // Test existence - if !ml.Exists(1) { - t.Error("Item 1 should exist") - } - - if ml.Exists(999) { - t.Error("Item 999 should not exist") - } - - // Test update - updatedItem := &TestItem{ID: 1, Name: "Updated Item One", Category: "A"} - if err := ml.Update(updatedItem); err != nil { - t.Errorf("Should successfully update item: %v", err) - } - - retrieved = ml.Get(1) - if retrieved.Name != "Updated Item One" { - t.Error("Item was not updated correctly") - } - - // Test update non-existent item - nonExistent := &TestItem{ID: 999, Name: "Non Existent", Category: "Z"} - if err := ml.Update(nonExistent); err == nil { - t.Error("Should fail to update non-existent item") - } - - // Test AddOrUpdate - newItem := &TestItem{ID: 4, Name: "Item Four", Category: "C"} - if !ml.AddOrUpdate(newItem) { - t.Error("Should successfully add new item with AddOrUpdate") - } - - updateExisting := &TestItem{ID: 1, Name: "Double Updated Item One", Category: "A"} - if !ml.AddOrUpdate(updateExisting) { - t.Error("Should successfully update existing item with AddOrUpdate") - } - - retrieved = ml.Get(1) - if retrieved.Name != "Double Updated Item One" { - t.Error("Item was not updated correctly with AddOrUpdate") - } - - if ml.Size() != 4 { - t.Errorf("Expected size 4 after AddOrUpdate, got %d", ml.Size()) - } - - // Test removal - if !ml.Remove(2) { - t.Error("Should successfully remove item2") - } - - if ml.Remove(2) { - t.Error("Should not remove already removed item") - } - - if ml.Size() != 3 { - t.Errorf("Expected size 3 after removal, got %d", ml.Size()) - } - - // Test GetAll - all := ml.GetAll() - if len(all) != 3 { - t.Errorf("Expected 3 items in GetAll, got %d", len(all)) - } - - // Verify we can modify the returned map without affecting the original - all[999] = &TestItem{ID: 999, Name: "Should not affect original", Category: "Z"} - if ml.Exists(999) { - t.Error("Modifying returned map should not affect original list") - } - - // Test GetAllSlice - slice := ml.GetAllSlice() - if len(slice) != 3 { - t.Errorf("Expected 3 items in GetAllSlice, got %d", len(slice)) - } - - // Test GetAllIDs - ids := ml.GetAllIDs() - if len(ids) != 3 { - t.Errorf("Expected 3 IDs in GetAllIDs, got %d", len(ids)) - } - - // Test Clear - ml.Clear() - if !ml.IsEmpty() { - t.Error("List should be empty after Clear") - } - - if ml.Size() != 0 { - t.Error("List should have size 0 after Clear") - } -} - -// TestMasterListSearch tests search functionality -func TestMasterListSearch(t *testing.T) { - ml := NewMasterList[int32, *TestItem]() - - // Add test items - items := []*TestItem{ - {ID: 1, Name: "Alpha", Category: "A"}, - {ID: 2, Name: "Beta", Category: "B"}, - {ID: 3, Name: "Gamma", Category: "A"}, - {ID: 4, Name: "Delta", Category: "C"}, - {ID: 5, Name: "Alpha Two", Category: "A"}, - } - - for _, item := range items { - ml.Add(item) - } - - // Test Filter - categoryA := ml.Filter(func(item *TestItem) bool { - return item.Category == "A" - }) - - if len(categoryA) != 3 { - t.Errorf("Expected 3 items in category A, got %d", len(categoryA)) - } - - // Test Find - found, exists := ml.Find(func(item *TestItem) bool { - return item.Name == "Beta" - }) - - if !exists || found.ID != 2 { - t.Error("Should find Beta with ID 2") - } - - notFound, exists := ml.Find(func(item *TestItem) bool { - return item.Name == "Nonexistent" - }) - - if exists || notFound != nil { - t.Error("Should not find nonexistent item") - } - - // Test Count - count := ml.Count(func(item *TestItem) bool { - return item.Category == "A" - }) - - if count != 3 { - t.Errorf("Expected count of 3 for category A, got %d", count) - } - - // Test ForEach - var visitedIDs []int32 - ml.ForEach(func(id int32, item *TestItem) { - visitedIDs = append(visitedIDs, id) - }) - - if len(visitedIDs) != 5 { - t.Errorf("Expected to visit 5 items, visited %d", len(visitedIDs)) - } -} - -// TestMasterListConcurrency tests thread safety (basic test) -func TestMasterListConcurrency(t *testing.T) { - ml := NewMasterList[int32, *TestItem]() - - // Test WithReadLock - ml.Add(&TestItem{ID: 1, Name: "Test", Category: "A"}) - - var foundItem *TestItem - ml.WithReadLock(func(items map[int32]*TestItem) { - foundItem = items[1] - }) - - if foundItem == nil || foundItem.Name != "Test" { - t.Error("WithReadLock should provide access to internal map") - } - - // Test WithWriteLock - ml.WithWriteLock(func(items map[int32]*TestItem) { - items[2] = &TestItem{ID: 2, Name: "Added via WriteLock", Category: "B"} - }) - - if !ml.Exists(2) { - t.Error("Item added via WithWriteLock should exist") - } - - retrieved := ml.Get(2) - if retrieved.Name != "Added via WriteLock" { - t.Error("Item added via WithWriteLock not found correctly") - } -} - - -// BenchmarkMasterList tests performance of basic operations -func BenchmarkMasterList(b *testing.B) { - ml := NewMasterList[int32, *TestItem]() - - // Pre-populate for benchmarks - for i := int32(0); i < 1000; i++ { - ml.Add(&TestItem{ - ID: i, - Name: fmt.Sprintf("Item %d", i), - Category: fmt.Sprintf("Category %d", i%10), - }) - } - - b.Run("Get", func(b *testing.B) { - for i := 0; i < b.N; i++ { - ml.Get(int32(i % 1000)) - } - }) - - b.Run("Add", func(b *testing.B) { - for i := 0; i < b.N; i++ { - ml.AddOrUpdate(&TestItem{ - ID: int32(1000 + i), - Name: fmt.Sprintf("Bench Item %d", i), - Category: "Bench", - }) - } - }) - - b.Run("Filter", func(b *testing.B) { - for i := 0; i < b.N; i++ { - ml.Filter(func(item *TestItem) bool { - return item.Category == "Category 5" - }) - } - }) -} \ No newline at end of file