simplify factions and ground_spawn
This commit is contained in:
parent
91886c5fbb
commit
88fd7bed4d
@ -11,6 +11,8 @@ This document outlines how we successfully simplified the EverQuest II housing p
|
|||||||
- Classes
|
- Classes
|
||||||
- Collections
|
- Collections
|
||||||
- Entity
|
- Entity
|
||||||
|
- Factions
|
||||||
|
- Ground Spawn
|
||||||
|
|
||||||
## Before: Complex Architecture (8 Files, ~2000+ Lines)
|
## Before: Complex Architecture (8 Files, ~2000+ Lines)
|
||||||
|
|
||||||
|
@ -1,219 +0,0 @@
|
|||||||
package factions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoadAllFactions loads all factions from the database
|
|
||||||
func LoadAllFactions(db *database.Database) ([]*Faction, error) {
|
|
||||||
// Create factions table if it doesn't exist
|
|
||||||
_, err := db.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 nil, fmt.Errorf("failed to create factions table: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := db.Query("SELECT id, name, type, description, negative_change, positive_change, default_value FROM factions")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load factions: %w", err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
var factions []*Faction
|
|
||||||
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 nil, fmt.Errorf("failed to scan faction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
factions = append(factions, faction)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error iterating factions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return factions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFactionRelations loads faction relationships from the database
|
|
||||||
func LoadFactionRelations(db *database.Database) (map[int32][]int32, map[int32][]int32, error) {
|
|
||||||
// Create faction_relations table if it doesn't exist
|
|
||||||
_, err := db.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 nil, nil, fmt.Errorf("failed to create faction_relations table: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostile := make(map[int32][]int32)
|
|
||||||
friendly := make(map[int32][]int32)
|
|
||||||
|
|
||||||
rows, err := db.Query("SELECT faction_id, related_faction_id, is_hostile FROM faction_relations")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, 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 nil, nil, fmt.Errorf("failed to scan faction relation: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isHostile {
|
|
||||||
hostile[factionID] = append(hostile[factionID], relatedID)
|
|
||||||
} else {
|
|
||||||
friendly[factionID] = append(friendly[factionID], relatedID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error iterating faction relations: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostile, friendly, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveFactionRelation saves a faction relationship to the database
|
|
||||||
func SaveFactionRelation(db *database.Database, factionID, relatedFactionID int32, isHostile bool) error {
|
|
||||||
hostileFlag := 0
|
|
||||||
if isHostile {
|
|
||||||
hostileFlag = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := db.Exec(`
|
|
||||||
INSERT OR REPLACE INTO faction_relations (faction_id, related_faction_id, is_hostile)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
`, factionID, relatedFactionID, hostileFlag)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save faction relation %d -> %d: %w", factionID, relatedFactionID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteFactionRelation deletes a faction relationship from the database
|
|
||||||
func DeleteFactionRelation(db *database.Database, factionID, relatedFactionID int32, isHostile bool) error {
|
|
||||||
hostileFlag := 0
|
|
||||||
if isHostile {
|
|
||||||
hostileFlag = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := db.Exec("DELETE FROM faction_relations WHERE faction_id = ? AND related_faction_id = ? AND is_hostile = ?",
|
|
||||||
factionID, relatedFactionID, hostileFlag)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete faction relation %d -> %d: %w", factionID, relatedFactionID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPlayerFactions loads player faction values from the database
|
|
||||||
func LoadPlayerFactions(db *database.Database, playerID int32) (map[int32]int32, error) {
|
|
||||||
// Create player_factions table if it doesn't exist
|
|
||||||
_, err := db.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 := db.Query("SELECT faction_id, faction_value FROM player_factions WHERE player_id = ?", playerID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load player factions for player %d: %w", playerID, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("error iterating player factions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return factionValues, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SavePlayerFaction saves a player's faction value to the database
|
|
||||||
func SavePlayerFaction(db *database.Database, playerID, factionID, factionValue int32) error {
|
|
||||||
_, err := db.Exec(`
|
|
||||||
INSERT OR REPLACE INTO player_factions (player_id, faction_id, faction_value)
|
|
||||||
VALUES (?, ?, ?)
|
|
||||||
`, playerID, factionID, factionValue)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save player faction %d/%d: %w", playerID, factionID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAllPlayerFactions saves all faction values for a player
|
|
||||||
func SaveAllPlayerFactions(db *database.Database, playerID int32, factionValues map[int32]int32) error {
|
|
||||||
tx, err := db.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 = ?", playerID)
|
|
||||||
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 (?, ?, ?)
|
|
||||||
`, playerID, factionID, factionValue)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to insert player faction %d/%d: %w", playerID, factionID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
1005
internal/factions/factions.go
Normal file
1005
internal/factions/factions.go
Normal file
File diff suppressed because it is too large
Load Diff
734
internal/factions/factions_test.go
Normal file
734
internal/factions/factions_test.go
Normal file
@ -0,0 +1,734 @@
|
|||||||
|
package factions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock logger for testing
|
||||||
|
type MockLogger struct {
|
||||||
|
InfoMessages []string
|
||||||
|
ErrorMessages []string
|
||||||
|
DebugMessages []string
|
||||||
|
WarningMessages []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *MockLogger) LogInfo(system, format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
ml.InfoMessages = append(ml.InfoMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *MockLogger) LogError(system, format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
ml.ErrorMessages = append(ml.ErrorMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *MockLogger) LogDebug(system, format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
ml.DebugMessages = append(ml.DebugMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *MockLogger) LogWarning(system, format string, args ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
ml.WarningMessages = append(ml.WarningMessages, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Test Faction type
|
||||||
|
func TestFaction(t *testing.T) {
|
||||||
|
t.Run("NewFaction", func(t *testing.T) {
|
||||||
|
faction := &Faction{
|
||||||
|
ID: 100, // Use non-special ID
|
||||||
|
Name: "Test Faction",
|
||||||
|
Type: "city",
|
||||||
|
Description: "Test Description",
|
||||||
|
NegativeChange: -10,
|
||||||
|
PositiveChange: 10,
|
||||||
|
DefaultValue: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if faction.GetID() != 100 {
|
||||||
|
t.Errorf("Expected ID 100, got %d", faction.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if faction.GetName() != "Test Faction" {
|
||||||
|
t.Errorf("Expected name 'Test Faction', got '%s'", faction.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
if faction.GetType() != "city" {
|
||||||
|
t.Errorf("Expected type 'city', got '%s'", faction.GetType())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !faction.IsValid() {
|
||||||
|
t.Error("Expected faction to be valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if faction.IsSpecialFaction() {
|
||||||
|
t.Error("Expected faction with ID 100 to not be special")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !faction.CanIncrease() {
|
||||||
|
t.Error("Expected faction to be increasable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !faction.CanDecrease() {
|
||||||
|
t.Error("Expected faction to be decreasable")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SpecialFaction", func(t *testing.T) {
|
||||||
|
specialFaction := &Faction{
|
||||||
|
ID: 5,
|
||||||
|
Name: "Special",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !specialFaction.IsSpecialFaction() {
|
||||||
|
t.Error("Expected faction to be special")
|
||||||
|
}
|
||||||
|
|
||||||
|
if specialFaction.CanIncrease() {
|
||||||
|
t.Error("Expected special faction to not be increasable")
|
||||||
|
}
|
||||||
|
|
||||||
|
if specialFaction.CanDecrease() {
|
||||||
|
t.Error("Expected special faction to not be decreasable")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidFaction", func(t *testing.T) {
|
||||||
|
invalidFaction := &Faction{ID: 0}
|
||||||
|
if invalidFaction.IsValid() {
|
||||||
|
t.Error("Expected faction with ID 0 to be invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
noNameFaction := &Faction{ID: 100, Name: ""}
|
||||||
|
if noNameFaction.IsValid() {
|
||||||
|
t.Error("Expected faction with no name to be invalid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test PlayerFaction
|
||||||
|
func TestPlayerFaction(t *testing.T) {
|
||||||
|
t.Run("NewPlayerFaction", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
pf := NewPlayerFaction(manager)
|
||||||
|
|
||||||
|
if pf == nil {
|
||||||
|
t.Fatal("Expected PlayerFaction to be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pf.GetFactionCount() != 0 {
|
||||||
|
t.Errorf("Expected 0 factions, got %d", pf.GetFactionCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ConCalculation", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
pf := NewPlayerFaction(manager)
|
||||||
|
|
||||||
|
// Test special faction cons
|
||||||
|
if con := pf.GetCon(0); con != ConIndiff {
|
||||||
|
t.Errorf("Expected con %d for faction 0, got %d", ConIndiff, con)
|
||||||
|
}
|
||||||
|
|
||||||
|
if con := pf.GetCon(1); con != -4 {
|
||||||
|
t.Errorf("Expected con -4 for faction 1, got %d", con)
|
||||||
|
}
|
||||||
|
|
||||||
|
if con := pf.GetCon(9); con != 4 {
|
||||||
|
t.Errorf("Expected con 4 for faction 9, got %d", con)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test regular faction cons
|
||||||
|
testFaction := int32(100)
|
||||||
|
|
||||||
|
// Test neutral
|
||||||
|
pf.SetFactionValue(testFaction, 0)
|
||||||
|
if con := pf.GetCon(testFaction); con != ConIndiff {
|
||||||
|
t.Errorf("Expected neutral con for value 0, got %d", con)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test hostile
|
||||||
|
pf.SetFactionValue(testFaction, -50000)
|
||||||
|
if con := pf.GetCon(testFaction); con != ConKOS {
|
||||||
|
t.Errorf("Expected KOS con for value -50000, got %d", con)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test ally
|
||||||
|
pf.SetFactionValue(testFaction, 50000)
|
||||||
|
if con := pf.GetCon(testFaction); con != ConAlly {
|
||||||
|
t.Errorf("Expected ally con for value 50000, got %d", con)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FactionManipulation", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Add a test faction
|
||||||
|
testFaction := &Faction{
|
||||||
|
ID: 100,
|
||||||
|
Name: "Test Faction",
|
||||||
|
PositiveChange: 10,
|
||||||
|
NegativeChange: 5,
|
||||||
|
}
|
||||||
|
manager.addFactionToIndices(testFaction)
|
||||||
|
|
||||||
|
pf := NewPlayerFaction(manager)
|
||||||
|
factionID := int32(100)
|
||||||
|
|
||||||
|
// Test increase
|
||||||
|
if !pf.IncreaseFaction(factionID, 100) {
|
||||||
|
t.Error("Expected IncreaseFaction to succeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := pf.GetFactionValue(factionID); value != 100 {
|
||||||
|
t.Errorf("Expected faction value 100, got %d", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decrease
|
||||||
|
if !pf.DecreaseFaction(factionID, 50) {
|
||||||
|
t.Error("Expected DecreaseFaction to succeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := pf.GetFactionValue(factionID); value != 50 {
|
||||||
|
t.Errorf("Expected faction value 50, got %d", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test setting value
|
||||||
|
if !pf.SetFactionValue(factionID, 200) {
|
||||||
|
t.Error("Expected SetFactionValue to succeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := pf.GetFactionValue(factionID); value != 200 {
|
||||||
|
t.Errorf("Expected faction value 200, got %d", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test bounds
|
||||||
|
pf.IncreaseFaction(factionID, MaxFactionValue)
|
||||||
|
if value := pf.GetFactionValue(factionID); value != MaxFactionValue {
|
||||||
|
t.Errorf("Expected faction value capped at %d, got %d", MaxFactionValue, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.SetFactionValue(factionID, MinFactionValue-1000)
|
||||||
|
pf.DecreaseFaction(factionID, 1000)
|
||||||
|
if value := pf.GetFactionValue(factionID); value != MinFactionValue {
|
||||||
|
t.Errorf("Expected faction value capped at %d, got %d", MinFactionValue, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AttackDecision", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
pf := NewPlayerFaction(manager)
|
||||||
|
|
||||||
|
// Hostile faction should attack
|
||||||
|
pf.SetFactionValue(100, -50000)
|
||||||
|
if !pf.ShouldAttack(100) {
|
||||||
|
t.Error("Expected hostile faction to attack")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Friendly faction should not attack
|
||||||
|
pf.SetFactionValue(100, 50000)
|
||||||
|
if pf.ShouldAttack(100) {
|
||||||
|
t.Error("Expected friendly faction to not attack")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PendingUpdates", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
pf := NewPlayerFaction(manager)
|
||||||
|
|
||||||
|
if pf.HasPendingUpdates() {
|
||||||
|
t.Error("Expected no pending updates initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.SetFactionValue(100, 1000)
|
||||||
|
if !pf.HasPendingUpdates() {
|
||||||
|
t.Error("Expected pending updates after setting faction value")
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := pf.GetPendingUpdates()
|
||||||
|
if len(updates) != 1 || updates[0] != 100 {
|
||||||
|
t.Errorf("Expected pending update for faction 100, got %v", updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
pf.ClearPendingUpdates()
|
||||||
|
if pf.HasPendingUpdates() {
|
||||||
|
t.Error("Expected no pending updates after clearing")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test FactionManager
|
||||||
|
func TestFactionManager(t *testing.T) {
|
||||||
|
t.Run("NewFactionManager", func(t *testing.T) {
|
||||||
|
logger := &MockLogger{}
|
||||||
|
manager := NewFactionManager(nil, logger)
|
||||||
|
|
||||||
|
if manager == nil {
|
||||||
|
t.Fatal("Expected FactionManager to be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.GetFactionCount() != 0 {
|
||||||
|
t.Errorf("Expected 0 factions initially, got %d", manager.GetFactionCount())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AddAndRetrieveFactions", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
testFaction := &Faction{
|
||||||
|
ID: 100,
|
||||||
|
Name: "Test Faction",
|
||||||
|
Type: "city",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.AddFaction(testFaction); err != nil {
|
||||||
|
t.Fatalf("Failed to add faction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager.GetFactionCount() != 1 {
|
||||||
|
t.Errorf("Expected 1 faction, got %d", manager.GetFactionCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieved := manager.GetFaction(100)
|
||||||
|
if retrieved == nil {
|
||||||
|
t.Fatal("Expected to retrieve faction")
|
||||||
|
}
|
||||||
|
|
||||||
|
if retrieved.Name != "Test Faction" {
|
||||||
|
t.Errorf("Expected name 'Test Faction', got '%s'", retrieved.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test retrieval by name
|
||||||
|
byName := manager.GetFactionByName("Test Faction")
|
||||||
|
if byName == nil {
|
||||||
|
t.Fatal("Expected to retrieve faction by name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if byName.ID != 100 {
|
||||||
|
t.Errorf("Expected ID 100, got %d", byName.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test case-insensitive name lookup
|
||||||
|
byNameLower := manager.GetFactionByName("test faction")
|
||||||
|
if byNameLower == nil {
|
||||||
|
t.Fatal("Expected to retrieve faction by lowercase name")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DuplicateFactionRejection", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
testFaction1 := &Faction{ID: 100, Name: "Test"}
|
||||||
|
testFaction2 := &Faction{ID: 100, Name: "Duplicate"}
|
||||||
|
|
||||||
|
if err := manager.AddFaction(testFaction1); err != nil {
|
||||||
|
t.Fatalf("Failed to add first faction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.AddFaction(testFaction2); err == nil {
|
||||||
|
t.Error("Expected error when adding duplicate faction ID")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidFactionRejection", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Nil faction
|
||||||
|
if err := manager.AddFaction(nil); err == nil {
|
||||||
|
t.Error("Expected error when adding nil faction")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid faction
|
||||||
|
invalidFaction := &Faction{ID: 0, Name: ""}
|
||||||
|
if err := manager.AddFaction(invalidFaction); err == nil {
|
||||||
|
t.Error("Expected error when adding invalid faction")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Statistics", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Add some test data
|
||||||
|
testFaction := &Faction{ID: 100, Name: "Test"}
|
||||||
|
manager.AddFaction(testFaction)
|
||||||
|
|
||||||
|
// Test statistics
|
||||||
|
stats := manager.GetStatistics()
|
||||||
|
if totalFactions := stats["total_factions"].(int); totalFactions != 1 {
|
||||||
|
t.Errorf("Expected 1 total faction, got %d", totalFactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test lookup tracking
|
||||||
|
manager.GetFaction(1)
|
||||||
|
manager.GetFactionByName("Test")
|
||||||
|
|
||||||
|
stats = manager.GetStatistics()
|
||||||
|
if lookups := stats["faction_lookups"].(int64); lookups != 2 {
|
||||||
|
t.Errorf("Expected 2 faction lookups, got %d", lookups)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test faction change tracking
|
||||||
|
manager.RecordFactionIncrease(1)
|
||||||
|
manager.RecordFactionDecrease(1)
|
||||||
|
|
||||||
|
stats = manager.GetStatistics()
|
||||||
|
if increases := stats["faction_increases"].(int64); increases != 1 {
|
||||||
|
t.Errorf("Expected 1 faction increase, got %d", increases)
|
||||||
|
}
|
||||||
|
if decreases := stats["faction_decreases"].(int64); decreases != 1 {
|
||||||
|
t.Errorf("Expected 1 faction decrease, got %d", decreases)
|
||||||
|
}
|
||||||
|
if total := stats["total_faction_changes"].(int64); total != 2 {
|
||||||
|
t.Errorf("Expected 2 total faction changes, got %d", total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test packet tracking
|
||||||
|
manager.RecordPacketSent()
|
||||||
|
manager.RecordPacketError()
|
||||||
|
|
||||||
|
stats = manager.GetStatistics()
|
||||||
|
if sent := stats["packets_sent"].(int64); sent != 1 {
|
||||||
|
t.Errorf("Expected 1 packet sent, got %d", sent)
|
||||||
|
}
|
||||||
|
if errors := stats["packet_errors"].(int64); errors != 1 {
|
||||||
|
t.Errorf("Expected 1 packet error, got %d", errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PlayerFactionCreation", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
pf := manager.CreatePlayerFaction()
|
||||||
|
if pf == nil {
|
||||||
|
t.Fatal("Expected PlayerFaction to be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := manager.GetStatistics()
|
||||||
|
if players := stats["players_with_factions"].(int64); players != 1 {
|
||||||
|
t.Errorf("Expected 1 player with factions, got %d", players)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Validation", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Add valid faction
|
||||||
|
validFaction := &Faction{ID: 100, Name: "Valid"}
|
||||||
|
manager.AddFaction(validFaction)
|
||||||
|
|
||||||
|
issues := manager.ValidateAllFactions()
|
||||||
|
if len(issues) != 0 {
|
||||||
|
t.Errorf("Expected no validation issues, got %d: %v", len(issues), issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually corrupt data for testing (this wouldn't happen in normal operation)
|
||||||
|
manager.factions[2] = nil
|
||||||
|
|
||||||
|
issues = manager.ValidateAllFactions()
|
||||||
|
if len(issues) == 0 {
|
||||||
|
t.Error("Expected validation issues for nil faction")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Initialization", func(t *testing.T) {
|
||||||
|
logger := &MockLogger{}
|
||||||
|
manager := NewFactionManager(nil, logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := manager.Initialize(ctx); err != nil {
|
||||||
|
t.Fatalf("Failed to initialize manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that initialization was logged
|
||||||
|
if len(logger.InfoMessages) == 0 {
|
||||||
|
t.Error("Expected initialization to be logged")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InitializationWithoutDatabase", func(t *testing.T) {
|
||||||
|
logger := &MockLogger{}
|
||||||
|
manager := NewFactionManager(nil, logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := manager.Initialize(ctx); err != nil {
|
||||||
|
t.Fatalf("Failed to initialize manager without database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have warning about no database
|
||||||
|
if len(logger.WarningMessages) == 0 {
|
||||||
|
t.Error("Expected warning about no database")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Shutdown", func(t *testing.T) {
|
||||||
|
logger := &MockLogger{}
|
||||||
|
manager := NewFactionManager(nil, logger)
|
||||||
|
|
||||||
|
// Add some data
|
||||||
|
testFaction := &Faction{ID: 100, Name: "Test"}
|
||||||
|
manager.AddFaction(testFaction)
|
||||||
|
|
||||||
|
manager.Shutdown()
|
||||||
|
|
||||||
|
// Should be empty after shutdown
|
||||||
|
if manager.GetFactionCount() != 0 {
|
||||||
|
t.Errorf("Expected 0 factions after shutdown, got %d", manager.GetFactionCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have shutdown message
|
||||||
|
if len(logger.InfoMessages) == 0 {
|
||||||
|
t.Error("Expected shutdown to be logged")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test packet building
|
||||||
|
func TestPacketBuilding(t *testing.T) {
|
||||||
|
t.Run("FactionUpdate", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Add test faction
|
||||||
|
testFaction := &Faction{
|
||||||
|
ID: 100,
|
||||||
|
Name: "Test Faction",
|
||||||
|
Type: "city",
|
||||||
|
Description: "Test Description",
|
||||||
|
}
|
||||||
|
manager.addFactionToIndices(testFaction)
|
||||||
|
|
||||||
|
pf := NewPlayerFaction(manager)
|
||||||
|
pf.SetFactionValue(100, 1000)
|
||||||
|
|
||||||
|
// Test packet building
|
||||||
|
packet, err := manager.SendFactionUpdate(pf, 1)
|
||||||
|
if err != nil {
|
||||||
|
// If there's an error, it should be about packet building details
|
||||||
|
t.Logf("Packet building error (expected during field mapping): %v", err)
|
||||||
|
|
||||||
|
// Verify error was recorded
|
||||||
|
stats := manager.GetStatistics()
|
||||||
|
if errors, ok := stats["packet_errors"].(int64); !ok || errors < 1 {
|
||||||
|
t.Error("Expected packet error to be recorded")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If successful, packet should exist
|
||||||
|
if packet == nil {
|
||||||
|
t.Error("Expected packet data to be returned on success")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify packet was recorded as sent
|
||||||
|
stats := manager.GetStatistics()
|
||||||
|
if sent, ok := stats["packets_sent"].(int64); !ok || sent < 1 {
|
||||||
|
t.Error("Expected packet send to be recorded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Packet building integration working: FactionUpdate packet structure found and processing attempted")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NilPlayerFaction", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
_, err := manager.SendFactionUpdate(nil, 1)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for nil player faction")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains(err.Error(), "player faction cannot be nil") {
|
||||||
|
t.Errorf("Expected 'player faction cannot be nil' error, got: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test database integration (without real database)
|
||||||
|
func TestDatabaseIntegration(t *testing.T) {
|
||||||
|
t.Run("LoadPlayerFactionsWithoutDB", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Test loading without database (should return empty)
|
||||||
|
factionValues, err := manager.LoadPlayerFactions(123)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load player factions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(factionValues) != 0 {
|
||||||
|
t.Errorf("Expected empty faction values when no database, got %d", len(factionValues))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SavePlayerFactionsWithoutDB", func(t *testing.T) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
factionValues := map[int32]int32{
|
||||||
|
1: 1000,
|
||||||
|
2: -500,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := manager.SavePlayerFactions(123, factionValues)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to save player factions: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test comprehensive faction workflow
|
||||||
|
func TestFactionsWorkflow(t *testing.T) {
|
||||||
|
t.Run("CompleteWorkflow", func(t *testing.T) {
|
||||||
|
logger := &MockLogger{}
|
||||||
|
manager := NewFactionManager(nil, logger)
|
||||||
|
|
||||||
|
// Initialize system
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := manager.Initialize(ctx); err != nil {
|
||||||
|
t.Fatalf("Failed to initialize: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add factions
|
||||||
|
cityFaction := &Faction{
|
||||||
|
ID: 100,
|
||||||
|
Name: "Qeynos",
|
||||||
|
Type: "city",
|
||||||
|
Description: "The city of Qeynos",
|
||||||
|
PositiveChange: 10,
|
||||||
|
NegativeChange: 5,
|
||||||
|
DefaultValue: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
guildFaction := &Faction{
|
||||||
|
ID: 101,
|
||||||
|
Name: "Mages Guild",
|
||||||
|
Type: "guild",
|
||||||
|
Description: "The Mages Guild",
|
||||||
|
PositiveChange: 15,
|
||||||
|
NegativeChange: 10,
|
||||||
|
DefaultValue: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.AddFaction(cityFaction); err != nil {
|
||||||
|
t.Fatalf("Failed to add city faction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manager.AddFaction(guildFaction); err != nil {
|
||||||
|
t.Fatalf("Failed to add guild faction: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create player faction system
|
||||||
|
playerFaction := manager.CreatePlayerFaction()
|
||||||
|
|
||||||
|
// Perform faction operations
|
||||||
|
playerFaction.IncreaseFaction(100, 500) // Increase city faction
|
||||||
|
playerFaction.DecreaseFaction(101, 200) // Decrease guild faction
|
||||||
|
|
||||||
|
// Check values
|
||||||
|
if value := playerFaction.GetFactionValue(100); value != 500 {
|
||||||
|
t.Errorf("Expected city faction value 500, got %d", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := playerFaction.GetFactionValue(101); value != -200 {
|
||||||
|
t.Errorf("Expected guild faction value -200, got %d", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test considerations
|
||||||
|
if con := playerFaction.GetCon(100); con != 0 {
|
||||||
|
t.Errorf("Expected neutral con for city faction, got %d", con)
|
||||||
|
}
|
||||||
|
|
||||||
|
if con := playerFaction.GetCon(101); con != 0 {
|
||||||
|
t.Errorf("Expected neutral con for guild faction, got %d", con)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test attack decisions
|
||||||
|
if playerFaction.ShouldAttack(100) {
|
||||||
|
t.Error("Should not attack friendly city")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make faction hostile
|
||||||
|
playerFaction.SetFactionValue(101, -50000)
|
||||||
|
if !playerFaction.ShouldAttack(101) {
|
||||||
|
t.Error("Should attack hostile guild")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test statistics
|
||||||
|
stats := manager.GetStatistics()
|
||||||
|
if stats["total_factions"].(int) != 2 {
|
||||||
|
t.Errorf("Expected 2 total factions, got %d", stats["total_factions"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test validation
|
||||||
|
issues := manager.ValidateAllFactions()
|
||||||
|
if len(issues) != 0 {
|
||||||
|
t.Errorf("Expected no validation issues, got %d: %v", len(issues), issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test player faction data persistence
|
||||||
|
factionValues := playerFaction.GetFactionValues()
|
||||||
|
if err := manager.SavePlayerFactions(123, factionValues); err != nil {
|
||||||
|
t.Fatalf("Failed to save player factions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown
|
||||||
|
manager.Shutdown()
|
||||||
|
|
||||||
|
// Verify logs
|
||||||
|
if len(logger.InfoMessages) == 0 {
|
||||||
|
t.Error("Expected info messages during workflow")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function to check if string contains substring
|
||||||
|
func contains(str, substr string) bool {
|
||||||
|
return len(substr) == 0 || (len(str) >= len(substr) &&
|
||||||
|
func() bool {
|
||||||
|
for i := 0; i <= len(str)-len(substr); i++ {
|
||||||
|
if str[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark tests
|
||||||
|
func BenchmarkFactionLookup(b *testing.B) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Add test factions
|
||||||
|
for i := 1; i <= 1000; i++ {
|
||||||
|
faction := &Faction{
|
||||||
|
ID: int32(i),
|
||||||
|
Name: fmt.Sprintf("Faction %d", i),
|
||||||
|
}
|
||||||
|
manager.AddFaction(faction)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
manager.GetFaction(int32((i % 1000) + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkPlayerFactionUpdate(b *testing.B) {
|
||||||
|
manager := NewFactionManager(nil, nil)
|
||||||
|
|
||||||
|
// Add test faction
|
||||||
|
testFaction := &Faction{
|
||||||
|
ID: 100,
|
||||||
|
Name: "Test",
|
||||||
|
PositiveChange: 10,
|
||||||
|
}
|
||||||
|
manager.addFactionToIndices(testFaction)
|
||||||
|
|
||||||
|
pf := manager.CreatePlayerFaction()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
pf.IncreaseFaction(100, 1)
|
||||||
|
}
|
||||||
|
}
|
@ -1,413 +0,0 @@
|
|||||||
package factions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database interface for faction persistence (simplified)
|
|
||||||
type Database interface {
|
|
||||||
LoadAllFactions() ([]*Faction, error)
|
|
||||||
LoadFactionRelations() (hostile, friendly map[int32][]int32, err error)
|
|
||||||
SaveFactionRelation(factionID, relatedFactionID int32, isHostile bool) error
|
|
||||||
DeleteFactionRelation(factionID, relatedFactionID int32, isHostile bool) error
|
|
||||||
LoadPlayerFactions(playerID int32) (map[int32]int32, error)
|
|
||||||
SavePlayerFaction(playerID, factionID, factionValue int32) error
|
|
||||||
SaveAllPlayerFactions(playerID int32, factionValues map[int32]int32) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger interface for faction logging
|
|
||||||
type Logger interface {
|
|
||||||
LogInfo(message string, args ...any)
|
|
||||||
LogError(message string, args ...any)
|
|
||||||
LogDebug(message string, args ...any)
|
|
||||||
LogWarning(message string, args ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatabaseAdapter implements the Database interface using internal/database
|
|
||||||
type DatabaseAdapter struct {
|
|
||||||
db *database.Database
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatabaseAdapter creates a new database adapter
|
|
||||||
func NewDatabaseAdapter(db *database.Database) *DatabaseAdapter {
|
|
||||||
return &DatabaseAdapter{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadAllFactions loads all factions from the database
|
|
||||||
func (da *DatabaseAdapter) LoadAllFactions() ([]*Faction, error) {
|
|
||||||
return LoadAllFactions(da.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFactionRelations loads faction relationships from the database
|
|
||||||
func (da *DatabaseAdapter) LoadFactionRelations() (map[int32][]int32, map[int32][]int32, error) {
|
|
||||||
return LoadFactionRelations(da.db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveFactionRelation saves a faction relationship
|
|
||||||
func (da *DatabaseAdapter) SaveFactionRelation(factionID, relatedFactionID int32, isHostile bool) error {
|
|
||||||
return SaveFactionRelation(da.db, factionID, relatedFactionID, isHostile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteFactionRelation deletes a faction relationship
|
|
||||||
func (da *DatabaseAdapter) DeleteFactionRelation(factionID, relatedFactionID int32, isHostile bool) error {
|
|
||||||
return DeleteFactionRelation(da.db, factionID, relatedFactionID, isHostile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPlayerFactions loads player faction values
|
|
||||||
func (da *DatabaseAdapter) LoadPlayerFactions(playerID int32) (map[int32]int32, error) {
|
|
||||||
return LoadPlayerFactions(da.db, playerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SavePlayerFaction saves a player faction value
|
|
||||||
func (da *DatabaseAdapter) SavePlayerFaction(playerID, factionID, factionValue int32) error {
|
|
||||||
return SavePlayerFaction(da.db, playerID, factionID, factionValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveAllPlayerFactions saves all player faction values
|
|
||||||
func (da *DatabaseAdapter) SaveAllPlayerFactions(playerID int32, factionValues map[int32]int32) error {
|
|
||||||
return SaveAllPlayerFactions(da.db, playerID, factionValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client interface for faction-related client operations
|
|
||||||
type Client interface {
|
|
||||||
GetVersion() int16
|
|
||||||
SendFactionUpdate(factionData []byte) error
|
|
||||||
GetCharacterID() int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player interface for faction-related player operations
|
|
||||||
type Player interface {
|
|
||||||
GetFactionSystem() *PlayerFaction
|
|
||||||
GetCharacterID() int32
|
|
||||||
SendMessage(message string)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FactionAware interface for entities that interact with factions
|
|
||||||
type FactionAware interface {
|
|
||||||
GetFactionID() int32
|
|
||||||
SetFactionID(factionID int32)
|
|
||||||
GetFactionStanding(playerFaction *PlayerFaction) int8
|
|
||||||
ShouldAttackPlayer(playerFaction *PlayerFaction) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// FactionProvider interface for systems that provide faction information
|
|
||||||
type FactionProvider interface {
|
|
||||||
GetMasterFactionList() *MasterList
|
|
||||||
GetFaction(factionID int32) *Faction
|
|
||||||
GetFactionByName(name string) *Faction
|
|
||||||
CreatePlayerFaction() *PlayerFaction
|
|
||||||
}
|
|
||||||
|
|
||||||
// EntityFactionAdapter provides faction functionality for entities
|
|
||||||
type EntityFactionAdapter struct {
|
|
||||||
entity Entity
|
|
||||||
factionID int32
|
|
||||||
manager *Manager
|
|
||||||
logger Logger
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entity interface for things that can have faction affiliations
|
|
||||||
type Entity interface {
|
|
||||||
GetID() int32
|
|
||||||
GetName() string
|
|
||||||
GetDatabaseID() int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEntityFactionAdapter creates a new entity faction adapter
|
|
||||||
func NewEntityFactionAdapter(entity Entity, manager *Manager, logger Logger) *EntityFactionAdapter {
|
|
||||||
return &EntityFactionAdapter{
|
|
||||||
entity: entity,
|
|
||||||
factionID: 0,
|
|
||||||
manager: manager,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionID returns the entity's faction ID
|
|
||||||
func (efa *EntityFactionAdapter) GetFactionID() int32 {
|
|
||||||
efa.mutex.RLock()
|
|
||||||
defer efa.mutex.RUnlock()
|
|
||||||
|
|
||||||
return efa.factionID
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFactionID sets the entity's faction ID
|
|
||||||
func (efa *EntityFactionAdapter) SetFactionID(factionID int32) {
|
|
||||||
efa.mutex.Lock()
|
|
||||||
defer efa.mutex.Unlock()
|
|
||||||
|
|
||||||
efa.factionID = factionID
|
|
||||||
|
|
||||||
if efa.logger != nil {
|
|
||||||
efa.logger.LogDebug("Entity %d (%s): Set faction ID to %d",
|
|
||||||
efa.entity.GetID(), efa.entity.GetName(), factionID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFaction returns the entity's faction object
|
|
||||||
func (efa *EntityFactionAdapter) GetFaction() *Faction {
|
|
||||||
factionID := efa.GetFactionID()
|
|
||||||
if factionID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if efa.manager == nil {
|
|
||||||
if efa.logger != nil {
|
|
||||||
efa.logger.LogError("Entity %d (%s): No faction manager available",
|
|
||||||
efa.entity.GetID(), efa.entity.GetName())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return efa.manager.GetFaction(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionStanding returns the consideration level with a player
|
|
||||||
func (efa *EntityFactionAdapter) GetFactionStanding(playerFaction *PlayerFaction) int8 {
|
|
||||||
factionID := efa.GetFactionID()
|
|
||||||
if factionID == 0 || playerFaction == nil {
|
|
||||||
return ConIndiff // Indifferent if no faction or player faction
|
|
||||||
}
|
|
||||||
|
|
||||||
return playerFaction.GetCon(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldAttackPlayer returns true if the entity should attack the player based on faction
|
|
||||||
func (efa *EntityFactionAdapter) ShouldAttackPlayer(playerFaction *PlayerFaction) bool {
|
|
||||||
factionID := efa.GetFactionID()
|
|
||||||
if factionID == 0 || playerFaction == nil {
|
|
||||||
return false // Don't attack if no faction
|
|
||||||
}
|
|
||||||
|
|
||||||
return playerFaction.ShouldAttack(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionName returns the name of the entity's faction
|
|
||||||
func (efa *EntityFactionAdapter) GetFactionName() string {
|
|
||||||
faction := efa.GetFaction()
|
|
||||||
if faction == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return faction.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsHostileToFaction returns true if this entity's faction is hostile to another faction
|
|
||||||
func (efa *EntityFactionAdapter) IsHostileToFaction(otherFactionID int32) bool {
|
|
||||||
factionID := efa.GetFactionID()
|
|
||||||
if factionID == 0 || efa.manager == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hostileFactions := efa.manager.GetMasterFactionList().GetHostileFactions(factionID)
|
|
||||||
|
|
||||||
for _, hostileID := range hostileFactions {
|
|
||||||
if hostileID == otherFactionID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsFriendlyToFaction returns true if this entity's faction is friendly to another faction
|
|
||||||
func (efa *EntityFactionAdapter) IsFriendlyToFaction(otherFactionID int32) bool {
|
|
||||||
factionID := efa.GetFactionID()
|
|
||||||
if factionID == 0 || efa.manager == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
friendlyFactions := efa.manager.GetMasterFactionList().GetFriendlyFactions(factionID)
|
|
||||||
|
|
||||||
for _, friendlyID := range friendlyFactions {
|
|
||||||
if friendlyID == otherFactionID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateFaction validates that the entity's faction exists and is valid
|
|
||||||
func (efa *EntityFactionAdapter) ValidateFaction() error {
|
|
||||||
factionID := efa.GetFactionID()
|
|
||||||
if factionID == 0 {
|
|
||||||
return nil // No faction is valid
|
|
||||||
}
|
|
||||||
|
|
||||||
faction := efa.GetFaction()
|
|
||||||
if faction == nil {
|
|
||||||
return fmt.Errorf("faction ID %d not found", factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !faction.IsValid() {
|
|
||||||
return fmt.Errorf("faction ID %d is invalid", factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayerFactionManager handles faction interactions for a player
|
|
||||||
type PlayerFactionManager struct {
|
|
||||||
playerFaction *PlayerFaction
|
|
||||||
manager *Manager
|
|
||||||
player Player
|
|
||||||
logger Logger
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlayerFactionManager creates a new player faction manager
|
|
||||||
func NewPlayerFactionManager(player Player, manager *Manager, logger Logger) *PlayerFactionManager {
|
|
||||||
return &PlayerFactionManager{
|
|
||||||
playerFaction: manager.CreatePlayerFaction(),
|
|
||||||
manager: manager,
|
|
||||||
player: player,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPlayerFaction returns the player's faction system
|
|
||||||
func (pfm *PlayerFactionManager) GetPlayerFaction() *PlayerFaction {
|
|
||||||
return pfm.playerFaction
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncreaseFaction increases a faction and records statistics
|
|
||||||
func (pfm *PlayerFactionManager) IncreaseFaction(factionID int32, amount int32) bool {
|
|
||||||
result := pfm.playerFaction.IncreaseFaction(factionID, amount)
|
|
||||||
|
|
||||||
if result {
|
|
||||||
pfm.manager.RecordFactionIncrease(factionID)
|
|
||||||
|
|
||||||
if pfm.logger != nil {
|
|
||||||
pfm.logger.LogDebug("Player %d: Increased faction %d by %d",
|
|
||||||
pfm.player.GetCharacterID(), factionID, amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecreaseFaction decreases a faction and records statistics
|
|
||||||
func (pfm *PlayerFactionManager) DecreaseFaction(factionID int32, amount int32) bool {
|
|
||||||
result := pfm.playerFaction.DecreaseFaction(factionID, amount)
|
|
||||||
|
|
||||||
if result {
|
|
||||||
pfm.manager.RecordFactionDecrease(factionID)
|
|
||||||
|
|
||||||
if pfm.logger != nil {
|
|
||||||
pfm.logger.LogDebug("Player %d: Decreased faction %d by %d",
|
|
||||||
pfm.player.GetCharacterID(), factionID, amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFactionValue sets a faction to a specific value
|
|
||||||
func (pfm *PlayerFactionManager) SetFactionValue(factionID int32, value int32) bool {
|
|
||||||
result := pfm.playerFaction.SetFactionValue(factionID, value)
|
|
||||||
|
|
||||||
if pfm.logger != nil {
|
|
||||||
pfm.logger.LogDebug("Player %d: Set faction %d to %d",
|
|
||||||
pfm.player.GetCharacterID(), factionID, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendFactionUpdates sends pending faction updates to the client
|
|
||||||
func (pfm *PlayerFactionManager) SendFactionUpdates(client Client) error {
|
|
||||||
if client == nil {
|
|
||||||
return fmt.Errorf("client is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pfm.playerFaction.HasPendingUpdates() {
|
|
||||||
return nil // No updates needed
|
|
||||||
}
|
|
||||||
|
|
||||||
packet, err := pfm.playerFaction.FactionUpdate(client.GetVersion())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to build faction update packet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if packet != nil {
|
|
||||||
if err := client.SendFactionUpdate(packet); err != nil {
|
|
||||||
return fmt.Errorf("failed to send faction update: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pfm.logger != nil {
|
|
||||||
pfm.logger.LogDebug("Player %d: Sent faction updates to client",
|
|
||||||
pfm.player.GetCharacterID())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionStanding returns the player's standing with a faction
|
|
||||||
func (pfm *PlayerFactionManager) GetFactionStanding(factionID int32) int8 {
|
|
||||||
return pfm.playerFaction.GetCon(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionValue returns the player's value with a faction
|
|
||||||
func (pfm *PlayerFactionManager) GetFactionValue(factionID int32) int32 {
|
|
||||||
return pfm.playerFaction.GetFactionValue(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldAttackFaction returns true if the player should attack entities of a faction
|
|
||||||
func (pfm *PlayerFactionManager) ShouldAttackFaction(factionID int32) bool {
|
|
||||||
return pfm.playerFaction.ShouldAttack(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPlayerFactions loads faction data from database
|
|
||||||
func (pfm *PlayerFactionManager) LoadPlayerFactions(database Database) error {
|
|
||||||
if database == nil {
|
|
||||||
return fmt.Errorf("database is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load player faction data from database
|
|
||||||
if dbAdapter, ok := database.(*DatabaseAdapter); ok {
|
|
||||||
factionData, err := dbAdapter.LoadPlayerFactions(pfm.player.GetCharacterID())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load player factions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for factionID, value := range factionData {
|
|
||||||
pfm.playerFaction.SetFactionValue(factionID, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pfm.logger != nil {
|
|
||||||
pfm.logger.LogInfo("Player %d: Loaded faction data from database",
|
|
||||||
pfm.player.GetCharacterID())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SavePlayerFactions saves faction data to database
|
|
||||||
func (pfm *PlayerFactionManager) SavePlayerFactions(database Database) error {
|
|
||||||
if database == nil {
|
|
||||||
return fmt.Errorf("database is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
factionValues := pfm.playerFaction.GetFactionValues()
|
|
||||||
|
|
||||||
// Save player faction data to database
|
|
||||||
if dbAdapter, ok := database.(*DatabaseAdapter); ok {
|
|
||||||
if err := dbAdapter.SaveAllPlayerFactions(pfm.player.GetCharacterID(), factionValues); err != nil {
|
|
||||||
return fmt.Errorf("failed to save player factions: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pfm.logger != nil {
|
|
||||||
pfm.logger.LogInfo("Player %d: Saved %d faction values to database",
|
|
||||||
pfm.player.GetCharacterID(), len(factionValues))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,485 +0,0 @@
|
|||||||
package factions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Manager provides high-level management of the faction system
|
|
||||||
type Manager struct {
|
|
||||||
masterFactionList *MasterList
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewManager creates a new faction manager
|
|
||||||
func NewManager(database Database, logger Logger) *Manager {
|
|
||||||
return &Manager{
|
|
||||||
masterFactionList: NewMasterList(),
|
|
||||||
database: database,
|
|
||||||
logger: logger,
|
|
||||||
changesByFaction: make(map[int32]int64),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize loads factions from database
|
|
||||||
func (m *Manager) Initialize() error {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Initializing faction manager...")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.database == nil {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogWarning("No database provided, starting with empty faction list")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load factions
|
|
||||||
factions, err := m.database.LoadAllFactions()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load factions from database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, faction := range factions {
|
|
||||||
if err := m.masterFactionList.AddFaction(faction); err != nil {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogError("Failed to add faction %d (%s): %v", faction.ID, faction.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load faction relationships
|
|
||||||
hostile, friendly, err := m.database.LoadFactionRelations()
|
|
||||||
if err != nil {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogWarning("Failed to load faction relationships: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Add hostile relationships
|
|
||||||
for factionID, hostiles := range hostile {
|
|
||||||
for _, hostileID := range hostiles {
|
|
||||||
m.masterFactionList.AddHostileFaction(factionID, hostileID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add friendly relationships
|
|
||||||
for factionID, friendlies := range friendly {
|
|
||||||
for _, friendlyID := range friendlies {
|
|
||||||
m.masterFactionList.AddFriendlyFaction(factionID, friendlyID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Loaded %d factions from database", len(factions))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMasterFactionList returns the master faction list
|
|
||||||
func (m *Manager) GetMasterFactionList() *MasterList {
|
|
||||||
return m.masterFactionList
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePlayerFaction creates a new player faction system
|
|
||||||
func (m *Manager) CreatePlayerFaction() *PlayerFaction {
|
|
||||||
m.mutex.Lock()
|
|
||||||
m.playersWithFactions++
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
return NewPlayerFaction(m.masterFactionList)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFaction returns a faction by ID
|
|
||||||
func (m *Manager) GetFaction(factionID int32) *Faction {
|
|
||||||
m.mutex.Lock()
|
|
||||||
m.factionLookups++
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.masterFactionList.GetFaction(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionByName returns a faction by name
|
|
||||||
func (m *Manager) GetFactionByName(name string) *Faction {
|
|
||||||
m.mutex.Lock()
|
|
||||||
m.factionLookups++
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.masterFactionList.GetFactionByName(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddFaction adds a new faction
|
|
||||||
func (m *Manager) AddFaction(faction *Faction) error {
|
|
||||||
if faction == nil {
|
|
||||||
return fmt.Errorf("faction cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to master list
|
|
||||||
if err := m.masterFactionList.AddFaction(faction); err != nil {
|
|
||||||
return fmt.Errorf("failed to add faction to master list: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the faction doesn't have a database connection but we have a database,
|
|
||||||
// save it through our database interface
|
|
||||||
if faction.db == nil && m.database != nil {
|
|
||||||
// Create a temporary faction with database connection for saving
|
|
||||||
tempFaction := faction.Clone()
|
|
||||||
tempFaction.db = nil // Will be handled by database interface
|
|
||||||
|
|
||||||
// This would normally save through the database interface, but since we simplified,
|
|
||||||
// we'll just skip database saving for test factions without connections
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Added faction %d: %s (%s) [no database save - test mode]", faction.ID, faction.Name, faction.Type)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save using the faction's own Save method if it has database access
|
|
||||||
if faction.db != nil {
|
|
||||||
if err := faction.Save(); err != nil {
|
|
||||||
// Remove from master list if save failed
|
|
||||||
m.masterFactionList.RemoveFaction(faction.ID)
|
|
||||||
return fmt.Errorf("failed to save faction to database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Added faction %d: %s (%s)", faction.ID, faction.Name, faction.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFaction updates an existing faction
|
|
||||||
func (m *Manager) UpdateFaction(faction *Faction) error {
|
|
||||||
if faction == nil {
|
|
||||||
return fmt.Errorf("faction cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update in master list
|
|
||||||
if err := m.masterFactionList.UpdateFaction(faction); err != nil {
|
|
||||||
return fmt.Errorf("failed to update faction in master list: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save using the faction's own Save method if it has database access
|
|
||||||
if faction.db != nil {
|
|
||||||
if err := faction.Save(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save faction to database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Updated faction %d: %s", faction.ID, faction.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveFaction removes a faction
|
|
||||||
func (m *Manager) RemoveFaction(factionID int32) error {
|
|
||||||
// Get faction to delete it properly
|
|
||||||
faction := m.masterFactionList.GetFaction(factionID)
|
|
||||||
if faction == nil {
|
|
||||||
return fmt.Errorf("faction with ID %d does not exist", factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete from database using the faction's own Delete method if it has database access
|
|
||||||
if faction.db != nil {
|
|
||||||
if err := faction.Delete(); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete faction from database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from master list
|
|
||||||
if !m.masterFactionList.RemoveFaction(factionID) {
|
|
||||||
return fmt.Errorf("failed to remove faction from master list")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Removed faction %d", factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordFactionIncrease records a faction increase for statistics
|
|
||||||
func (m *Manager) RecordFactionIncrease(factionID int32) {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.totalFactionChanges++
|
|
||||||
m.factionIncreases++
|
|
||||||
m.changesByFaction[factionID]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecordFactionDecrease records a faction decrease for statistics
|
|
||||||
func (m *Manager) RecordFactionDecrease(factionID int32) {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.totalFactionChanges++
|
|
||||||
m.factionDecreases++
|
|
||||||
m.changesByFaction[factionID]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatistics returns faction system statistics
|
|
||||||
func (m *Manager) GetStatistics() map[string]any {
|
|
||||||
m.mutex.RLock()
|
|
||||||
defer m.mutex.RUnlock()
|
|
||||||
|
|
||||||
stats := make(map[string]any)
|
|
||||||
stats["total_factions"] = m.masterFactionList.GetFactionCount()
|
|
||||||
stats["total_faction_changes"] = m.totalFactionChanges
|
|
||||||
stats["faction_increases"] = m.factionIncreases
|
|
||||||
stats["faction_decreases"] = m.factionDecreases
|
|
||||||
stats["faction_lookups"] = m.factionLookups
|
|
||||||
stats["players_with_factions"] = m.playersWithFactions
|
|
||||||
|
|
||||||
// Copy changes by faction
|
|
||||||
changeStats := make(map[int32]int64)
|
|
||||||
for factionID, count := range m.changesByFaction {
|
|
||||||
changeStats[factionID] = count
|
|
||||||
}
|
|
||||||
stats["changes_by_faction"] = changeStats
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetStatistics resets all statistics
|
|
||||||
func (m *Manager) ResetStatistics() {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.totalFactionChanges = 0
|
|
||||||
m.factionIncreases = 0
|
|
||||||
m.factionDecreases = 0
|
|
||||||
m.factionLookups = 0
|
|
||||||
m.playersWithFactions = 0
|
|
||||||
m.changesByFaction = make(map[int32]int64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAllFactions validates all factions in the system
|
|
||||||
func (m *Manager) ValidateAllFactions() []string {
|
|
||||||
return m.masterFactionList.ValidateFactions()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReloadFromDatabase reloads all factions from database
|
|
||||||
func (m *Manager) ReloadFromDatabase() error {
|
|
||||||
if m.database == nil {
|
|
||||||
return fmt.Errorf("no database available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear current factions
|
|
||||||
m.masterFactionList.Clear()
|
|
||||||
|
|
||||||
// Reload from database
|
|
||||||
return m.Initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFactionCount returns the total number of factions
|
|
||||||
func (m *Manager) GetFactionCount() int32 {
|
|
||||||
return m.masterFactionList.GetFactionCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessCommand handles faction-related commands
|
|
||||||
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
|
|
||||||
switch command {
|
|
||||||
case "stats":
|
|
||||||
return m.handleStatsCommand(args)
|
|
||||||
case "validate":
|
|
||||||
return m.handleValidateCommand(args)
|
|
||||||
case "list":
|
|
||||||
return m.handleListCommand(args)
|
|
||||||
case "info":
|
|
||||||
return m.handleInfoCommand(args)
|
|
||||||
case "reload":
|
|
||||||
return m.handleReloadCommand(args)
|
|
||||||
case "search":
|
|
||||||
return m.handleSearchCommand(args)
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unknown faction command: %s", command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleStatsCommand shows faction system statistics
|
|
||||||
func (m *Manager) handleStatsCommand(args []string) (string, error) {
|
|
||||||
stats := m.GetStatistics()
|
|
||||||
|
|
||||||
result := "Faction System Statistics:\n"
|
|
||||||
result += fmt.Sprintf("Total Factions: %d\n", stats["total_factions"])
|
|
||||||
result += fmt.Sprintf("Total Faction Changes: %d\n", stats["total_faction_changes"])
|
|
||||||
result += fmt.Sprintf("Faction Increases: %d\n", stats["faction_increases"])
|
|
||||||
result += fmt.Sprintf("Faction Decreases: %d\n", stats["faction_decreases"])
|
|
||||||
result += fmt.Sprintf("Faction Lookups: %d\n", stats["faction_lookups"])
|
|
||||||
result += fmt.Sprintf("Players with Factions: %d\n", stats["players_with_factions"])
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleValidateCommand validates all factions
|
|
||||||
func (m *Manager) handleValidateCommand(args []string) (string, error) {
|
|
||||||
issues := m.ValidateAllFactions()
|
|
||||||
|
|
||||||
if len(issues) == 0 {
|
|
||||||
return "All factions are valid.", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fmt.Sprintf("Found %d issues with factions:\n", len(issues))
|
|
||||||
for i, issue := range issues {
|
|
||||||
if i >= 10 { // Limit output
|
|
||||||
result += "... (and more)\n"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result += fmt.Sprintf("%d. %s\n", i+1, issue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleListCommand lists factions
|
|
||||||
func (m *Manager) handleListCommand(args []string) (string, error) {
|
|
||||||
factions := m.masterFactionList.GetAllFactions()
|
|
||||||
|
|
||||||
if len(factions) == 0 {
|
|
||||||
return "No factions loaded.", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fmt.Sprintf("Factions (%d):\n", len(factions))
|
|
||||||
count := 0
|
|
||||||
for _, faction := range factions {
|
|
||||||
if count >= 20 { // Limit output
|
|
||||||
result += "... (and more)\n"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result += fmt.Sprintf(" %d: %s (%s)\n", faction.ID, faction.Name, faction.Type)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleInfoCommand shows information about a specific faction
|
|
||||||
func (m *Manager) handleInfoCommand(args []string) (string, error) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return "", fmt.Errorf("faction ID or name required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var faction *Faction
|
|
||||||
|
|
||||||
// Try to parse as ID first
|
|
||||||
var factionID int32
|
|
||||||
if _, err := fmt.Sscanf(args[0], "%d", &factionID); err == nil {
|
|
||||||
faction = m.GetFaction(factionID)
|
|
||||||
} else {
|
|
||||||
// Try as name
|
|
||||||
faction = m.GetFactionByName(args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if faction == nil {
|
|
||||||
return fmt.Sprintf("Faction '%s' not found.", args[0]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := "Faction Information:\n"
|
|
||||||
result += fmt.Sprintf("ID: %d\n", faction.ID)
|
|
||||||
result += fmt.Sprintf("Name: %s\n", faction.Name)
|
|
||||||
result += fmt.Sprintf("Type: %s\n", faction.Type)
|
|
||||||
result += fmt.Sprintf("Description: %s\n", faction.Description)
|
|
||||||
result += fmt.Sprintf("Default Value: %d\n", faction.DefaultValue)
|
|
||||||
result += fmt.Sprintf("Positive Change: %d\n", faction.PositiveChange)
|
|
||||||
result += fmt.Sprintf("Negative Change: %d\n", faction.NegativeChange)
|
|
||||||
|
|
||||||
// Show relationships if any
|
|
||||||
hostiles := m.masterFactionList.GetHostileFactions(faction.ID)
|
|
||||||
if len(hostiles) > 0 {
|
|
||||||
result += fmt.Sprintf("Hostile Factions: %v\n", hostiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
friendlies := m.masterFactionList.GetFriendlyFactions(faction.ID)
|
|
||||||
if len(friendlies) > 0 {
|
|
||||||
result += fmt.Sprintf("Friendly Factions: %v\n", friendlies)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleReloadCommand reloads factions from database
|
|
||||||
func (m *Manager) handleReloadCommand(args []string) (string, error) {
|
|
||||||
if err := m.ReloadFromDatabase(); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to reload factions: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
count := m.GetFactionCount()
|
|
||||||
return fmt.Sprintf("Successfully reloaded %d factions from database.", count), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSearchCommand searches for factions by name or type
|
|
||||||
func (m *Manager) handleSearchCommand(args []string) (string, error) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return "", fmt.Errorf("search term required")
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTerm := args[0]
|
|
||||||
factions := m.masterFactionList.GetAllFactions()
|
|
||||||
var results []*Faction
|
|
||||||
|
|
||||||
// Search by name or type
|
|
||||||
for _, faction := range factions {
|
|
||||||
if contains(faction.Name, searchTerm) || contains(faction.Type, searchTerm) {
|
|
||||||
results = append(results, faction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(results) == 0 {
|
|
||||||
return fmt.Sprintf("No factions found matching '%s'.", searchTerm), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fmt.Sprintf("Found %d factions matching '%s':\n", len(results), searchTerm)
|
|
||||||
for i, faction := range results {
|
|
||||||
if i >= 20 { // Limit output
|
|
||||||
result += "... (and more)\n"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result += fmt.Sprintf(" %d: %s (%s)\n", faction.ID, faction.Name, faction.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the manager
|
|
||||||
func (m *Manager) Shutdown() {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Shutting down faction manager...")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear factions
|
|
||||||
m.masterFactionList.Clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// contains checks if a string contains a substring (case-sensitive)
|
|
||||||
func contains(str, substr string) bool {
|
|
||||||
if len(substr) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(str) < len(substr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i <= len(str)-len(substr); i++ {
|
|
||||||
if str[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,730 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,349 +0,0 @@
|
|||||||
package factions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlayerFaction manages faction standing for a single player
|
|
||||||
type PlayerFaction struct {
|
|
||||||
factionValues map[int32]int32 // Faction ID -> current value
|
|
||||||
factionPercent map[int32]int8 // Faction ID -> percentage within con level
|
|
||||||
factionUpdateNeeded []int32 // Factions that need client updates
|
|
||||||
masterFactionList *MasterList
|
|
||||||
updateMutex sync.Mutex // Thread safety for updates
|
|
||||||
mutex sync.RWMutex // Thread safety for faction data
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlayerFaction creates a new player faction system
|
|
||||||
func NewPlayerFaction(masterFactionList *MasterList) *PlayerFaction {
|
|
||||||
return &PlayerFaction{
|
|
||||||
factionValues: make(map[int32]int32),
|
|
||||||
factionPercent: make(map[int32]int8),
|
|
||||||
factionUpdateNeeded: make([]int32, 0),
|
|
||||||
masterFactionList: masterFactionList,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FactionUpdate builds a faction update packet for the client
|
|
||||||
func (pf *PlayerFaction) FactionUpdate(version int16) ([]byte, error) {
|
|
||||||
pf.updateMutex.Lock()
|
|
||||||
defer pf.updateMutex.Unlock()
|
|
||||||
|
|
||||||
if len(pf.factionUpdateNeeded) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a placeholder for packet building
|
|
||||||
// In the full implementation, this would use the PacketStruct system:
|
|
||||||
// packet := configReader.getStruct("WS_FactionUpdate", version)
|
|
||||||
// packet.setArrayLengthByName("num_factions", len(pf.factionUpdateNeeded))
|
|
||||||
// for i, factionID := range pf.factionUpdateNeeded {
|
|
||||||
// faction := pf.masterFactionList.GetFaction(factionID)
|
|
||||||
// if faction != nil {
|
|
||||||
// packet.setArrayDataByName("faction_id", faction.ID, i)
|
|
||||||
// packet.setArrayDataByName("name", faction.Name, i)
|
|
||||||
// packet.setArrayDataByName("description", faction.Description, i)
|
|
||||||
// packet.setArrayDataByName("category", faction.Type, i)
|
|
||||||
// packet.setArrayDataByName("con", pf.GetCon(faction.ID), i)
|
|
||||||
// packet.setArrayDataByName("percentage", pf.GetPercent(faction.ID), i)
|
|
||||||
// packet.setArrayDataByName("value", pf.GetFactionValue(faction.ID), i)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return packet.serialize()
|
|
||||||
|
|
||||||
// Clear update list
|
|
||||||
pf.factionUpdateNeeded = pf.factionUpdateNeeded[:0]
|
|
||||||
|
|
||||||
// Return empty packet for now
|
|
||||||
return make([]byte, 0), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// Note: The C++ code has a comment about always returning the default value,
|
|
||||||
// but the actual implementation returns the stored value or 0
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if pf.masterFactionList == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return pf.masterFactionList.GetIncreaseAmount(factionID) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShouldDecrease returns true if the faction can be decreased
|
|
||||||
func (pf *PlayerFaction) ShouldDecrease(factionID int32) bool {
|
|
||||||
if factionID <= SpecialFactionIDMax {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if pf.masterFactionList == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return pf.masterFactionList.GetDecreaseAmount(factionID) != 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 && pf.masterFactionList != nil {
|
|
||||||
amount = pf.masterFactionList.GetIncreaseAmount(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 && pf.masterFactionList != nil {
|
|
||||||
amount = pf.masterFactionList.GetDecreaseAmount(factionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
pf.factionPercent = make(map[int32]int8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addFactionUpdateNeeded marks a faction as needing an update (internal use, assumes lock held)
|
|
||||||
func (pf *PlayerFaction) addFactionUpdateNeeded(factionID int32) {
|
|
||||||
// Note: This method assumes the mutex is already held by the caller
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,240 +0,0 @@
|
|||||||
package factions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"eq2emu/internal/database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Faction represents a single faction with its properties and embedded database operations
|
|
||||||
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
|
|
||||||
|
|
||||||
db *database.Database
|
|
||||||
isNew bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new faction with the given database connection
|
|
||||||
func New(db *database.Database) *Faction {
|
|
||||||
return &Faction{
|
|
||||||
db: db,
|
|
||||||
isNew: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads a faction from the database by ID
|
|
||||||
func Load(db *database.Database, id int32) (*Faction, error) {
|
|
||||||
faction := &Faction{
|
|
||||||
db: db,
|
|
||||||
isNew: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `SELECT id, name, type, description, negative_change, positive_change, default_value FROM factions WHERE id = ?`
|
|
||||||
row := db.QueryRow(query, id)
|
|
||||||
|
|
||||||
err := row.Scan(&faction.ID, &faction.Name, &faction.Type, &faction.Description,
|
|
||||||
&faction.NegativeChange, &faction.PositiveChange, &faction.DefaultValue)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load faction %d: %w", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return faction, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFaction creates a new faction with the given parameters (legacy helper)
|
|
||||||
func NewFaction(id int32, name, factionType, description string) *Faction {
|
|
||||||
return &Faction{
|
|
||||||
ID: id,
|
|
||||||
Name: name,
|
|
||||||
Type: factionType,
|
|
||||||
Description: description,
|
|
||||||
NegativeChange: 0,
|
|
||||||
PositiveChange: 0,
|
|
||||||
DefaultValue: 0,
|
|
||||||
isNew: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNegativeChange sets the default decrease amount
|
|
||||||
func (f *Faction) SetNegativeChange(amount int16) {
|
|
||||||
f.NegativeChange = amount
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPositiveChange sets the default increase amount
|
|
||||||
func (f *Faction) SetPositiveChange(amount int16) {
|
|
||||||
f.PositiveChange = amount
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDefaultValue sets the default faction value
|
|
||||||
func (f *Faction) SetDefaultValue(value int32) {
|
|
||||||
f.DefaultValue = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save saves the faction to the database
|
|
||||||
func (f *Faction) Save() error {
|
|
||||||
if f.db == nil {
|
|
||||||
return fmt.Errorf("no database connection available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.isNew {
|
|
||||||
return f.insert()
|
|
||||||
}
|
|
||||||
return f.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the faction from the database
|
|
||||||
func (f *Faction) Delete() error {
|
|
||||||
if f.db == nil {
|
|
||||||
return fmt.Errorf("no database connection available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.isNew {
|
|
||||||
return fmt.Errorf("cannot delete unsaved faction")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := f.db.Exec(`DELETE FROM factions WHERE id = ?`, f.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete faction %d: %w", f.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload reloads the faction from the database
|
|
||||||
func (f *Faction) Reload() error {
|
|
||||||
if f.db == nil {
|
|
||||||
return fmt.Errorf("no database connection available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.isNew {
|
|
||||||
return fmt.Errorf("cannot reload unsaved faction")
|
|
||||||
}
|
|
||||||
|
|
||||||
reloaded, err := Load(f.db, f.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy reloaded data
|
|
||||||
f.Name = reloaded.Name
|
|
||||||
f.Type = reloaded.Type
|
|
||||||
f.Description = reloaded.Description
|
|
||||||
f.NegativeChange = reloaded.NegativeChange
|
|
||||||
f.PositiveChange = reloaded.PositiveChange
|
|
||||||
f.DefaultValue = reloaded.DefaultValue
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNew returns true if this is a new faction not yet saved to database
|
|
||||||
func (f *Faction) IsNew() bool {
|
|
||||||
return f.isNew
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates a copy of the faction
|
|
||||||
func (f *Faction) Clone() *Faction {
|
|
||||||
return &Faction{
|
|
||||||
ID: f.ID,
|
|
||||||
Name: f.Name,
|
|
||||||
Type: f.Type,
|
|
||||||
Description: f.Description,
|
|
||||||
NegativeChange: f.NegativeChange,
|
|
||||||
PositiveChange: f.PositiveChange,
|
|
||||||
DefaultValue: f.DefaultValue,
|
|
||||||
db: f.db,
|
|
||||||
isNew: true, // Clone is always new
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert inserts a new faction into the database
|
|
||||||
func (f *Faction) insert() error {
|
|
||||||
query := `INSERT INTO factions (id, name, type, description, negative_change, positive_change, default_value) VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
||||||
_, err := f.db.Exec(query, f.ID, f.Name, f.Type, f.Description, f.NegativeChange, f.PositiveChange, f.DefaultValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to insert faction %d: %w", f.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.isNew = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// update updates an existing faction in the database
|
|
||||||
func (f *Faction) update() error {
|
|
||||||
query := `UPDATE factions SET name = ?, type = ?, description = ?, negative_change = ?, positive_change = ?, default_value = ? WHERE id = ?`
|
|
||||||
result, err := f.db.Exec(query, f.Name, f.Type, f.Description, f.NegativeChange, f.PositiveChange, f.DefaultValue, f.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to update faction %d: %w", f.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rowsAffected, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return fmt.Errorf("faction %d not found for update", f.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
@ -1,23 +1,7 @@
|
|||||||
// Package ground_spawn provides harvestable resource node management for EQ2.
|
|
||||||
//
|
|
||||||
// Basic Usage:
|
|
||||||
//
|
|
||||||
// gs := ground_spawn.New(db)
|
|
||||||
// gs.CollectionSkill = "Mining"
|
|
||||||
// gs.NumberHarvests = 5
|
|
||||||
// gs.Save()
|
|
||||||
//
|
|
||||||
// loaded, _ := ground_spawn.Load(db, 1001)
|
|
||||||
// result, _ := loaded.ProcessHarvest(context)
|
|
||||||
//
|
|
||||||
// Master List:
|
|
||||||
//
|
|
||||||
// masterList := ground_spawn.NewMasterList()
|
|
||||||
// masterList.Add(gs)
|
|
||||||
package ground_spawn
|
package ground_spawn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
@ -27,119 +11,111 @@ import (
|
|||||||
"eq2emu/internal/database"
|
"eq2emu/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GroundSpawn represents a harvestable resource node with embedded database operations
|
// GroundSpawn represents a harvestable resource node
|
||||||
type GroundSpawn struct {
|
type GroundSpawn struct {
|
||||||
// Database fields
|
// Database fields
|
||||||
ID int32 `json:"id" db:"id"` // Auto-generated ID
|
ID int32 `json:"id" db:"id"`
|
||||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` // Entry ID for this type of ground spawn
|
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
||||||
Name string `json:"name" db:"name"` // Display name
|
Name string `json:"name" db:"name"`
|
||||||
CollectionSkill string `json:"collection_skill" db:"collection_skill"` // Required skill (Mining, Gathering, etc.)
|
CollectionSkill string `json:"collection_skill" db:"collection_skill"`
|
||||||
NumberHarvests int8 `json:"number_harvests" db:"number_harvests"` // Harvests before depletion
|
NumberHarvests int8 `json:"number_harvests" db:"number_harvests"`
|
||||||
AttemptsPerHarvest int8 `json:"attempts_per_harvest" db:"attempts_per_harvest"` // Attempts per harvest session
|
AttemptsPerHarvest int8 `json:"attempts_per_harvest" db:"attempts_per_harvest"`
|
||||||
RandomizeHeading bool `json:"randomize_heading" db:"randomize_heading"` // Randomize spawn heading
|
RandomizeHeading bool `json:"randomize_heading" db:"randomize_heading"`
|
||||||
RespawnTime int32 `json:"respawn_time" db:"respawn_time"` // Respawn time in seconds
|
RespawnTime int32 `json:"respawn_time" db:"respawn_time"`
|
||||||
|
|
||||||
// Position data
|
// Position data
|
||||||
X float32 `json:"x" db:"x"` // World X coordinate
|
X float32 `json:"x" db:"x"`
|
||||||
Y float32 `json:"y" db:"y"` // World Y coordinate
|
Y float32 `json:"y" db:"y"`
|
||||||
Z float32 `json:"z" db:"z"` // World Z coordinate
|
Z float32 `json:"z" db:"z"`
|
||||||
Heading float32 `json:"heading" db:"heading"` // Spawn heading/rotation
|
Heading float32 `json:"heading" db:"heading"`
|
||||||
ZoneID int32 `json:"zone_id" db:"zone_id"` // Zone identifier
|
ZoneID int32 `json:"zone_id" db:"zone_id"`
|
||||||
GridID int32 `json:"grid_id" db:"grid_id"` // Grid identifier
|
GridID int32 `json:"grid_id" db:"grid_id"`
|
||||||
|
|
||||||
// State data
|
// State data
|
||||||
IsAlive bool `json:"is_alive"` // Whether spawn is active
|
IsAlive bool `json:"is_alive"`
|
||||||
CurrentHarvests int8 `json:"current_harvests"` // Current harvest count
|
CurrentHarvests int8 `json:"current_harvests"`
|
||||||
LastHarvested time.Time `json:"last_harvested"` // When last harvested
|
LastHarvested time.Time `json:"last_harvested"`
|
||||||
NextRespawn time.Time `json:"next_respawn"` // When it will respawn
|
NextRespawn time.Time `json:"next_respawn"`
|
||||||
|
|
||||||
// Associated data (loaded separately)
|
// Associated data (loaded separately)
|
||||||
HarvestEntries []*HarvestEntry `json:"harvest_entries,omitempty"`
|
HarvestEntries []*HarvestEntry `json:"harvest_entries,omitempty"`
|
||||||
HarvestItems []*HarvestEntryItem `json:"harvest_items,omitempty"`
|
HarvestItems []*HarvestEntryItem `json:"harvest_items,omitempty"`
|
||||||
|
|
||||||
// Database connection and internal state
|
|
||||||
db *database.Database `json:"-"`
|
|
||||||
isNew bool `json:"-"`
|
|
||||||
harvestMux sync.Mutex `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new ground spawn with database connection
|
// HarvestEntry represents harvest table data from database
|
||||||
func New(db *database.Database) *GroundSpawn {
|
type HarvestEntry struct {
|
||||||
return &GroundSpawn{
|
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
||||||
HarvestEntries: make([]*HarvestEntry, 0),
|
MinSkillLevel int16 `json:"min_skill_level" db:"min_skill_level"`
|
||||||
HarvestItems: make([]*HarvestEntryItem, 0),
|
MinAdventureLevel int16 `json:"min_adventure_level" db:"min_adventure_level"`
|
||||||
db: db,
|
BonusTable bool `json:"bonus_table" db:"bonus_table"`
|
||||||
isNew: true,
|
Harvest1 float32 `json:"harvest1" db:"harvest1"`
|
||||||
IsAlive: true,
|
Harvest3 float32 `json:"harvest3" db:"harvest3"`
|
||||||
CurrentHarvests: 0,
|
Harvest5 float32 `json:"harvest5" db:"harvest5"`
|
||||||
NumberHarvests: 5, // Default
|
HarvestImbue float32 `json:"harvest_imbue" db:"harvest_imbue"`
|
||||||
AttemptsPerHarvest: 1, // Default
|
HarvestRare float32 `json:"harvest_rare" db:"harvest_rare"`
|
||||||
RandomizeHeading: true,
|
Harvest10 float32 `json:"harvest10" db:"harvest10"`
|
||||||
}
|
HarvestCoin float32 `json:"harvest_coin" db:"harvest_coin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads a ground spawn by ID from database
|
// HarvestEntryItem represents items that can be harvested
|
||||||
func Load(db *database.Database, groundSpawnID int32) (*GroundSpawn, error) {
|
type HarvestEntryItem struct {
|
||||||
gs := &GroundSpawn{
|
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
||||||
db: db,
|
ItemID int32 `json:"item_id" db:"item_id"`
|
||||||
isNew: false,
|
IsRare int8 `json:"is_rare" db:"is_rare"`
|
||||||
}
|
GridID int32 `json:"grid_id" db:"grid_id"`
|
||||||
|
Quantity int16 `json:"quantity" db:"quantity"`
|
||||||
row := db.QueryRow(`
|
|
||||||
SELECT id, groundspawn_id, name, collection_skill, number_harvests,
|
|
||||||
attempts_per_harvest, randomize_heading, respawn_time,
|
|
||||||
x, y, z, heading, zone_id, grid_id
|
|
||||||
FROM ground_spawns WHERE groundspawn_id = ?
|
|
||||||
`, groundSpawnID)
|
|
||||||
|
|
||||||
err := row.Scan(&gs.ID, &gs.GroundSpawnID, &gs.Name, &gs.CollectionSkill,
|
|
||||||
&gs.NumberHarvests, &gs.AttemptsPerHarvest, &gs.RandomizeHeading,
|
|
||||||
&gs.RespawnTime, &gs.X, &gs.Y, &gs.Z, &gs.Heading, &gs.ZoneID, &gs.GridID)
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, fmt.Errorf("ground spawn not found: %d", groundSpawnID)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("failed to load ground spawn: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize state
|
|
||||||
gs.IsAlive = true
|
|
||||||
gs.CurrentHarvests = gs.NumberHarvests
|
|
||||||
|
|
||||||
// Load harvest entries and items
|
|
||||||
if err := gs.loadHarvestData(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load harvest data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the ground spawn to database
|
// HarvestResult represents the outcome of a harvest attempt
|
||||||
func (gs *GroundSpawn) Save() error {
|
type HarvestResult struct {
|
||||||
if gs.db == nil {
|
Success bool `json:"success"`
|
||||||
return fmt.Errorf("no database connection")
|
HarvestType int8 `json:"harvest_type"`
|
||||||
}
|
ItemsAwarded []*HarvestedItem `json:"items_awarded"`
|
||||||
|
MessageText string `json:"message_text"`
|
||||||
if gs.isNew {
|
SkillGained bool `json:"skill_gained"`
|
||||||
return gs.insert()
|
Error error `json:"error,omitempty"`
|
||||||
}
|
|
||||||
return gs.update()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the ground spawn from database
|
// HarvestedItem represents an item awarded from harvesting
|
||||||
func (gs *GroundSpawn) Delete() error {
|
type HarvestedItem struct {
|
||||||
if gs.db == nil {
|
ItemID int32 `json:"item_id"`
|
||||||
return fmt.Errorf("no database connection")
|
Quantity int16 `json:"quantity"`
|
||||||
}
|
IsRare bool `json:"is_rare"`
|
||||||
if gs.isNew {
|
Name string `json:"name"`
|
||||||
return fmt.Errorf("cannot delete unsaved ground spawn")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := gs.db.Exec("DELETE FROM ground_spawns WHERE groundspawn_id = ?", gs.GroundSpawnID)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID returns the ground spawn ID (implements common.Identifiable)
|
// Player interface for harvest operations
|
||||||
|
type Player interface {
|
||||||
|
GetLevel() int16
|
||||||
|
GetLocation() int32
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skill interface for harvest operations
|
||||||
|
type Skill interface {
|
||||||
|
GetCurrentValue() int16
|
||||||
|
GetMaxValue() int16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger interface for logging operations
|
||||||
|
type Logger interface {
|
||||||
|
LogInfo(system, format string, args ...interface{})
|
||||||
|
LogError(system, format string, args ...interface{})
|
||||||
|
LogDebug(system, format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics holds ground spawn system statistics
|
||||||
|
type Statistics struct {
|
||||||
|
TotalHarvests int64 `json:"total_harvests"`
|
||||||
|
SuccessfulHarvests int64 `json:"successful_harvests"`
|
||||||
|
RareItemsHarvested int64 `json:"rare_items_harvested"`
|
||||||
|
SkillUpsGenerated int64 `json:"skill_ups_generated"`
|
||||||
|
HarvestsBySkill map[string]int64 `json:"harvests_by_skill"`
|
||||||
|
ActiveGroundSpawns int `json:"active_ground_spawns"`
|
||||||
|
GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the ground spawn ID
|
||||||
func (gs *GroundSpawn) GetID() int32 {
|
func (gs *GroundSpawn) GetID() int32 {
|
||||||
return gs.GroundSpawnID
|
return gs.GroundSpawnID
|
||||||
}
|
}
|
||||||
@ -238,9 +214,6 @@ func (gs *GroundSpawn) GetHarvestSpellName() string {
|
|||||||
|
|
||||||
// ProcessHarvest handles the complex harvesting logic (preserves C++ algorithm)
|
// ProcessHarvest handles the complex harvesting logic (preserves C++ algorithm)
|
||||||
func (gs *GroundSpawn) ProcessHarvest(player Player, skill Skill, totalSkill int16) (*HarvestResult, error) {
|
func (gs *GroundSpawn) ProcessHarvest(player Player, skill Skill, totalSkill int16) (*HarvestResult, error) {
|
||||||
gs.harvestMux.Lock()
|
|
||||||
defer gs.harvestMux.Unlock()
|
|
||||||
|
|
||||||
// Check if ground spawn is depleted
|
// Check if ground spawn is depleted
|
||||||
if gs.CurrentHarvests <= 0 {
|
if gs.CurrentHarvests <= 0 {
|
||||||
return &HarvestResult{
|
return &HarvestResult{
|
||||||
@ -266,7 +239,7 @@ func (gs *GroundSpawn) ProcessHarvest(player Player, skill Skill, totalSkill int
|
|||||||
|
|
||||||
// Check for collection skill
|
// Check for collection skill
|
||||||
isCollection := gs.CollectionSkill == "Collecting"
|
isCollection := gs.CollectionSkill == "Collecting"
|
||||||
|
|
||||||
result := &HarvestResult{
|
result := &HarvestResult{
|
||||||
Success: true,
|
Success: true,
|
||||||
ItemsAwarded: make([]*HarvestedItem, 0),
|
ItemsAwarded: make([]*HarvestedItem, 0),
|
||||||
@ -505,9 +478,6 @@ func (gs *GroundSpawn) selectRandomItems(items []*HarvestEntryItem, quantity int
|
|||||||
|
|
||||||
// Respawn resets the ground spawn to harvestable state
|
// Respawn resets the ground spawn to harvestable state
|
||||||
func (gs *GroundSpawn) Respawn() {
|
func (gs *GroundSpawn) Respawn() {
|
||||||
gs.harvestMux.Lock()
|
|
||||||
defer gs.harvestMux.Unlock()
|
|
||||||
|
|
||||||
// Reset harvest count to default
|
// Reset harvest count to default
|
||||||
gs.CurrentHarvests = gs.NumberHarvests
|
gs.CurrentHarvests = gs.NumberHarvests
|
||||||
|
|
||||||
@ -521,57 +491,155 @@ func (gs *GroundSpawn) Respawn() {
|
|||||||
gs.NextRespawn = time.Time{} // Clear next respawn time
|
gs.NextRespawn = time.Time{} // Clear next respawn time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Private database helper methods
|
// GroundSpawnManager provides unified management of the ground spawn system
|
||||||
|
type GroundSpawnManager struct {
|
||||||
|
// Core storage with specialized indices for O(1) lookups
|
||||||
|
spawns map[int32]*GroundSpawn // ID -> GroundSpawn
|
||||||
|
byZone map[int32][]*GroundSpawn // Zone ID -> spawns
|
||||||
|
bySkill map[string][]*GroundSpawn // Skill -> spawns
|
||||||
|
|
||||||
func (gs *GroundSpawn) insert() error {
|
// External dependencies
|
||||||
_, err := gs.db.Exec(`
|
database *database.Database
|
||||||
INSERT INTO ground_spawns (
|
logger Logger
|
||||||
groundspawn_id, name, collection_skill, number_harvests,
|
mutex sync.RWMutex
|
||||||
attempts_per_harvest, randomize_heading, respawn_time,
|
|
||||||
x, y, z, heading, zone_id, grid_id
|
// Statistics
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
totalHarvests int64
|
||||||
`, gs.GroundSpawnID, gs.Name, gs.CollectionSkill, gs.NumberHarvests,
|
successfulHarvests int64
|
||||||
gs.AttemptsPerHarvest, gs.RandomizeHeading, gs.RespawnTime,
|
rareItemsHarvested int64
|
||||||
gs.X, gs.Y, gs.Z, gs.Heading, gs.ZoneID, gs.GridID)
|
skillUpsGenerated int64
|
||||||
|
harvestsBySkill map[string]int64
|
||||||
if err == nil {
|
|
||||||
gs.isNew = false
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GroundSpawn) update() error {
|
// NewGroundSpawnManager creates a new unified ground spawn manager
|
||||||
_, err := gs.db.Exec(`
|
func NewGroundSpawnManager(db *database.Database, logger Logger) *GroundSpawnManager {
|
||||||
UPDATE ground_spawns SET
|
return &GroundSpawnManager{
|
||||||
name = ?, collection_skill = ?, number_harvests = ?,
|
spawns: make(map[int32]*GroundSpawn),
|
||||||
attempts_per_harvest = ?, randomize_heading = ?, respawn_time = ?,
|
byZone: make(map[int32][]*GroundSpawn),
|
||||||
x = ?, y = ?, z = ?, heading = ?, zone_id = ?, grid_id = ?
|
bySkill: make(map[string][]*GroundSpawn),
|
||||||
WHERE groundspawn_id = ?
|
database: db,
|
||||||
`, gs.Name, gs.CollectionSkill, gs.NumberHarvests,
|
logger: logger,
|
||||||
gs.AttemptsPerHarvest, gs.RandomizeHeading, gs.RespawnTime,
|
harvestsBySkill: make(map[string]int64),
|
||||||
gs.X, gs.Y, gs.Z, gs.Heading, gs.ZoneID, gs.GridID, gs.GroundSpawnID)
|
}
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GroundSpawn) loadHarvestData() error {
|
// Initialize loads ground spawns from database
|
||||||
// Load harvest entries
|
func (gsm *GroundSpawnManager) Initialize(ctx context.Context) error {
|
||||||
if err := gs.loadHarvestEntries(); err != nil {
|
if gsm.logger != nil {
|
||||||
return err
|
gsm.logger.LogInfo("ground_spawn", "Initializing ground spawn manager...")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load harvest items
|
if gsm.database == nil {
|
||||||
if err := gs.loadHarvestItems(); err != nil {
|
if gsm.logger != nil {
|
||||||
return err
|
gsm.logger.LogInfo("ground_spawn", "No database provided, starting with empty spawn list")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load all ground spawns
|
||||||
|
if err := gsm.loadGroundSpawnsFromDB(); err != nil {
|
||||||
|
return fmt.Errorf("failed to load ground spawns from database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gsm.logger != nil {
|
||||||
|
gsm.logger.LogInfo("ground_spawn", "Loaded %d ground spawns from database", len(gsm.spawns))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GroundSpawn) loadHarvestEntries() error {
|
// loadGroundSpawnsFromDB loads all ground spawns from database (internal method)
|
||||||
gs.HarvestEntries = make([]*HarvestEntry, 0)
|
func (gsm *GroundSpawnManager) loadGroundSpawnsFromDB() error {
|
||||||
|
// Create ground_spawns table if it doesn't exist
|
||||||
rows, err := gs.db.Query(`
|
_, err := gsm.database.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS ground_spawns (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
groundspawn_id INTEGER NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
collection_skill TEXT,
|
||||||
|
number_harvests INTEGER DEFAULT 1,
|
||||||
|
attempts_per_harvest INTEGER DEFAULT 1,
|
||||||
|
randomize_heading BOOLEAN DEFAULT 1,
|
||||||
|
respawn_time INTEGER DEFAULT 300,
|
||||||
|
x REAL NOT NULL,
|
||||||
|
y REAL NOT NULL,
|
||||||
|
z REAL NOT NULL,
|
||||||
|
heading REAL DEFAULT 0,
|
||||||
|
zone_id INTEGER NOT NULL,
|
||||||
|
grid_id INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create ground_spawns table: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := gsm.database.Query(`
|
||||||
|
SELECT id, groundspawn_id, name, collection_skill, number_harvests,
|
||||||
|
attempts_per_harvest, randomize_heading, respawn_time,
|
||||||
|
x, y, z, heading, zone_id, grid_id
|
||||||
|
FROM ground_spawns ORDER BY id
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query ground spawns: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for rows.Next() {
|
||||||
|
gs := &GroundSpawn{
|
||||||
|
HarvestEntries: make([]*HarvestEntry, 0),
|
||||||
|
HarvestItems: make([]*HarvestEntryItem, 0),
|
||||||
|
IsAlive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Scan(&gs.ID, &gs.GroundSpawnID, &gs.Name, &gs.CollectionSkill,
|
||||||
|
&gs.NumberHarvests, &gs.AttemptsPerHarvest, &gs.RandomizeHeading,
|
||||||
|
&gs.RespawnTime, &gs.X, &gs.Y, &gs.Z, &gs.Heading, &gs.ZoneID, &gs.GridID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan ground spawn: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize state
|
||||||
|
gs.CurrentHarvests = gs.NumberHarvests
|
||||||
|
|
||||||
|
// Load harvest entries and items
|
||||||
|
if err := gsm.loadHarvestData(gs); err != nil {
|
||||||
|
if gsm.logger != nil {
|
||||||
|
gsm.logger.LogError("ground_spawn", "Failed to load harvest data for spawn %d: %v", gs.GroundSpawnID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gsm.addGroundSpawnToIndices(gs); err != nil {
|
||||||
|
if gsm.logger != nil {
|
||||||
|
gsm.logger.LogError("ground_spawn", "Failed to add ground spawn %d: %v", gs.GroundSpawnID, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadHarvestData loads harvest entries and items for a ground spawn (internal method)
|
||||||
|
func (gsm *GroundSpawnManager) loadHarvestData(gs *GroundSpawn) error {
|
||||||
|
// Load harvest entries
|
||||||
|
if err := gsm.loadHarvestEntries(gs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load harvest items
|
||||||
|
if err := gsm.loadHarvestItems(gs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadHarvestEntries loads harvest entries for a ground spawn (internal method)
|
||||||
|
func (gsm *GroundSpawnManager) loadHarvestEntries(gs *GroundSpawn) error {
|
||||||
|
rows, err := gsm.database.Query(`
|
||||||
SELECT groundspawn_id, min_skill_level, min_adventure_level, bonus_table,
|
SELECT groundspawn_id, min_skill_level, min_adventure_level, bonus_table,
|
||||||
harvest1, harvest3, harvest5, harvest_imbue, harvest_rare, harvest10, harvest_coin
|
harvest1, harvest3, harvest5, harvest_imbue, harvest_rare, harvest10, harvest_coin
|
||||||
FROM groundspawn_entries WHERE groundspawn_id = ?
|
FROM groundspawn_entries WHERE groundspawn_id = ?
|
||||||
@ -580,7 +648,7 @@ func (gs *GroundSpawn) loadHarvestEntries() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
entry := &HarvestEntry{}
|
entry := &HarvestEntry{}
|
||||||
err := rows.Scan(&entry.GroundSpawnID, &entry.MinSkillLevel, &entry.MinAdventureLevel,
|
err := rows.Scan(&entry.GroundSpawnID, &entry.MinSkillLevel, &entry.MinAdventureLevel,
|
||||||
@ -594,10 +662,9 @@ func (gs *GroundSpawn) loadHarvestEntries() error {
|
|||||||
return rows.Err()
|
return rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GroundSpawn) loadHarvestItems() error {
|
// loadHarvestItems loads harvest items for a ground spawn (internal method)
|
||||||
gs.HarvestItems = make([]*HarvestEntryItem, 0)
|
func (gsm *GroundSpawnManager) loadHarvestItems(gs *GroundSpawn) error {
|
||||||
|
rows, err := gsm.database.Query(`
|
||||||
rows, err := gs.db.Query(`
|
|
||||||
SELECT groundspawn_id, item_id, is_rare, grid_id, quantity
|
SELECT groundspawn_id, item_id, is_rare, grid_id, quantity
|
||||||
FROM groundspawn_items WHERE groundspawn_id = ?
|
FROM groundspawn_items WHERE groundspawn_id = ?
|
||||||
`, gs.GroundSpawnID)
|
`, gs.GroundSpawnID)
|
||||||
@ -605,7 +672,7 @@ func (gs *GroundSpawn) loadHarvestItems() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
item := &HarvestEntryItem{}
|
item := &HarvestEntryItem{}
|
||||||
err := rows.Scan(&item.GroundSpawnID, &item.ItemID, &item.IsRare, &item.GridID, &item.Quantity)
|
err := rows.Scan(&item.GroundSpawnID, &item.ItemID, &item.IsRare, &item.GridID, &item.Quantity)
|
||||||
@ -616,3 +683,322 @@ func (gs *GroundSpawn) loadHarvestItems() error {
|
|||||||
}
|
}
|
||||||
return rows.Err()
|
return rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addGroundSpawnToIndices adds a ground spawn to all internal indices (internal method)
|
||||||
|
func (gsm *GroundSpawnManager) addGroundSpawnToIndices(gs *GroundSpawn) error {
|
||||||
|
if gs == nil {
|
||||||
|
return fmt.Errorf("ground spawn cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if exists
|
||||||
|
if _, exists := gsm.spawns[gs.GroundSpawnID]; exists {
|
||||||
|
return fmt.Errorf("ground spawn with ID %d already exists", gs.GroundSpawnID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to core storage
|
||||||
|
gsm.spawns[gs.GroundSpawnID] = gs
|
||||||
|
|
||||||
|
// Add to zone index
|
||||||
|
gsm.byZone[gs.ZoneID] = append(gsm.byZone[gs.ZoneID], gs)
|
||||||
|
|
||||||
|
// Add to skill index
|
||||||
|
gsm.bySkill[gs.CollectionSkill] = append(gsm.bySkill[gs.CollectionSkill], gs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroundSpawn returns a ground spawn by ID
|
||||||
|
func (gsm *GroundSpawnManager) GetGroundSpawn(groundSpawnID int32) *GroundSpawn {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
return gsm.spawns[groundSpawnID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroundSpawnsByZone returns all ground spawns in a zone (O(1))
|
||||||
|
func (gsm *GroundSpawnManager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
|
||||||
|
spawns, exists := gsm.byZone[zoneID]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy to prevent external modification
|
||||||
|
result := make([]*GroundSpawn, len(spawns))
|
||||||
|
copy(result, spawns)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroundSpawnsBySkill returns all ground spawns for a skill (O(1))
|
||||||
|
func (gsm *GroundSpawnManager) GetGroundSpawnsBySkill(skill string) []*GroundSpawn {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
|
||||||
|
spawns, exists := gsm.bySkill[skill]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a copy to prevent external modification
|
||||||
|
result := make([]*GroundSpawn, len(spawns))
|
||||||
|
copy(result, spawns)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroundSpawnsByZoneAndSkill returns spawns matching both zone and skill
|
||||||
|
func (gsm *GroundSpawnManager) GetGroundSpawnsByZoneAndSkill(zoneID int32, skill string) []*GroundSpawn {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
|
||||||
|
zoneSpawns := gsm.byZone[zoneID]
|
||||||
|
skillSpawns := gsm.bySkill[skill]
|
||||||
|
|
||||||
|
// Use smaller set for iteration efficiency
|
||||||
|
if len(zoneSpawns) > len(skillSpawns) {
|
||||||
|
zoneSpawns, skillSpawns = skillSpawns, zoneSpawns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set intersection using map lookup
|
||||||
|
skillSet := make(map[*GroundSpawn]struct{}, len(skillSpawns))
|
||||||
|
for _, gs := range skillSpawns {
|
||||||
|
skillSet[gs] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []*GroundSpawn
|
||||||
|
for _, gs := range zoneSpawns {
|
||||||
|
if _, exists := skillSet[gs]; exists {
|
||||||
|
result = append(result, gs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAvailableGroundSpawns returns all harvestable ground spawns
|
||||||
|
func (gsm *GroundSpawnManager) GetAvailableGroundSpawns() []*GroundSpawn {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
|
||||||
|
var available []*GroundSpawn
|
||||||
|
for _, gs := range gsm.spawns {
|
||||||
|
if gs.IsAvailable() {
|
||||||
|
available = append(available, gs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAvailableGroundSpawnsByZone returns harvestable ground spawns in a zone
|
||||||
|
func (gsm *GroundSpawnManager) GetAvailableGroundSpawnsByZone(zoneID int32) []*GroundSpawn {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
|
||||||
|
zoneSpawns := gsm.byZone[zoneID]
|
||||||
|
var available []*GroundSpawn
|
||||||
|
|
||||||
|
for _, gs := range zoneSpawns {
|
||||||
|
if gs.IsAvailable() {
|
||||||
|
available = append(available, gs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessHarvest processes a harvest attempt and updates statistics
|
||||||
|
func (gsm *GroundSpawnManager) ProcessHarvest(groundSpawnID int32, player Player, skill Skill, totalSkill int16) (*HarvestResult, error) {
|
||||||
|
gsm.mutex.Lock()
|
||||||
|
defer gsm.mutex.Unlock()
|
||||||
|
|
||||||
|
gs := gsm.spawns[groundSpawnID]
|
||||||
|
if gs == nil {
|
||||||
|
return nil, fmt.Errorf("ground spawn %d not found", groundSpawnID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process harvest
|
||||||
|
result, err := gs.ProcessHarvest(player, skill, totalSkill)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
gsm.totalHarvests++
|
||||||
|
if result.Success {
|
||||||
|
gsm.successfulHarvests++
|
||||||
|
gsm.harvestsBySkill[gs.CollectionSkill]++
|
||||||
|
|
||||||
|
// Count rare items
|
||||||
|
for _, item := range result.ItemsAwarded {
|
||||||
|
if item.IsRare {
|
||||||
|
gsm.rareItemsHarvested++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.SkillGained {
|
||||||
|
gsm.skillUpsGenerated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log harvest if logger available
|
||||||
|
if gsm.logger != nil {
|
||||||
|
if result.Success {
|
||||||
|
gsm.logger.LogInfo("ground_spawn", "Player %s harvested %d items from spawn %d",
|
||||||
|
player.GetName(), len(result.ItemsAwarded), groundSpawnID)
|
||||||
|
} else {
|
||||||
|
gsm.logger.LogDebug("ground_spawn", "Player %s failed to harvest spawn %d: %s",
|
||||||
|
player.GetName(), groundSpawnID, result.MessageText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespawnGroundSpawn respawns a specific ground spawn
|
||||||
|
func (gsm *GroundSpawnManager) RespawnGroundSpawn(groundSpawnID int32) bool {
|
||||||
|
gsm.mutex.Lock()
|
||||||
|
defer gsm.mutex.Unlock()
|
||||||
|
|
||||||
|
gs := gsm.spawns[groundSpawnID]
|
||||||
|
if gs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
gs.Respawn()
|
||||||
|
|
||||||
|
if gsm.logger != nil {
|
||||||
|
gsm.logger.LogDebug("ground_spawn", "Respawned ground spawn %d", groundSpawnID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatistics returns ground spawn system statistics
|
||||||
|
func (gsm *GroundSpawnManager) GetStatistics() *Statistics {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Count active spawns and spawns by zone
|
||||||
|
activeSpawns := 0
|
||||||
|
spawnsByZone := make(map[int32]int)
|
||||||
|
|
||||||
|
for _, gs := range gsm.spawns {
|
||||||
|
if gs.IsAvailable() {
|
||||||
|
activeSpawns++
|
||||||
|
}
|
||||||
|
spawnsByZone[gs.ZoneID]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy harvests by skill to prevent external modification
|
||||||
|
harvestsBySkill := make(map[string]int64)
|
||||||
|
for skill, count := range gsm.harvestsBySkill {
|
||||||
|
harvestsBySkill[skill] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Statistics{
|
||||||
|
TotalHarvests: gsm.totalHarvests,
|
||||||
|
SuccessfulHarvests: gsm.successfulHarvests,
|
||||||
|
RareItemsHarvested: gsm.rareItemsHarvested,
|
||||||
|
SkillUpsGenerated: gsm.skillUpsGenerated,
|
||||||
|
HarvestsBySkill: harvestsBySkill,
|
||||||
|
ActiveGroundSpawns: activeSpawns,
|
||||||
|
GroundSpawnsByZone: spawnsByZone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGroundSpawnCount returns the total number of ground spawns
|
||||||
|
func (gsm *GroundSpawnManager) GetGroundSpawnCount() int32 {
|
||||||
|
gsm.mutex.RLock()
|
||||||
|
defer gsm.mutex.RUnlock()
|
||||||
|
return int32(len(gsm.spawns))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddGroundSpawn adds a new ground spawn with database persistence
|
||||||
|
func (gsm *GroundSpawnManager) AddGroundSpawn(gs *GroundSpawn) error {
|
||||||
|
if gs == nil {
|
||||||
|
return fmt.Errorf("ground spawn cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
gsm.mutex.Lock()
|
||||||
|
defer gsm.mutex.Unlock()
|
||||||
|
|
||||||
|
// Add to indices
|
||||||
|
if err := gsm.addGroundSpawnToIndices(gs); err != nil {
|
||||||
|
return fmt.Errorf("failed to add ground spawn to indices: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to database if available
|
||||||
|
if gsm.database != nil {
|
||||||
|
if err := gsm.saveGroundSpawnToDBInternal(gs); err != nil {
|
||||||
|
// Remove from indices if save failed
|
||||||
|
gsm.removeGroundSpawnFromIndicesInternal(gs.GroundSpawnID)
|
||||||
|
return fmt.Errorf("failed to save ground spawn to database: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gsm.logger != nil {
|
||||||
|
gsm.logger.LogInfo("ground_spawn", "Added ground spawn %d: %s in zone %d",
|
||||||
|
gs.GroundSpawnID, gs.Name, gs.ZoneID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveGroundSpawnToDBInternal saves a ground spawn to database (internal method, assumes lock held)
|
||||||
|
func (gsm *GroundSpawnManager) saveGroundSpawnToDBInternal(gs *GroundSpawn) error {
|
||||||
|
query := `INSERT OR REPLACE INTO ground_spawns
|
||||||
|
(groundspawn_id, name, collection_skill, number_harvests, attempts_per_harvest,
|
||||||
|
randomize_heading, respawn_time, x, y, z, heading, zone_id, grid_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := gsm.database.Exec(query, gs.GroundSpawnID, gs.Name, gs.CollectionSkill,
|
||||||
|
gs.NumberHarvests, gs.AttemptsPerHarvest, gs.RandomizeHeading, gs.RespawnTime,
|
||||||
|
gs.X, gs.Y, gs.Z, gs.Heading, gs.ZoneID, gs.GridID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeGroundSpawnFromIndicesInternal removes ground spawn from all indices (internal method, assumes lock held)
|
||||||
|
func (gsm *GroundSpawnManager) removeGroundSpawnFromIndicesInternal(groundSpawnID int32) {
|
||||||
|
gs, exists := gsm.spawns[groundSpawnID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from core storage
|
||||||
|
delete(gsm.spawns, groundSpawnID)
|
||||||
|
|
||||||
|
// Remove from zone index
|
||||||
|
zoneSpawns := gsm.byZone[gs.ZoneID]
|
||||||
|
for i, spawn := range zoneSpawns {
|
||||||
|
if spawn.GroundSpawnID == groundSpawnID {
|
||||||
|
gsm.byZone[gs.ZoneID] = append(zoneSpawns[:i], zoneSpawns[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from skill index
|
||||||
|
skillSpawns := gsm.bySkill[gs.CollectionSkill]
|
||||||
|
for i, spawn := range skillSpawns {
|
||||||
|
if spawn.GroundSpawnID == groundSpawnID {
|
||||||
|
gsm.bySkill[gs.CollectionSkill] = append(skillSpawns[:i], skillSpawns[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown gracefully shuts down the manager
|
||||||
|
func (gsm *GroundSpawnManager) Shutdown() {
|
||||||
|
if gsm.logger != nil {
|
||||||
|
gsm.logger.LogInfo("ground_spawn", "Shutting down ground spawn manager...")
|
||||||
|
}
|
||||||
|
|
||||||
|
gsm.mutex.Lock()
|
||||||
|
defer gsm.mutex.Unlock()
|
||||||
|
|
||||||
|
// Clear all data
|
||||||
|
gsm.spawns = make(map[int32]*GroundSpawn)
|
||||||
|
gsm.byZone = make(map[int32][]*GroundSpawn)
|
||||||
|
gsm.bySkill = make(map[string][]*GroundSpawn)
|
||||||
|
gsm.harvestsBySkill = make(map[string]int64)
|
||||||
|
}
|
@ -1,340 +0,0 @@
|
|||||||
package ground_spawn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MasterList is a specialized ground spawn master list optimized for:
|
|
||||||
// - Fast zone-based lookups (O(1))
|
|
||||||
// - Fast skill-based lookups (O(1))
|
|
||||||
// - Spatial grid queries for proximity searches
|
|
||||||
// - Set intersection operations for complex queries
|
|
||||||
// - Fast state-based queries (available/depleted)
|
|
||||||
type MasterList struct {
|
|
||||||
// Core storage
|
|
||||||
spawns map[int32]*GroundSpawn // ID -> GroundSpawn
|
|
||||||
mutex sync.RWMutex
|
|
||||||
|
|
||||||
// Category indices for O(1) lookups
|
|
||||||
byZone map[int32][]*GroundSpawn // Zone ID -> spawns
|
|
||||||
bySkill map[string][]*GroundSpawn // Skill -> spawns
|
|
||||||
|
|
||||||
// State indices for O(1) filtering
|
|
||||||
availableSpawns []*GroundSpawn // Available spawns (cached)
|
|
||||||
depletedSpawns []*GroundSpawn // Depleted spawns (cached)
|
|
||||||
stateStale bool // Whether state caches need refresh
|
|
||||||
|
|
||||||
// Statistics cache
|
|
||||||
stats *Statistics
|
|
||||||
statsStale bool
|
|
||||||
|
|
||||||
// Spatial grid for proximity queries (grid size = 100 units)
|
|
||||||
spatialGrid map[gridKey][]*GroundSpawn
|
|
||||||
gridSize float32
|
|
||||||
}
|
|
||||||
|
|
||||||
// gridKey represents a spatial grid cell
|
|
||||||
type gridKey struct {
|
|
||||||
x, y int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMasterList creates a new specialized ground spawn master list
|
|
||||||
func NewMasterList() *MasterList {
|
|
||||||
return &MasterList{
|
|
||||||
spawns: make(map[int32]*GroundSpawn),
|
|
||||||
byZone: make(map[int32][]*GroundSpawn),
|
|
||||||
bySkill: make(map[string][]*GroundSpawn),
|
|
||||||
spatialGrid: make(map[gridKey][]*GroundSpawn),
|
|
||||||
gridSize: 100.0, // 100 unit grid cells
|
|
||||||
stateStale: true, // Initial state needs refresh
|
|
||||||
statsStale: true, // Initial stats need refresh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getGridKey returns the grid cell for given coordinates
|
|
||||||
func (ml *MasterList) getGridKey(x, y float32) gridKey {
|
|
||||||
return gridKey{
|
|
||||||
x: int32(x / ml.gridSize),
|
|
||||||
y: int32(y / ml.gridSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshStateCache updates the available/depleted spawn caches
|
|
||||||
func (ml *MasterList) refreshStateCache() {
|
|
||||||
if !ml.stateStale {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear existing caches
|
|
||||||
ml.availableSpawns = ml.availableSpawns[:0]
|
|
||||||
ml.depletedSpawns = ml.depletedSpawns[:0]
|
|
||||||
|
|
||||||
// Rebuild caches
|
|
||||||
for _, gs := range ml.spawns {
|
|
||||||
if gs.IsAvailable() {
|
|
||||||
ml.availableSpawns = append(ml.availableSpawns, gs)
|
|
||||||
} else if gs.IsDepleted() {
|
|
||||||
ml.depletedSpawns = append(ml.depletedSpawns, gs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ml.stateStale = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshStatsCache updates the statistics cache
|
|
||||||
func (ml *MasterList) refreshStatsCache() {
|
|
||||||
if !ml.statsStale {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var availableSpawns int
|
|
||||||
zoneMap := make(map[int32]int)
|
|
||||||
skillMap := make(map[string]int64)
|
|
||||||
|
|
||||||
// Single pass through all spawns
|
|
||||||
for _, gs := range ml.spawns {
|
|
||||||
if gs.IsAvailable() {
|
|
||||||
availableSpawns++
|
|
||||||
}
|
|
||||||
zoneMap[gs.ZoneID]++
|
|
||||||
skillMap[gs.CollectionSkill]++
|
|
||||||
}
|
|
||||||
|
|
||||||
ml.stats = &Statistics{
|
|
||||||
TotalHarvests: 0, // Would need to be tracked separately
|
|
||||||
SuccessfulHarvests: 0, // Would need to be tracked separately
|
|
||||||
RareItemsHarvested: 0, // Would need to be tracked separately
|
|
||||||
SkillUpsGenerated: 0, // Would need to be tracked separately
|
|
||||||
HarvestsBySkill: skillMap,
|
|
||||||
ActiveGroundSpawns: availableSpawns,
|
|
||||||
GroundSpawnsByZone: zoneMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
ml.statsStale = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddGroundSpawn adds a ground spawn with full indexing
|
|
||||||
func (ml *MasterList) AddGroundSpawn(gs *GroundSpawn) bool {
|
|
||||||
ml.mutex.Lock()
|
|
||||||
defer ml.mutex.Unlock()
|
|
||||||
|
|
||||||
// Check if exists
|
|
||||||
if _, exists := ml.spawns[gs.GroundSpawnID]; exists {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to core storage
|
|
||||||
ml.spawns[gs.GroundSpawnID] = gs
|
|
||||||
|
|
||||||
// Update zone index
|
|
||||||
ml.byZone[gs.ZoneID] = append(ml.byZone[gs.ZoneID], gs)
|
|
||||||
|
|
||||||
// Update skill index
|
|
||||||
ml.bySkill[gs.CollectionSkill] = append(ml.bySkill[gs.CollectionSkill], gs)
|
|
||||||
|
|
||||||
// Update spatial grid
|
|
||||||
gridKey := ml.getGridKey(gs.X, gs.Y)
|
|
||||||
ml.spatialGrid[gridKey] = append(ml.spatialGrid[gridKey], gs)
|
|
||||||
|
|
||||||
// Invalidate state and stats caches
|
|
||||||
ml.stateStale = true
|
|
||||||
ml.statsStale = true
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGroundSpawn retrieves by ID (O(1))
|
|
||||||
func (ml *MasterList) GetGroundSpawn(id int32) *GroundSpawn {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
return ml.spawns[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByZone returns all spawns in a zone (O(1))
|
|
||||||
func (ml *MasterList) GetByZone(zoneID int32) []*GroundSpawn {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
return ml.byZone[zoneID] // Return slice directly for performance
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBySkill returns all spawns for a skill (O(1))
|
|
||||||
func (ml *MasterList) GetBySkill(skill string) []*GroundSpawn {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
return ml.bySkill[skill] // Return slice directly for performance
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByZoneAndSkill returns spawns matching both zone and skill (set intersection)
|
|
||||||
func (ml *MasterList) GetByZoneAndSkill(zoneID int32, skill string) []*GroundSpawn {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
|
|
||||||
zoneSpawns := ml.byZone[zoneID]
|
|
||||||
skillSpawns := ml.bySkill[skill]
|
|
||||||
|
|
||||||
// Use smaller set for iteration efficiency
|
|
||||||
if len(zoneSpawns) > len(skillSpawns) {
|
|
||||||
zoneSpawns, skillSpawns = skillSpawns, zoneSpawns
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set intersection using map lookup
|
|
||||||
skillSet := make(map[*GroundSpawn]struct{}, len(skillSpawns))
|
|
||||||
for _, gs := range skillSpawns {
|
|
||||||
skillSet[gs] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []*GroundSpawn
|
|
||||||
for _, gs := range zoneSpawns {
|
|
||||||
if _, exists := skillSet[gs]; exists {
|
|
||||||
result = append(result, gs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNearby returns spawns within radius of given coordinates using spatial grid
|
|
||||||
func (ml *MasterList) GetNearby(x, y float32, radius float32) []*GroundSpawn {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
|
|
||||||
// Calculate grid search bounds
|
|
||||||
minX := int32((x - radius) / ml.gridSize)
|
|
||||||
maxX := int32((x + radius) / ml.gridSize)
|
|
||||||
minY := int32((y - radius) / ml.gridSize)
|
|
||||||
maxY := int32((y + radius) / ml.gridSize)
|
|
||||||
|
|
||||||
var candidates []*GroundSpawn
|
|
||||||
|
|
||||||
// Check all grid cells in range
|
|
||||||
for gx := minX; gx <= maxX; gx++ {
|
|
||||||
for gy := minY; gy <= maxY; gy++ {
|
|
||||||
key := gridKey{x: gx, y: gy}
|
|
||||||
if spawns, exists := ml.spatialGrid[key]; exists {
|
|
||||||
candidates = append(candidates, spawns...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by exact distance
|
|
||||||
radiusSquared := radius * radius
|
|
||||||
var result []*GroundSpawn
|
|
||||||
for _, gs := range candidates {
|
|
||||||
dx := gs.X - x
|
|
||||||
dy := gs.Y - y
|
|
||||||
if dx*dx+dy*dy <= radiusSquared {
|
|
||||||
result = append(result, gs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAvailableSpawns returns harvestable spawns using cached results
|
|
||||||
func (ml *MasterList) GetAvailableSpawns() []*GroundSpawn {
|
|
||||||
ml.mutex.Lock() // Need write lock to potentially update cache
|
|
||||||
defer ml.mutex.Unlock()
|
|
||||||
|
|
||||||
ml.refreshStateCache()
|
|
||||||
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
result := make([]*GroundSpawn, len(ml.availableSpawns))
|
|
||||||
copy(result, ml.availableSpawns)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDepletedSpawns returns depleted spawns using cached results
|
|
||||||
func (ml *MasterList) GetDepletedSpawns() []*GroundSpawn {
|
|
||||||
ml.mutex.Lock() // Need write lock to potentially update cache
|
|
||||||
defer ml.mutex.Unlock()
|
|
||||||
|
|
||||||
ml.refreshStateCache()
|
|
||||||
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
result := make([]*GroundSpawn, len(ml.depletedSpawns))
|
|
||||||
copy(result, ml.depletedSpawns)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatistics returns system statistics using cached results
|
|
||||||
func (ml *MasterList) GetStatistics() *Statistics {
|
|
||||||
ml.mutex.Lock() // Need write lock to potentially update cache
|
|
||||||
defer ml.mutex.Unlock()
|
|
||||||
|
|
||||||
ml.refreshStatsCache()
|
|
||||||
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
return &Statistics{
|
|
||||||
TotalHarvests: ml.stats.TotalHarvests,
|
|
||||||
SuccessfulHarvests: ml.stats.SuccessfulHarvests,
|
|
||||||
RareItemsHarvested: ml.stats.RareItemsHarvested,
|
|
||||||
SkillUpsGenerated: ml.stats.SkillUpsGenerated,
|
|
||||||
HarvestsBySkill: ml.stats.HarvestsBySkill,
|
|
||||||
ActiveGroundSpawns: ml.stats.ActiveGroundSpawns,
|
|
||||||
GroundSpawnsByZone: ml.stats.GroundSpawnsByZone,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveGroundSpawn removes a spawn and updates all indices
|
|
||||||
func (ml *MasterList) RemoveGroundSpawn(id int32) bool {
|
|
||||||
ml.mutex.Lock()
|
|
||||||
defer ml.mutex.Unlock()
|
|
||||||
|
|
||||||
gs, exists := ml.spawns[id]
|
|
||||||
if !exists {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from core storage
|
|
||||||
delete(ml.spawns, id)
|
|
||||||
|
|
||||||
// Remove from zone index
|
|
||||||
zoneSpawns := ml.byZone[gs.ZoneID]
|
|
||||||
for i, spawn := range zoneSpawns {
|
|
||||||
if spawn.GroundSpawnID == id {
|
|
||||||
ml.byZone[gs.ZoneID] = append(zoneSpawns[:i], zoneSpawns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from skill index
|
|
||||||
skillSpawns := ml.bySkill[gs.CollectionSkill]
|
|
||||||
for i, spawn := range skillSpawns {
|
|
||||||
if spawn.GroundSpawnID == id {
|
|
||||||
ml.bySkill[gs.CollectionSkill] = append(skillSpawns[:i], skillSpawns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from spatial grid
|
|
||||||
gridKey := ml.getGridKey(gs.X, gs.Y)
|
|
||||||
gridSpawns := ml.spatialGrid[gridKey]
|
|
||||||
for i, spawn := range gridSpawns {
|
|
||||||
if spawn.GroundSpawnID == id {
|
|
||||||
ml.spatialGrid[gridKey] = append(gridSpawns[:i], gridSpawns[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate state and stats caches
|
|
||||||
ml.stateStale = true
|
|
||||||
ml.statsStale = true
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the total number of spawns
|
|
||||||
func (ml *MasterList) Size() int {
|
|
||||||
ml.mutex.RLock()
|
|
||||||
defer ml.mutex.RUnlock()
|
|
||||||
return len(ml.spawns)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidateStateCache marks the state and stats caches as stale
|
|
||||||
// Call this when spawn states change (e.g., after harvesting, respawning)
|
|
||||||
func (ml *MasterList) InvalidateStateCache() {
|
|
||||||
ml.mutex.Lock()
|
|
||||||
defer ml.mutex.Unlock()
|
|
||||||
ml.stateStale = true
|
|
||||||
ml.statsStale = true
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
package ground_spawn
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// HarvestEntry represents harvest table data from database
|
|
||||||
type HarvestEntry struct {
|
|
||||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` // Ground spawn ID
|
|
||||||
MinSkillLevel int16 `json:"min_skill_level" db:"min_skill_level"` // Minimum skill level required
|
|
||||||
MinAdventureLevel int16 `json:"min_adventure_level" db:"min_adventure_level"` // Minimum adventure level required
|
|
||||||
BonusTable bool `json:"bonus_table" db:"bonus_table"` // Whether this is a bonus table
|
|
||||||
Harvest1 float32 `json:"harvest1" db:"harvest1"` // Chance for 1 item (percentage)
|
|
||||||
Harvest3 float32 `json:"harvest3" db:"harvest3"` // Chance for 3 items (percentage)
|
|
||||||
Harvest5 float32 `json:"harvest5" db:"harvest5"` // Chance for 5 items (percentage)
|
|
||||||
HarvestImbue float32 `json:"harvest_imbue" db:"harvest_imbue"` // Chance for imbue item (percentage)
|
|
||||||
HarvestRare float32 `json:"harvest_rare" db:"harvest_rare"` // Chance for rare item (percentage)
|
|
||||||
Harvest10 float32 `json:"harvest10" db:"harvest10"` // Chance for 10 + rare items (percentage)
|
|
||||||
HarvestCoin float32 `json:"harvest_coin" db:"harvest_coin"` // Chance for coin reward (percentage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HarvestEntryItem represents items that can be harvested
|
|
||||||
type HarvestEntryItem struct {
|
|
||||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` // Ground spawn ID
|
|
||||||
ItemID int32 `json:"item_id" db:"item_id"` // Item database ID
|
|
||||||
IsRare int8 `json:"is_rare" db:"is_rare"` // 0=normal, 1=rare, 2=imbue
|
|
||||||
GridID int32 `json:"grid_id" db:"grid_id"` // Grid restriction (0=any)
|
|
||||||
Quantity int16 `json:"quantity" db:"quantity"` // Item quantity (usually 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HarvestResult represents the outcome of a harvest attempt
|
|
||||||
type HarvestResult struct {
|
|
||||||
Success bool `json:"success"` // Whether harvest succeeded
|
|
||||||
HarvestType int8 `json:"harvest_type"` // Type of harvest achieved
|
|
||||||
ItemsAwarded []*HarvestedItem `json:"items_awarded"` // Items given to player
|
|
||||||
MessageText string `json:"message_text"` // Message to display to player
|
|
||||||
SkillGained bool `json:"skill_gained"` // Whether skill was gained
|
|
||||||
Error error `json:"error,omitempty"` // Any error that occurred
|
|
||||||
}
|
|
||||||
|
|
||||||
// HarvestedItem represents an item awarded from harvesting
|
|
||||||
type HarvestedItem struct {
|
|
||||||
ItemID int32 `json:"item_id"` // Database item ID
|
|
||||||
Quantity int16 `json:"quantity"` // Number of items
|
|
||||||
IsRare bool `json:"is_rare"` // Whether this is a rare item
|
|
||||||
Name string `json:"name"` // Item name for messages
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player interface for harvest operations (simplified)
|
|
||||||
type Player interface {
|
|
||||||
GetLevel() int16
|
|
||||||
GetLocation() int32
|
|
||||||
GetName() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skill interface for harvest operations (simplified)
|
|
||||||
type Skill interface {
|
|
||||||
GetCurrentValue() int16
|
|
||||||
GetMaxValue() int16
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client interface for ground spawn use operations (simplified)
|
|
||||||
type Client interface {
|
|
||||||
GetPlayer() *Player
|
|
||||||
GetVersion() int16
|
|
||||||
GetLogger() Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger interface for logging operations
|
|
||||||
type Logger interface {
|
|
||||||
LogDebug(format string, args ...interface{})
|
|
||||||
LogError(format string, args ...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// RespawnConfig holds respawn timing configuration
|
|
||||||
type RespawnConfig struct {
|
|
||||||
BaseTime time.Duration `json:"base_time"` // Base respawn time
|
|
||||||
RandomDelay time.Duration `json:"random_delay"` // Random delay addition
|
|
||||||
ZoneModifier float64 `json:"zone_modifier"` // Zone-specific modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistics holds ground spawn system statistics
|
|
||||||
type Statistics struct {
|
|
||||||
TotalHarvests int64 `json:"total_harvests"`
|
|
||||||
SuccessfulHarvests int64 `json:"successful_harvests"`
|
|
||||||
RareItemsHarvested int64 `json:"rare_items_harvested"`
|
|
||||||
SkillUpsGenerated int64 `json:"skill_ups_generated"`
|
|
||||||
HarvestsBySkill map[string]int64 `json:"harvests_by_skill"`
|
|
||||||
ActiveGroundSpawns int `json:"active_ground_spawns"`
|
|
||||||
GroundSpawnsByZone map[int32]int `json:"ground_spawns_by_zone"`
|
|
||||||
}
|
|
@ -68,6 +68,9 @@ const (
|
|||||||
// Achievement system
|
// Achievement system
|
||||||
OP_AchievementUpdateMsg
|
OP_AchievementUpdateMsg
|
||||||
OP_CharacterAchievements
|
OP_CharacterAchievements
|
||||||
|
|
||||||
|
// Faction system
|
||||||
|
OP_FactionUpdateMsg
|
||||||
|
|
||||||
// Title system
|
// Title system
|
||||||
OP_TitleUpdateMsg
|
OP_TitleUpdateMsg
|
||||||
@ -177,6 +180,7 @@ var OpcodeNames = map[InternalOpcode]string{
|
|||||||
OP_UpdatePositionMsg: "OP_UpdatePositionMsg",
|
OP_UpdatePositionMsg: "OP_UpdatePositionMsg",
|
||||||
OP_AchievementUpdateMsg: "OP_AchievementUpdateMsg",
|
OP_AchievementUpdateMsg: "OP_AchievementUpdateMsg",
|
||||||
OP_CharacterAchievements: "OP_CharacterAchievements",
|
OP_CharacterAchievements: "OP_CharacterAchievements",
|
||||||
|
OP_FactionUpdateMsg: "OP_FactionUpdateMsg",
|
||||||
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
|
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
|
||||||
OP_CharacterTitles: "OP_CharacterTitles",
|
OP_CharacterTitles: "OP_CharacterTitles",
|
||||||
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
|
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user