package factions import ( "context" "fmt" "maps" "strings" "sync" "eq2emu/internal/database" "eq2emu/internal/packets" ) // Faction represents a single faction with its properties type Faction struct { ID int32 // Faction ID Name string // Faction name Type string // Faction type/category Description string // Faction description NegativeChange int16 // Amount faction decreases by default PositiveChange int16 // Amount faction increases by default DefaultValue int32 // Default faction value for new characters } // GetID returns the faction ID func (f *Faction) GetID() int32 { return f.ID } // GetName returns the faction name func (f *Faction) GetName() string { return f.Name } // GetType returns the faction type func (f *Faction) GetType() string { return f.Type } // GetDescription returns the faction description func (f *Faction) GetDescription() string { return f.Description } // GetNegativeChange returns the default decrease amount func (f *Faction) GetNegativeChange() int16 { return f.NegativeChange } // GetPositiveChange returns the default increase amount func (f *Faction) GetPositiveChange() int16 { return f.PositiveChange } // GetDefaultValue returns the default faction value func (f *Faction) GetDefaultValue() int32 { return f.DefaultValue } // IsValid returns true if the faction has valid data func (f *Faction) IsValid() bool { return f.ID > 0 && len(f.Name) > 0 } // IsSpecialFaction returns true if this is a special faction (ID <= 10) func (f *Faction) IsSpecialFaction() bool { return f.ID <= SpecialFactionIDMax } // CanIncrease returns true if this faction can be increased func (f *Faction) CanIncrease() bool { return !f.IsSpecialFaction() && f.PositiveChange != 0 } // CanDecrease returns true if this faction can be decreased func (f *Faction) CanDecrease() bool { return !f.IsSpecialFaction() && f.NegativeChange != 0 } // PlayerFaction manages faction standing for a single player type PlayerFaction struct { factionValues map[int32]int32 // Faction ID -> current value factionUpdateNeeded []int32 // Factions that need client updates factionManager *FactionManager updateMutex sync.Mutex // Thread safety for updates mutex sync.RWMutex // Thread safety for faction data } // NewPlayerFaction creates a new player faction system func NewPlayerFaction(factionManager *FactionManager) *PlayerFaction { return &PlayerFaction{ factionValues: make(map[int32]int32), factionManager: factionManager, } } // GetMaxValue returns the maximum faction value for a given consideration level func (pf *PlayerFaction) GetMaxValue(con int8) int32 { if con < 0 { return int32(con) * ConMultiplier } return (int32(con) * ConMultiplier) + ConRemainder } // GetMinValue returns the minimum faction value for a given consideration level func (pf *PlayerFaction) GetMinValue(con int8) int32 { if con <= 0 { return (int32(con) * ConMultiplier) - ConRemainder } return int32(con) * ConMultiplier } // ShouldAttack returns true if the player should attack based on faction func (pf *PlayerFaction) ShouldAttack(factionID int32) bool { return pf.GetCon(factionID) <= AttackThreshold } // GetCon returns the consideration level (-4 to 4) for a faction func (pf *PlayerFaction) GetCon(factionID int32) int8 { // Special faction IDs have predefined cons if factionID <= SpecialFactionIDMax { if factionID == 0 { return ConIndiff } return int8(factionID - 5) } value := pf.GetFactionValue(factionID) // Neutral range if value >= ConNeutralMin && value <= ConNeutralMax { return ConIndiff } // Maximum ally if value >= ConAllyMin { return ConAlly } // Maximum hostile if value <= ConHostileMax { return ConKOS } // Calculate con based on value return int8(value / ConMultiplier) } // GetPercent returns the percentage within the current consideration level func (pf *PlayerFaction) GetPercent(factionID int32) int8 { // Special factions have no percentage if factionID <= SpecialFactionIDMax { return 0 } con := pf.GetCon(factionID) value := pf.GetFactionValue(factionID) if con != ConIndiff { // Make value positive for calculation if value <= 0 { value *= -1 } // Make con positive for calculation if con < 0 { con *= -1 } // Calculate percentage within the con level value -= int32(con) * ConMultiplier value *= PercentMultiplier return int8(value / ConMultiplier) } else { // Neutral range calculation value += PercentNeutralOffset value *= PercentMultiplier return int8(value / PercentNeutralDivisor) } } // GetFactionValue returns the current faction value for a faction func (pf *PlayerFaction) GetFactionValue(factionID int32) int32 { // Special factions always return 0 if factionID <= SpecialFactionIDMax { return 0 } pf.mutex.RLock() defer pf.mutex.RUnlock() // Return current value or 0 if not set return pf.factionValues[factionID] } // ShouldIncrease returns true if the faction can be increased func (pf *PlayerFaction) ShouldIncrease(factionID int32) bool { if factionID <= SpecialFactionIDMax { return false } faction := pf.factionManager.GetFaction(factionID) if faction == nil { return false } return faction.PositiveChange != 0 } // ShouldDecrease returns true if the faction can be decreased func (pf *PlayerFaction) ShouldDecrease(factionID int32) bool { if factionID <= SpecialFactionIDMax { return false } faction := pf.factionManager.GetFaction(factionID) if faction == nil { return false } return faction.NegativeChange != 0 } // IncreaseFaction increases a faction value func (pf *PlayerFaction) IncreaseFaction(factionID int32, amount int32) bool { // Special factions cannot be changed if factionID <= SpecialFactionIDMax { return true } pf.mutex.Lock() defer pf.mutex.Unlock() // Use default amount if not specified if amount == 0 { faction := pf.factionManager.GetFaction(factionID) if faction != nil { amount = int32(faction.PositiveChange) } } // Increase the faction value pf.factionValues[factionID] += amount canContinue := true // Cap at maximum value if pf.factionValues[factionID] >= MaxFactionValue { pf.factionValues[factionID] = MaxFactionValue canContinue = false } // Mark for update pf.addFactionUpdateNeeded(factionID) return canContinue } // DecreaseFaction decreases a faction value func (pf *PlayerFaction) DecreaseFaction(factionID int32, amount int32) bool { // Special factions cannot be changed if factionID <= SpecialFactionIDMax { return true } pf.mutex.Lock() defer pf.mutex.Unlock() // Use default amount if not specified if amount == 0 { faction := pf.factionManager.GetFaction(factionID) if faction != nil { amount = int32(faction.NegativeChange) } } // Cannot decrease if no amount specified if amount == 0 { return false } // Decrease the faction value pf.factionValues[factionID] -= amount canContinue := true // Cap at minimum value if pf.factionValues[factionID] <= MinFactionValue { pf.factionValues[factionID] = MinFactionValue canContinue = false } // Mark for update pf.addFactionUpdateNeeded(factionID) return canContinue } // SetFactionValue sets a faction to a specific value func (pf *PlayerFaction) SetFactionValue(factionID int32, value int32) bool { pf.mutex.Lock() defer pf.mutex.Unlock() pf.factionValues[factionID] = value // Mark for update pf.addFactionUpdateNeeded(factionID) return true } // GetFactionValues returns a copy of all faction values func (pf *PlayerFaction) GetFactionValues() map[int32]int32 { pf.mutex.RLock() defer pf.mutex.RUnlock() // Return a copy to prevent external modification result := make(map[int32]int32) for id, value := range pf.factionValues { result[id] = value } return result } // HasFaction returns true if the player has a value for the given faction func (pf *PlayerFaction) HasFaction(factionID int32) bool { pf.mutex.RLock() defer pf.mutex.RUnlock() _, exists := pf.factionValues[factionID] return exists } // GetFactionCount returns the number of factions the player has values for func (pf *PlayerFaction) GetFactionCount() int { pf.mutex.RLock() defer pf.mutex.RUnlock() return len(pf.factionValues) } // ClearFactionValues removes all faction values func (pf *PlayerFaction) ClearFactionValues() { pf.mutex.Lock() defer pf.mutex.Unlock() pf.factionValues = make(map[int32]int32) } // addFactionUpdateNeeded marks a faction as needing an update (internal use, assumes lock held) func (pf *PlayerFaction) addFactionUpdateNeeded(factionID int32) { pf.updateMutex.Lock() defer pf.updateMutex.Unlock() pf.factionUpdateNeeded = append(pf.factionUpdateNeeded, factionID) } // GetPendingUpdates returns factions that need client updates func (pf *PlayerFaction) GetPendingUpdates() []int32 { pf.updateMutex.Lock() defer pf.updateMutex.Unlock() if len(pf.factionUpdateNeeded) == 0 { return nil } // Return a copy result := make([]int32, len(pf.factionUpdateNeeded)) copy(result, pf.factionUpdateNeeded) return result } // ClearPendingUpdates clears the pending update list func (pf *PlayerFaction) ClearPendingUpdates() { pf.updateMutex.Lock() defer pf.updateMutex.Unlock() pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0] } // HasPendingUpdates returns true if there are pending faction updates func (pf *PlayerFaction) HasPendingUpdates() bool { pf.updateMutex.Lock() defer pf.updateMutex.Unlock() return len(pf.factionUpdateNeeded) > 0 } // FactionUpdate builds a faction update packet for the client using the centralized packet system func (pf *PlayerFaction) FactionUpdate(clientVersion uint32) ([]byte, error) { pf.updateMutex.Lock() defer pf.updateMutex.Unlock() if len(pf.factionUpdateNeeded) == 0 { return nil, nil } // Get the FactionUpdate packet definition packet, exists := packets.GetPacket("FactionUpdate") if !exists { return nil, fmt.Errorf("FactionUpdate packet structure not found") } // Build faction update data var factionsData []map[string]interface{} for _, factionID := range pf.factionUpdateNeeded { faction := pf.factionManager.GetFaction(factionID) if faction != nil { factionData := map[string]interface{}{ "faction_id": faction.ID, "name": faction.Name, "category": faction.Type, "description": faction.Description, "unknown": "", "con": pf.GetCon(faction.ID), "percentage": pf.GetPercent(faction.ID), "value": pf.GetFactionValue(faction.ID), } // Add unknown2 field for version 562+ if clientVersion >= 562 { factionData["unknown2"] = uint8(0) } factionsData = append(factionsData, factionData) } } // Build the packet data packetData := map[string]interface{}{ "num_factions": uint16(len(factionsData)), "response_array": factionsData, "unknown3": uint8(0), } // Create packet builder builder := packets.NewPacketBuilder(packet, clientVersion, 0) // Build the packet data, err := builder.Build(packetData) if err != nil { return nil, fmt.Errorf("failed to build faction update packet: %w", err) } // Clear the update list pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0] return data, nil } // Logger interface for faction logging type Logger interface { LogInfo(system, format string, args ...interface{}) LogError(system, format string, args ...interface{}) LogDebug(system, format string, args ...interface{}) LogWarning(system, format string, args ...interface{}) } // FactionManager provides unified management of the faction system type FactionManager struct { // Core storage with specialized indices for O(1) lookups factions map[int32]*Faction // ID -> Faction 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 // External dependencies database *database.Database logger Logger mutex sync.RWMutex // Statistics totalFactionChanges int64 factionIncreases int64 factionDecreases int64 factionLookups int64 playersWithFactions int64 changesByFaction map[int32]int64 // Faction ID -> total changes packetsSent int64 packetErrors int64 } // NewFactionManager creates a new unified faction manager func NewFactionManager(db *database.Database, logger Logger) *FactionManager { return &FactionManager{ 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), database: db, logger: logger, changesByFaction: make(map[int32]int64), } } // Initialize loads factions and relationships from database func (fm *FactionManager) Initialize(ctx context.Context) error { if fm.logger != nil { fm.logger.LogInfo("factions", "Initializing faction manager...") } if fm.database == nil { if fm.logger != nil { fm.logger.LogWarning("factions", "No database provided, starting with empty faction list") } return nil } // Load factions if err := fm.loadFactionsFromDB(); err != nil { return fmt.Errorf("failed to load factions from database: %w", err) } // Load faction relationships if err := fm.loadFactionRelationsFromDB(); err != nil { if fm.logger != nil { fm.logger.LogWarning("factions", "Failed to load faction relationships: %v", err) } } if fm.logger != nil { fm.logger.LogInfo("factions", "Loaded %d factions from database", len(fm.factions)) } return nil } // loadFactionsFromDB loads all factions from the database (internal method) func (fm *FactionManager) loadFactionsFromDB() error { // Create factions table if it doesn't exist _, err := fm.database.Exec(` CREATE TABLE IF NOT EXISTS factions ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, type TEXT, description TEXT, negative_change INTEGER DEFAULT 0, positive_change INTEGER DEFAULT 0, default_value INTEGER DEFAULT 0 ) `) if err != nil { return fmt.Errorf("failed to create factions table: %w", err) } rows, err := fm.database.Query("SELECT id, name, type, description, negative_change, positive_change, default_value FROM factions ORDER BY id") if err != nil { return fmt.Errorf("failed to query factions: %w", err) } defer rows.Close() count := 0 for rows.Next() { faction := &Faction{} 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 := fm.addFactionToIndices(faction); err != nil { if fm.logger != nil { fm.logger.LogError("factions", "Failed to add faction %d (%s): %v", faction.ID, faction.Name, err) } continue } count++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating faction rows: %w", err) } return nil } // loadFactionRelationsFromDB loads faction relationships from database (internal method) func (fm *FactionManager) loadFactionRelationsFromDB() error { // Create faction_relations table if it doesn't exist _, err := fm.database.Exec(` CREATE TABLE IF NOT EXISTS faction_relations ( faction_id INTEGER NOT NULL, related_faction_id INTEGER NOT NULL, is_hostile INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (faction_id, related_faction_id), FOREIGN KEY (faction_id) REFERENCES factions(id), FOREIGN KEY (related_faction_id) REFERENCES factions(id) ) `) if err != nil { return fmt.Errorf("failed to create faction_relations table: %w", err) } rows, err := fm.database.Query("SELECT faction_id, related_faction_id, is_hostile FROM faction_relations") if err != nil { return fmt.Errorf("failed to load faction relations: %w", err) } defer rows.Close() for rows.Next() { var factionID, relatedID int32 var isHostile bool if err := rows.Scan(&factionID, &relatedID, &isHostile); err != nil { return fmt.Errorf("failed to scan faction relation: %w", err) } if isHostile { fm.hostileFactions[factionID] = append(fm.hostileFactions[factionID], relatedID) } else { fm.friendlyFactions[factionID] = append(fm.friendlyFactions[factionID], relatedID) } } return rows.Err() } // addFactionToIndices adds a faction to all internal indices (internal method) func (fm *FactionManager) addFactionToIndices(faction *Faction) error { if faction == nil { return fmt.Errorf("faction cannot be nil") } if !faction.IsValid() { return fmt.Errorf("faction is not valid") } // Check if exists if _, exists := fm.factions[faction.ID]; exists { return fmt.Errorf("faction with ID %d already exists", faction.ID) } // Add to core storage fm.factions[faction.ID] = faction // Add to name index fm.byName[strings.ToLower(faction.GetName())] = faction // Add to type index factionType := faction.GetType() if factionType != "" { fm.byType[factionType] = append(fm.byType[factionType], faction) } // Add to special/regular index if faction.IsSpecialFaction() { fm.specialFactions[faction.ID] = faction } else { fm.regularFactions[faction.ID] = faction } return nil } // CreatePlayerFaction creates a new player faction system func (fm *FactionManager) CreatePlayerFaction() *PlayerFaction { fm.mutex.Lock() fm.playersWithFactions++ fm.mutex.Unlock() return NewPlayerFaction(fm) } // GetFaction returns a faction by ID with statistics tracking func (fm *FactionManager) GetFaction(factionID int32) *Faction { fm.mutex.Lock() fm.factionLookups++ fm.mutex.Unlock() fm.mutex.RLock() defer fm.mutex.RUnlock() return fm.factions[factionID] } // GetFactionByName returns a faction by name (case-insensitive) func (fm *FactionManager) GetFactionByName(name string) *Faction { fm.mutex.Lock() fm.factionLookups++ fm.mutex.Unlock() fm.mutex.RLock() defer fm.mutex.RUnlock() return fm.byName[strings.ToLower(name)] } // GetAllFactions returns a copy of all factions map func (fm *FactionManager) GetAllFactions() map[int32]*Faction { fm.mutex.RLock() defer fm.mutex.RUnlock() // Return a copy to prevent external modification result := make(map[int32]*Faction, len(fm.factions)) maps.Copy(result, fm.factions) return result } // GetFactionCount returns the total number of factions func (fm *FactionManager) GetFactionCount() int32 { fm.mutex.RLock() defer fm.mutex.RUnlock() return int32(len(fm.factions)) } // AddFaction adds a new faction with database persistence func (fm *FactionManager) AddFaction(faction *Faction) error { if faction == nil { return fmt.Errorf("faction cannot be nil") } fm.mutex.Lock() defer fm.mutex.Unlock() // Add to indices if err := fm.addFactionToIndices(faction); err != nil { return fmt.Errorf("failed to add faction to indices: %w", err) } // Save to database if available if fm.database != nil { if err := fm.saveFactionToDBInternal(faction); err != nil { // Remove from indices if save failed fm.removeFactionFromIndicesInternal(faction.ID) return fmt.Errorf("failed to save faction to database: %w", err) } } if fm.logger != nil { fm.logger.LogInfo("factions", "Added faction %d: %s (%s)", faction.ID, faction.Name, faction.Type) } return nil } // saveFactionToDBInternal saves a faction to database (internal method, assumes lock held) func (fm *FactionManager) saveFactionToDBInternal(faction *Faction) error { query := `INSERT OR REPLACE INTO factions (id, name, type, description, negative_change, positive_change, default_value) VALUES (?, ?, ?, ?, ?, ?, ?)` _, err := fm.database.Exec(query, faction.ID, faction.Name, faction.Type, faction.Description, faction.NegativeChange, faction.PositiveChange, faction.DefaultValue) return err } // removeFactionFromIndicesInternal removes faction from all indices (internal method, assumes lock held) func (fm *FactionManager) removeFactionFromIndicesInternal(factionID int32) { faction, exists := fm.factions[factionID] if !exists { return } // Remove from core storage delete(fm.factions, factionID) // Remove from name index delete(fm.byName, strings.ToLower(faction.GetName())) // Remove from type index factionType := faction.GetType() if factionType != "" { typeFactionsSlice := fm.byType[factionType] for i, f := range typeFactionsSlice { if f.ID == faction.ID { fm.byType[factionType] = append(typeFactionsSlice[:i], typeFactionsSlice[i+1:]...) break } } } // Remove from special/regular index delete(fm.specialFactions, factionID) delete(fm.regularFactions, factionID) } // GetHostileFactions returns all hostile factions for a given faction func (fm *FactionManager) GetHostileFactions(factionID int32) []int32 { fm.mutex.RLock() defer fm.mutex.RUnlock() if factions, exists := fm.hostileFactions[factionID]; exists { result := make([]int32, len(factions)) copy(result, factions) return result } return nil } // GetFriendlyFactions returns all friendly factions for a given faction func (fm *FactionManager) GetFriendlyFactions(factionID int32) []int32 { fm.mutex.RLock() defer fm.mutex.RUnlock() if factions, exists := fm.friendlyFactions[factionID]; exists { result := make([]int32, len(factions)) copy(result, factions) return result } return nil } // RecordFactionIncrease records a faction increase for statistics func (fm *FactionManager) RecordFactionIncrease(factionID int32) { fm.mutex.Lock() defer fm.mutex.Unlock() fm.totalFactionChanges++ fm.factionIncreases++ fm.changesByFaction[factionID]++ } // RecordFactionDecrease records a faction decrease for statistics func (fm *FactionManager) RecordFactionDecrease(factionID int32) { fm.mutex.Lock() defer fm.mutex.Unlock() fm.totalFactionChanges++ fm.factionDecreases++ fm.changesByFaction[factionID]++ } // RecordPacketSent records a successful packet send func (fm *FactionManager) RecordPacketSent() { fm.mutex.Lock() defer fm.mutex.Unlock() fm.packetsSent++ } // RecordPacketError records a packet error func (fm *FactionManager) RecordPacketError() { fm.mutex.Lock() defer fm.mutex.Unlock() fm.packetErrors++ } // GetStatistics returns faction system statistics func (fm *FactionManager) GetStatistics() map[string]any { fm.mutex.RLock() defer fm.mutex.RUnlock() stats := make(map[string]any) stats["total_factions"] = len(fm.factions) stats["total_faction_changes"] = fm.totalFactionChanges stats["faction_increases"] = fm.factionIncreases stats["faction_decreases"] = fm.factionDecreases stats["faction_lookups"] = fm.factionLookups stats["players_with_factions"] = fm.playersWithFactions stats["packets_sent"] = fm.packetsSent stats["packet_errors"] = fm.packetErrors // Copy changes by faction changeStats := make(map[int32]int64) for factionID, count := range fm.changesByFaction { changeStats[factionID] = count } stats["changes_by_faction"] = changeStats return stats } // LoadPlayerFactions loads player faction data from database func (fm *FactionManager) LoadPlayerFactions(characterID int32) (map[int32]int32, error) { if fm.database == nil { return make(map[int32]int32), nil } // Create player_factions table if it doesn't exist _, err := fm.database.Exec(` CREATE TABLE IF NOT EXISTS player_factions ( player_id INTEGER NOT NULL, faction_id INTEGER NOT NULL, faction_value INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (player_id, faction_id), FOREIGN KEY (faction_id) REFERENCES factions(id) ) `) if err != nil { return nil, fmt.Errorf("failed to create player_factions table: %w", err) } factionValues := make(map[int32]int32) rows, err := fm.database.Query("SELECT faction_id, faction_value FROM player_factions WHERE player_id = ?", characterID) if err != nil { return nil, fmt.Errorf("failed to load player factions for player %d: %w", characterID, err) } defer rows.Close() for rows.Next() { var factionID, factionValue int32 if err := rows.Scan(&factionID, &factionValue); err != nil { return nil, fmt.Errorf("failed to scan player faction: %w", err) } factionValues[factionID] = factionValue } return factionValues, rows.Err() } // SavePlayerFactions saves all player faction data to database func (fm *FactionManager) SavePlayerFactions(characterID int32, factionValues map[int32]int32) error { if fm.database == nil { return nil // No database available } tx, err := fm.database.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Clear existing faction values for this player _, err = tx.Exec("DELETE FROM player_factions WHERE player_id = ?", characterID) if err != nil { return fmt.Errorf("failed to clear player factions: %w", err) } // Insert all current faction values for factionID, factionValue := range factionValues { _, err = tx.Exec(` INSERT INTO player_factions (player_id, faction_id, faction_value) VALUES (?, ?, ?) `, characterID, factionID, factionValue) if err != nil { return fmt.Errorf("failed to insert player faction %d/%d: %w", characterID, factionID, err) } } return tx.Commit() } // SendFactionUpdate builds and returns a faction update packet for a player func (fm *FactionManager) SendFactionUpdate(playerFaction *PlayerFaction, clientVersion uint32) ([]byte, error) { if playerFaction == nil { return nil, fmt.Errorf("player faction cannot be nil") } data, err := playerFaction.FactionUpdate(clientVersion) if err != nil { fm.RecordPacketError() return nil, err } if data != nil { fm.RecordPacketSent() } return data, nil } // ValidateAllFactions checks all factions for consistency func (fm *FactionManager) ValidateAllFactions() []string { fm.mutex.RLock() defer fm.mutex.RUnlock() var issues []string for id, faction := range fm.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)) } } return issues } // Shutdown gracefully shuts down the manager func (fm *FactionManager) Shutdown() { if fm.logger != nil { fm.logger.LogInfo("factions", "Shutting down faction manager...") } fm.mutex.Lock() defer fm.mutex.Unlock() // Clear all data fm.factions = make(map[int32]*Faction) fm.byName = make(map[string]*Faction) fm.byType = make(map[string][]*Faction) fm.specialFactions = make(map[int32]*Faction) fm.regularFactions = make(map[int32]*Faction) fm.hostileFactions = make(map[int32][]int32) fm.friendlyFactions = make(map[int32][]int32) }