diff --git a/internal/factions/benchmark_test.go b/internal/factions/benchmark_test.go index ec09004..d774c8f 100644 --- a/internal/factions/benchmark_test.go +++ b/internal/factions/benchmark_test.go @@ -1,6 +1,7 @@ package factions import ( + "fmt" "testing" ) @@ -543,3 +544,293 @@ func BenchmarkScalability(b *testing.B) { }) } } + +// Benchmark bespoke MasterList features +func BenchmarkMasterListBespokeFeatures(b *testing.B) { + // Setup function for consistent test data + setupMasterList := func() *MasterList { + mfl := NewMasterList() + + // Add factions across different types + types := []string{"City", "Guild", "Religion", "Race", "Organization"} + + for i := 1; i <= 1000; i++ { + factionType := types[i%len(types)] + faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") + mfl.AddFaction(faction) + } + return mfl + } + + b.Run("GetFactionSafe", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + factionID := int32((i % 1000) + 1) + _, _ = mfl.GetFactionSafe(factionID) + } + }) + + b.Run("GetFactionByName", func(b *testing.B) { + mfl := setupMasterList() + names := []string{"faction1", "faction50", "faction100", "faction500", "faction1000"} + b.ResetTimer() + for i := 0; i < b.N; i++ { + name := names[i%len(names)] + _ = mfl.GetFactionByName(name) + } + }) + + b.Run("GetFactionsByType", func(b *testing.B) { + mfl := setupMasterList() + types := []string{"City", "Guild", "Religion", "Race", "Organization"} + b.ResetTimer() + for i := 0; i < b.N; i++ { + factionType := types[i%len(types)] + _ = mfl.GetFactionsByType(factionType) + } + }) + + b.Run("GetSpecialFactions", func(b *testing.B) { + mfl := setupMasterList() + // Add some special factions + for i := int32(1); i <= 5; i++ { + faction := NewFaction(i, fmt.Sprintf("Special%d", i), "Special", "Special faction") + mfl.AddFaction(faction) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mfl.GetSpecialFactions() + } + }) + + b.Run("GetRegularFactions", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mfl.GetRegularFactions() + } + }) + + b.Run("GetTypes", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mfl.GetTypes() + } + }) + + b.Run("GetAllFactionsList", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mfl.GetAllFactionsList() + } + }) + + b.Run("GetFactionIDs", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mfl.GetFactionIDs() + } + }) + + b.Run("UpdateFaction", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + factionID := int32((i % 1000) + 1) + updatedFaction := &Faction{ + ID: factionID, + Name: fmt.Sprintf("Updated%d", i), + Type: "Updated", + Description: "Updated faction", + } + mfl.UpdateFaction(updatedFaction) + } + }) + + b.Run("ForEach", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + count := 0 + mfl.ForEach(func(id int32, faction *Faction) { + count++ + }) + } + }) + + b.Run("GetStatistics", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mfl.GetStatistics() + } + }) + + b.Run("RemoveFaction", func(b *testing.B) { + b.StopTimer() + mfl := setupMasterList() + initialCount := mfl.GetFactionCount() + + // Pre-populate with factions we'll remove + for i := 0; i < b.N; i++ { + faction := NewFaction(int32(20000+i), fmt.Sprintf("ToRemove%d", i), "Temporary", "Temporary faction") + mfl.AddFaction(faction) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + mfl.RemoveFaction(int32(20000 + i)) + } + + b.StopTimer() + if mfl.GetFactionCount() != initialCount { + b.Errorf("Expected %d factions after removal, got %d", initialCount, mfl.GetFactionCount()) + } + }) +} + +// Memory allocation benchmarks for bespoke features +func BenchmarkMasterListBespokeFeatures_Allocs(b *testing.B) { + setupMasterList := func() *MasterList { + mfl := NewMasterList() + types := []string{"City", "Guild", "Religion", "Race", "Organization"} + + for i := 1; i <= 100; i++ { + factionType := types[i%len(types)] + faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") + mfl.AddFaction(faction) + } + return mfl + } + + b.Run("GetFactionByName_Allocs", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = mfl.GetFactionByName("faction1") + } + }) + + b.Run("GetFactionsByType_Allocs", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = mfl.GetFactionsByType("City") + } + }) + + b.Run("GetTypes_Allocs", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = mfl.GetTypes() + } + }) + + b.Run("GetSpecialFactions_Allocs", func(b *testing.B) { + mfl := setupMasterList() + // Add some special factions + for i := int32(1); i <= 5; i++ { + faction := NewFaction(i, fmt.Sprintf("Special%d", i), "Special", "Special faction") + mfl.AddFaction(faction) + } + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = mfl.GetSpecialFactions() + } + }) + + b.Run("GetStatistics_Allocs", func(b *testing.B) { + mfl := setupMasterList() + b.ResetTimer() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _ = mfl.GetStatistics() + } + }) +} + +// Concurrent benchmarks for bespoke features +func BenchmarkMasterListBespokeConcurrent(b *testing.B) { + b.Run("ConcurrentReads", func(b *testing.B) { + mfl := NewMasterList() + + // Setup test data + types := []string{"City", "Guild", "Religion", "Race", "Organization"} + for i := 1; i <= 100; i++ { + factionType := types[i%len(types)] + faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") + mfl.AddFaction(faction) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + // Mix of read operations + switch i % 6 { + case 0: + mfl.GetFaction(int32(i%100 + 1)) + case 1: + mfl.GetFactionsByType("City") + case 2: + mfl.GetFactionByName("faction1") + case 3: + mfl.GetSpecialFactions() + case 4: + mfl.GetRegularFactions() + case 5: + mfl.GetTypes() + } + i++ + } + }) + }) + + b.Run("ConcurrentMixed", func(b *testing.B) { + mfl := NewMasterList() + + // Setup test data + types := []string{"City", "Guild", "Religion", "Race", "Organization"} + for i := 1; i <= 100; i++ { + factionType := types[i%len(types)] + faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") + mfl.AddFaction(faction) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + // Mix of read and write operations (mostly reads) + switch i % 10 { + case 0: // 10% writes + faction := NewFaction(int32(i+50000), fmt.Sprintf("Concurrent%d", i), "Concurrent", "Concurrent test") + mfl.AddFaction(faction) + default: // 90% reads + switch i % 5 { + case 0: + mfl.GetFaction(int32(i%100 + 1)) + case 1: + mfl.GetFactionsByType("City") + case 2: + mfl.GetFactionByName("faction1") + case 3: + mfl.GetSpecialFactions() + case 4: + mfl.GetTypes() + } + } + i++ + } + }) + }) +} diff --git a/internal/factions/factions_test.go b/internal/factions/factions_test.go index 025d247..8fab74b 100644 --- a/internal/factions/factions_test.go +++ b/internal/factions/factions_test.go @@ -174,3 +174,177 @@ func TestFactionValidation(t *testing.T) { t.Error("Expected error when adding faction with empty name") } } + +func TestMasterListBespokeFeatures(t *testing.T) { + mfl := NewMasterList() + + // Create test factions with different properties + faction1 := NewFaction(1, "Special Faction", "Special", "A special faction") + faction2 := NewFaction(20, "City Faction", "City", "A city faction") + faction3 := NewFaction(21, "Guild Faction", "Guild", "A guild faction") + faction4 := NewFaction(30, "Another City", "City", "Another city faction") + + // Add factions + mfl.AddFaction(faction1) + mfl.AddFaction(faction2) + mfl.AddFaction(faction3) + mfl.AddFaction(faction4) + + // Test GetFactionSafe + retrieved, exists := mfl.GetFactionSafe(20) + if !exists || retrieved == nil { + t.Error("GetFactionSafe should return existing faction and true") + } + + _, exists = mfl.GetFactionSafe(9999) + if exists { + t.Error("GetFactionSafe should return false for non-existent ID") + } + + // Test GetFactionByName (case-insensitive) + found := mfl.GetFactionByName("city faction") + if found == nil || found.ID != 20 { + t.Error("GetFactionByName should find 'City Faction' (case insensitive)") + } + + found = mfl.GetFactionByName("GUILD FACTION") + if found == nil || found.ID != 21 { + t.Error("GetFactionByName should find 'Guild Faction' (uppercase)") + } + + found = mfl.GetFactionByName("NonExistent") + if found != nil { + t.Error("GetFactionByName should return nil for non-existent faction") + } + + // Test GetFactionsByType + cityFactions := mfl.GetFactionsByType("City") + if len(cityFactions) != 2 { + t.Errorf("GetFactionsByType('City') returned %v results, want 2", len(cityFactions)) + } + + guildFactions := mfl.GetFactionsByType("Guild") + if len(guildFactions) != 1 { + t.Errorf("GetFactionsByType('Guild') returned %v results, want 1", len(guildFactions)) + } + + // Test GetSpecialFactions and GetRegularFactions + specialFactions := mfl.GetSpecialFactions() + if len(specialFactions) != 1 { + t.Errorf("GetSpecialFactions() returned %v results, want 1", len(specialFactions)) + } + + regularFactions := mfl.GetRegularFactions() + if len(regularFactions) != 3 { + t.Errorf("GetRegularFactions() returned %v results, want 3", len(regularFactions)) + } + + // Test GetTypes + types := mfl.GetTypes() + if len(types) < 3 { + t.Errorf("GetTypes() returned %v types, want at least 3", len(types)) + } + + // Verify types contains expected values + typeMap := make(map[string]bool) + for _, factionType := range types { + typeMap[factionType] = true + } + if !typeMap["Special"] || !typeMap["City"] || !typeMap["Guild"] { + t.Error("GetTypes() should contain 'Special', 'City', and 'Guild'") + } + + // Test UpdateFaction + updatedFaction := &Faction{ + ID: 20, + Name: "Updated City Faction", + Type: "UpdatedCity", + Description: "An updated city faction", + } + + err := mfl.UpdateFaction(updatedFaction) + if err != nil { + t.Errorf("UpdateFaction failed: %v", err) + } + + // Verify the update worked + retrieved = mfl.GetFaction(20) + if retrieved.Name != "Updated City Faction" { + t.Errorf("Expected updated name 'Updated City Faction', got '%s'", retrieved.Name) + } + + if retrieved.Type != "UpdatedCity" { + t.Errorf("Expected updated type 'UpdatedCity', got '%s'", retrieved.Type) + } + + // Test updating non-existent faction + nonExistentFaction := &Faction{ID: 9999, Name: "Non-existent"} + err = mfl.UpdateFaction(nonExistentFaction) + if err == nil { + t.Error("UpdateFaction should fail for non-existent faction") + } + + // Test GetAllFactionsList + allList := mfl.GetAllFactionsList() + if len(allList) != 4 { + t.Errorf("GetAllFactionsList() returned %v factions, want 4", len(allList)) + } + + // Test GetFactionIDs + ids := mfl.GetFactionIDs() + if len(ids) != 4 { + t.Errorf("GetFactionIDs() returned %v IDs, want 4", len(ids)) + } +} + +func TestMasterListConcurrency(t *testing.T) { + mfl := NewMasterList() + + // Add initial factions + for i := 1; i <= 50; i++ { + faction := NewFaction(int32(i+100), "Faction", "Test", "Test faction") + mfl.AddFaction(faction) + } + + // Test concurrent access + done := make(chan bool, 10) + + // Concurrent readers + for i := 0; i < 5; i++ { + go func() { + defer func() { done <- true }() + for j := 0; j < 100; j++ { + mfl.GetFaction(int32(j%50 + 101)) + mfl.GetFactionsByType("Test") + mfl.GetFactionByName("faction") + mfl.HasFaction(int32(j%50 + 101)) + } + }() + } + + // Concurrent writers + for i := 0; i < 5; i++ { + go func(workerID int) { + defer func() { done <- true }() + for j := 0; j < 10; j++ { + factionID := int32(workerID*1000 + j + 1000) + faction := NewFaction(factionID, "Worker Faction", "Worker", "Worker test faction") + mfl.AddFaction(faction) // Some may fail due to concurrent additions + } + }(i) + } + + // Wait for all goroutines + for i := 0; i < 10; i++ { + <-done + } + + // Verify final state - should have at least 50 initial factions + finalCount := mfl.GetFactionCount() + if finalCount < 50 { + t.Errorf("Expected at least 50 factions after concurrent operations, got %d", finalCount) + } + if finalCount > 100 { + t.Errorf("Expected at most 100 factions after concurrent operations, got %d", finalCount) + } +} diff --git a/internal/factions/master.go b/internal/factions/master.go index c82901c..80e4dfb 100644 --- a/internal/factions/master.go +++ b/internal/factions/master.go @@ -2,31 +2,125 @@ package factions import ( "fmt" + "maps" + "strings" "sync" - "eq2emu/internal/common" + "eq2emu/internal/database" ) -// MasterList manages all factions using the generic MasterList base +// MasterList is a specialized faction master list optimized for: +// - Fast ID-based lookups (O(1)) +// - Fast name-based lookups (O(1)) +// - Fast type-based filtering (indexed) +// - Efficient faction relationships management +// - Special faction handling +// - Value range queries and validation type MasterList struct { - *common.MasterList[int32, *Faction] - factionNameList map[string]*Faction // Factions by name lookup - hostileFactions map[int32][]int32 // Hostile faction relationships - friendlyFactions map[int32][]int32 // Friendly faction relationships - mutex sync.RWMutex // Additional mutex for relationships + // Core storage + factions map[int32]*Faction // ID -> Faction + mutex sync.RWMutex + + // Specialized indices for O(1) lookups + byName map[string]*Faction // Lowercase name -> faction + byType map[string][]*Faction // Type -> factions + specialFactions map[int32]*Faction // Special factions (ID <= SpecialFactionIDMax) + regularFactions map[int32]*Faction // Regular factions (ID > SpecialFactionIDMax) + + // Faction relationships + hostileFactions map[int32][]int32 // Hostile faction relationships + friendlyFactions map[int32][]int32 // Friendly faction relationships + + // Cached metadata + types []string // Unique types (cached) + typeStats map[string]int // Type -> count + metaStale bool // Whether metadata cache needs refresh } -// NewMasterList creates a new master faction list +// NewMasterList creates a new specialized faction master list func NewMasterList() *MasterList { return &MasterList{ - MasterList: common.NewMasterList[int32, *Faction](), - factionNameList: make(map[string]*Faction), + factions: make(map[int32]*Faction), + byName: make(map[string]*Faction), + byType: make(map[string][]*Faction), + specialFactions: make(map[int32]*Faction), + regularFactions: make(map[int32]*Faction), hostileFactions: make(map[int32][]int32), friendlyFactions: make(map[int32][]int32), + typeStats: make(map[string]int), + metaStale: true, } } -// AddFaction adds a faction to the master list +// refreshMetaCache updates the cached metadata +func (ml *MasterList) refreshMetaCache() { + if !ml.metaStale { + return + } + + // Clear and rebuild type stats + ml.typeStats = make(map[string]int) + typeSet := make(map[string]struct{}) + + // Collect unique values and stats + for _, faction := range ml.factions { + factionType := faction.GetType() + if factionType != "" { + ml.typeStats[factionType]++ + typeSet[factionType] = struct{}{} + } + } + + // Clear and rebuild cached slices + ml.types = ml.types[:0] + for factionType := range typeSet { + ml.types = append(ml.types, factionType) + } + + ml.metaStale = false +} + +// updateFactionIndices updates all indices for a faction +func (ml *MasterList) updateFactionIndices(faction *Faction, add bool) { + if add { + // Add to name index + ml.byName[strings.ToLower(faction.GetName())] = faction + + // Add to type index + factionType := faction.GetType() + if factionType != "" { + ml.byType[factionType] = append(ml.byType[factionType], faction) + } + + // Add to special/regular index + if faction.IsSpecialFaction() { + ml.specialFactions[faction.ID] = faction + } else { + ml.regularFactions[faction.ID] = faction + } + } else { + // Remove from name index + delete(ml.byName, strings.ToLower(faction.GetName())) + + // Remove from type index + factionType := faction.GetType() + if factionType != "" { + typeFactionsSlice := ml.byType[factionType] + for i, f := range typeFactionsSlice { + if f.ID == faction.ID { + ml.byType[factionType] = append(typeFactionsSlice[:i], typeFactionsSlice[i+1:]...) + break + } + } + } + + // Remove from special/regular index + delete(ml.specialFactions, faction.ID) + delete(ml.regularFactions, faction.ID) + } +} + +// AddFaction adds a faction with full indexing func (ml *MasterList) AddFaction(faction *Faction) error { if faction == nil { return fmt.Errorf("faction cannot be nil") @@ -36,61 +130,79 @@ func (ml *MasterList) AddFaction(faction *Faction) error { return fmt.Errorf("faction is not valid") } - // Use generic base for main storage - if !ml.MasterList.Add(faction) { + ml.mutex.Lock() + defer ml.mutex.Unlock() + + // Check if exists + if _, exists := ml.factions[faction.ID]; exists { return fmt.Errorf("faction with ID %d already exists", faction.ID) } - // Update name lookup - ml.mutex.Lock() - ml.factionNameList[faction.Name] = faction - ml.mutex.Unlock() + // Add to core storage + ml.factions[faction.ID] = faction + + // Update all indices + ml.updateFactionIndices(faction, true) + + // Invalidate metadata cache + ml.metaStale = true return nil } -// GetFaction returns a faction by ID +// GetFaction retrieves by ID (O(1)) func (ml *MasterList) GetFaction(id int32) *Faction { - return ml.MasterList.Get(id) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + return ml.factions[id] } -// GetFactionByName returns a faction by name +// GetFactionSafe retrieves a faction by ID with existence check +func (ml *MasterList) GetFactionSafe(id int32) (*Faction, bool) { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + faction, exists := ml.factions[id] + return faction, exists +} + +// GetFactionByName retrieves a faction by name (case-insensitive, O(1)) func (ml *MasterList) GetFactionByName(name string) *Faction { ml.mutex.RLock() defer ml.mutex.RUnlock() - return ml.factionNameList[name] + return ml.byName[strings.ToLower(name)] } // HasFaction checks if a faction exists by ID func (ml *MasterList) HasFaction(factionID int32) bool { - return ml.MasterList.Exists(factionID) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + _, exists := ml.factions[factionID] + return exists } // HasFactionByName checks if a faction exists by name func (ml *MasterList) HasFactionByName(name string) bool { ml.mutex.RLock() defer ml.mutex.RUnlock() - _, exists := ml.factionNameList[name] + _, exists := ml.byName[strings.ToLower(name)] return exists } -// RemoveFaction removes a faction by ID +// RemoveFaction removes a faction and updates all indices func (ml *MasterList) RemoveFaction(factionID int32) bool { - faction := ml.MasterList.Get(factionID) - if faction == nil { - return false - } - - // Remove from generic base - if !ml.MasterList.Remove(factionID) { - return false - } - ml.mutex.Lock() defer ml.mutex.Unlock() - // Remove from name lookup - delete(ml.factionNameList, faction.Name) + faction, exists := ml.factions[factionID] + if !exists { + return false + } + + // Remove from core storage + delete(ml.factions, factionID) + + // Update all indices + ml.updateFactionIndices(faction, false) // Remove from relationship maps delete(ml.hostileFactions, factionID) @@ -117,10 +229,13 @@ func (ml *MasterList) RemoveFaction(factionID int32) bool { ml.friendlyFactions[id] = newFriendlies } + // Invalidate metadata cache + ml.metaStale = true + return true } -// UpdateFaction updates an existing faction +// UpdateFaction updates an existing faction and refreshes indices func (ml *MasterList) UpdateFaction(faction *Faction) error { if faction == nil { return fmt.Errorf("faction cannot be nil") @@ -130,65 +245,153 @@ func (ml *MasterList) UpdateFaction(faction *Faction) error { return fmt.Errorf("faction is not valid") } - oldFaction := ml.MasterList.Get(faction.ID) - if oldFaction == nil { - return fmt.Errorf("faction with ID %d does not exist", faction.ID) - } - - // Update in generic base - if err := ml.MasterList.Update(faction); err != nil { - return err - } - ml.mutex.Lock() defer ml.mutex.Unlock() - // If name changed, update name map - if oldFaction.Name != faction.Name { - delete(ml.factionNameList, oldFaction.Name) - ml.factionNameList[faction.Name] = faction + // Check if exists + old, exists := ml.factions[faction.ID] + if !exists { + return fmt.Errorf("faction %d not found", faction.ID) } + // Remove old faction from indices (but not core storage yet) + ml.updateFactionIndices(old, false) + + // Update core storage + ml.factions[faction.ID] = faction + + // Add new faction to indices + ml.updateFactionIndices(faction, true) + + // Invalidate metadata cache + ml.metaStale = true + return nil } // GetFactionCount returns the total number of factions func (ml *MasterList) GetFactionCount() int32 { - return int32(ml.MasterList.Size()) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + return int32(len(ml.factions)) } -// GetAllFactions returns a copy of all factions +// GetAllFactions returns a copy of all factions map func (ml *MasterList) GetAllFactions() map[int32]*Faction { - return ml.MasterList.GetAll() + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + // Return a copy to prevent external modification + result := make(map[int32]*Faction, len(ml.factions)) + maps.Copy(result, ml.factions) + return result +} + +// GetAllFactionsList returns all factions as a slice +func (ml *MasterList) GetAllFactionsList() []*Faction { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + result := make([]*Faction, 0, len(ml.factions)) + for _, faction := range ml.factions { + result = append(result, faction) + } + return result } // GetFactionIDs returns all faction IDs func (ml *MasterList) GetFactionIDs() []int32 { - return ml.MasterList.GetAllIDs() + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + result := make([]int32, 0, len(ml.factions)) + for id := range ml.factions { + result = append(result, id) + } + return result } -// GetFactionsByType returns all factions of a specific type +// GetFactionsByType returns all factions of a specific type (O(1)) func (ml *MasterList) GetFactionsByType(factionType string) []*Faction { - return ml.MasterList.Filter(func(f *Faction) bool { - return f.Type == factionType - }) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + return ml.byType[factionType] +} + +// GetSpecialFactions returns all special factions (ID <= SpecialFactionIDMax) +func (ml *MasterList) GetSpecialFactions() map[int32]*Faction { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + // Return a copy to prevent external modification + result := make(map[int32]*Faction, len(ml.specialFactions)) + maps.Copy(result, ml.specialFactions) + return result +} + +// GetRegularFactions returns all regular factions (ID > SpecialFactionIDMax) +func (ml *MasterList) GetRegularFactions() map[int32]*Faction { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + // Return a copy to prevent external modification + result := make(map[int32]*Faction, len(ml.regularFactions)) + maps.Copy(result, ml.regularFactions) + return result +} + +// Size returns the total number of factions +func (ml *MasterList) Size() int { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + return len(ml.factions) +} + +// IsEmpty returns true if the master list is empty +func (ml *MasterList) IsEmpty() bool { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + return len(ml.factions) == 0 } // Clear removes all factions and relationships func (ml *MasterList) Clear() { - ml.MasterList.Clear() - ml.mutex.Lock() defer ml.mutex.Unlock() - ml.factionNameList = make(map[string]*Faction) + // Clear all maps + ml.factions = make(map[int32]*Faction) + ml.byName = make(map[string]*Faction) + ml.byType = make(map[string][]*Faction) + ml.specialFactions = make(map[int32]*Faction) + ml.regularFactions = make(map[int32]*Faction) ml.hostileFactions = make(map[int32][]int32) ml.friendlyFactions = make(map[int32][]int32) + + // Clear cached metadata + ml.types = ml.types[:0] + ml.typeStats = make(map[string]int) + ml.metaStale = true +} + +// GetTypes returns all unique faction types using cached results +func (ml *MasterList) GetTypes() []string { + ml.mutex.Lock() // Need write lock to potentially update cache + defer ml.mutex.Unlock() + + ml.refreshMetaCache() + + // Return a copy to prevent external modification + result := make([]string, len(ml.types)) + copy(result, ml.types) + return result } // GetDefaultFactionValue returns the default value for a faction func (ml *MasterList) GetDefaultFactionValue(factionID int32) int32 { - faction := ml.MasterList.Get(factionID) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + faction := ml.factions[factionID] if faction != nil { return faction.DefaultValue } @@ -197,7 +400,9 @@ func (ml *MasterList) GetDefaultFactionValue(factionID int32) int32 { // GetIncreaseAmount returns the default increase amount for a faction func (ml *MasterList) GetIncreaseAmount(factionID int32) int32 { - faction := ml.MasterList.Get(factionID) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + faction := ml.factions[factionID] if faction != nil { return int32(faction.PositiveChange) } @@ -206,7 +411,9 @@ func (ml *MasterList) GetIncreaseAmount(factionID int32) int32 { // GetDecreaseAmount returns the default decrease amount for a faction func (ml *MasterList) GetDecreaseAmount(factionID int32) int32 { - faction := ml.MasterList.Get(factionID) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + faction := ml.factions[factionID] if faction != nil { return int32(faction.NegativeChange) } @@ -216,7 +423,9 @@ func (ml *MasterList) GetDecreaseAmount(factionID int32) int32 { // GetFactionNameByID returns the faction name for a given ID func (ml *MasterList) GetFactionNameByID(factionID int32) string { if factionID > 0 { - faction := ml.MasterList.Get(factionID) + ml.mutex.RLock() + defer ml.mutex.RUnlock() + faction := ml.factions[factionID] if faction != nil { return faction.Name } @@ -270,103 +479,111 @@ func (ml *MasterList) ValidateFactions() []string { defer ml.mutex.RUnlock() var issues []string - - // Use WithReadLock to avoid copying the entire map - var seenIDs map[int32]*Faction - ml.MasterList.WithReadLock(func(allFactions map[int32]*Faction) { - seenIDs = make(map[int32]*Faction, len(allFactions)) - // Pass 1: Validate main faction list and build seenID map - for id, faction := range allFactions { - if faction == nil { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id)) - continue - } - - if faction.ID <= 0 || faction.Name == "" { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction ID %d is invalid or unnamed", id)) - } - - if faction.ID != id { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID)) - } - - seenIDs[id] = faction - } - }) - - // Pass 2: Validate factionNameList - for name, faction := range ml.factionNameList { + // Pass 1: Validate main faction list + for id, faction := range ml.factions { + if faction == nil { + issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id)) + continue + } + + if faction.ID <= 0 || faction.Name == "" { + issues = append(issues, fmt.Sprintf("Faction ID %d is invalid or unnamed", id)) + } + + if faction.ID != id { + issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID)) + } + } + + // Pass 2: Validate byName index + for name, faction := range ml.byName { if faction == nil { - if issues == nil { - issues = make([]string, 0, 10) - } issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name)) continue } - if faction.Name != name { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction name mismatch: map key '%s' != faction name '%s'", name, faction.Name)) + if strings.ToLower(faction.Name) != name { + issues = append(issues, fmt.Sprintf("Faction name index mismatch: map key '%s' != lowercase faction name '%s'", name, strings.ToLower(faction.Name))) } - if _, ok := seenIDs[faction.ID]; !ok { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name map but not in ID map", name, faction.ID)) + if _, ok := ml.factions[faction.ID]; !ok { + issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name index but not in main storage", faction.Name, faction.ID)) } } - // Pass 3: Validate relationships using prebuilt seenIDs - for sourceID, targets := range ml.hostileFactions { - if _, ok := seenIDs[sourceID]; !ok { - if issues == nil { - issues = make([]string, 0, 10) + // Pass 3: Validate byType index + for factionType, factions := range ml.byType { + for _, faction := range factions { + if faction == nil { + issues = append(issues, fmt.Sprintf("Type '%s' has nil faction", factionType)) + continue } + + if faction.Type != factionType { + issues = append(issues, fmt.Sprintf("Faction %d (type '%s') found in wrong type index '%s'", faction.ID, faction.Type, factionType)) + } + + if _, ok := ml.factions[faction.ID]; !ok { + issues = append(issues, fmt.Sprintf("Faction %d exists in type index but not in main storage", faction.ID)) + } + } + } + + // Pass 4: Validate special/regular faction indices + for id, faction := range ml.specialFactions { + if faction == nil { + issues = append(issues, fmt.Sprintf("Special faction ID %d is nil", id)) + continue + } + + if !faction.IsSpecialFaction() { + issues = append(issues, fmt.Sprintf("Faction %d is in special index but is not special (ID > %d)", id, SpecialFactionIDMax)) + } + + if _, ok := ml.factions[id]; !ok { + issues = append(issues, fmt.Sprintf("Special faction %d exists in special index but not in main storage", id)) + } + } + + for id, faction := range ml.regularFactions { + if faction == nil { + issues = append(issues, fmt.Sprintf("Regular faction ID %d is nil", id)) + continue + } + + if faction.IsSpecialFaction() { + issues = append(issues, fmt.Sprintf("Faction %d is in regular index but is special (ID <= %d)", id, SpecialFactionIDMax)) + } + + if _, ok := ml.factions[id]; !ok { + issues = append(issues, fmt.Sprintf("Regular faction %d exists in regular index but not in main storage", id)) + } + } + + // Pass 5: Validate relationships + for sourceID, targets := range ml.hostileFactions { + if _, ok := ml.factions[sourceID]; !ok { issues = append(issues, fmt.Sprintf("Hostile relationship defined for non-existent faction %d", sourceID)) } for _, targetID := range targets { - if _, ok := seenIDs[targetID]; !ok { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction %d has Hostile relationship with non-existent faction %d", sourceID, targetID)) + if _, ok := ml.factions[targetID]; !ok { + issues = append(issues, fmt.Sprintf("Faction %d has hostile relationship with non-existent faction %d", sourceID, targetID)) } } } for sourceID, targets := range ml.friendlyFactions { - if _, ok := seenIDs[sourceID]; !ok { - if issues == nil { - issues = make([]string, 0, 10) - } + if _, ok := ml.factions[sourceID]; !ok { issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", sourceID)) } for _, targetID := range targets { - if _, ok := seenIDs[targetID]; !ok { - if issues == nil { - issues = make([]string, 0, 10) - } - issues = append(issues, fmt.Sprintf("Faction %d has Friendly relationship with non-existent faction %d", sourceID, targetID)) + if _, ok := ml.factions[targetID]; !ok { + issues = append(issues, fmt.Sprintf("Faction %d has friendly relationship with non-existent faction %d", sourceID, targetID)) } } } - if issues == nil { - return []string{} - } return issues } @@ -375,3 +592,139 @@ func (ml *MasterList) IsValid() bool { issues := ml.ValidateFactions() return len(issues) == 0 } + +// ForEach executes a function for each faction +func (ml *MasterList) ForEach(fn func(int32, *Faction)) { + ml.mutex.RLock() + defer ml.mutex.RUnlock() + + for id, faction := range ml.factions { + fn(id, faction) + } +} + +// GetStatistics returns statistics about the faction system using cached data +func (ml *MasterList) GetStatistics() map[string]any { + ml.mutex.Lock() // Need write lock to potentially update cache + defer ml.mutex.Unlock() + + ml.refreshMetaCache() + + stats := make(map[string]any) + stats["total_factions"] = len(ml.factions) + + if len(ml.factions) == 0 { + return stats + } + + // Use cached type stats + stats["factions_by_type"] = ml.typeStats + + // Calculate additional stats + var specialCount, regularCount int + var minID, maxID int32 + var minDefaultValue, maxDefaultValue int32 = MaxFactionValue, MinFactionValue + var totalPositiveChange, totalNegativeChange int64 + first := true + + for id, faction := range ml.factions { + if faction.IsSpecialFaction() { + specialCount++ + } else { + regularCount++ + } + + if first { + minID = id + maxID = id + minDefaultValue = faction.DefaultValue + maxDefaultValue = faction.DefaultValue + first = false + } else { + if id < minID { + minID = id + } + if id > maxID { + maxID = id + } + if faction.DefaultValue < minDefaultValue { + minDefaultValue = faction.DefaultValue + } + if faction.DefaultValue > maxDefaultValue { + maxDefaultValue = faction.DefaultValue + } + } + + totalPositiveChange += int64(faction.PositiveChange) + totalNegativeChange += int64(faction.NegativeChange) + } + + stats["special_factions"] = specialCount + stats["regular_factions"] = regularCount + stats["min_id"] = minID + stats["max_id"] = maxID + stats["id_range"] = maxID - minID + stats["min_default_value"] = minDefaultValue + stats["max_default_value"] = maxDefaultValue + stats["total_positive_change"] = totalPositiveChange + stats["total_negative_change"] = totalNegativeChange + + // Relationship stats + stats["total_hostile_relationships"] = len(ml.hostileFactions) + stats["total_friendly_relationships"] = len(ml.friendlyFactions) + + return stats +} + +// LoadAllFactions loads all factions from the database into the master list +func (ml *MasterList) LoadAllFactions(db *database.Database) error { + if db == nil { + return fmt.Errorf("database connection is nil") + } + + // Clear existing factions + ml.Clear() + + query := `SELECT id, name, type, description, negative_change, positive_change, default_value FROM factions ORDER BY id` + rows, err := db.Query(query) + if err != nil { + return fmt.Errorf("failed to query factions: %w", err) + } + defer rows.Close() + + count := 0 + for rows.Next() { + faction := &Faction{ + db: db, + isNew: false, + } + + err := rows.Scan(&faction.ID, &faction.Name, &faction.Type, &faction.Description, + &faction.NegativeChange, &faction.PositiveChange, &faction.DefaultValue) + if err != nil { + return fmt.Errorf("failed to scan faction: %w", err) + } + + if err := ml.AddFaction(faction); err != nil { + return fmt.Errorf("failed to add faction %d to master list: %w", faction.ID, err) + } + + count++ + } + + if err := rows.Err(); err != nil { + return fmt.Errorf("error iterating faction rows: %w", err) + } + + return nil +} + +// LoadAllFactionsFromDatabase is a convenience function that creates a master list and loads all factions +func LoadAllFactionsFromDatabase(db *database.Database) (*MasterList, error) { + masterList := NewMasterList() + err := masterList.LoadAllFactions(db) + if err != nil { + return nil, err + } + return masterList, nil +}