2025-08-08 12:25:55 -05:00

731 lines
20 KiB
Go

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
}