simplify and improve faction master list validation

This commit is contained in:
Sky Johnson 2025-08-02 12:18:01 -05:00
parent d817080bc9
commit e210141b8f
3 changed files with 135 additions and 143 deletions

View File

@ -9,34 +9,34 @@ import (
// Stress test MasterFactionList with concurrent operations // Stress test MasterFactionList with concurrent operations
func TestMasterFactionListConcurrency(t *testing.T) { func TestMasterFactionListConcurrency(t *testing.T) {
mfl := NewMasterFactionList() mfl := NewMasterFactionList()
// Pre-populate with some factions // Pre-populate with some factions
for i := int32(1); i <= 10; i++ { for i := int32(1); i <= 10; i++ {
faction := NewFaction(i, "Test Faction", "Test", "Test faction") faction := NewFaction(i, "Test Faction", "Test", "Test faction")
mfl.AddFaction(faction) mfl.AddFaction(faction)
} }
const numGoroutines = 100 const numGoroutines = 100
const operationsPerGoroutine = 100 const operationsPerGoroutine = 100
var wg sync.WaitGroup var wg sync.WaitGroup
t.Run("ConcurrentFactionAccess", func(t *testing.T) { t.Run("ConcurrentFactionAccess", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
// Mix of read operations // Mix of read operations
faction := mfl.GetFaction(factionID) faction := mfl.GetFaction(factionID)
if faction == nil { if faction == nil {
t.Errorf("Goroutine %d: Expected faction %d to exist", goroutineID, factionID) t.Errorf("Goroutine %d: Expected faction %d to exist", goroutineID, factionID)
} }
_ = mfl.GetFactionByName("Test Faction") _ = mfl.GetFactionByName("Test Faction")
_ = mfl.HasFaction(factionID) _ = mfl.HasFaction(factionID)
_ = mfl.GetFactionCount() _ = mfl.GetFactionCount()
@ -46,21 +46,21 @@ func TestMasterFactionListConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
t.Run("ConcurrentRelationshipManagement", func(t *testing.T) { t.Run("ConcurrentRelationshipManagement", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
targetID := int32(((goroutineID+1)%10) + 1) targetID := int32(((goroutineID + 1) % 10) + 1)
// Concurrent relationship operations // Concurrent relationship operations
if goroutineID%2 == 0 { if goroutineID%2 == 0 {
mfl.AddHostileFaction(factionID, targetID) mfl.AddHostileFaction(factionID, targetID)
@ -72,26 +72,26 @@ func TestMasterFactionListConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
t.Run("ConcurrentFactionModification", func(t *testing.T) { t.Run("ConcurrentFactionModification", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
newFactionID := int32(1000 + goroutineID*operationsPerGoroutine + j) newFactionID := int32(1000 + goroutineID*operationsPerGoroutine + j)
faction := NewFaction(newFactionID, "Concurrent Faction", "Test", "Concurrent test") faction := NewFaction(newFactionID, "Concurrent Faction", "Test", "Concurrent test")
err := mfl.AddFaction(faction) err := mfl.AddFaction(faction)
if err != nil { if err != nil {
t.Errorf("Goroutine %d: Failed to add faction %d: %v", goroutineID, newFactionID, err) t.Errorf("Goroutine %d: Failed to add faction %d: %v", goroutineID, newFactionID, err)
} }
// Verify it was added // Verify it was added
retrieved := mfl.GetFaction(newFactionID) retrieved := mfl.GetFaction(newFactionID)
if retrieved == nil { if retrieved == nil {
@ -100,7 +100,7 @@ func TestMasterFactionListConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
} }
@ -108,7 +108,7 @@ func TestMasterFactionListConcurrency(t *testing.T) {
// Stress test PlayerFaction with concurrent operations // Stress test PlayerFaction with concurrent operations
func TestPlayerFactionConcurrency(t *testing.T) { func TestPlayerFactionConcurrency(t *testing.T) {
mfl := NewMasterFactionList() mfl := NewMasterFactionList()
// Add test factions with proper values // Add test factions with proper values
for i := int32(1); i <= 10; i++ { for i := int32(1); i <= 10; i++ {
faction := NewFaction(i, "Test Faction", "Test", "Test faction") faction := NewFaction(i, "Test Faction", "Test", "Test faction")
@ -116,24 +116,24 @@ func TestPlayerFactionConcurrency(t *testing.T) {
faction.NegativeChange = 50 faction.NegativeChange = 50
mfl.AddFaction(faction) mfl.AddFaction(faction)
} }
pf := NewPlayerFaction(mfl) pf := NewPlayerFaction(mfl)
const numGoroutines = 100 const numGoroutines = 100
const operationsPerGoroutine = 100 const operationsPerGoroutine = 100
var wg sync.WaitGroup var wg sync.WaitGroup
t.Run("ConcurrentFactionValueOperations", func(t *testing.T) { t.Run("ConcurrentFactionValueOperations", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
// Mix of faction value operations // Mix of faction value operations
switch j % 4 { switch j % 4 {
case 0: case 0:
@ -150,27 +150,27 @@ func TestPlayerFactionConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
t.Run("ConcurrentUpdateManagement", func(t *testing.T) { t.Run("ConcurrentUpdateManagement", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
// Operations that trigger updates // Operations that trigger updates
pf.IncreaseFaction(factionID, 1) pf.IncreaseFaction(factionID, 1)
// Check for pending updates // Check for pending updates
_ = pf.HasPendingUpdates() _ = pf.HasPendingUpdates()
_ = pf.GetPendingUpdates() _ = pf.GetPendingUpdates()
// Occasionally clear updates // Occasionally clear updates
if j%10 == 0 { if j%10 == 0 {
pf.ClearPendingUpdates() pf.ClearPendingUpdates()
@ -178,22 +178,22 @@ func TestPlayerFactionConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
t.Run("ConcurrentPacketGeneration", func(t *testing.T) { t.Run("ConcurrentPacketGeneration", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
// Trigger faction updates // Trigger faction updates
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
pf.IncreaseFaction(factionID, 1) pf.IncreaseFaction(factionID, 1)
// Generate packets concurrently // Generate packets concurrently
_, err := pf.FactionUpdate(int16(goroutineID % 10)) _, err := pf.FactionUpdate(int16(goroutineID % 10))
if err != nil { if err != nil {
@ -202,7 +202,7 @@ func TestPlayerFactionConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
} }
@ -210,22 +210,22 @@ func TestPlayerFactionConcurrency(t *testing.T) {
// Stress test Manager with concurrent operations // Stress test Manager with concurrent operations
func TestManagerConcurrency(t *testing.T) { func TestManagerConcurrency(t *testing.T) {
manager := NewManager(nil, nil) // No database or logger for testing manager := NewManager(nil, nil) // No database or logger for testing
const numGoroutines = 100 const numGoroutines = 100
const operationsPerGoroutine = 100 const operationsPerGoroutine = 100
var wg sync.WaitGroup var wg sync.WaitGroup
t.Run("ConcurrentStatisticsOperations", func(t *testing.T) { t.Run("ConcurrentStatisticsOperations", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
// Mix of statistics operations // Mix of statistics operations
switch j % 4 { switch j % 4 {
case 0: case 0:
@ -240,56 +240,56 @@ func TestManagerConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
// Verify statistics consistency // Verify statistics consistency
stats := manager.GetStatistics() stats := manager.GetStatistics()
totalChanges := stats["total_faction_changes"].(int64) totalChanges := stats["total_faction_changes"].(int64)
increases := stats["faction_increases"].(int64) increases := stats["faction_increases"].(int64)
decreases := stats["faction_decreases"].(int64) decreases := stats["faction_decreases"].(int64)
if totalChanges != increases+decreases { if totalChanges != increases+decreases {
t.Errorf("Statistics inconsistency: total %d != increases %d + decreases %d", t.Errorf("Statistics inconsistency: total %d != increases %d + decreases %d",
totalChanges, increases, decreases) totalChanges, increases, decreases)
} }
}) })
t.Run("ConcurrentFactionManagement", func(t *testing.T) { t.Run("ConcurrentFactionManagement", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32(2000 + goroutineID*operationsPerGoroutine + j) factionID := int32(2000 + goroutineID*operationsPerGoroutine + j)
faction := NewFaction(factionID, "Manager Test", "Test", "Manager test") faction := NewFaction(factionID, "Manager Test", "Test", "Manager test")
// Add faction // Add faction
err := manager.AddFaction(faction) err := manager.AddFaction(faction)
if err != nil { if err != nil {
t.Errorf("Goroutine %d: Failed to add faction %d: %v", goroutineID, factionID, err) t.Errorf("Goroutine %d: Failed to add faction %d: %v", goroutineID, factionID, err)
continue continue
} }
// Read operations // Read operations
_ = manager.GetFaction(factionID) _ = manager.GetFaction(factionID)
_ = manager.GetFactionByName("Manager Test") _ = manager.GetFactionByName("Manager Test")
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
t.Run("ConcurrentPlayerFactionCreation", func(t *testing.T) { t.Run("ConcurrentPlayerFactionCreation", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
// Create player factions concurrently // Create player factions concurrently
pf := manager.CreatePlayerFaction() pf := manager.CreatePlayerFaction()
@ -299,14 +299,14 @@ func TestManagerConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
// Check statistics // Check statistics
stats := manager.GetStatistics() stats := manager.GetStatistics()
expectedPlayers := int64(numGoroutines * operationsPerGoroutine) expectedPlayers := int64(numGoroutines * operationsPerGoroutine)
actualPlayers := stats["players_with_factions"].(int64) actualPlayers := stats["players_with_factions"].(int64)
if actualPlayers != expectedPlayers { if actualPlayers != expectedPlayers {
t.Errorf("Expected %d players with factions, got %d", expectedPlayers, actualPlayers) t.Errorf("Expected %d players with factions, got %d", expectedPlayers, actualPlayers)
} }
@ -316,26 +316,26 @@ func TestManagerConcurrency(t *testing.T) {
// Stress test EntityFactionAdapter with concurrent operations // Stress test EntityFactionAdapter with concurrent operations
func TestEntityFactionAdapterConcurrency(t *testing.T) { func TestEntityFactionAdapterConcurrency(t *testing.T) {
manager := NewManager(nil, nil) manager := NewManager(nil, nil)
// Mock entity // Mock entity
entity := &mockEntity{id: 123, name: "Test Entity", dbID: 456} entity := &mockEntity{id: 123, name: "Test Entity", dbID: 456}
adapter := NewEntityFactionAdapter(entity, manager, nil) adapter := NewEntityFactionAdapter(entity, manager, nil)
const numGoroutines = 100 const numGoroutines = 100
const operationsPerGoroutine = 100 const operationsPerGoroutine = 100
var wg sync.WaitGroup var wg sync.WaitGroup
t.Run("ConcurrentFactionIDOperations", func(t *testing.T) { t.Run("ConcurrentFactionIDOperations", func(t *testing.T) {
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
// Mix of faction ID operations // Mix of faction ID operations
switch j % 3 { switch j % 3 {
case 0: case 0:
@ -349,43 +349,43 @@ func TestEntityFactionAdapterConcurrency(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
t.Run("ConcurrentRelationshipChecks", func(t *testing.T) { t.Run("ConcurrentRelationshipChecks", func(t *testing.T) {
// Set up some factions first // Set up some factions first
for i := int32(1); i <= 10; i++ { for i := int32(1); i <= 10; i++ {
faction := NewFaction(i, "Test Faction", "Test", "Test faction") faction := NewFaction(i, "Test Faction", "Test", "Test faction")
manager.AddFaction(faction) manager.AddFaction(faction)
} }
// Set up relationships // Set up relationships
mfl := manager.GetMasterFactionList() mfl := manager.GetMasterFactionList()
mfl.AddHostileFaction(1, 2) mfl.AddHostileFaction(1, 2)
mfl.AddFriendlyFaction(1, 3) mfl.AddFriendlyFaction(1, 3)
adapter.SetFactionID(1) adapter.SetFactionID(1)
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
otherFactionID := int32((goroutineID%10) + 1) otherFactionID := int32((goroutineID % 10) + 1)
// Concurrent relationship checks // Concurrent relationship checks
_ = adapter.IsHostileToFaction(otherFactionID) _ = adapter.IsHostileToFaction(otherFactionID)
_ = adapter.IsFriendlyToFaction(otherFactionID) _ = adapter.IsFriendlyToFaction(otherFactionID)
// Validation // Validation
_ = adapter.ValidateFaction() _ = adapter.ValidateFaction()
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
}) })
} }
@ -413,20 +413,20 @@ func (e *mockEntity) GetDatabaseID() int32 {
func TestDeadlockPrevention(t *testing.T) { func TestDeadlockPrevention(t *testing.T) {
mfl := NewMasterFactionList() mfl := NewMasterFactionList()
manager := NewManager(nil, nil) manager := NewManager(nil, nil)
// Add test factions // Add test factions
for i := int32(1); i <= 10; i++ { for i := int32(1); i <= 10; i++ {
faction := NewFaction(i, "Test Faction", "Test", "Test faction") faction := NewFaction(i, "Test Faction", "Test", "Test faction")
manager.AddFaction(faction) manager.AddFaction(faction)
} }
const numGoroutines = 50 const numGoroutines = 50
var wg sync.WaitGroup var wg sync.WaitGroup
// Test potential deadlock scenarios // Test potential deadlock scenarios
t.Run("MixedOperations", func(t *testing.T) { t.Run("MixedOperations", func(t *testing.T) {
done := make(chan bool, 1) done := make(chan bool, 1)
// Set a timeout to detect deadlocks // Set a timeout to detect deadlocks
go func() { go func() {
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
@ -435,19 +435,18 @@ func TestDeadlockPrevention(t *testing.T) {
return return
default: default:
t.Error("Potential deadlock detected - test timed out") t.Error("Potential deadlock detected - test timed out")
t.FailNow()
} }
}() }()
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < 100; j++ { for j := 0; j < 100; j++ {
factionID := int32((goroutineID%10) + 1) factionID := int32((goroutineID % 10) + 1)
// Mix operations that could potentially deadlock // Mix operations that could potentially deadlock
switch j % 6 { switch j % 6 {
case 0: case 0:
@ -473,7 +472,7 @@ func TestDeadlockPrevention(t *testing.T) {
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
done <- true done <- true
}) })
@ -484,42 +483,42 @@ func TestRaceConditions(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("Skipping race condition test in short mode") t.Skip("Skipping race condition test in short mode")
} }
// This test is designed to be run with: go test -race // This test is designed to be run with: go test -race
mfl := NewMasterFactionList() mfl := NewMasterFactionList()
manager := NewManager(nil, nil) manager := NewManager(nil, nil)
// Rapid concurrent operations to trigger race conditions // Rapid concurrent operations to trigger race conditions
const numGoroutines = 200 const numGoroutines = 200
const operationsPerGoroutine = 50 const operationsPerGoroutine = 50
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(numGoroutines) wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ { for i := 0; i < numGoroutines; i++ {
go func(goroutineID int) { go func(goroutineID int) {
defer wg.Done() defer wg.Done()
for j := 0; j < operationsPerGoroutine; j++ { for j := 0; j < operationsPerGoroutine; j++ {
factionID := int32((goroutineID%20) + 1) factionID := int32((goroutineID % 20) + 1)
// Rapid-fire operations // Rapid-fire operations
faction := NewFaction(factionID, "Race Test", "Test", "Race test") faction := NewFaction(factionID, "Race Test", "Test", "Race test")
mfl.AddFaction(faction) mfl.AddFaction(faction)
_ = mfl.GetFaction(factionID) _ = mfl.GetFaction(factionID)
mfl.AddHostileFaction(factionID, factionID+1) mfl.AddHostileFaction(factionID, factionID+1)
_ = mfl.GetHostileFactions(factionID) _ = mfl.GetHostileFactions(factionID)
manager.AddFaction(faction) manager.AddFaction(faction)
manager.RecordFactionIncrease(factionID) manager.RecordFactionIncrease(factionID)
_ = manager.GetStatistics() _ = manager.GetStatistics()
pf := manager.CreatePlayerFaction() pf := manager.CreatePlayerFaction()
pf.IncreaseFaction(factionID, 1) pf.IncreaseFaction(factionID, 1)
_ = pf.GetFactionValue(factionID) _ = pf.GetFactionValue(factionID)
} }
}(i) }(i)
} }
wg.Wait() wg.Wait()
} }

View File

@ -393,7 +393,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) {
return fmt.Sprintf("Faction '%s' not found.", args[0]), nil return fmt.Sprintf("Faction '%s' not found.", args[0]), nil
} }
result := fmt.Sprintf("Faction Information:\n") result := "Faction Information:\n"
result += fmt.Sprintf("ID: %d\n", faction.ID) result += fmt.Sprintf("ID: %d\n", faction.ID)
result += fmt.Sprintf("Name: %s\n", faction.Name) result += fmt.Sprintf("Name: %s\n", faction.Name)
result += fmt.Sprintf("Type: %s\n", faction.Type) result += fmt.Sprintf("Type: %s\n", faction.Type)

View File

@ -317,30 +317,30 @@ func (mfl *MasterFactionList) ValidateFactions() []string {
mfl.mutex.RLock() mfl.mutex.RLock()
defer mfl.mutex.RUnlock() defer mfl.mutex.RUnlock()
// Pre-allocate slice with reasonable capacity to avoid repeated allocations
issues := make([]string, 0, 10) issues := make([]string, 0, 10)
// Single pass through globalFactionList - check faction validity and build seen set seenIDs := make(map[int32]*Faction, len(mfl.globalFactionList))
seenInGlobal := make(map[int32]bool, len(mfl.globalFactionList)) seenNames := make(map[string]*Faction, len(mfl.factionNameList))
for id, faction := range mfl.globalFactionList {
seenInGlobal[id] = true
// Pass 1: Validate globalFactionList and build seenID map
for id, faction := range mfl.globalFactionList {
if faction == nil { if faction == nil {
issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id)) issues = append(issues, fmt.Sprintf("Faction ID %d is nil", id))
continue continue
} }
// Combine multiple faction checks in one pass (inlined IsValid()) if faction.ID <= 0 || faction.Name == "" {
if faction.ID <= 0 || len(faction.Name) == 0 { issues = append(issues, fmt.Sprintf("Faction ID %d is invalid or unnamed", id))
issues = append(issues, fmt.Sprintf("Faction ID %d is invalid", id))
} }
if faction.ID != id { if faction.ID != id {
issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID)) issues = append(issues, fmt.Sprintf("Faction ID mismatch: map key %d != faction ID %d", id, faction.ID))
} }
seenIDs[id] = faction
} }
// Single pass through factionNameList // Pass 2: Validate factionNameList and build seenName map
for name, faction := range mfl.factionNameList { for name, faction := range mfl.factionNameList {
if faction == nil { if faction == nil {
issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name)) issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name))
@ -351,36 +351,29 @@ func (mfl *MasterFactionList) ValidateFactions() []string {
issues = append(issues, fmt.Sprintf("Faction name mismatch: map key '%s' != faction name '%s'", name, faction.Name)) issues = append(issues, fmt.Sprintf("Faction name mismatch: map key '%s' != faction name '%s'", name, faction.Name))
} }
// Use the pre-built set instead of map lookup if _, ok := seenIDs[faction.ID]; !ok {
if !seenInGlobal[faction.ID] {
issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name map but not in ID map", name, faction.ID)) issues = append(issues, fmt.Sprintf("Faction '%s' (ID %d) exists in name map but not in ID map", name, faction.ID))
} }
seenNames[name] = faction
} }
// Check relationship consistency - use the pre-built set for faster lookups // Pass 3: Validate relationships using prebuilt seenIDs
for factionID, hostiles := range mfl.hostileFactions { validateRelations := func(relations map[int32][]int32, relType string) {
if !seenInGlobal[factionID] { for sourceID, targets := range relations {
issues = append(issues, fmt.Sprintf("Hostile relationship defined for non-existent faction %d", factionID)) if _, ok := seenIDs[sourceID]; !ok {
} issues = append(issues, fmt.Sprintf("%s relationship defined for non-existent faction %d", relType, sourceID))
}
for _, hostileID := range hostiles { for _, targetID := range targets {
if !seenInGlobal[hostileID] { if _, ok := seenIDs[targetID]; !ok {
issues = append(issues, fmt.Sprintf("Faction %d has hostile relationship with non-existent faction %d", factionID, hostileID)) issues = append(issues, fmt.Sprintf("Faction %d has %s relationship with non-existent faction %d", sourceID, relType, targetID))
}
} }
} }
} }
for factionID, friendlies := range mfl.friendlyFactions { validateRelations(mfl.hostileFactions, "Hostile")
if !seenInGlobal[factionID] { validateRelations(mfl.friendlyFactions, "Friendly")
issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", factionID))
}
for _, friendlyID := range friendlies {
if !seenInGlobal[friendlyID] {
issues = append(issues, fmt.Sprintf("Faction %d has friendly relationship with non-existent faction %d", factionID, friendlyID))
}
}
}
return issues return issues
} }