eq2go/internal/factions/factions.go

1005 lines
27 KiB
Go

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)
}