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
|
||||
- Collections
|
||||
- Entity
|
||||
- Factions
|
||||
- Ground Spawn
|
||||
|
||||
## 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
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
@ -27,119 +11,111 @@ import (
|
||||
"eq2emu/internal/database"
|
||||
)
|
||||
|
||||
// GroundSpawn represents a harvestable resource node with embedded database operations
|
||||
// GroundSpawn represents a harvestable resource node
|
||||
type GroundSpawn struct {
|
||||
// Database fields
|
||||
ID int32 `json:"id" db:"id"` // Auto-generated ID
|
||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"` // Entry ID for this type of ground spawn
|
||||
Name string `json:"name" db:"name"` // Display name
|
||||
CollectionSkill string `json:"collection_skill" db:"collection_skill"` // Required skill (Mining, Gathering, etc.)
|
||||
NumberHarvests int8 `json:"number_harvests" db:"number_harvests"` // Harvests before depletion
|
||||
AttemptsPerHarvest int8 `json:"attempts_per_harvest" db:"attempts_per_harvest"` // Attempts per harvest session
|
||||
RandomizeHeading bool `json:"randomize_heading" db:"randomize_heading"` // Randomize spawn heading
|
||||
RespawnTime int32 `json:"respawn_time" db:"respawn_time"` // Respawn time in seconds
|
||||
|
||||
ID int32 `json:"id" db:"id"`
|
||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
CollectionSkill string `json:"collection_skill" db:"collection_skill"`
|
||||
NumberHarvests int8 `json:"number_harvests" db:"number_harvests"`
|
||||
AttemptsPerHarvest int8 `json:"attempts_per_harvest" db:"attempts_per_harvest"`
|
||||
RandomizeHeading bool `json:"randomize_heading" db:"randomize_heading"`
|
||||
RespawnTime int32 `json:"respawn_time" db:"respawn_time"`
|
||||
|
||||
// Position data
|
||||
X float32 `json:"x" db:"x"` // World X coordinate
|
||||
Y float32 `json:"y" db:"y"` // World Y coordinate
|
||||
Z float32 `json:"z" db:"z"` // World Z coordinate
|
||||
Heading float32 `json:"heading" db:"heading"` // Spawn heading/rotation
|
||||
ZoneID int32 `json:"zone_id" db:"zone_id"` // Zone identifier
|
||||
GridID int32 `json:"grid_id" db:"grid_id"` // Grid identifier
|
||||
X float32 `json:"x" db:"x"`
|
||||
Y float32 `json:"y" db:"y"`
|
||||
Z float32 `json:"z" db:"z"`
|
||||
Heading float32 `json:"heading" db:"heading"`
|
||||
ZoneID int32 `json:"zone_id" db:"zone_id"`
|
||||
GridID int32 `json:"grid_id" db:"grid_id"`
|
||||
|
||||
// State data
|
||||
IsAlive bool `json:"is_alive"` // Whether spawn is active
|
||||
CurrentHarvests int8 `json:"current_harvests"` // Current harvest count
|
||||
LastHarvested time.Time `json:"last_harvested"` // When last harvested
|
||||
NextRespawn time.Time `json:"next_respawn"` // When it will respawn
|
||||
IsAlive bool `json:"is_alive"`
|
||||
CurrentHarvests int8 `json:"current_harvests"`
|
||||
LastHarvested time.Time `json:"last_harvested"`
|
||||
NextRespawn time.Time `json:"next_respawn"`
|
||||
|
||||
// Associated data (loaded separately)
|
||||
HarvestEntries []*HarvestEntry `json:"harvest_entries,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
|
||||
func New(db *database.Database) *GroundSpawn {
|
||||
return &GroundSpawn{
|
||||
HarvestEntries: make([]*HarvestEntry, 0),
|
||||
HarvestItems: make([]*HarvestEntryItem, 0),
|
||||
db: db,
|
||||
isNew: true,
|
||||
IsAlive: true,
|
||||
CurrentHarvests: 0,
|
||||
NumberHarvests: 5, // Default
|
||||
AttemptsPerHarvest: 1, // Default
|
||||
RandomizeHeading: true,
|
||||
}
|
||||
// HarvestEntry represents harvest table data from database
|
||||
type HarvestEntry struct {
|
||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
||||
MinSkillLevel int16 `json:"min_skill_level" db:"min_skill_level"`
|
||||
MinAdventureLevel int16 `json:"min_adventure_level" db:"min_adventure_level"`
|
||||
BonusTable bool `json:"bonus_table" db:"bonus_table"`
|
||||
Harvest1 float32 `json:"harvest1" db:"harvest1"`
|
||||
Harvest3 float32 `json:"harvest3" db:"harvest3"`
|
||||
Harvest5 float32 `json:"harvest5" db:"harvest5"`
|
||||
HarvestImbue float32 `json:"harvest_imbue" db:"harvest_imbue"`
|
||||
HarvestRare float32 `json:"harvest_rare" db:"harvest_rare"`
|
||||
Harvest10 float32 `json:"harvest10" db:"harvest10"`
|
||||
HarvestCoin float32 `json:"harvest_coin" db:"harvest_coin"`
|
||||
}
|
||||
|
||||
// Load loads a ground spawn by ID from database
|
||||
func Load(db *database.Database, groundSpawnID int32) (*GroundSpawn, error) {
|
||||
gs := &GroundSpawn{
|
||||
db: db,
|
||||
isNew: false,
|
||||
}
|
||||
|
||||
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
|
||||
// HarvestEntryItem represents items that can be harvested
|
||||
type HarvestEntryItem struct {
|
||||
GroundSpawnID int32 `json:"groundspawn_id" db:"groundspawn_id"`
|
||||
ItemID int32 `json:"item_id" db:"item_id"`
|
||||
IsRare int8 `json:"is_rare" db:"is_rare"`
|
||||
GridID int32 `json:"grid_id" db:"grid_id"`
|
||||
Quantity int16 `json:"quantity" db:"quantity"`
|
||||
}
|
||||
|
||||
// Save saves the ground spawn to database
|
||||
func (gs *GroundSpawn) Save() error {
|
||||
if gs.db == nil {
|
||||
return fmt.Errorf("no database connection")
|
||||
}
|
||||
|
||||
if gs.isNew {
|
||||
return gs.insert()
|
||||
}
|
||||
return gs.update()
|
||||
// HarvestResult represents the outcome of a harvest attempt
|
||||
type HarvestResult struct {
|
||||
Success bool `json:"success"`
|
||||
HarvestType int8 `json:"harvest_type"`
|
||||
ItemsAwarded []*HarvestedItem `json:"items_awarded"`
|
||||
MessageText string `json:"message_text"`
|
||||
SkillGained bool `json:"skill_gained"`
|
||||
Error error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Delete removes the ground spawn from database
|
||||
func (gs *GroundSpawn) Delete() error {
|
||||
if gs.db == nil {
|
||||
return fmt.Errorf("no database connection")
|
||||
}
|
||||
if gs.isNew {
|
||||
return fmt.Errorf("cannot delete unsaved ground spawn")
|
||||
}
|
||||
|
||||
_, err := gs.db.Exec("DELETE FROM ground_spawns WHERE groundspawn_id = ?", gs.GroundSpawnID)
|
||||
return err
|
||||
// HarvestedItem represents an item awarded from harvesting
|
||||
type HarvestedItem struct {
|
||||
ItemID int32 `json:"item_id"`
|
||||
Quantity int16 `json:"quantity"`
|
||||
IsRare bool `json:"is_rare"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return gs.GroundSpawnID
|
||||
}
|
||||
@ -238,9 +214,6 @@ func (gs *GroundSpawn) GetHarvestSpellName() string {
|
||||
|
||||
// ProcessHarvest handles the complex harvesting logic (preserves C++ algorithm)
|
||||
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
|
||||
if gs.CurrentHarvests <= 0 {
|
||||
return &HarvestResult{
|
||||
@ -266,7 +239,7 @@ func (gs *GroundSpawn) ProcessHarvest(player Player, skill Skill, totalSkill int
|
||||
|
||||
// Check for collection skill
|
||||
isCollection := gs.CollectionSkill == "Collecting"
|
||||
|
||||
|
||||
result := &HarvestResult{
|
||||
Success: true,
|
||||
ItemsAwarded: make([]*HarvestedItem, 0),
|
||||
@ -505,9 +478,6 @@ func (gs *GroundSpawn) selectRandomItems(items []*HarvestEntryItem, quantity int
|
||||
|
||||
// Respawn resets the ground spawn to harvestable state
|
||||
func (gs *GroundSpawn) Respawn() {
|
||||
gs.harvestMux.Lock()
|
||||
defer gs.harvestMux.Unlock()
|
||||
|
||||
// Reset harvest count to default
|
||||
gs.CurrentHarvests = gs.NumberHarvests
|
||||
|
||||
@ -521,57 +491,155 @@ func (gs *GroundSpawn) Respawn() {
|
||||
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 {
|
||||
_, err := gs.db.Exec(`
|
||||
INSERT 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, 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 {
|
||||
gs.isNew = false
|
||||
}
|
||||
return err
|
||||
// External dependencies
|
||||
database *database.Database
|
||||
logger Logger
|
||||
mutex sync.RWMutex
|
||||
|
||||
// Statistics
|
||||
totalHarvests int64
|
||||
successfulHarvests int64
|
||||
rareItemsHarvested int64
|
||||
skillUpsGenerated int64
|
||||
harvestsBySkill map[string]int64
|
||||
}
|
||||
|
||||
func (gs *GroundSpawn) update() error {
|
||||
_, err := gs.db.Exec(`
|
||||
UPDATE ground_spawns SET
|
||||
name = ?, collection_skill = ?, number_harvests = ?,
|
||||
attempts_per_harvest = ?, randomize_heading = ?, respawn_time = ?,
|
||||
x = ?, y = ?, z = ?, heading = ?, zone_id = ?, grid_id = ?
|
||||
WHERE groundspawn_id = ?
|
||||
`, gs.Name, gs.CollectionSkill, gs.NumberHarvests,
|
||||
gs.AttemptsPerHarvest, gs.RandomizeHeading, gs.RespawnTime,
|
||||
gs.X, gs.Y, gs.Z, gs.Heading, gs.ZoneID, gs.GridID, gs.GroundSpawnID)
|
||||
|
||||
return err
|
||||
// NewGroundSpawnManager creates a new unified ground spawn manager
|
||||
func NewGroundSpawnManager(db *database.Database, logger Logger) *GroundSpawnManager {
|
||||
return &GroundSpawnManager{
|
||||
spawns: make(map[int32]*GroundSpawn),
|
||||
byZone: make(map[int32][]*GroundSpawn),
|
||||
bySkill: make(map[string][]*GroundSpawn),
|
||||
database: db,
|
||||
logger: logger,
|
||||
harvestsBySkill: make(map[string]int64),
|
||||
}
|
||||
}
|
||||
|
||||
func (gs *GroundSpawn) loadHarvestData() error {
|
||||
// Load harvest entries
|
||||
if err := gs.loadHarvestEntries(); err != nil {
|
||||
return err
|
||||
// Initialize loads ground spawns from database
|
||||
func (gsm *GroundSpawnManager) Initialize(ctx context.Context) error {
|
||||
if gsm.logger != nil {
|
||||
gsm.logger.LogInfo("ground_spawn", "Initializing ground spawn manager...")
|
||||
}
|
||||
|
||||
// Load harvest items
|
||||
if err := gs.loadHarvestItems(); err != nil {
|
||||
return err
|
||||
|
||||
if gsm.database == nil {
|
||||
if gsm.logger != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (gs *GroundSpawn) loadHarvestEntries() error {
|
||||
gs.HarvestEntries = make([]*HarvestEntry, 0)
|
||||
|
||||
rows, err := gs.db.Query(`
|
||||
// loadGroundSpawnsFromDB loads all ground spawns from database (internal method)
|
||||
func (gsm *GroundSpawnManager) loadGroundSpawnsFromDB() error {
|
||||
// Create ground_spawns table if it doesn't exist
|
||||
_, 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,
|
||||
harvest1, harvest3, harvest5, harvest_imbue, harvest_rare, harvest10, harvest_coin
|
||||
FROM groundspawn_entries WHERE groundspawn_id = ?
|
||||
@ -580,7 +648,7 @@ func (gs *GroundSpawn) loadHarvestEntries() error {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
for rows.Next() {
|
||||
entry := &HarvestEntry{}
|
||||
err := rows.Scan(&entry.GroundSpawnID, &entry.MinSkillLevel, &entry.MinAdventureLevel,
|
||||
@ -594,10 +662,9 @@ func (gs *GroundSpawn) loadHarvestEntries() error {
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func (gs *GroundSpawn) loadHarvestItems() error {
|
||||
gs.HarvestItems = make([]*HarvestEntryItem, 0)
|
||||
|
||||
rows, err := gs.db.Query(`
|
||||
// loadHarvestItems loads harvest items for a ground spawn (internal method)
|
||||
func (gsm *GroundSpawnManager) loadHarvestItems(gs *GroundSpawn) error {
|
||||
rows, err := gsm.database.Query(`
|
||||
SELECT groundspawn_id, item_id, is_rare, grid_id, quantity
|
||||
FROM groundspawn_items WHERE groundspawn_id = ?
|
||||
`, gs.GroundSpawnID)
|
||||
@ -605,7 +672,7 @@ func (gs *GroundSpawn) loadHarvestItems() error {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
|
||||
for rows.Next() {
|
||||
item := &HarvestEntryItem{}
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
OP_AchievementUpdateMsg
|
||||
OP_CharacterAchievements
|
||||
|
||||
// Faction system
|
||||
OP_FactionUpdateMsg
|
||||
|
||||
// Title system
|
||||
OP_TitleUpdateMsg
|
||||
@ -177,6 +180,7 @@ var OpcodeNames = map[InternalOpcode]string{
|
||||
OP_UpdatePositionMsg: "OP_UpdatePositionMsg",
|
||||
OP_AchievementUpdateMsg: "OP_AchievementUpdateMsg",
|
||||
OP_CharacterAchievements: "OP_CharacterAchievements",
|
||||
OP_FactionUpdateMsg: "OP_FactionUpdateMsg",
|
||||
OP_TitleUpdateMsg: "OP_TitleUpdateMsg",
|
||||
OP_CharacterTitles: "OP_CharacterTitles",
|
||||
OP_SetActiveTitleMsg: "OP_SetActiveTitleMsg",
|
||||
|
Loading…
x
Reference in New Issue
Block a user