1005 lines
27 KiB
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)
|
|
} |