fix factions
This commit is contained in:
parent
6cad1bd9f9
commit
27e720e703
@ -1,6 +1,7 @@
|
|||||||
package factions
|
package factions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"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++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -174,3 +174,177 @@ func TestFactionValidation(t *testing.T) {
|
|||||||
t.Error("Expected error when adding faction with empty name")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,31 +2,125 @@ package factions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"strings"
|
||||||
"sync"
|
"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 {
|
type MasterList struct {
|
||||||
*common.MasterList[int32, *Faction]
|
// Core storage
|
||||||
factionNameList map[string]*Faction // Factions by name lookup
|
factions map[int32]*Faction // ID -> Faction
|
||||||
hostileFactions map[int32][]int32 // Hostile faction relationships
|
mutex sync.RWMutex
|
||||||
friendlyFactions map[int32][]int32 // Friendly faction relationships
|
|
||||||
mutex sync.RWMutex // Additional mutex for relationships
|
// 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 {
|
func NewMasterList() *MasterList {
|
||||||
return &MasterList{
|
return &MasterList{
|
||||||
MasterList: common.NewMasterList[int32, *Faction](),
|
factions: make(map[int32]*Faction),
|
||||||
factionNameList: make(map[string]*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),
|
hostileFactions: make(map[int32][]int32),
|
||||||
friendlyFactions: 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 {
|
func (ml *MasterList) AddFaction(faction *Faction) error {
|
||||||
if faction == nil {
|
if faction == nil {
|
||||||
return fmt.Errorf("faction cannot be 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")
|
return fmt.Errorf("faction is not valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use generic base for main storage
|
ml.mutex.Lock()
|
||||||
if !ml.MasterList.Add(faction) {
|
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)
|
return fmt.Errorf("faction with ID %d already exists", faction.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update name lookup
|
// Add to core storage
|
||||||
ml.mutex.Lock()
|
ml.factions[faction.ID] = faction
|
||||||
ml.factionNameList[faction.Name] = faction
|
|
||||||
ml.mutex.Unlock()
|
// Update all indices
|
||||||
|
ml.updateFactionIndices(faction, true)
|
||||||
|
|
||||||
|
// Invalidate metadata cache
|
||||||
|
ml.metaStale = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFaction returns a faction by ID
|
// GetFaction retrieves by ID (O(1))
|
||||||
func (ml *MasterList) GetFaction(id int32) *Faction {
|
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 {
|
func (ml *MasterList) GetFactionByName(name string) *Faction {
|
||||||
ml.mutex.RLock()
|
ml.mutex.RLock()
|
||||||
defer ml.mutex.RUnlock()
|
defer ml.mutex.RUnlock()
|
||||||
return ml.factionNameList[name]
|
return ml.byName[strings.ToLower(name)]
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasFaction checks if a faction exists by ID
|
// HasFaction checks if a faction exists by ID
|
||||||
func (ml *MasterList) HasFaction(factionID int32) bool {
|
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
|
// HasFactionByName checks if a faction exists by name
|
||||||
func (ml *MasterList) HasFactionByName(name string) bool {
|
func (ml *MasterList) HasFactionByName(name string) bool {
|
||||||
ml.mutex.RLock()
|
ml.mutex.RLock()
|
||||||
defer ml.mutex.RUnlock()
|
defer ml.mutex.RUnlock()
|
||||||
_, exists := ml.factionNameList[name]
|
_, exists := ml.byName[strings.ToLower(name)]
|
||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFaction removes a faction by ID
|
// RemoveFaction removes a faction and updates all indices
|
||||||
func (ml *MasterList) RemoveFaction(factionID int32) bool {
|
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()
|
ml.mutex.Lock()
|
||||||
defer ml.mutex.Unlock()
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
// Remove from name lookup
|
faction, exists := ml.factions[factionID]
|
||||||
delete(ml.factionNameList, faction.Name)
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from core storage
|
||||||
|
delete(ml.factions, factionID)
|
||||||
|
|
||||||
|
// Update all indices
|
||||||
|
ml.updateFactionIndices(faction, false)
|
||||||
|
|
||||||
// Remove from relationship maps
|
// Remove from relationship maps
|
||||||
delete(ml.hostileFactions, factionID)
|
delete(ml.hostileFactions, factionID)
|
||||||
@ -117,10 +229,13 @@ func (ml *MasterList) RemoveFaction(factionID int32) bool {
|
|||||||
ml.friendlyFactions[id] = newFriendlies
|
ml.friendlyFactions[id] = newFriendlies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate metadata cache
|
||||||
|
ml.metaStale = true
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateFaction updates an existing faction
|
// UpdateFaction updates an existing faction and refreshes indices
|
||||||
func (ml *MasterList) UpdateFaction(faction *Faction) error {
|
func (ml *MasterList) UpdateFaction(faction *Faction) error {
|
||||||
if faction == nil {
|
if faction == nil {
|
||||||
return fmt.Errorf("faction cannot be 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")
|
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()
|
ml.mutex.Lock()
|
||||||
defer ml.mutex.Unlock()
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
// If name changed, update name map
|
// Check if exists
|
||||||
if oldFaction.Name != faction.Name {
|
old, exists := ml.factions[faction.ID]
|
||||||
delete(ml.factionNameList, oldFaction.Name)
|
if !exists {
|
||||||
ml.factionNameList[faction.Name] = faction
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFactionCount returns the total number of factions
|
// GetFactionCount returns the total number of factions
|
||||||
func (ml *MasterList) GetFactionCount() int32 {
|
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 {
|
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
|
// GetFactionIDs returns all faction IDs
|
||||||
func (ml *MasterList) GetFactionIDs() []int32 {
|
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 {
|
func (ml *MasterList) GetFactionsByType(factionType string) []*Faction {
|
||||||
return ml.MasterList.Filter(func(f *Faction) bool {
|
ml.mutex.RLock()
|
||||||
return f.Type == factionType
|
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
|
// Clear removes all factions and relationships
|
||||||
func (ml *MasterList) Clear() {
|
func (ml *MasterList) Clear() {
|
||||||
ml.MasterList.Clear()
|
|
||||||
|
|
||||||
ml.mutex.Lock()
|
ml.mutex.Lock()
|
||||||
defer ml.mutex.Unlock()
|
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.hostileFactions = make(map[int32][]int32)
|
||||||
ml.friendlyFactions = 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
|
// GetDefaultFactionValue returns the default value for a faction
|
||||||
func (ml *MasterList) GetDefaultFactionValue(factionID int32) int32 {
|
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 {
|
if faction != nil {
|
||||||
return faction.DefaultValue
|
return faction.DefaultValue
|
||||||
}
|
}
|
||||||
@ -197,7 +400,9 @@ func (ml *MasterList) GetDefaultFactionValue(factionID int32) int32 {
|
|||||||
|
|
||||||
// GetIncreaseAmount returns the default increase amount for a faction
|
// GetIncreaseAmount returns the default increase amount for a faction
|
||||||
func (ml *MasterList) GetIncreaseAmount(factionID int32) int32 {
|
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 {
|
if faction != nil {
|
||||||
return int32(faction.PositiveChange)
|
return int32(faction.PositiveChange)
|
||||||
}
|
}
|
||||||
@ -206,7 +411,9 @@ func (ml *MasterList) GetIncreaseAmount(factionID int32) int32 {
|
|||||||
|
|
||||||
// GetDecreaseAmount returns the default decrease amount for a faction
|
// GetDecreaseAmount returns the default decrease amount for a faction
|
||||||
func (ml *MasterList) GetDecreaseAmount(factionID int32) int32 {
|
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 {
|
if faction != nil {
|
||||||
return int32(faction.NegativeChange)
|
return int32(faction.NegativeChange)
|
||||||
}
|
}
|
||||||
@ -216,7 +423,9 @@ func (ml *MasterList) GetDecreaseAmount(factionID int32) int32 {
|
|||||||
// GetFactionNameByID returns the faction name for a given ID
|
// GetFactionNameByID returns the faction name for a given ID
|
||||||
func (ml *MasterList) GetFactionNameByID(factionID int32) string {
|
func (ml *MasterList) GetFactionNameByID(factionID int32) string {
|
||||||
if factionID > 0 {
|
if factionID > 0 {
|
||||||
faction := ml.MasterList.Get(factionID)
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
faction := ml.factions[factionID]
|
||||||
if faction != nil {
|
if faction != nil {
|
||||||
return faction.Name
|
return faction.Name
|
||||||
}
|
}
|
||||||
@ -270,103 +479,111 @@ func (ml *MasterList) ValidateFactions() []string {
|
|||||||
defer ml.mutex.RUnlock()
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
var issues []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
|
// Pass 1: Validate main faction list
|
||||||
for id, faction := range allFactions {
|
for id, faction := range ml.factions {
|
||||||
if faction == nil {
|
if faction == nil {
|
||||||
if issues == nil {
|
issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id))
|
||||||
issues = make([]string, 0, 10)
|
continue
|
||||||
}
|
}
|
||||||
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 <= 0 || faction.Name == "" {
|
|
||||||
if issues == nil {
|
if faction.ID != id {
|
||||||
issues = make([]string, 0, 10)
|
issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID))
|
||||||
}
|
}
|
||||||
issues = append(issues, fmt.Sprintf("Faction ID %d is invalid or unnamed", id))
|
}
|
||||||
}
|
|
||||||
|
// Pass 2: Validate byName index
|
||||||
if faction.ID != id {
|
for name, faction := range ml.byName {
|
||||||
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 {
|
|
||||||
if faction == nil {
|
if faction == nil {
|
||||||
if issues == nil {
|
|
||||||
issues = make([]string, 0, 10)
|
|
||||||
}
|
|
||||||
issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name))
|
issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if faction.Name != name {
|
if strings.ToLower(faction.Name) != name {
|
||||||
if issues == nil {
|
issues = append(issues, fmt.Sprintf("Faction name index mismatch: map key '%s' != lowercase faction name '%s'", name, strings.ToLower(faction.Name)))
|
||||||
issues = make([]string, 0, 10)
|
|
||||||
}
|
|
||||||
issues = append(issues, fmt.Sprintf("Faction name mismatch: map key '%s' != faction name '%s'", name, faction.Name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := seenIDs[faction.ID]; !ok {
|
if _, ok := ml.factions[faction.ID]; !ok {
|
||||||
if issues == nil {
|
issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name index but not in main storage", faction.Name, faction.ID))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 3: Validate relationships using prebuilt seenIDs
|
// Pass 3: Validate byType index
|
||||||
for sourceID, targets := range ml.hostileFactions {
|
for factionType, factions := range ml.byType {
|
||||||
if _, ok := seenIDs[sourceID]; !ok {
|
for _, faction := range factions {
|
||||||
if issues == nil {
|
if faction == nil {
|
||||||
issues = make([]string, 0, 10)
|
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))
|
issues = append(issues, fmt.Sprintf("Hostile relationship defined for non-existent faction %d", sourceID))
|
||||||
}
|
}
|
||||||
for _, targetID := range targets {
|
for _, targetID := range targets {
|
||||||
if _, ok := seenIDs[targetID]; !ok {
|
if _, ok := ml.factions[targetID]; !ok {
|
||||||
if issues == nil {
|
issues = append(issues, fmt.Sprintf("Faction %d has hostile relationship with non-existent faction %d", sourceID, targetID))
|
||||||
issues = make([]string, 0, 10)
|
|
||||||
}
|
|
||||||
issues = append(issues, fmt.Sprintf("Faction %d has Hostile relationship with non-existent faction %d", sourceID, targetID))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for sourceID, targets := range ml.friendlyFactions {
|
for sourceID, targets := range ml.friendlyFactions {
|
||||||
if _, ok := seenIDs[sourceID]; !ok {
|
if _, ok := ml.factions[sourceID]; !ok {
|
||||||
if issues == nil {
|
|
||||||
issues = make([]string, 0, 10)
|
|
||||||
}
|
|
||||||
issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", sourceID))
|
issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", sourceID))
|
||||||
}
|
}
|
||||||
for _, targetID := range targets {
|
for _, targetID := range targets {
|
||||||
if _, ok := seenIDs[targetID]; !ok {
|
if _, ok := ml.factions[targetID]; !ok {
|
||||||
if issues == nil {
|
issues = append(issues, fmt.Sprintf("Faction %d has friendly relationship with non-existent faction %d", sourceID, targetID))
|
||||||
issues = make([]string, 0, 10)
|
|
||||||
}
|
|
||||||
issues = append(issues, fmt.Sprintf("Faction %d has Friendly relationship with non-existent faction %d", sourceID, targetID))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if issues == nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return issues
|
return issues
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,3 +592,139 @@ func (ml *MasterList) IsValid() bool {
|
|||||||
issues := ml.ValidateFactions()
|
issues := ml.ValidateFactions()
|
||||||
return len(issues) == 0
|
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
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user