simplify factions and ground_spawn

This commit is contained in:
Sky Johnson 2025-08-29 13:47:08 -05:00
parent 91886c5fbb
commit 88fd7bed4d
13 changed files with 2296 additions and 3031 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"`
}

View File

@ -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",