package factions import ( "fmt" "maps" "strings" "sync" "eq2emu/internal/database" ) // 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 { // 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 specialized faction master list func NewMasterList() *MasterList { return &MasterList{ 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, } } // 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") } if !faction.IsValid() { return fmt.Errorf("faction is not valid") } 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) } // 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 retrieves by ID (O(1)) func (ml *MasterList) GetFaction(id int32) *Faction { ml.mutex.RLock() defer ml.mutex.RUnlock() return ml.factions[id] } // 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.byName[strings.ToLower(name)] } // HasFaction checks if a faction exists by ID func (ml *MasterList) HasFaction(factionID int32) bool { 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.byName[strings.ToLower(name)] return exists } // RemoveFaction removes a faction and updates all indices func (ml *MasterList) RemoveFaction(factionID int32) bool { ml.mutex.Lock() defer ml.mutex.Unlock() 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) delete(ml.friendlyFactions, factionID) // Remove references to this faction in other faction's relationships for id, hostiles := range ml.hostileFactions { newHostiles := make([]int32, 0, len(hostiles)) for _, hostileID := range hostiles { if hostileID != factionID { newHostiles = append(newHostiles, hostileID) } } ml.hostileFactions[id] = newHostiles } for id, friendlies := range ml.friendlyFactions { newFriendlies := make([]int32, 0, len(friendlies)) for _, friendlyID := range friendlies { if friendlyID != factionID { newFriendlies = append(newFriendlies, friendlyID) } } ml.friendlyFactions[id] = newFriendlies } // Invalidate metadata cache ml.metaStale = true return true } // 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") } if !faction.IsValid() { return fmt.Errorf("faction is not valid") } ml.mutex.Lock() defer ml.mutex.Unlock() // 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 { ml.mutex.RLock() defer ml.mutex.RUnlock() return int32(len(ml.factions)) } // GetAllFactions returns a copy of all factions map func (ml *MasterList) GetAllFactions() map[int32]*Faction { 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 { 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 (O(1)) func (ml *MasterList) GetFactionsByType(factionType string) []*Faction { 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.mutex.Lock() defer ml.mutex.Unlock() // 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 { ml.mutex.RLock() defer ml.mutex.RUnlock() faction := ml.factions[factionID] if faction != nil { return faction.DefaultValue } return 0 } // GetIncreaseAmount returns the default increase amount for a faction func (ml *MasterList) GetIncreaseAmount(factionID int32) int32 { ml.mutex.RLock() defer ml.mutex.RUnlock() faction := ml.factions[factionID] if faction != nil { return int32(faction.PositiveChange) } return 0 } // GetDecreaseAmount returns the default decrease amount for a faction func (ml *MasterList) GetDecreaseAmount(factionID int32) int32 { ml.mutex.RLock() defer ml.mutex.RUnlock() faction := ml.factions[factionID] if faction != nil { return int32(faction.NegativeChange) } return 0 } // GetFactionNameByID returns the faction name for a given ID func (ml *MasterList) GetFactionNameByID(factionID int32) string { if factionID > 0 { ml.mutex.RLock() defer ml.mutex.RUnlock() faction := ml.factions[factionID] if faction != nil { return faction.Name } } return "" } // AddHostileFaction adds a hostile relationship between factions func (ml *MasterList) AddHostileFaction(factionID, hostileFactionID int32) { ml.mutex.Lock() defer ml.mutex.Unlock() ml.hostileFactions[factionID] = append(ml.hostileFactions[factionID], hostileFactionID) } // AddFriendlyFaction adds a friendly relationship between factions func (ml *MasterList) AddFriendlyFaction(factionID, friendlyFactionID int32) { ml.mutex.Lock() defer ml.mutex.Unlock() ml.friendlyFactions[factionID] = append(ml.friendlyFactions[factionID], friendlyFactionID) } // GetFriendlyFactions returns all friendly factions for a given faction func (ml *MasterList) GetFriendlyFactions(factionID int32) []int32 { ml.mutex.RLock() defer ml.mutex.RUnlock() if factions, exists := ml.friendlyFactions[factionID]; exists { result := make([]int32, len(factions)) copy(result, factions) return result } return nil } // GetHostileFactions returns all hostile factions for a given faction func (ml *MasterList) GetHostileFactions(factionID int32) []int32 { ml.mutex.RLock() defer ml.mutex.RUnlock() if factions, exists := ml.hostileFactions[factionID]; exists { result := make([]int32, len(factions)) copy(result, factions) return result } return nil } // ValidateFactions checks all factions for consistency func (ml *MasterList) ValidateFactions() []string { ml.mutex.RLock() defer ml.mutex.RUnlock() var issues []string // 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 { issues = append(issues, fmt.Sprintf("Faction name '%s' maps to nil", name)) continue } 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 := 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 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 := 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 := ml.factions[sourceID]; !ok { issues = append(issues, fmt.Sprintf("Friendly relationship defined for non-existent faction %d", sourceID)) } for _, targetID := range targets { if _, ok := ml.factions[targetID]; !ok { issues = append(issues, fmt.Sprintf("Faction %d has friendly relationship with non-existent faction %d", sourceID, targetID)) } } } return issues } // IsValid returns true if all factions are valid 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 }