fix factions

This commit is contained in:
Sky Johnson 2025-08-08 12:25:55 -05:00
parent 6cad1bd9f9
commit 27e720e703
3 changed files with 954 additions and 136 deletions

View File

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

View File

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

View File

@ -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
}
@ -271,102 +480,110 @@ func (ml *MasterList) ValidateFactions() []string {
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
}