From 0534c496103538975c38415883e1412183d5c51c Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 1 Aug 2025 22:16:43 -0500 Subject: [PATCH] implement tests and beches for ground_spawn package --- internal/alt_advancement/README.md | 6 +- internal/alt_advancement/aa_test.go | 947 +++++++++--------- internal/alt_advancement/database.go | 4 +- internal/alt_advancement/interfaces.go | 38 +- internal/alt_advancement/manager.go | 8 +- internal/alt_advancement/master_list.go | 8 +- internal/appearances/appearances.go | 4 +- internal/appearances/appearances_test.go | 430 ++++---- internal/appearances/interfaces.go | 8 +- internal/appearances/manager.go | 2 +- internal/classes/classes.go | 4 +- internal/classes/classes_test.go | 10 +- internal/classes/integration.go | 18 +- internal/classes/manager.go | 12 +- internal/classes/utils.go | 4 +- internal/collections/interfaces.go | 8 +- internal/entity/entity.go | 2 +- internal/factions/interfaces.go | 8 +- internal/factions/manager.go | 4 +- internal/ground_spawn/benchmark_test.go | 562 +++++++++++ internal/ground_spawn/concurrency_test.go | 523 ++++++++++ .../ground_spawn/core_concurrency_test.go | 333 ++++++ internal/ground_spawn/database.go | 249 +++++ internal/ground_spawn/ground_spawn.go | 56 +- internal/ground_spawn/interfaces.go | 22 +- internal/ground_spawn/manager.go | 113 ++- internal/ground_spawn/test_utils.go | 8 + internal/ground_spawn/types.go | 7 +- internal/groups/README.md | 2 +- internal/groups/group.go | 6 +- internal/groups/interfaces.go | 20 +- internal/groups/manager.go | 2 +- internal/groups/types.go | 18 +- internal/guilds/interfaces.go | 12 +- internal/heroic_ops/interfaces.go | 16 +- internal/heroic_ops/master_list.go | 4 +- internal/housing/interfaces.go | 16 +- internal/items/interfaces.go | 8 +- internal/items/loot/chest.go | 46 +- internal/items/loot/database.go | 88 +- internal/items/loot/integration.go | 58 +- internal/items/loot/loot_test.go | 18 +- internal/languages/interfaces.go | 10 +- internal/npc/ai/interfaces.go | 12 +- internal/npc/ai/variants.go | 2 +- internal/npc/interfaces.go | 40 +- internal/object/integration.go | 6 +- internal/object/manager.go | 4 +- internal/object/object.go | 4 +- internal/player/interfaces.go | 12 +- internal/player/manager.go | 4 +- internal/quests/interfaces.go | 16 +- internal/races/integration.go | 18 +- internal/races/manager.go | 12 +- internal/races/races.go | 4 +- internal/races/utils.go | 4 +- internal/recipes/recipe.go | 4 +- internal/rules/interfaces.go | 12 +- internal/sign/interfaces.go | 10 +- internal/sign/manager.go | 4 +- internal/skills/integration.go | 6 +- internal/skills/manager.go | 4 +- internal/spawn/spawn.go | 2 +- internal/spells/spell_manager.go | 4 +- internal/spells/spell_targeting.go | 6 +- internal/titles/master_list.go | 12 +- internal/titles/player_titles.go | 10 +- internal/titles/title_manager.go | 4 +- internal/trade/manager.go | 6 +- internal/trade/trade.go | 4 +- internal/trade/utils.go | 10 +- internal/tradeskills/interfaces.go | 4 +- internal/traits/interfaces.go | 8 +- internal/traits/manager.go | 2 +- internal/transmute/manager.go | 4 +- internal/transmute/types.go | 2 +- internal/widget/interfaces.go | 6 +- internal/widget/manager.go | 4 +- internal/zone/interfaces.go | 174 ++-- 79 files changed, 2967 insertions(+), 1195 deletions(-) create mode 100644 internal/ground_spawn/benchmark_test.go create mode 100644 internal/ground_spawn/concurrency_test.go create mode 100644 internal/ground_spawn/core_concurrency_test.go create mode 100644 internal/ground_spawn/database.go create mode 100644 internal/ground_spawn/test_utils.go diff --git a/internal/alt_advancement/README.md b/internal/alt_advancement/README.md index 6e42e0f..dd1dae1 100644 --- a/internal/alt_advancement/README.md +++ b/internal/alt_advancement/README.md @@ -368,17 +368,17 @@ fmt.Printf("Cache hits: %d, misses: %d\n", // Custom packet handler type MyAAPacketHandler struct{} -func (ph *MyAAPacketHandler) GetAAListPacket(client interface{}) ([]byte, error) { +func (ph *MyAAPacketHandler) GetAAListPacket(client any) ([]byte, error) { // Build AA list packet for client return []byte{}, nil } -func (ph *MyAAPacketHandler) SendAAUpdate(client interface{}, playerState *AAPlayerState) error { +func (ph *MyAAPacketHandler) SendAAUpdate(client any, playerState *AAPlayerState) error { // Send AA update to client return nil } -func (ph *MyAAPacketHandler) HandleAAPurchase(client interface{}, nodeID int32, rank int8) error { +func (ph *MyAAPacketHandler) HandleAAPurchase(client any, nodeID int32, rank int8) error { // Handle AA purchase from client return nil } diff --git a/internal/alt_advancement/aa_test.go b/internal/alt_advancement/aa_test.go index 49fb68e..d943c53 100644 --- a/internal/alt_advancement/aa_test.go +++ b/internal/alt_advancement/aa_test.go @@ -27,23 +27,23 @@ func TestAltAdvanceData(t *testing.T) { ClassName: "Fighter", SubclassName: "Guardian", } - + copy := aa.Copy() if copy == aa { t.Error("Copy should return a new instance, not the same pointer") } - + if !reflect.DeepEqual(aa, copy) { t.Error("Copy should have identical field values") } - + // Modify original and ensure copy is unaffected aa.Name = "Modified" if copy.Name == "Modified" { t.Error("Copy should not be affected by changes to original") } }) - + t.Run("IsValid", func(t *testing.T) { tests := []struct { name string @@ -112,7 +112,7 @@ func TestAltAdvanceData(t *testing.T) { expected: false, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if result := tt.aa.IsValid(); result != tt.expected { @@ -130,36 +130,36 @@ func TestMasterAAList(t *testing.T) { if masterList == nil { t.Fatal("NewMasterAAList returned nil") } - + if masterList.Size() != 0 { t.Error("Expected empty list to have size 0") } - + if masterList.aaList == nil { t.Error("Expected aaList to be initialized") } - + if masterList.aaBySpellID == nil || masterList.aaByNodeID == nil || masterList.aaByGroup == nil { t.Error("Expected lookup maps to be initialized") } }) - + t.Run("AddAltAdvancement", func(t *testing.T) { masterList := NewMasterAAList() - + // Test adding nil err := masterList.AddAltAdvancement(nil) if err == nil { t.Error("Expected error when adding nil AA") } - + // Test adding invalid AA invalidAA := &AltAdvanceData{SpellID: 0} err = masterList.AddAltAdvancement(invalidAA) if err == nil { t.Error("Expected error when adding invalid AA") } - + // Test adding valid AA validAA := &AltAdvanceData{ SpellID: 100, @@ -169,16 +169,16 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + err = masterList.AddAltAdvancement(validAA) if err != nil { t.Errorf("Unexpected error adding valid AA: %v", err) } - + if masterList.Size() != 1 { t.Error("Expected size to be 1 after adding AA") } - + // Test duplicate spell ID dupSpellAA := &AltAdvanceData{ SpellID: 100, // Same spell ID @@ -187,12 +187,12 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + err = masterList.AddAltAdvancement(dupSpellAA) if err == nil { t.Error("Expected error when adding duplicate spell ID") } - + // Test duplicate node ID dupNodeAA := &AltAdvanceData{ SpellID: 101, @@ -201,16 +201,16 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + err = masterList.AddAltAdvancement(dupNodeAA) if err == nil { t.Error("Expected error when adding duplicate node ID") } }) - + t.Run("GetAltAdvancement", func(t *testing.T) { masterList := NewMasterAAList() - + aa := &AltAdvanceData{ SpellID: 100, NodeID: 200, @@ -219,33 +219,33 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa) - + // Test getting existing AA retrieved := masterList.GetAltAdvancement(100) if retrieved == nil { t.Fatal("Expected to retrieve AA by spell ID") } - + if retrieved == aa { t.Error("GetAltAdvancement should return a copy, not the original") } - + if retrieved.SpellID != aa.SpellID || retrieved.Name != aa.Name { t.Error("Retrieved AA should have same data as original") } - + // Test getting non-existent AA notFound := masterList.GetAltAdvancement(999) if notFound != nil { t.Error("Expected nil for non-existent spell ID") } }) - + t.Run("GetAltAdvancementByNodeID", func(t *testing.T) { masterList := NewMasterAAList() - + aa := &AltAdvanceData{ SpellID: 100, NodeID: 200, @@ -254,29 +254,29 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa) - + // Test getting existing AA retrieved := masterList.GetAltAdvancementByNodeID(200) if retrieved == nil { t.Fatal("Expected to retrieve AA by node ID") } - + if retrieved.NodeID != aa.NodeID { t.Error("Retrieved AA should have same node ID") } - + // Test getting non-existent AA notFound := masterList.GetAltAdvancementByNodeID(999) if notFound != nil { t.Error("Expected nil for non-existent node ID") } }) - + t.Run("GetAAsByGroup", func(t *testing.T) { masterList := NewMasterAAList() - + // Add AAs to different groups aa1 := &AltAdvanceData{ SpellID: 100, @@ -286,7 +286,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa2 := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -295,7 +295,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa3 := &AltAdvanceData{ SpellID: 102, NodeID: 202, @@ -304,37 +304,37 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa1) masterList.AddAltAdvancement(aa2) masterList.AddAltAdvancement(aa3) - + // Test getting AAs by group classAAs := masterList.GetAAsByGroup(AA_CLASS) if len(classAAs) != 2 { t.Errorf("Expected 2 class AAs, got %d", len(classAAs)) } - + heroicAAs := masterList.GetAAsByGroup(AA_HEROIC) if len(heroicAAs) != 1 { t.Errorf("Expected 1 heroic AA, got %d", len(heroicAAs)) } - + // Test getting empty group emptyGroup := masterList.GetAAsByGroup(AA_DRAGON) if len(emptyGroup) != 0 { t.Error("Expected empty slice for group with no AAs") } - + // Verify copies are returned if &classAAs[0] == &aa1 { t.Error("GetAAsByGroup should return copies, not originals") } }) - + t.Run("GetAAsByClass", func(t *testing.T) { masterList := NewMasterAAList() - + // Add AAs with different class requirements aaAllClasses := &AltAdvanceData{ SpellID: 100, @@ -345,7 +345,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aaFighterOnly := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -355,7 +355,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aaMageOnly := &AltAdvanceData{ SpellID: 102, NodeID: 202, @@ -365,33 +365,33 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aaAllClasses) masterList.AddAltAdvancement(aaFighterOnly) masterList.AddAltAdvancement(aaMageOnly) - + // Test getting AAs for fighter class fighterAAs := masterList.GetAAsByClass(1) if len(fighterAAs) != 2 { t.Errorf("Expected 2 AAs for fighter (all classes + fighter only), got %d", len(fighterAAs)) } - + // Test getting AAs for mage class mageAAs := masterList.GetAAsByClass(20) if len(mageAAs) != 2 { t.Errorf("Expected 2 AAs for mage (all classes + mage only), got %d", len(mageAAs)) } - + // Test getting AAs for class with no specific AAs priestAAs := masterList.GetAAsByClass(10) if len(priestAAs) != 1 { t.Errorf("Expected 1 AA for priest (all classes only), got %d", len(priestAAs)) } }) - + t.Run("GetAAsByLevel", func(t *testing.T) { masterList := NewMasterAAList() - + // Add AAs with different level requirements aaLevel10 := &AltAdvanceData{ SpellID: 100, @@ -402,7 +402,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aaLevel30 := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -412,7 +412,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aaLevel50 := &AltAdvanceData{ SpellID: 102, NodeID: 202, @@ -422,36 +422,36 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aaLevel10) masterList.AddAltAdvancement(aaLevel30) masterList.AddAltAdvancement(aaLevel50) - + // Test getting AAs at different levels level20AAs := masterList.GetAAsByLevel(20) if len(level20AAs) != 1 { t.Errorf("Expected 1 AA at level 20, got %d", len(level20AAs)) } - + level40AAs := masterList.GetAAsByLevel(40) if len(level40AAs) != 2 { t.Errorf("Expected 2 AAs at level 40, got %d", len(level40AAs)) } - + level60AAs := masterList.GetAAsByLevel(60) if len(level60AAs) != 3 { t.Errorf("Expected 3 AAs at level 60, got %d", len(level60AAs)) } - + level5AAs := masterList.GetAAsByLevel(5) if len(level5AAs) != 0 { t.Errorf("Expected 0 AAs at level 5, got %d", len(level5AAs)) } }) - + t.Run("SortAAsByGroup", func(t *testing.T) { masterList := NewMasterAAList() - + // Add AAs in random order aa1 := &AltAdvanceData{ SpellID: 100, @@ -463,7 +463,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa2 := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -474,7 +474,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa3 := &AltAdvanceData{ SpellID: 102, NodeID: 202, @@ -485,29 +485,29 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa1) masterList.AddAltAdvancement(aa2) masterList.AddAltAdvancement(aa3) - + // Sort and verify order masterList.SortAAsByGroup() - + classAAs := masterList.GetAAsByGroup(AA_CLASS) if len(classAAs) != 3 { t.Fatal("Expected 3 AAs") } - + // Should be sorted by row then column // aa3 (1,2), aa2 (1,5), aa1 (2,3) if classAAs[0].Name != "AA3" || classAAs[1].Name != "AA2" || classAAs[2].Name != "AA1" { t.Error("AAs not sorted correctly by row and column") } }) - + t.Run("GetGroups", func(t *testing.T) { masterList := NewMasterAAList() - + // Add AAs to different groups aa1 := &AltAdvanceData{ SpellID: 100, @@ -517,7 +517,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa2 := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -526,7 +526,7 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa3 := &AltAdvanceData{ SpellID: 102, NodeID: 202, @@ -535,30 +535,30 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa1) masterList.AddAltAdvancement(aa2) masterList.AddAltAdvancement(aa3) - + groups := masterList.GetGroups() if len(groups) != 3 { t.Errorf("Expected 3 groups, got %d", len(groups)) } - + // Check that all expected groups are present groupMap := make(map[int8]bool) for _, g := range groups { groupMap[g] = true } - + if !groupMap[AA_CLASS] || !groupMap[AA_HEROIC] || !groupMap[AA_TRADESKILL] { t.Error("Not all expected groups were returned") } }) - + t.Run("DestroyAltAdvancements", func(t *testing.T) { masterList := NewMasterAAList() - + // Add some AAs aa := &AltAdvanceData{ SpellID: 100, @@ -568,29 +568,29 @@ func TestMasterAAList(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa) - + if masterList.Size() != 1 { t.Error("Expected size to be 1 before destroy") } - + // Destroy all AAs masterList.DestroyAltAdvancements() - + if masterList.Size() != 0 { t.Error("Expected size to be 0 after destroy") } - + // Verify maps are cleared if len(masterList.aaBySpellID) != 0 || len(masterList.aaByNodeID) != 0 || len(masterList.aaByGroup) != 0 { t.Error("Expected all maps to be cleared after destroy") } }) - + t.Run("ConcurrentAccess", func(t *testing.T) { masterList := NewMasterAAList() - + // Add initial AAs for i := int32(1); i <= 10; i++ { aa := &AltAdvanceData{ @@ -603,11 +603,11 @@ func TestMasterAAList(t *testing.T) { } masterList.AddAltAdvancement(aa) } - + // Concurrent reads and writes var wg sync.WaitGroup errors := make(chan error, 100) - + // Multiple readers for i := 0; i < 10; i++ { wg.Add(1) @@ -620,7 +620,7 @@ func TestMasterAAList(t *testing.T) { } }() } - + // Multiple writers (trying to add new unique AAs) for i := 0; i < 5; i++ { wg.Add(1) @@ -639,17 +639,17 @@ func TestMasterAAList(t *testing.T) { } }(int32(i)) } - + wg.Wait() close(errors) - + // Check for unexpected errors errorCount := 0 for err := range errors { errorCount++ t.Logf("Concurrent write error (expected for duplicates): %v", err) } - + // Check that we have expected number of AAs finalSize := masterList.Size() if finalSize < 10 || finalSize > 15 { // 10 initial + 0-5 concurrent (some may fail due to race) @@ -665,119 +665,119 @@ func TestMasterAANodeList(t *testing.T) { if nodeList == nil { t.Fatal("NewMasterAANodeList returned nil") } - + if nodeList.Size() != 0 { t.Error("Expected empty list to have size 0") } }) - + t.Run("AddTreeNode", func(t *testing.T) { nodeList := NewMasterAANodeList() - + // Test adding nil err := nodeList.AddTreeNode(nil) if err == nil { t.Error("Expected error when adding nil node") } - + // Test adding valid node node := &TreeNodeData{ ClassID: 1, TreeID: 100, AATreeID: 200, } - + err = nodeList.AddTreeNode(node) if err != nil { t.Errorf("Unexpected error adding valid node: %v", err) } - + if nodeList.Size() != 1 { t.Error("Expected size to be 1 after adding node") } - + // Test duplicate tree ID dupNode := &TreeNodeData{ ClassID: 2, TreeID: 100, // Same tree ID AATreeID: 201, } - + err = nodeList.AddTreeNode(dupNode) if err == nil { t.Error("Expected error when adding duplicate tree ID") } }) - + t.Run("GetTreeNodeByTreeID", func(t *testing.T) { nodeList := NewMasterAANodeList() - + node := &TreeNodeData{ ClassID: 1, TreeID: 100, AATreeID: 200, } - + nodeList.AddTreeNode(node) - + // Test getting existing node retrieved := nodeList.GetTreeNode(100) if retrieved == nil { t.Fatal("Expected to retrieve node by tree ID") } - + if retrieved == node { t.Error("GetTreeNode should return a copy, not the original") } - + if retrieved.TreeID != node.TreeID { t.Error("Retrieved node should have same tree ID") } - + // Test getting non-existent node notFound := nodeList.GetTreeNode(999) if notFound != nil { t.Error("Expected nil for non-existent tree ID") } }) - + t.Run("GetTreeNodesByClass", func(t *testing.T) { nodeList := NewMasterAANodeList() - + // Add nodes for different classes node1 := &TreeNodeData{ ClassID: 1, TreeID: 100, AATreeID: 200, } - + node2 := &TreeNodeData{ ClassID: 1, TreeID: 101, AATreeID: 201, } - + node3 := &TreeNodeData{ ClassID: 2, TreeID: 102, AATreeID: 202, } - + nodeList.AddTreeNode(node1) nodeList.AddTreeNode(node2) nodeList.AddTreeNode(node3) - + // Test getting nodes by class class1Nodes := nodeList.GetTreeNodesByClass(1) if len(class1Nodes) != 2 { t.Errorf("Expected 2 nodes for class 1, got %d", len(class1Nodes)) } - + class2Nodes := nodeList.GetTreeNodesByClass(2) if len(class2Nodes) != 1 { t.Errorf("Expected 1 node for class 2, got %d", len(class2Nodes)) } - + // Test getting empty class emptyClass := nodeList.GetTreeNodesByClass(999) if len(emptyClass) != 0 { @@ -794,43 +794,43 @@ func TestAATemplate(t *testing.T) { if template == nil { t.Fatal("NewAATemplate returned nil") } - + if template.TemplateID != AA_TEMPLATE_PERSONAL_1 { t.Error("Expected template ID to match") } - + if template.Name != "Personal Template 1" { t.Error("Expected template name to match") } - + if !template.IsPersonal { t.Error("Expected IsPersonal to be true for template ID 1") } - + if template.IsServer { t.Error("Expected IsServer to be false for template ID 1") } - + if template.IsCurrent { t.Error("Expected IsCurrent to be false for template ID 1") } - + // Test server template serverTemplate := NewAATemplate(AA_TEMPLATE_SERVER_1, "Server Template 1") if !serverTemplate.IsServer { t.Error("Expected IsServer to be true for template ID 4") } - + // Test current template currentTemplate := NewAATemplate(AA_TEMPLATE_CURRENT, "Current") if !currentTemplate.IsCurrent { t.Error("Expected IsCurrent to be true for template ID 7") } }) - + t.Run("AddEntry", func(t *testing.T) { template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - + entry := &AAEntry{ TemplateID: AA_TEMPLATE_PERSONAL_1, TabID: AA_CLASS, @@ -838,22 +838,22 @@ func TestAATemplate(t *testing.T) { Order: 1, TreeID: 1, } - + // Add entry template.AddEntry(entry) - + if len(template.Entries) != 1 { t.Error("Expected 1 entry after adding") } - + if template.Entries[0] != entry { t.Error("Expected entry to be added to template") } }) - + t.Run("GetEntry", func(t *testing.T) { template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - + entry1 := &AAEntry{ TemplateID: AA_TEMPLATE_PERSONAL_1, TabID: AA_CLASS, @@ -861,7 +861,7 @@ func TestAATemplate(t *testing.T) { Order: 1, TreeID: 1, } - + entry2 := &AAEntry{ TemplateID: AA_TEMPLATE_PERSONAL_1, TabID: AA_CLASS, @@ -869,30 +869,30 @@ func TestAATemplate(t *testing.T) { Order: 2, TreeID: 2, } - + template.AddEntry(entry1) template.AddEntry(entry2) - + // Test getting existing entry found := template.GetEntry(100) if found == nil { t.Error("Expected to find entry with AAID 100") } - + if found != entry1 { t.Error("Expected to get correct entry") } - + // Test getting non-existent entry notFound := template.GetEntry(999) if notFound != nil { t.Error("Expected nil for non-existent AAID") } }) - + t.Run("RemoveEntry", func(t *testing.T) { template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") - + entry := &AAEntry{ TemplateID: AA_TEMPLATE_PERSONAL_1, TabID: AA_CLASS, @@ -900,19 +900,19 @@ func TestAATemplate(t *testing.T) { Order: 1, TreeID: 1, } - + template.AddEntry(entry) - + // Remove entry removed := template.RemoveEntry(100) if !removed { t.Error("Expected RemoveEntry to return true") } - + if len(template.Entries) != 0 { t.Error("Expected 0 entries after removing") } - + // Try to remove non-existent entry removed = template.RemoveEntry(999) if removed { @@ -928,27 +928,27 @@ func TestAAPlayerState(t *testing.T) { if playerState == nil { t.Fatal("NewAAPlayerState returned nil") } - + if playerState.CharacterID != 123 { t.Error("Expected character ID to be 123") } - + if playerState.TotalPoints != 0 { t.Error("Expected initial total points to be 0") } - + if playerState.ActiveTemplate != AA_TEMPLATE_CURRENT { t.Error("Expected active template to be current template") } - + if playerState.Templates == nil || playerState.Tabs == nil || playerState.AAProgress == nil { t.Error("Expected maps to be initialized") } }) - + t.Run("AddAAProgress", func(t *testing.T) { playerState := NewAAPlayerState(123) - + progress := &PlayerAAData{ CharacterID: 123, NodeID: 100, @@ -957,50 +957,50 @@ func TestAAPlayerState(t *testing.T) { TemplateID: AA_TEMPLATE_CURRENT, TabID: AA_CLASS, } - + playerState.AddAAProgress(progress) - + if len(playerState.AAProgress) != 1 { t.Error("Expected 1 AA progress entry") } - + if playerState.AAProgress[100] != progress { t.Error("Expected progress to be added correctly") } }) - + t.Run("GetAAProgress", func(t *testing.T) { playerState := NewAAPlayerState(123) - + progress := &PlayerAAData{ CharacterID: 123, NodeID: 100, CurrentRank: 1, PointsSpent: 2, } - + playerState.AddAAProgress(progress) - + // Test getting existing progress found := playerState.GetAAProgress(100) if found == nil { t.Error("Expected to find AA progress") } - + if found != progress { t.Error("Expected to get correct progress") } - + // Test getting non-existent progress notFound := playerState.GetAAProgress(999) if notFound != nil { t.Error("Expected nil for non-existent node ID") } }) - + t.Run("CalculateSpentPoints", func(t *testing.T) { playerState := NewAAPlayerState(123) - + // Add multiple progress entries progress1 := &PlayerAAData{ CharacterID: 123, @@ -1008,63 +1008,63 @@ func TestAAPlayerState(t *testing.T) { CurrentRank: 2, PointsSpent: 4, } - + progress2 := &PlayerAAData{ CharacterID: 123, NodeID: 101, CurrentRank: 3, PointsSpent: 6, } - + playerState.AddAAProgress(progress1) playerState.AddAAProgress(progress2) - + total := playerState.CalculateSpentPoints() if total != 10 { t.Errorf("Expected total spent points to be 10, got %d", total) } }) - + t.Run("UpdatePoints", func(t *testing.T) { playerState := NewAAPlayerState(123) - + // Update points playerState.UpdatePoints(100, 20, 5) - + if playerState.TotalPoints != 100 { t.Error("Expected total points to be 100") } - + if playerState.SpentPoints != 20 { t.Error("Expected spent points to be 20") } - + if playerState.BankedPoints != 5 { t.Error("Expected banked points to be 5") } - + if playerState.AvailablePoints != 80 { t.Error("Expected available points to be 80 (100 - 20)") } }) - + t.Run("SetActiveTemplate", func(t *testing.T) { playerState := NewAAPlayerState(123) - + // Create and add a template template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Personal 1") playerState.Templates[AA_TEMPLATE_PERSONAL_1] = template - + // Set active template success := playerState.SetActiveTemplate(AA_TEMPLATE_PERSONAL_1) if !success { t.Error("Expected SetActiveTemplate to succeed") } - + if playerState.ActiveTemplate != AA_TEMPLATE_PERSONAL_1 { t.Error("Expected active template to be updated") } - + // Try to set non-existent template success = playerState.SetActiveTemplate(AA_TEMPLATE_PERSONAL_2) if success { @@ -1092,7 +1092,7 @@ func TestUtilityFunctions(t *testing.T) { {AA_FARSEAS, MAX_FARSEAS_AA}, {99, 100}, // Unknown group } - + for _, tt := range tests { t.Run(GetTabName(tt.group), func(t *testing.T) { result := GetMaxAAForTab(tt.group) @@ -1102,7 +1102,7 @@ func TestUtilityFunctions(t *testing.T) { }) } }) - + t.Run("GetTabName", func(t *testing.T) { tests := []struct { group int8 @@ -1115,7 +1115,7 @@ func TestUtilityFunctions(t *testing.T) { {AA_TRADESKILL, "Tradeskill"}, {99, "Unknown"}, } - + for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { result := GetTabName(tt.group) @@ -1125,7 +1125,7 @@ func TestUtilityFunctions(t *testing.T) { }) } }) - + t.Run("GetTemplateName", func(t *testing.T) { tests := []struct { templateID int8 @@ -1137,7 +1137,7 @@ func TestUtilityFunctions(t *testing.T) { {AA_TEMPLATE_CURRENT, "Current"}, {99, "Unknown"}, } - + for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { result := GetTemplateName(tt.templateID) @@ -1147,7 +1147,7 @@ func TestUtilityFunctions(t *testing.T) { }) } }) - + t.Run("IsExpansionRequired", func(t *testing.T) { tests := []struct { name string @@ -1161,7 +1161,7 @@ func TestUtilityFunctions(t *testing.T) { {"Multiple expansions - has one", EXPANSION_KOS | EXPANSION_EOF, EXPANSION_KOS, true}, {"Multiple expansions - has different", EXPANSION_KOS | EXPANSION_EOF, EXPANSION_ROK, false}, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsExpansionRequired(tt.flags, tt.expansion) @@ -1176,23 +1176,23 @@ func TestUtilityFunctions(t *testing.T) { // Test DefaultAAManagerConfig func TestDefaultAAManagerConfig(t *testing.T) { config := DefaultAAManagerConfig() - + if !config.EnableAASystem { t.Error("Expected EnableAASystem to be true by default") } - + if !config.EnableCaching { t.Error("Expected EnableCaching to be true by default") } - + if config.AAPointsPerLevel != DEFAULT_AA_POINTS_PER_LEVEL { t.Errorf("Expected AAPointsPerLevel to be %d, got %d", DEFAULT_AA_POINTS_PER_LEVEL, config.AAPointsPerLevel) } - + if config.MaxBankedPoints != DEFAULT_AA_MAX_BANKED_POINTS { t.Errorf("Expected MaxBankedPoints to be %d, got %d", DEFAULT_AA_MAX_BANKED_POINTS, config.MaxBankedPoints) } - + if config.CacheSize != AA_CACHE_SIZE { t.Errorf("Expected CacheSize to be %d, got %d", AA_CACHE_SIZE, config.CacheSize) } @@ -1201,29 +1201,29 @@ func TestDefaultAAManagerConfig(t *testing.T) { // Test AAManager basic functionality func TestAAManagerBasics(t *testing.T) { config := DefaultAAManagerConfig() - + manager := NewAAManager(config) if manager == nil { t.Fatal("NewAAManager returned nil") } - + // Test configuration currentConfig := manager.GetConfig() if currentConfig.UpdateInterval != config.UpdateInterval { t.Error("Expected config to match") } - + // Test stats stats := manager.GetSystemStats() if stats == nil { t.Error("Expected valid stats") } - + // Test that manager is initialized if manager.masterAAList == nil { t.Error("Expected master AA list to be initialized") } - + if manager.masterNodeList == nil { t.Error("Expected master node list to be initialized") } @@ -1259,7 +1259,7 @@ func (t *AATemplate) RemoveEntry(aaID int32) bool { func (ps *AAPlayerState) AddAAProgress(progress *PlayerAAData) { ps.mutex.Lock() defer ps.mutex.Unlock() - + ps.AAProgress[progress.NodeID] = progress ps.needsSync = true ps.lastUpdate = time.Now() @@ -1268,14 +1268,14 @@ func (ps *AAPlayerState) AddAAProgress(progress *PlayerAAData) { func (ps *AAPlayerState) GetAAProgress(nodeID int32) *PlayerAAData { ps.mutex.RLock() defer ps.mutex.RUnlock() - + return ps.AAProgress[nodeID] } func (ps *AAPlayerState) CalculateSpentPoints() int32 { ps.mutex.RLock() defer ps.mutex.RUnlock() - + var total int32 for _, progress := range ps.AAProgress { total += progress.PointsSpent @@ -1286,7 +1286,7 @@ func (ps *AAPlayerState) CalculateSpentPoints() int32 { func (ps *AAPlayerState) UpdatePoints(total, spent, banked int32) { ps.mutex.Lock() defer ps.mutex.Unlock() - + ps.TotalPoints = total ps.SpentPoints = spent ps.BankedPoints = banked @@ -1298,7 +1298,7 @@ func (ps *AAPlayerState) UpdatePoints(total, spent, banked int32) { func (ps *AAPlayerState) SetActiveTemplate(templateID int8) bool { ps.mutex.Lock() defer ps.mutex.Unlock() - + if _, exists := ps.Templates[templateID]; exists { ps.ActiveTemplate = templateID ps.needsSync = true @@ -1319,68 +1319,68 @@ func TestAAManager(t *testing.T) { t.Run("LoadAAData", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Test without database err := manager.LoadAAData() if err == nil { t.Error("Expected error when loading without database") } - + // Test with mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + err = manager.LoadAAData() if err != nil { t.Errorf("Unexpected error loading AA data: %v", err) } - + if !mockDB.loadAAsCalled || !mockDB.loadNodesCalled { t.Error("Expected database methods to be called") } }) - + t.Run("GetPlayerAAState", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Set up mock database that returns no existing data mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Test getting non-existent player state, err := manager.GetPlayerAAState(123) if err != nil { t.Errorf("Unexpected error: %v", err) } - + if state == nil { t.Error("Expected new player state to be created") } - + if state != nil && state.CharacterID != 123 { t.Error("Expected character ID to match") } - + // Test getting existing player (should be cached) state2, err := manager.GetPlayerAAState(123) if err != nil { t.Errorf("Unexpected error: %v", err) } - + if state != nil && state2 != nil && state != state2 { t.Error("Expected same state instance for same character from cache") } }) - + t.Run("PurchaseAA", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Add a test AA to the master list aa := &AltAdvanceData{ SpellID: 100, @@ -1392,7 +1392,7 @@ func TestAAManager(t *testing.T) { MinLevel: 10, } manager.masterAAList.AddAltAdvancement(aa) - + // Create a player with points state, err := manager.GetPlayerAAState(123) if err != nil { @@ -1403,71 +1403,71 @@ func TestAAManager(t *testing.T) { } state.TotalPoints = 10 state.AvailablePoints = 10 - + // Test purchasing AA err = manager.PurchaseAA(123, 200, 1) if err != nil { t.Errorf("Unexpected error purchasing AA: %v", err) } - + // Verify purchase progress := state.GetAAProgress(200) if progress == nil { t.Fatal("Expected AA progress to exist") } - + if progress.CurrentRank != 1 { t.Error("Expected rank to be 1") } - + if state.AvailablePoints != 8 { t.Error("Expected available points to be reduced by 2") } - + // Test purchasing non-existent AA err = manager.PurchaseAA(123, 999, 1) if err == nil { t.Error("Expected error when purchasing non-existent AA") } }) - + t.Run("AwardAAPoints", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Set up mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Award points to player err := manager.AwardAAPoints(123, 50, "Level up") if err != nil { t.Errorf("Unexpected error awarding points: %v", err) } - + // Verify points total, spent, available, err := manager.GetAAPoints(123) if err != nil { t.Errorf("Unexpected error getting points: %v", err) } - + if total != 50 { t.Errorf("Expected total points to be 50, got %d", total) } - + if spent != 0 { t.Errorf("Expected spent points to be 0, got %d", spent) } - + if available != 50 { t.Errorf("Expected available points to be 50, got %d", available) } }) - + t.Run("GetAA", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Add test AA aa := &AltAdvanceData{ SpellID: 100, @@ -1478,65 +1478,65 @@ func TestAAManager(t *testing.T) { RankCost: 2, } manager.masterAAList.AddAltAdvancement(aa) - + // Test getting by node ID retrieved, err := manager.GetAA(200) if err != nil { t.Errorf("Unexpected error: %v", err) } - + if retrieved == nil { t.Fatal("Expected to get AA") } - + if retrieved.NodeID != 200 { t.Error("Expected node ID to match") } - + // Test getting by spell ID retrieved, err = manager.GetAABySpellID(100) if err != nil { t.Errorf("Unexpected error: %v", err) } - + if retrieved == nil { t.Fatal("Expected to get AA by spell ID") } - + if retrieved.SpellID != 100 { t.Error("Expected spell ID to match") } }) - + t.Run("StartStop", func(t *testing.T) { config := DefaultAAManagerConfig() config.UpdateInterval = 10 * time.Millisecond config.SaveInterval = 10 * time.Millisecond config.AutoSave = true - + manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Start manager err := manager.Start() if err != nil { t.Errorf("Unexpected error starting: %v", err) } - + if !manager.IsRunning() { t.Error("Expected manager to be running") } - + // Let background processes run briefly time.Sleep(50 * time.Millisecond) - + // Stop manager err = manager.Stop() if err != nil { t.Errorf("Unexpected error stopping: %v", err) } - + if manager.IsRunning() { t.Error("Expected manager to be stopped") } @@ -1585,8 +1585,8 @@ func (m *mockAADatabase) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry return make(map[int8][]*AAEntry), nil } -func (m *mockAADatabase) GetAAStatistics() (map[string]interface{}, error) { - return make(map[string]interface{}), nil +func (m *mockAADatabase) GetAAStatistics() (map[string]any, error) { + return make(map[string]any), nil } // Test mock event handler @@ -1659,24 +1659,24 @@ func (m *mockAAEventHandler) LogEvent(message string) { func TestAAEventHandling(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Add mock event handler mockHandler := &mockAAEventHandler{} manager.SetEventHandler(mockHandler) - + // Add mock database mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Test system loaded event err := manager.LoadAAData() if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Wait a bit for goroutines to complete time.Sleep(10 * time.Millisecond) - + // Check that event was fired mockHandler.mu.Lock() hasSystemLoaded := false @@ -1690,7 +1690,7 @@ func TestAAEventHandling(t *testing.T) { t.Error("Expected system_loaded event") } mockHandler.mu.Unlock() - + // Test purchase event aa := &AltAdvanceData{ SpellID: 100, @@ -1702,19 +1702,19 @@ func TestAAEventHandling(t *testing.T) { MinLevel: 1, } manager.masterAAList.AddAltAdvancement(aa) - + state, _ := manager.GetPlayerAAState(123) state.TotalPoints = 10 state.AvailablePoints = 10 - + err = manager.PurchaseAA(123, 200, 1) if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Wait a bit for goroutines to complete time.Sleep(10 * time.Millisecond) - + // Check that purchase event was fired mockHandler.mu.Lock() found := false @@ -1728,16 +1728,16 @@ func TestAAEventHandling(t *testing.T) { t.Error("Expected purchased event") } mockHandler.mu.Unlock() - + // Test points changed event err = manager.AwardAAPoints(123, 50, "Test award") if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Wait a bit for goroutines to complete time.Sleep(10 * time.Millisecond) - + mockHandler.mu.Lock() found = false for _, event := range mockHandler.events { @@ -1759,116 +1759,116 @@ func TestInterfacesAndAdapters(t *testing.T) { manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + adapter := NewAAAdapter(manager, 123) if adapter == nil { t.Fatal("NewAAAdapter returned nil") } - + if adapter.GetCharacterID() != 123 { t.Error("Expected character ID to match") } - + if adapter.GetManager() != manager { t.Error("Expected manager to match") } - + // Test AwardPoints err := adapter.AwardPoints(100, "Test") if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Test GetAAPoints total, _, _, err := adapter.GetAAPoints() if err != nil { t.Errorf("Unexpected error: %v", err) } - + if total != 100 { t.Errorf("Expected total 100, got %d", total) } - + // Test GetAAState state, err := adapter.GetAAState() if err != nil { t.Errorf("Unexpected error: %v", err) } - + if state == nil { t.Error("Expected state to be returned") } - + // Test SaveAAState err = adapter.SaveAAState() if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Test GetTemplates templates, err := adapter.GetTemplates() if err != nil { t.Errorf("Unexpected error: %v", err) } - + if templates == nil { t.Error("Expected templates map") } - + stats := adapter.GetPlayerStats() if stats == nil { t.Error("Expected stats map") } }) - + t.Run("PlayerAAAdapter", func(t *testing.T) { mockPlayer := &mockPlayer{ - characterID: 123, + characterID: 123, level: 50, class: 1, adventureClass: 1, race: 1, name: "TestPlayer", } - + adapter := NewPlayerAAAdapter(mockPlayer) if adapter == nil { t.Fatal("NewPlayerAAAdapter returned nil") } - + if adapter.GetPlayer() != mockPlayer { t.Error("Expected player to match") } - + if adapter.GetCharacterID() != 123 { t.Error("Expected character ID to match") } - + if adapter.GetLevel() != 50 { t.Error("Expected level to match") } - + if adapter.GetClass() != 1 { t.Error("Expected class to match") } - + if adapter.GetAdventureClass() != 1 { t.Error("Expected adventure class to match") } - + if adapter.GetRace() != 1 { t.Error("Expected race to match") } - + if adapter.GetName() != "TestPlayer" { t.Error("Expected name to match") } - + if !adapter.HasExpansion(EXPANSION_NONE) { t.Error("Expected expansion check to work") } }) - + t.Run("ClientAAAdapter", func(t *testing.T) { mockClient := &mockClient{ characterID: 123, @@ -1878,41 +1878,41 @@ func TestInterfacesAndAdapters(t *testing.T) { }, version: 1096, } - + adapter := NewClientAAAdapter(mockClient) if adapter == nil { t.Fatal("NewClientAAAdapter returned nil") } - + if adapter.GetClient() != mockClient { t.Error("Expected client to match") } - + if adapter.GetCharacterID() != 123 { t.Error("Expected character ID to match") } - + if adapter.GetPlayer() != mockClient.player { t.Error("Expected player to match") } - + if adapter.GetClientVersion() != 1096 { t.Error("Expected client version to match") } - + // Test SendPacket err := adapter.SendPacket([]byte("test")) if err != nil { t.Errorf("Unexpected error: %v", err) } }) - + t.Run("SimpleAACache", func(t *testing.T) { cache := NewSimpleAACache(10) if cache == nil { t.Fatal("NewSimpleAACache returned nil") } - + // Test AA caching aa := &AltAdvanceData{ SpellID: 100, @@ -1921,95 +1921,95 @@ func TestInterfacesAndAdapters(t *testing.T) { MaxRank: 5, RankCost: 2, } - + // Test miss cached, found := cache.GetAA(200) if found || cached != nil { t.Error("Expected cache miss") } - + // Set and get cache.SetAA(200, aa) cached, found = cache.GetAA(200) if !found || cached == nil { t.Error("Expected cache hit") } - + if cached == aa { t.Error("Expected cached copy, not original") } - + if cached.NodeID != aa.NodeID { t.Error("Expected cached data to match") } - + // Test invalidation cache.InvalidateAA(200) cached, found = cache.GetAA(200) if found || cached != nil { t.Error("Expected cache miss after invalidation") } - + // Test player state caching playerState := NewAAPlayerState(123) - + // Test miss cachedState, found := cache.GetPlayerState(123) if found || cachedState != nil { t.Error("Expected cache miss") } - + // Set and get cache.SetPlayerState(123, playerState) cachedState, found = cache.GetPlayerState(123) if !found || cachedState == nil { t.Error("Expected cache hit") } - + if cachedState != playerState { t.Error("Expected same player state instance") } - + // Test tree node caching node := &TreeNodeData{ ClassID: 1, TreeID: 100, AATreeID: 200, } - + // Test miss cachedNode, found := cache.GetTreeNode(100) if found || cachedNode != nil { t.Error("Expected cache miss") } - + // Set and get cache.SetTreeNode(100, node) cachedNode, found = cache.GetTreeNode(100) if !found || cachedNode == nil { t.Error("Expected cache hit") } - + if cachedNode == node { t.Error("Expected cached copy, not original") } - + if cachedNode.TreeID != node.TreeID { t.Error("Expected cached data to match") } - + // Test stats stats := cache.GetStats() if stats == nil { t.Error("Expected stats map") } - + // Test max size cache.SetMaxSize(20) if cache.maxSize != 20 { t.Error("Expected max size to be updated") } - + // Test clear cache.Clear() _, found = cache.GetAA(200) @@ -2024,55 +2024,55 @@ func TestEdgeCases(t *testing.T) { t.Run("AAManagerWithoutDatabase", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Test operations without database err := manager.LoadAAData() if err == nil { t.Error("Expected error without database") } - + err = manager.SavePlayerAA(123) if err == nil { t.Error("Expected error without database") } - + _, err = manager.GetPlayerAAState(123) if err == nil { t.Error("Expected error without database") } }) - + t.Run("AAManagerErrorPaths", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Set up mock database that returns errors mockDB := &mockAADatabaseWithErrors{} manager.SetDatabase(mockDB) - + // Test load errors err := manager.LoadAAData() if err == nil { t.Error("Expected error from database") } - + err = manager.ReloadAAData() if err == nil { t.Error("Expected error from database") } }) - + t.Run("PurchaseAAErrorCases", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Test with insufficient points state, _ := manager.GetPlayerAAState(123) state.TotalPoints = 1 state.AvailablePoints = 1 - + aa := &AltAdvanceData{ SpellID: 100, NodeID: 200, @@ -2083,94 +2083,94 @@ func TestEdgeCases(t *testing.T) { MinLevel: 1, } manager.masterAAList.AddAltAdvancement(aa) - + err := manager.PurchaseAA(123, 200, 1) if err == nil { t.Error("Expected error due to insufficient points") } - + // Test purchasing non-existent AA err = manager.PurchaseAA(123, 999, 1) if err == nil { t.Error("Expected error for non-existent AA") } }) - + t.Run("RefundAAErrorCases", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Test refunding non-existent AA err := manager.RefundAA(123, 999) if err == nil { t.Error("Expected error for non-existent AA") } - + // Test refunding from player without state err = manager.RefundAA(999, 200) if err == nil { t.Error("Expected error for non-existent player") } }) - + t.Run("TemplateOperations", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Test with non-existent player err := manager.ChangeAATemplate(999, AA_TEMPLATE_PERSONAL_1) if err == nil { t.Error("Expected error for non-existent player") } - + templates, err := manager.GetAATemplates(999) if err == nil { t.Error("Expected error for non-existent player") } - + if templates != nil { t.Error("Expected nil templates for non-existent player") } - + err = manager.SaveAATemplate(999, AA_TEMPLATE_PERSONAL_1, "Test") if err == nil { t.Error("Expected error for non-existent player") } }) - + t.Run("GetAvailableAAsErrorCases", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Test with non-existent player aas, err := manager.GetAvailableAAs(999, AA_CLASS) if err == nil { t.Error("Expected error for non-existent player") } - + if aas != nil { t.Error("Expected nil AAs for non-existent player") } }) - + t.Run("DataValidation", func(t *testing.T) { masterList := NewMasterAAList() - + // Test adding AA with missing required fields invalidAAs := []*AltAdvanceData{ - {NodeID: 200, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing SpellID - {SpellID: 100, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing NodeID - {SpellID: 100, NodeID: 200, MaxRank: 5, RankCost: 2}, // Missing Name + {NodeID: 200, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing SpellID + {SpellID: 100, Name: "Test", MaxRank: 5, RankCost: 2}, // Missing NodeID + {SpellID: 100, NodeID: 200, MaxRank: 5, RankCost: 2}, // Missing Name {SpellID: 100, NodeID: 200, Name: "Test", RankCost: 2}, // Missing MaxRank - {SpellID: 100, NodeID: 200, Name: "Test", MaxRank: 5}, // Missing RankCost + {SpellID: 100, NodeID: 200, Name: "Test", MaxRank: 5}, // Missing RankCost } - + for i, aa := range invalidAAs { err := masterList.AddAltAdvancement(aa) if err == nil { @@ -2187,40 +2187,40 @@ func TestManagerLifecycle(t *testing.T) { config.UpdateInterval = 5 * time.Millisecond config.SaveInterval = 5 * time.Millisecond config.AutoSave = true - + manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Start manager err := manager.Start() if err != nil { t.Errorf("Unexpected error starting: %v", err) } - + if !manager.IsRunning() { t.Error("Expected manager to be running after start") } - + // Let it run briefly time.Sleep(20 * time.Millisecond) - + // Stop manager err = manager.Stop() if err != nil { t.Errorf("Unexpected error stopping: %v", err) } - + // The IsRunning check consumes the channel close signal, so we can't test it reliably // Just verify that stopping doesn't cause errors }) - + t.Run("ReloadData", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Add some data aa := &AltAdvanceData{ SpellID: 100, @@ -2230,29 +2230,29 @@ func TestManagerLifecycle(t *testing.T) { RankCost: 2, } manager.masterAAList.AddAltAdvancement(aa) - + // Create player state _, err := manager.GetPlayerAAState(123) if err != nil { t.Fatalf("Failed to get player state: %v", err) } - + // Verify data exists if manager.masterAAList.Size() != 1 { t.Error("Expected 1 AA before reload") } - + // Reload data err = manager.ReloadAAData() if err != nil { t.Errorf("Unexpected error reloading: %v", err) } - + // Verify data was cleared and reloaded if manager.masterAAList.Size() != 0 { t.Error("Expected AAs to be cleared after reload") } - + // Player states should be cleared if len(manager.playerStates) != 0 { t.Error("Expected player states to be cleared after reload") @@ -2262,7 +2262,7 @@ func TestManagerLifecycle(t *testing.T) { // Mock implementations for testing type mockPlayer struct { - characterID int32 + characterID int32 level int8 class int8 adventureClass int8 @@ -2270,12 +2270,12 @@ type mockPlayer struct { name string } -func (m *mockPlayer) GetCharacterID() int32 { return m.characterID } -func (m *mockPlayer) GetLevel() int8 { return m.level } -func (m *mockPlayer) GetClass() int8 { return m.class } -func (m *mockPlayer) GetRace() int8 { return m.race } -func (m *mockPlayer) GetName() string { return m.name } -func (m *mockPlayer) GetAdventureClass() int8 { return m.adventureClass } +func (m *mockPlayer) GetCharacterID() int32 { return m.characterID } +func (m *mockPlayer) GetLevel() int8 { return m.level } +func (m *mockPlayer) GetClass() int8 { return m.class } +func (m *mockPlayer) GetRace() int8 { return m.race } +func (m *mockPlayer) GetName() string { return m.name } +func (m *mockPlayer) GetAdventureClass() int8 { return m.adventureClass } func (m *mockPlayer) HasExpansion(expansionFlag int8) bool { return expansionFlag == EXPANSION_NONE } type mockClient struct { @@ -2284,20 +2284,30 @@ type mockClient struct { version int16 } -func (m *mockClient) GetCharacterID() int32 { return m.characterID } -func (m *mockClient) GetPlayer() Player { return m.player } +func (m *mockClient) GetCharacterID() int32 { return m.characterID } +func (m *mockClient) GetPlayer() Player { return m.player } func (m *mockClient) SendPacket(data []byte) error { return nil } -func (m *mockClient) GetClientVersion() int16 { return m.version } +func (m *mockClient) GetClientVersion() int16 { return m.version } type mockAADatabaseWithErrors struct{} func (m *mockAADatabaseWithErrors) LoadAltAdvancements() error { return fmt.Errorf("load AA error") } -func (m *mockAADatabaseWithErrors) LoadTreeNodes() error { return fmt.Errorf("load nodes error") } -func (m *mockAADatabaseWithErrors) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { return nil, fmt.Errorf("load player error") } -func (m *mockAADatabaseWithErrors) SavePlayerAA(playerState *AAPlayerState) error { return fmt.Errorf("save player error") } -func (m *mockAADatabaseWithErrors) DeletePlayerAA(characterID int32) error { return fmt.Errorf("delete player error") } -func (m *mockAADatabaseWithErrors) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { return nil, fmt.Errorf("load defaults error") } -func (m *mockAADatabaseWithErrors) GetAAStatistics() (map[string]interface{}, error) { return nil, fmt.Errorf("stats error") } +func (m *mockAADatabaseWithErrors) LoadTreeNodes() error { return fmt.Errorf("load nodes error") } +func (m *mockAADatabaseWithErrors) LoadPlayerAA(characterID int32) (*AAPlayerState, error) { + return nil, fmt.Errorf("load player error") +} +func (m *mockAADatabaseWithErrors) SavePlayerAA(playerState *AAPlayerState) error { + return fmt.Errorf("save player error") +} +func (m *mockAADatabaseWithErrors) DeletePlayerAA(characterID int32) error { + return fmt.Errorf("delete player error") +} +func (m *mockAADatabaseWithErrors) LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) { + return nil, fmt.Errorf("load defaults error") +} +func (m *mockAADatabaseWithErrors) GetAAStatistics() (map[string]any, error) { + return nil, fmt.Errorf("stats error") +} // Test more adapter methods func TestAdapterMethods(t *testing.T) { @@ -2306,7 +2316,7 @@ func TestAdapterMethods(t *testing.T) { manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Add test AA aa := &AltAdvanceData{ SpellID: 100, @@ -2318,9 +2328,9 @@ func TestAdapterMethods(t *testing.T) { MinLevel: 1, } manager.masterAAList.AddAltAdvancement(aa) - + adapter := NewAAAdapter(manager, 123) - + // Set up player with points state, err := manager.GetPlayerAAState(123) if err != nil { @@ -2328,39 +2338,39 @@ func TestAdapterMethods(t *testing.T) { } state.TotalPoints = 10 state.AvailablePoints = 10 - + // Test PurchaseAA err = adapter.PurchaseAA(200, 1) if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Test RefundAA err = adapter.RefundAA(200) if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Test GetAvailableAAs aas, err := adapter.GetAvailableAAs(AA_CLASS) if err != nil { t.Errorf("Unexpected error: %v", err) } - + if aas == nil { t.Error("Expected AAs list") } - + // Test ChangeTemplate (should work even without existing template since no validator is set) err = adapter.ChangeTemplate(AA_TEMPLATE_PERSONAL_1) if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Create template and test again template := NewAATemplate(AA_TEMPLATE_PERSONAL_1, "Test Template") state.Templates[AA_TEMPLATE_PERSONAL_1] = template - + err = adapter.ChangeTemplate(AA_TEMPLATE_PERSONAL_2) if err != nil { t.Errorf("Unexpected error: %v", err) @@ -2374,20 +2384,20 @@ func TestDatabaseImpl(t *testing.T) { masterAAList := NewMasterAAList() masterNodeList := NewMasterAANodeList() logger := log.New(&testLogWriter{}, "test", 0) - + dbImpl := NewDatabaseImpl(nil, masterAAList, masterNodeList, logger) if dbImpl == nil { t.Fatal("NewDatabaseImpl returned nil") } - + if dbImpl.masterAAList != masterAAList { t.Error("Expected master AA list to match") } - + if dbImpl.masterNodeList != masterNodeList { t.Error("Expected master node list to match") } - + if dbImpl.logger != logger { t.Error("Expected logger to match") } @@ -2401,7 +2411,7 @@ func TestAdditionalEdgeCases(t *testing.T) { manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Add test AA aa := &AltAdvanceData{ SpellID: 100, @@ -2413,101 +2423,101 @@ func TestAdditionalEdgeCases(t *testing.T) { MinLevel: 1, } manager.masterAAList.AddAltAdvancement(aa) - + // Create player and purchase AA first state, _ := manager.GetPlayerAAState(123) state.TotalPoints = 10 state.AvailablePoints = 10 - + // Purchase AA err := manager.PurchaseAA(123, 200, 1) if err != nil { t.Fatalf("Failed to purchase AA: %v", err) } - + // Verify purchase progress := state.GetAAProgress(200) if progress == nil { t.Fatal("Expected AA progress") } - + if progress.CurrentRank != 1 { t.Error("Expected rank 1") } - + // Test refund err = manager.RefundAA(123, 200) if err != nil { t.Errorf("Unexpected error refunding: %v", err) } - + // Verify refund progress = state.GetAAProgress(200) if progress != nil { t.Error("Expected AA progress to be removed after refund") } - + // Test refunding AA that's not purchased err = manager.RefundAA(123, 200) if err == nil { t.Error("Expected error refunding unpurchased AA") } }) - + t.Run("TemplateManagement", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Create player state, _ := manager.GetPlayerAAState(123) - + // Test saving template err := manager.SaveAATemplate(123, AA_TEMPLATE_PERSONAL_1, "My Template") if err != nil { t.Errorf("Unexpected error saving template: %v", err) } - + // Verify template was created template := state.Templates[AA_TEMPLATE_PERSONAL_1] if template == nil { t.Error("Expected template to be created") } - + if template.Name != "My Template" { t.Error("Expected correct template name") } - + // Test changing template err = manager.ChangeAATemplate(123, AA_TEMPLATE_PERSONAL_1) if err != nil { t.Errorf("Unexpected error changing template: %v", err) } - + if state.ActiveTemplate != AA_TEMPLATE_PERSONAL_1 { t.Error("Expected active template to be changed") } - + // Test getting templates templates, err := manager.GetAATemplates(123) if err != nil { t.Errorf("Unexpected error getting templates: %v", err) } - + if len(templates) != 1 { t.Error("Expected 1 template") } - + if templates[AA_TEMPLATE_PERSONAL_1] == nil { t.Error("Expected template to be in map") } }) - + t.Run("GetAAsByMethods", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Add test AAs aa1 := &AltAdvanceData{ SpellID: 100, @@ -2517,7 +2527,7 @@ func TestAdditionalEdgeCases(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa2 := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -2526,38 +2536,38 @@ func TestAdditionalEdgeCases(t *testing.T) { MaxRank: 5, RankCost: 2, } - + manager.masterAAList.AddAltAdvancement(aa1) manager.masterAAList.AddAltAdvancement(aa2) - + // Test GetAAsByGroup classAAs, err := manager.GetAAsByGroup(AA_CLASS) if err != nil { t.Errorf("Unexpected error: %v", err) } - + if len(classAAs) != 1 { t.Errorf("Expected 1 class AA, got %d", len(classAAs)) } - + // Test GetAAsByClass allClassAAs, err := manager.GetAAsByClass(0) // All classes if err != nil { t.Errorf("Unexpected error: %v", err) } - + if len(allClassAAs) != 2 { t.Errorf("Expected 2 AAs for all classes, got %d", len(allClassAAs)) } }) - + t.Run("AAValidation", func(t *testing.T) { // Test IsAAAvailable method indirectly through GetAvailableAAs config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Add AA with prerequisites prereqAA := &AltAdvanceData{ SpellID: 100, @@ -2568,7 +2578,7 @@ func TestAdditionalEdgeCases(t *testing.T) { RankCost: 1, MinLevel: 1, } - + dependentAA := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -2580,19 +2590,19 @@ func TestAdditionalEdgeCases(t *testing.T) { RankPrereqID: 200, RankPrereq: 1, } - + manager.masterAAList.AddAltAdvancement(prereqAA) manager.masterAAList.AddAltAdvancement(dependentAA) - + // Create player state, _ := manager.GetPlayerAAState(123) - + // Get available AAs - dependent AA should not be available without prereq availableAAs, err := manager.GetAvailableAAs(123, AA_CLASS) if err != nil { t.Errorf("Unexpected error: %v", err) } - + // Should only have prerequisite AA available hasPrereq := false hasDependent := false @@ -2604,15 +2614,15 @@ func TestAdditionalEdgeCases(t *testing.T) { hasDependent = true } } - + if !hasPrereq { t.Error("Expected prerequisite AA to be available") } - + if hasDependent { t.Error("Expected dependent AA to not be available without prerequisite") } - + // Purchase prerequisite state.TotalPoints = 10 state.AvailablePoints = 10 @@ -2620,20 +2630,20 @@ func TestAdditionalEdgeCases(t *testing.T) { if err != nil { t.Fatalf("Failed to purchase prerequisite: %v", err) } - + // Now dependent AA should be available availableAAs, err = manager.GetAvailableAAs(123, AA_CLASS) if err != nil { t.Errorf("Unexpected error: %v", err) } - + hasDependent = false for _, aa := range availableAAs { if aa.NodeID == 201 { hasDependent = true } } - + if !hasDependent { t.Error("Expected dependent AA to be available after prerequisite purchased") } @@ -2651,7 +2661,7 @@ func (w *testLogWriter) Write(p []byte) (n int, err error) { func TestCacheEviction(t *testing.T) { t.Run("CacheEviction", func(t *testing.T) { cache := NewSimpleAACache(2) // Small cache for testing eviction - + // Add first AA aa1 := &AltAdvanceData{ SpellID: 100, @@ -2661,7 +2671,7 @@ func TestCacheEviction(t *testing.T) { RankCost: 2, } cache.SetAA(200, aa1) - + // Add second AA aa2 := &AltAdvanceData{ SpellID: 101, @@ -2671,7 +2681,7 @@ func TestCacheEviction(t *testing.T) { RankCost: 2, } cache.SetAA(201, aa2) - + // Add third AA (should evict one) aa3 := &AltAdvanceData{ SpellID: 102, @@ -2681,52 +2691,52 @@ func TestCacheEviction(t *testing.T) { RankCost: 2, } cache.SetAA(202, aa3) - + // Check that only 2 items remain stats := cache.GetStats() if stats["aa_data_count"].(int) != 2 { t.Errorf("Expected 2 cached AAs after eviction, got %d", stats["aa_data_count"].(int)) } - + // Test player state eviction state1 := NewAAPlayerState(123) state2 := NewAAPlayerState(124) state3 := NewAAPlayerState(125) - + cache.SetPlayerState(123, state1) cache.SetPlayerState(124, state2) cache.SetPlayerState(125, state3) // Should evict one - + stats = cache.GetStats() if stats["player_count"].(int) != 2 { t.Errorf("Expected 2 cached player states after eviction, got %d", stats["player_count"].(int)) } - + // Test tree node eviction node1 := &TreeNodeData{ClassID: 1, TreeID: 100, AATreeID: 200} node2 := &TreeNodeData{ClassID: 2, TreeID: 101, AATreeID: 201} node3 := &TreeNodeData{ClassID: 3, TreeID: 102, AATreeID: 202} - + cache.SetTreeNode(100, node1) cache.SetTreeNode(101, node2) cache.SetTreeNode(102, node3) // Should evict one - + stats = cache.GetStats() if stats["tree_node_count"].(int) != 2 { t.Errorf("Expected 2 cached tree nodes after eviction, got %d", stats["tree_node_count"].(int)) } - + // Test some get operations to generate statistics cache.GetAA(999) // Should generate a miss cache.GetAA(200) // Should generate a hit - + // Test invalidate cache.InvalidatePlayerState(123) cache.InvalidateTreeNode(100) - + // Get final stats finalStats := cache.GetStats() - + // Verify hits and misses are tracked (should have at least some activity) hits := finalStats["hits"].(int64) misses := finalStats["misses"].(int64) @@ -2740,7 +2750,7 @@ func TestCacheEviction(t *testing.T) { func TestMasterAAListAdditionalMethods(t *testing.T) { t.Run("GetAllAAs", func(t *testing.T) { masterList := NewMasterAAList() - + // Add test AAs aa1 := &AltAdvanceData{ SpellID: 100, @@ -2750,7 +2760,7 @@ func TestMasterAAListAdditionalMethods(t *testing.T) { MaxRank: 5, RankCost: 2, } - + aa2 := &AltAdvanceData{ SpellID: 101, NodeID: 201, @@ -2759,16 +2769,16 @@ func TestMasterAAListAdditionalMethods(t *testing.T) { MaxRank: 5, RankCost: 2, } - + masterList.AddAltAdvancement(aa1) masterList.AddAltAdvancement(aa2) - + // Test GetAllAAs allAAs := masterList.GetAllAAs() if len(allAAs) != 2 { t.Errorf("Expected 2 AAs, got %d", len(allAAs)) } - + // Verify they are copies (different pointers but same content) if allAAs[0] == aa1 || allAAs[1] == aa1 { t.Error("GetAllAAs should return copies, not originals") @@ -2780,58 +2790,58 @@ func TestMasterAAListAdditionalMethods(t *testing.T) { func TestMasterAANodeListAdditionalMethods(t *testing.T) { t.Run("GetTreeNodes", func(t *testing.T) { nodeList := NewMasterAANodeList() - + // Add test nodes node1 := &TreeNodeData{ ClassID: 1, TreeID: 100, AATreeID: 200, } - + node2 := &TreeNodeData{ ClassID: 2, TreeID: 101, AATreeID: 201, } - + nodeList.AddTreeNode(node1) nodeList.AddTreeNode(node2) - + // Test GetTreeNodes allNodes := nodeList.GetTreeNodes() if len(allNodes) != 2 { t.Errorf("Expected 2 nodes, got %d", len(allNodes)) } - + // Verify they are copies (different pointers but same content) if allNodes[0] == node1 || allNodes[1] == node1 { t.Error("GetTreeNodes should return copies, not originals") } }) - + t.Run("DestroyTreeNodes", func(t *testing.T) { nodeList := NewMasterAANodeList() - + // Add test node node := &TreeNodeData{ ClassID: 1, TreeID: 100, AATreeID: 200, } - + nodeList.AddTreeNode(node) - + if nodeList.Size() != 1 { t.Error("Expected 1 node before destroy") } - + // Destroy all nodes nodeList.DestroyTreeNodes() - + if nodeList.Size() != 0 { t.Error("Expected 0 nodes after destroy") } - + // Verify maps are cleared if len(nodeList.nodesByTree) != 0 || len(nodeList.nodesByClass) != 0 || len(nodeList.nodeList) != 0 { t.Error("Expected all maps to be cleared after destroy") @@ -2844,29 +2854,29 @@ func TestConfigValidation(t *testing.T) { t.Run("ConfigUpdate", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) - + // Test SetConfig newConfig := AAManagerConfig{ - EnableAASystem: false, - AAPointsPerLevel: 10, - MaxBankedPoints: 2000, - EnableCaching: false, - CacheSize: 200, - UpdateInterval: 30 * time.Second, - SaveInterval: 120 * time.Second, - AutoSave: false, + EnableAASystem: false, + AAPointsPerLevel: 10, + MaxBankedPoints: 2000, + EnableCaching: false, + CacheSize: 200, + UpdateInterval: 30 * time.Second, + SaveInterval: 120 * time.Second, + AutoSave: false, } - + err := manager.SetConfig(newConfig) if err != nil { t.Errorf("Unexpected error setting config: %v", err) } - + retrievedConfig := manager.GetConfig() if retrievedConfig.EnableAASystem != false { t.Error("Expected EnableAASystem to be updated") } - + if retrievedConfig.AAPointsPerLevel != 10 { t.Error("Expected AAPointsPerLevel to be updated") } @@ -2880,7 +2890,7 @@ func TestComplexScenarios(t *testing.T) { manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Add multiple AAs for i := 1; i <= 5; i++ { aa := &AltAdvanceData{ @@ -2894,7 +2904,7 @@ func TestComplexScenarios(t *testing.T) { } manager.masterAAList.AddAltAdvancement(aa) } - + // Create multiple players players := []int32{123, 124, 125} for _, playerID := range players { @@ -2902,10 +2912,10 @@ func TestComplexScenarios(t *testing.T) { if err != nil { t.Fatalf("Failed to get player state: %v", err) } - + state.TotalPoints = 20 state.AvailablePoints = 20 - + // Each player purchases different AAs nodeID := int32(200 + int(playerID-122)) err = manager.PurchaseAA(playerID, nodeID, 1) @@ -2913,7 +2923,7 @@ func TestComplexScenarios(t *testing.T) { t.Errorf("Failed to purchase AA for player %d: %v", playerID, err) } } - + // Verify each player has their purchase for _, playerID := range players { state := manager.getPlayerState(playerID) @@ -2921,75 +2931,74 @@ func TestComplexScenarios(t *testing.T) { t.Errorf("Player state not found for %d", playerID) continue } - + if len(state.AAProgress) != 1 { t.Errorf("Expected 1 AA progress for player %d, got %d", playerID, len(state.AAProgress)) } } - + // Manually update statistics since background processes aren't running manager.updateStatistics() - + // Test system stats stats := manager.GetSystemStats() if stats.ActivePlayers != 3 { t.Errorf("Expected 3 active players, got %d", stats.ActivePlayers) } }) - + t.Run("TemplateOperationsWithMultipleTemplates", func(t *testing.T) { config := DefaultAAManagerConfig() manager := NewAAManager(config) mockDB := &mockAADatabase{} manager.SetDatabase(mockDB) - + // Create player state, _ := manager.GetPlayerAAState(123) - + // Create multiple templates templateNames := []string{"Build 1", "Build 2", "Build 3"} templateIDs := []int8{AA_TEMPLATE_PERSONAL_1, AA_TEMPLATE_PERSONAL_2, AA_TEMPLATE_PERSONAL_3} - + for i, templateID := range templateIDs { err := manager.SaveAATemplate(123, templateID, templateNames[i]) if err != nil { t.Errorf("Failed to save template %d: %v", templateID, err) } } - + // Verify all templates were created templates, err := manager.GetAATemplates(123) if err != nil { t.Errorf("Failed to get templates: %v", err) } - + if len(templates) != 3 { t.Errorf("Expected 3 templates, got %d", len(templates)) } - + for i, templateID := range templateIDs { template := templates[templateID] if template == nil { t.Errorf("Template %d not found", templateID) continue } - + if template.Name != templateNames[i] { t.Errorf("Expected template name %s, got %s", templateNames[i], template.Name) } } - + // Test changing between templates for _, templateID := range templateIDs { err := manager.ChangeAATemplate(123, templateID) if err != nil { t.Errorf("Failed to change to template %d: %v", templateID, err) } - + if state.ActiveTemplate != templateID { t.Errorf("Expected active template %d, got %d", templateID, state.ActiveTemplate) } } }) } - diff --git a/internal/alt_advancement/database.go b/internal/alt_advancement/database.go index 068d2a3..d797783 100644 --- a/internal/alt_advancement/database.go +++ b/internal/alt_advancement/database.go @@ -514,8 +514,8 @@ func (db *DatabaseImpl) DeletePlayerAA(characterID int32) error { } // GetAAStatistics returns statistics about AA usage -func (db *DatabaseImpl) GetAAStatistics() (map[string]interface{}, error) { - stats := make(map[string]interface{}) +func (db *DatabaseImpl) GetAAStatistics() (map[string]any, error) { + stats := make(map[string]any) // Get total players with AA data var totalPlayers int64 diff --git a/internal/alt_advancement/interfaces.go b/internal/alt_advancement/interfaces.go index 00e5632..2924ac5 100644 --- a/internal/alt_advancement/interfaces.go +++ b/internal/alt_advancement/interfaces.go @@ -22,26 +22,26 @@ type AADatabase interface { LoadPlayerAADefaults(classID int8) (map[int8][]*AAEntry, error) // Statistics - GetAAStatistics() (map[string]interface{}, error) + GetAAStatistics() (map[string]any, error) } // AAPacketHandler interface for handling AA-related packets type AAPacketHandler interface { // List packets - GetAAListPacket(client interface{}) ([]byte, error) - SendAAUpdate(client interface{}, playerState *AAPlayerState) error + GetAAListPacket(client any) ([]byte, error) + SendAAUpdate(client any, playerState *AAPlayerState) error // Purchase packets - HandleAAPurchase(client interface{}, nodeID int32, rank int8) error - SendAAPurchaseResponse(client interface{}, success bool, nodeID int32, newRank int8) error + HandleAAPurchase(client any, nodeID int32, rank int8) error + SendAAPurchaseResponse(client any, success bool, nodeID int32, newRank int8) error // Template packets - SendAATemplateList(client interface{}, templates map[int8]*AATemplate) error - HandleAATemplateChange(client interface{}, templateID int8) error + SendAATemplateList(client any, templates map[int8]*AATemplate) error + HandleAATemplateChange(client any, templateID int8) error // Display packets - DisplayAA(client interface{}, templateID int8, changeMode int8) error - SendAATabUpdate(client interface{}, tabID int8, tab *AATab) error + DisplayAA(client any, templateID int8, changeMode int8) error + SendAATabUpdate(client any, tabID int8, tab *AATab) error } // AAEventHandler interface for handling AA events @@ -124,8 +124,8 @@ type AAStatistics interface { // Aggregated statistics GetAAPurchaseStats() map[int32]int64 GetPopularAAs() map[int32]int64 - GetPlayerProgressStats() map[string]interface{} - GetSystemPerformanceStats() map[string]interface{} + GetPlayerProgressStats() map[string]any + GetSystemPerformanceStats() map[string]any } // AACache interface for caching AA data @@ -147,7 +147,7 @@ type AACache interface { // Cache management Clear() - GetStats() map[string]interface{} + GetStats() map[string]any SetMaxSize(maxSize int32) } @@ -172,9 +172,9 @@ type Player interface { // Transaction interface for database transactions type Transaction interface { - Exec(query string, args ...interface{}) (sql.Result, error) - Query(query string, args ...interface{}) (*sql.Rows, error) - QueryRow(query string, args ...interface{}) *sql.Row + Exec(query string, args ...any) (sql.Result, error) + Query(query string, args ...any) (*sql.Rows, error) + QueryRow(query string, args ...any) *sql.Row Commit() error Rollback() error } @@ -235,7 +235,7 @@ type AAManagerInterface interface { // Statistics GetSystemStats() *AAManagerStats - GetPlayerStats(characterID int32) map[string]interface{} + GetPlayerStats(characterID int32) map[string]any // Configuration SetConfig(config AAManagerConfig) error @@ -327,7 +327,7 @@ func (aa *AAAdapter) GetTemplates() (map[int8]*AATemplate, error) { } // GetPlayerStats returns AA statistics for the character -func (aa *AAAdapter) GetPlayerStats() map[string]interface{} { +func (aa *AAAdapter) GetPlayerStats() map[string]any { return aa.manager.GetPlayerStats(aa.characterID) } @@ -579,11 +579,11 @@ func (c *SimpleAACache) Clear() { } // GetStats returns cache statistics -func (c *SimpleAACache) GetStats() map[string]interface{} { +func (c *SimpleAACache) GetStats() map[string]any { c.mutex.RLock() defer c.mutex.RUnlock() - return map[string]interface{}{ + return map[string]any{ "hits": c.hits, "misses": c.misses, "aa_data_count": len(c.aaData), diff --git a/internal/alt_advancement/manager.go b/internal/alt_advancement/manager.go index 2c04979..c0eef27 100644 --- a/internal/alt_advancement/manager.go +++ b/internal/alt_advancement/manager.go @@ -164,7 +164,7 @@ func (am *AAManager) GetPlayerAAState(characterID int32) (*AAPlayerState, error) // Need to load from database, use write lock to prevent race condition am.statesMutex.Lock() defer am.statesMutex.Unlock() - + // Double-check pattern: another goroutine might have loaded it while we waited if playerState, exists := am.playerStates[characterID]; exists { return playerState, nil @@ -456,16 +456,16 @@ func (am *AAManager) GetSystemStats() *AAManagerStats { } // GetPlayerStats returns player-specific statistics -func (am *AAManager) GetPlayerStats(characterID int32) map[string]interface{} { +func (am *AAManager) GetPlayerStats(characterID int32) map[string]any { playerState := am.getPlayerState(characterID) if playerState == nil { - return map[string]interface{}{"error": "player not found"} + return map[string]any{"error": "player not found"} } playerState.mutex.RLock() defer playerState.mutex.RUnlock() - return map[string]interface{}{ + return map[string]any{ "character_id": characterID, "total_points": playerState.TotalPoints, "spent_points": playerState.SpentPoints, diff --git a/internal/alt_advancement/master_list.go b/internal/alt_advancement/master_list.go index d345a4b..7c138c3 100644 --- a/internal/alt_advancement/master_list.go +++ b/internal/alt_advancement/master_list.go @@ -251,11 +251,11 @@ func (mal *MasterAAList) ValidateAAData() []error { } // GetStats returns statistics about the master AA list -func (mal *MasterAAList) GetStats() map[string]interface{} { +func (mal *MasterAAList) GetStats() map[string]any { mal.mutex.RLock() defer mal.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats[STAT_TOTAL_AAS_LOADED] = mal.totalLoaded stats["last_load_time"] = mal.lastLoadTime stats["groups_count"] = len(mal.aaByGroup) @@ -429,11 +429,11 @@ func (manl *MasterAANodeList) ValidateTreeNodes() []error { } // GetStats returns statistics about the master node list -func (manl *MasterAANodeList) GetStats() map[string]interface{} { +func (manl *MasterAANodeList) GetStats() map[string]any { manl.mutex.RLock() defer manl.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats[STAT_TOTAL_NODES_LOADED] = manl.totalLoaded stats["last_load_time"] = manl.lastLoadTime stats["classes_count"] = len(manl.nodesByClass) diff --git a/internal/appearances/appearances.go b/internal/appearances/appearances.go index 77a61e0..b97dbdd 100644 --- a/internal/appearances/appearances.go +++ b/internal/appearances/appearances.go @@ -227,11 +227,11 @@ func (a *Appearances) IsValid() bool { } // GetStatistics returns statistics about the appearance collection -func (a *Appearances) GetStatistics() map[string]interface{} { +func (a *Appearances) GetStatistics() map[string]any { a.mutex.RLock() defer a.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_appearances"] = len(a.appearanceMap) // Count by minimum client version diff --git a/internal/appearances/appearances_test.go b/internal/appearances/appearances_test.go index f056ecf..dbf6e5a 100644 --- a/internal/appearances/appearances_test.go +++ b/internal/appearances/appearances_test.go @@ -40,26 +40,26 @@ func TestNewAppearance(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { app := NewAppearance(tt.id, tt.appearanceName, tt.minClientVersion) - + if tt.wantNil { if app != nil { t.Errorf("expected nil appearance, got %v", app) } return } - + if app == nil { t.Fatal("expected non-nil appearance, got nil") } - + if app.GetID() != tt.id { t.Errorf("ID = %v, want %v", app.GetID(), tt.id) } - + if app.GetName() != tt.appearanceName { t.Errorf("Name = %v, want %v", app.GetName(), tt.appearanceName) } - + if app.GetMinClientVersion() != tt.minClientVersion { t.Errorf("MinClientVersion = %v, want %v", app.GetMinClientVersion(), tt.minClientVersion) } @@ -69,19 +69,19 @@ func TestNewAppearance(t *testing.T) { func TestAppearanceGetters(t *testing.T) { app := NewAppearance(123, "Test Appearance", 1096) - + if id := app.GetID(); id != 123 { t.Errorf("GetID() = %v, want 123", id) } - + if name := app.GetName(); name != "Test Appearance" { t.Errorf("GetName() = %v, want Test Appearance", name) } - + if nameStr := app.GetNameString(); nameStr != "Test Appearance" { t.Errorf("GetNameString() = %v, want Test Appearance", nameStr) } - + if minVer := app.GetMinClientVersion(); minVer != 1096 { t.Errorf("GetMinClientVersion() = %v, want 1096", minVer) } @@ -89,12 +89,12 @@ func TestAppearanceGetters(t *testing.T) { func TestAppearanceSetters(t *testing.T) { app := NewAppearance(100, "Original", 1000) - + app.SetName("Modified Name") if app.GetName() != "Modified Name" { t.Errorf("SetName failed: got %v, want Modified Name", app.GetName()) } - + app.SetMinClientVersion(2000) if app.GetMinClientVersion() != 2000 { t.Errorf("SetMinClientVersion failed: got %v, want 2000", app.GetMinClientVersion()) @@ -103,7 +103,7 @@ func TestAppearanceSetters(t *testing.T) { func TestIsCompatibleWithClient(t *testing.T) { app := NewAppearance(100, "Test", 1096) - + tests := []struct { clientVersion int16 want bool @@ -114,7 +114,7 @@ func TestIsCompatibleWithClient(t *testing.T) { {2000, true}, // Well above minimum {0, false}, // Zero version } - + for _, tt := range tests { t.Run("", func(t *testing.T) { if got := app.IsCompatibleWithClient(tt.clientVersion); got != tt.want { @@ -127,27 +127,27 @@ func TestIsCompatibleWithClient(t *testing.T) { func TestAppearanceClone(t *testing.T) { original := NewAppearance(500, "Original Appearance", 1200) clone := original.Clone() - + if clone == nil { t.Fatal("Clone returned nil") } - + if clone == original { t.Error("Clone returned same pointer as original") } - + if clone.GetID() != original.GetID() { t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID()) } - + if clone.GetName() != original.GetName() { t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName()) } - + if clone.GetMinClientVersion() != original.GetMinClientVersion() { t.Errorf("Clone MinClientVersion = %v, want %v", clone.GetMinClientVersion(), original.GetMinClientVersion()) } - + // Verify modification independence clone.SetName("Modified Clone") if original.GetName() == "Modified Clone" { @@ -157,11 +157,11 @@ func TestAppearanceClone(t *testing.T) { func TestNewAppearances(t *testing.T) { apps := NewAppearances() - + if apps == nil { t.Fatal("NewAppearances returned nil") } - + if count := apps.GetAppearanceCount(); count != 0 { t.Errorf("New appearances collection should be empty, got count %v", count) } @@ -169,25 +169,25 @@ func TestNewAppearances(t *testing.T) { func TestAppearancesInsertAndFind(t *testing.T) { apps := NewAppearances() - + // Test nil insertion err := apps.InsertAppearance(nil) if err == nil { t.Error("InsertAppearance(nil) should return error") } - + // Insert valid appearances app1 := NewAppearance(100, "Appearance 1", 1000) app2 := NewAppearance(200, "Appearance 2", 1100) - + if err := apps.InsertAppearance(app1); err != nil { t.Errorf("InsertAppearance failed: %v", err) } - + if err := apps.InsertAppearance(app2); err != nil { t.Errorf("InsertAppearance failed: %v", err) } - + // Test finding by ID found := apps.FindAppearanceByID(100) if found == nil { @@ -195,7 +195,7 @@ func TestAppearancesInsertAndFind(t *testing.T) { } else if found.GetName() != "Appearance 1" { t.Errorf("FindAppearanceByID(100) returned wrong appearance: %v", found.GetName()) } - + // Test finding non-existent ID notFound := apps.FindAppearanceByID(999) if notFound != nil { @@ -207,11 +207,11 @@ func TestAppearancesHasAppearance(t *testing.T) { apps := NewAppearances() app := NewAppearance(300, "Test", 1000) apps.InsertAppearance(app) - + if !apps.HasAppearance(300) { t.Error("HasAppearance(300) should return true") } - + if apps.HasAppearance(999) { t.Error("HasAppearance(999) should return false") } @@ -219,22 +219,22 @@ func TestAppearancesHasAppearance(t *testing.T) { func TestAppearancesGetAllAndCount(t *testing.T) { apps := NewAppearances() - + // Add multiple appearances for i := int32(1); i <= 5; i++ { app := NewAppearance(i*100, "Appearance", 1000) apps.InsertAppearance(app) } - + if count := apps.GetAppearanceCount(); count != 5 { t.Errorf("GetAppearanceCount() = %v, want 5", count) } - + all := apps.GetAllAppearances() if len(all) != 5 { t.Errorf("GetAllAppearances() returned %v items, want 5", len(all)) } - + // Verify it's a copy by modifying returned map delete(all, 100) if apps.GetAppearanceCount() != 5 { @@ -245,23 +245,23 @@ func TestAppearancesGetAllAndCount(t *testing.T) { func TestAppearancesGetIDs(t *testing.T) { apps := NewAppearances() expectedIDs := []int32{100, 200, 300} - + for _, id := range expectedIDs { app := NewAppearance(id, "Test", 1000) apps.InsertAppearance(app) } - + ids := apps.GetAppearanceIDs() if len(ids) != len(expectedIDs) { t.Errorf("GetAppearanceIDs() returned %v IDs, want %v", len(ids), len(expectedIDs)) } - + // Check all expected IDs are present idMap := make(map[int32]bool) for _, id := range ids { idMap[id] = true } - + for _, expected := range expectedIDs { if !idMap[expected] { t.Errorf("Expected ID %v not found in returned IDs", expected) @@ -271,30 +271,30 @@ func TestAppearancesGetIDs(t *testing.T) { func TestAppearancesFindByName(t *testing.T) { apps := NewAppearances() - + apps.InsertAppearance(NewAppearance(1, "Human Male", 1000)) apps.InsertAppearance(NewAppearance(2, "Human Female", 1000)) apps.InsertAppearance(NewAppearance(3, "Elf Male", 1000)) apps.InsertAppearance(NewAppearance(4, "Dwarf Female", 1000)) - + // Test partial matching humanApps := apps.FindAppearancesByName("Human") if len(humanApps) != 2 { t.Errorf("FindAppearancesByName('Human') returned %v results, want 2", len(humanApps)) } - + // Test case sensitivity maleApps := apps.FindAppearancesByName("Male") if len(maleApps) != 2 { t.Errorf("FindAppearancesByName('Male') returned %v results, want 2", len(maleApps)) } - + // Test empty substring allApps := apps.FindAppearancesByName("") if len(allApps) != 4 { t.Errorf("FindAppearancesByName('') returned %v results, want 4", len(allApps)) } - + // Test no matches noMatches := apps.FindAppearancesByName("Orc") if len(noMatches) != 0 { @@ -304,17 +304,17 @@ func TestAppearancesFindByName(t *testing.T) { func TestAppearancesFindByMinClient(t *testing.T) { apps := NewAppearances() - + apps.InsertAppearance(NewAppearance(1, "Old", 1000)) apps.InsertAppearance(NewAppearance(2, "Medium1", 1096)) apps.InsertAppearance(NewAppearance(3, "Medium2", 1096)) apps.InsertAppearance(NewAppearance(4, "New", 1200)) - + results := apps.FindAppearancesByMinClient(1096) if len(results) != 2 { t.Errorf("FindAppearancesByMinClient(1096) returned %v results, want 2", len(results)) } - + results = apps.FindAppearancesByMinClient(999) if len(results) != 0 { t.Errorf("FindAppearancesByMinClient(999) returned %v results, want 0", len(results)) @@ -323,24 +323,24 @@ func TestAppearancesFindByMinClient(t *testing.T) { func TestAppearancesGetCompatible(t *testing.T) { apps := NewAppearances() - + apps.InsertAppearance(NewAppearance(1, "Old", 1000)) apps.InsertAppearance(NewAppearance(2, "Medium", 1096)) apps.InsertAppearance(NewAppearance(3, "New", 1200)) apps.InsertAppearance(NewAppearance(4, "Newer", 1300)) - + // Client version 1100 should get Old and Medium compatible := apps.GetCompatibleAppearances(1100) if len(compatible) != 2 { t.Errorf("GetCompatibleAppearances(1100) returned %v results, want 2", len(compatible)) } - + // Client version 1500 should get all compatible = apps.GetCompatibleAppearances(1500) if len(compatible) != 4 { t.Errorf("GetCompatibleAppearances(1500) returned %v results, want 4", len(compatible)) } - + // Client version 500 should get none compatible = apps.GetCompatibleAppearances(500) if len(compatible) != 0 { @@ -352,16 +352,16 @@ func TestAppearancesRemove(t *testing.T) { apps := NewAppearances() app := NewAppearance(100, "Test", 1000) apps.InsertAppearance(app) - + // Remove existing if !apps.RemoveAppearance(100) { t.Error("RemoveAppearance(100) should return true") } - + if apps.HasAppearance(100) { t.Error("Appearance 100 should have been removed") } - + // Remove non-existent if apps.RemoveAppearance(100) { t.Error("RemoveAppearance(100) should return false for non-existent ID") @@ -370,35 +370,35 @@ func TestAppearancesRemove(t *testing.T) { func TestAppearancesUpdate(t *testing.T) { apps := NewAppearances() - + // Test updating nil err := apps.UpdateAppearance(nil) if err == nil { t.Error("UpdateAppearance(nil) should return error") } - + // Insert and update original := NewAppearance(100, "Original", 1000) apps.InsertAppearance(original) - + updated := NewAppearance(100, "Updated", 1100) err = apps.UpdateAppearance(updated) if err != nil { t.Errorf("UpdateAppearance failed: %v", err) } - + found := apps.FindAppearanceByID(100) if found.GetName() != "Updated" { t.Errorf("Updated appearance name = %v, want Updated", found.GetName()) } - + // Update non-existent (should insert) new := NewAppearance(200, "New", 1200) err = apps.UpdateAppearance(new) if err != nil { t.Errorf("UpdateAppearance failed: %v", err) } - + if !apps.HasAppearance(200) { t.Error("UpdateAppearance should insert non-existent appearance") } @@ -406,29 +406,29 @@ func TestAppearancesUpdate(t *testing.T) { func TestAppearancesGetByIDRange(t *testing.T) { apps := NewAppearances() - + // Insert appearances with various IDs for _, id := range []int32{5, 10, 15, 20, 25, 30} { apps.InsertAppearance(NewAppearance(id, "Test", 1000)) } - + results := apps.GetAppearancesByIDRange(10, 20) if len(results) != 3 { // Should get 10, 15, 20 t.Errorf("GetAppearancesByIDRange(10, 20) returned %v results, want 3", len(results)) } - + // Verify correct IDs idMap := make(map[int32]bool) for _, app := range results { idMap[app.GetID()] = true } - + for _, expectedID := range []int32{10, 15, 20} { if !idMap[expectedID] { t.Errorf("Expected ID %v not found in range results", expectedID) } } - + // Test empty range results = apps.GetAppearancesByIDRange(100, 200) if len(results) != 0 { @@ -438,31 +438,31 @@ func TestAppearancesGetByIDRange(t *testing.T) { func TestAppearancesValidate(t *testing.T) { apps := NewAppearances() - + // Valid appearances apps.InsertAppearance(NewAppearance(100, "Valid", 1000)) - + issues := apps.ValidateAppearances() if len(issues) != 0 { t.Errorf("ValidateAppearances() returned issues for valid data: %v", issues) } - + if !apps.IsValid() { t.Error("IsValid() should return true for valid data") } - + // Force invalid state by directly modifying map apps.mutex.Lock() apps.appearanceMap[200] = nil apps.appearanceMap[300] = NewAppearance(301, "", 1000) // ID mismatch and empty name apps.appearanceMap[400] = NewAppearance(400, "Negative", -100) apps.mutex.Unlock() - + issues = apps.ValidateAppearances() if len(issues) < 3 { t.Errorf("ValidateAppearances() should return at least 3 issues, got %v", len(issues)) } - + if apps.IsValid() { t.Error("IsValid() should return false for invalid data") } @@ -470,32 +470,32 @@ func TestAppearancesValidate(t *testing.T) { func TestAppearancesStatistics(t *testing.T) { apps := NewAppearances() - + // Add appearances with different client versions apps.InsertAppearance(NewAppearance(10, "A", 1000)) apps.InsertAppearance(NewAppearance(20, "B", 1000)) apps.InsertAppearance(NewAppearance(30, "C", 1096)) apps.InsertAppearance(NewAppearance(40, "D", 1096)) apps.InsertAppearance(NewAppearance(50, "E", 1096)) - + stats := apps.GetStatistics() - + if total, ok := stats["total_appearances"].(int); !ok || total != 5 { t.Errorf("total_appearances = %v, want 5", stats["total_appearances"]) } - + if minID, ok := stats["min_id"].(int32); !ok || minID != 10 { t.Errorf("min_id = %v, want 10", stats["min_id"]) } - + if maxID, ok := stats["max_id"].(int32); !ok || maxID != 50 { t.Errorf("max_id = %v, want 50", stats["max_id"]) } - + if idRange, ok := stats["id_range"].(int32); !ok || idRange != 40 { t.Errorf("id_range = %v, want 40", stats["id_range"]) } - + if versionCounts, ok := stats["appearances_by_min_client"].(map[int16]int); ok { if versionCounts[1000] != 2 { t.Errorf("appearances with min client 1000 = %v, want 2", versionCounts[1000]) @@ -510,27 +510,27 @@ func TestAppearancesStatistics(t *testing.T) { func TestAppearancesClearAndReset(t *testing.T) { apps := NewAppearances() - + // Add some appearances for i := int32(1); i <= 3; i++ { apps.InsertAppearance(NewAppearance(i*100, "Test", 1000)) } - + if apps.GetAppearanceCount() != 3 { t.Error("Setup failed: should have 3 appearances") } - + // Test ClearAppearances apps.ClearAppearances() if apps.GetAppearanceCount() != 0 { t.Errorf("ClearAppearances() failed: count = %v, want 0", apps.GetAppearanceCount()) } - + // Add again and test Reset for i := int32(1); i <= 3; i++ { apps.InsertAppearance(NewAppearance(i*100, "Test", 1000)) } - + apps.Reset() if apps.GetAppearanceCount() != 0 { t.Errorf("Reset() failed: count = %v, want 0", apps.GetAppearanceCount()) @@ -540,7 +540,7 @@ func TestAppearancesClearAndReset(t *testing.T) { func TestAppearancesConcurrency(t *testing.T) { apps := NewAppearances() var wg sync.WaitGroup - + // Concurrent insertions for i := 0; i < 100; i++ { wg.Add(1) @@ -550,7 +550,7 @@ func TestAppearancesConcurrency(t *testing.T) { apps.InsertAppearance(app) }(int32(i)) } - + // Concurrent reads for i := 0; i < 50; i++ { wg.Add(1) @@ -561,7 +561,7 @@ func TestAppearancesConcurrency(t *testing.T) { _ = apps.FindAppearanceByID(25) }() } - + // Concurrent searches for i := 0; i < 20; i++ { wg.Add(1) @@ -571,9 +571,9 @@ func TestAppearancesConcurrency(t *testing.T) { _ = apps.GetCompatibleAppearances(1100) }() } - + wg.Wait() - + // Verify all insertions succeeded if count := apps.GetAppearanceCount(); count != 100 { t.Errorf("After concurrent operations, count = %v, want 100", count) @@ -595,7 +595,7 @@ func TestContainsFunction(t *testing.T) { {"abcdef", "cde", true}, {"abcdef", "xyz", false}, } - + for _, tt := range tests { t.Run("", func(t *testing.T) { if got := contains(tt.str, tt.substr); got != tt.want { @@ -608,7 +608,7 @@ func TestContainsFunction(t *testing.T) { // Benchmarks func BenchmarkAppearanceInsert(b *testing.B) { apps := NewAppearances() - + b.ResetTimer() for i := 0; i < b.N; i++ { app := NewAppearance(int32(i), "Benchmark", 1000) @@ -618,13 +618,13 @@ func BenchmarkAppearanceInsert(b *testing.B) { func BenchmarkAppearanceFindByID(b *testing.B) { apps := NewAppearances() - + // Pre-populate for i := 0; i < 10000; i++ { app := NewAppearance(int32(i), "Benchmark", 1000) apps.InsertAppearance(app) } - + b.ResetTimer() for i := 0; i < b.N; i++ { apps.FindAppearanceByID(int32(i % 10000)) @@ -633,14 +633,14 @@ func BenchmarkAppearanceFindByID(b *testing.B) { func BenchmarkAppearanceFindByName(b *testing.B) { apps := NewAppearances() - + // Pre-populate with varied names names := []string{"Human Male", "Human Female", "Elf Male", "Elf Female", "Dwarf Male"} for i := 0; i < 1000; i++ { app := NewAppearance(int32(i), names[i%len(names)], 1000) apps.InsertAppearance(app) } - + b.ResetTimer() for i := 0; i < b.N; i++ { apps.FindAppearancesByName("Male") @@ -684,19 +684,19 @@ type MockLogger struct { logs []string } -func (m *MockLogger) LogInfo(message string, args ...interface{}) { +func (m *MockLogger) LogInfo(message string, args ...any) { m.logs = append(m.logs, fmt.Sprintf("INFO: "+message, args...)) } -func (m *MockLogger) LogError(message string, args ...interface{}) { +func (m *MockLogger) LogError(message string, args ...any) { m.logs = append(m.logs, fmt.Sprintf("ERROR: "+message, args...)) } -func (m *MockLogger) LogDebug(message string, args ...interface{}) { +func (m *MockLogger) LogDebug(message string, args ...any) { m.logs = append(m.logs, fmt.Sprintf("DEBUG: "+message, args...)) } -func (m *MockLogger) LogWarning(message string, args ...interface{}) { +func (m *MockLogger) LogWarning(message string, args ...any) { m.logs = append(m.logs, fmt.Sprintf("WARNING: "+message, args...)) } @@ -715,28 +715,28 @@ type MockClient struct { sendError error } -func (m *MockClient) GetVersion() int16 { return m.version } +func (m *MockClient) GetVersion() int16 { return m.version } func (m *MockClient) SendAppearanceUpdate(appearanceID int32) error { return m.sendError } // Manager tests func TestNewManager(t *testing.T) { db := &MockDatabase{} logger := &MockLogger{} - + manager := NewManager(db, logger) - + if manager == nil { t.Fatal("NewManager returned nil") } - + if manager.database != db { t.Error("Manager database not set correctly") } - + if manager.logger != logger { t.Error("Manager logger not set correctly") } - + if manager.appearances == nil { t.Error("Manager appearances not initialized") } @@ -779,7 +779,7 @@ func TestManagerInitialize(t *testing.T) { wantLogInfo: true, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { logger := &MockLogger{} @@ -793,17 +793,17 @@ func TestManagerInitialize(t *testing.T) { logger: logger, } } - + err := manager.Initialize() - + if (err != nil) != tt.wantError { t.Errorf("Initialize() error = %v, wantError %v", err, tt.wantError) } - + if count := manager.GetAppearanceCount(); count != tt.wantCount { t.Errorf("GetAppearanceCount() = %v, want %v", count, tt.wantCount) } - + if tt.wantLogInfo && len(logger.logs) == 0 { t.Error("Expected log messages, got none") } @@ -820,29 +820,29 @@ func TestManagerFindAppearanceByID(t *testing.T) { logger := &MockLogger{} manager := NewManager(db, logger) manager.Initialize() - + // Test successful lookup appearance := manager.FindAppearanceByID(100) if appearance == nil { t.Error("FindAppearanceByID(100) returned nil") } - + // Test failed lookup appearance = manager.FindAppearanceByID(999) if appearance != nil { t.Error("FindAppearanceByID(999) should return nil") } - + // Check statistics stats := manager.GetStatistics() if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 2 { t.Errorf("total_lookups = %v, want 2", stats["total_lookups"]) } - + if successfulLookups, ok := stats["successful_lookups"].(int64); !ok || successfulLookups != 1 { t.Errorf("successful_lookups = %v, want 1", stats["successful_lookups"]) } - + if failedLookups, ok := stats["failed_lookups"].(int64); !ok || failedLookups != 1 { t.Errorf("failed_lookups = %v, want 1", stats["failed_lookups"]) } @@ -895,27 +895,27 @@ func TestManagerAddAppearance(t *testing.T) { errorContains: "database", }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { db := &MockDatabase{saveError: tt.saveError} logger := &MockLogger{} manager := NewManager(db, logger) - + if tt.existingID > 0 { manager.appearances.InsertAppearance(NewAppearance(tt.existingID, "Existing", 1000)) } - + err := manager.AddAppearance(tt.appearance) - + if (err != nil) != tt.wantError { t.Errorf("AddAppearance() error = %v, wantError %v", err, tt.wantError) } - + if err != nil && tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { t.Errorf("Error message %q doesn't contain %q", err.Error(), tt.errorContains) } - + // Verify appearance was added/not added if !tt.wantError && tt.appearance != nil { if !manager.appearances.HasAppearance(tt.appearance.GetID()) { @@ -930,30 +930,30 @@ func TestManagerUpdateAppearance(t *testing.T) { db := &MockDatabase{} logger := &MockLogger{} manager := NewManager(db, logger) - + // Add initial appearance original := NewAppearance(100, "Original", 1000) manager.AddAppearance(original) - + // Test successful update updated := NewAppearance(100, "Updated", 1100) err := manager.UpdateAppearance(updated) if err != nil { t.Errorf("UpdateAppearance failed: %v", err) } - + found := manager.FindAppearanceByID(100) if found.GetName() != "Updated" { t.Error("Appearance was not updated") } - + // Test update non-existent notExist := NewAppearance(999, "NotExist", 1000) err = manager.UpdateAppearance(notExist) if err == nil { t.Error("UpdateAppearance should fail for non-existent appearance") } - + // Test nil appearance err = manager.UpdateAppearance(nil) if err == nil { @@ -965,27 +965,27 @@ func TestManagerRemoveAppearance(t *testing.T) { db := &MockDatabase{} logger := &MockLogger{} manager := NewManager(db, logger) - + // Add appearance app := NewAppearance(100, "Test", 1000) manager.AddAppearance(app) - + // Test successful removal err := manager.RemoveAppearance(100) if err != nil { t.Errorf("RemoveAppearance failed: %v", err) } - + if manager.appearances.HasAppearance(100) { t.Error("Appearance was not removed") } - + // Test removing non-existent err = manager.RemoveAppearance(999) if err == nil { t.Error("RemoveAppearance should fail for non-existent appearance") } - + // Test database delete error db.deleteError = fmt.Errorf("delete failed") manager.AddAppearance(NewAppearance(200, "Test2", 1000)) @@ -1005,7 +1005,7 @@ func TestManagerCommands(t *testing.T) { logger := &MockLogger{} manager := NewManager(db, logger) manager.Initialize() - + // Test stats command result, err := manager.ProcessCommand("stats", nil) if err != nil { @@ -1014,7 +1014,7 @@ func TestManagerCommands(t *testing.T) { if !contains(result, "Appearance System Statistics") { t.Error("Stats command output incorrect") } - + // Test validate command result, err = manager.ProcessCommand("validate", nil) if err != nil { @@ -1023,7 +1023,7 @@ func TestManagerCommands(t *testing.T) { if !contains(result, "valid") { t.Error("Validate command output incorrect") } - + // Test search command result, err = manager.ProcessCommand("search", []string{"Human"}) if err != nil { @@ -1032,13 +1032,13 @@ func TestManagerCommands(t *testing.T) { if !contains(result, "Found 2 appearances") { t.Error("Search command output incorrect") } - + // Test search without args _, err = manager.ProcessCommand("search", nil) if err == nil { t.Error("Search command should fail without arguments") } - + // Test info command result, err = manager.ProcessCommand("info", []string{"100"}) if err != nil { @@ -1047,13 +1047,13 @@ func TestManagerCommands(t *testing.T) { if !contains(result, "Human Male") { t.Error("Info command output incorrect") } - + // Test info without args _, err = manager.ProcessCommand("info", nil) if err == nil { t.Error("Info command should fail without arguments") } - + // Test unknown command _, err = manager.ProcessCommand("unknown", nil) if err == nil { @@ -1063,19 +1063,19 @@ func TestManagerCommands(t *testing.T) { func TestManagerResetStatistics(t *testing.T) { manager := NewManager(nil, nil) - + // Perform some lookups manager.FindAppearanceByID(100) manager.FindAppearanceByID(200) - + stats := manager.GetStatistics() if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 2 { t.Error("Statistics not tracked correctly") } - + // Reset statistics manager.ResetStatistics() - + stats = manager.GetStatistics() if totalLookups, ok := stats["total_lookups"].(int64); !ok || totalLookups != 0 { t.Error("Statistics not reset correctly") @@ -1085,15 +1085,15 @@ func TestManagerResetStatistics(t *testing.T) { func TestManagerShutdown(t *testing.T) { logger := &MockLogger{} manager := NewManager(nil, logger) - + manager.appearances.InsertAppearance(NewAppearance(100, "Test", 1000)) - + manager.Shutdown() - + if manager.GetAppearanceCount() != 0 { t.Error("Shutdown did not clear appearances") } - + // Check for shutdown log found := false for _, log := range logger.logs { @@ -1112,44 +1112,44 @@ func TestEntityAppearanceAdapter(t *testing.T) { entity := &MockEntity{id: 1, name: "TestEntity", databaseID: 100} logger := &MockLogger{} manager := NewManager(nil, logger) - + // Add test appearance app := NewAppearance(500, "TestAppearance", 1000) manager.AddAppearance(app) - + adapter := NewEntityAppearanceAdapter(entity, manager, logger) - + // Test initial state if adapter.GetAppearanceID() != 0 { t.Error("Initial appearance ID should be 0") } - + if adapter.GetAppearance() != nil { t.Error("Initial appearance should be nil") } - + // Test setting appearance ID adapter.SetAppearanceID(500) if adapter.GetAppearanceID() != 500 { t.Error("SetAppearanceID failed") } - + // Test getting appearance appearance := adapter.GetAppearance() if appearance == nil || appearance.GetID() != 500 { t.Error("GetAppearance failed") } - + // Test appearance name if name := adapter.GetAppearanceName(); name != "TestAppearance" { t.Errorf("GetAppearanceName() = %v, want TestAppearance", name) } - + // Test validation if err := adapter.ValidateAppearance(); err != nil { t.Errorf("ValidateAppearance() failed: %v", err) } - + // Test invalid appearance adapter.SetAppearanceID(999) if err := adapter.ValidateAppearance(); err == nil { @@ -1160,24 +1160,24 @@ func TestEntityAppearanceAdapter(t *testing.T) { func TestEntityAppearanceAdapterClientCompatibility(t *testing.T) { entity := &MockEntity{id: 1, name: "TestEntity"} manager := NewManager(nil, nil) - + // Add appearance with version requirement app := NewAppearance(100, "Test", 1096) manager.AddAppearance(app) - + adapter := NewEntityAppearanceAdapter(entity, manager, nil) adapter.SetAppearanceID(100) - + // Test compatible client if !adapter.IsCompatibleWithClient(1100) { t.Error("Should be compatible with client version 1100") } - + // Test incompatible client if adapter.IsCompatibleWithClient(1000) { t.Error("Should not be compatible with client version 1000") } - + // Test no appearance (always compatible) adapter.SetAppearanceID(0) if !adapter.IsCompatibleWithClient(500) { @@ -1189,34 +1189,34 @@ func TestEntityAppearanceAdapterSendToClient(t *testing.T) { entity := &MockEntity{id: 1, name: "TestEntity"} logger := &MockLogger{} manager := NewManager(nil, logger) - + app := NewAppearance(100, "Test", 1096) manager.AddAppearance(app) - + adapter := NewEntityAppearanceAdapter(entity, manager, logger) adapter.SetAppearanceID(100) - + // Test successful send client := &MockClient{version: 1100} err := adapter.SendAppearanceToClient(client) if err != nil { t.Errorf("SendAppearanceToClient failed: %v", err) } - + // Test incompatible client lowClient := &MockClient{version: 1000} err = adapter.SendAppearanceToClient(lowClient) if err == nil { t.Error("SendAppearanceToClient should fail for incompatible client") } - + // Test client send error errorClient := &MockClient{version: 1100, sendError: fmt.Errorf("send failed")} err = adapter.SendAppearanceToClient(errorClient) if err == nil { t.Error("SendAppearanceToClient should propagate client error") } - + // Test nil client err = adapter.SendAppearanceToClient(nil) if err == nil { @@ -1227,37 +1227,37 @@ func TestEntityAppearanceAdapterSendToClient(t *testing.T) { // Cache tests func TestSimpleAppearanceCache(t *testing.T) { cache := NewSimpleAppearanceCache() - + app1 := NewAppearance(100, "Test1", 1000) app2 := NewAppearance(200, "Test2", 1096) - + // Test Set and Get cache.Set(100, app1) cache.Set(200, app2) - + if got := cache.Get(100); got != app1 { t.Error("Cache Get(100) failed") } - + if got := cache.Get(999); got != nil { t.Error("Cache Get(999) should return nil") } - + // Test GetSize if size := cache.GetSize(); size != 2 { t.Errorf("GetSize() = %v, want 2", size) } - + // Test Remove cache.Remove(100) if cache.Get(100) != nil { t.Error("Remove(100) failed") } - + if size := cache.GetSize(); size != 1 { t.Errorf("GetSize() after remove = %v, want 1", size) } - + // Test Clear cache.Clear() if size := cache.GetSize(); size != 0 { @@ -1269,66 +1269,66 @@ func TestCachedAppearanceManager(t *testing.T) { db := &MockDatabase{} logger := &MockLogger{} baseManager := NewManager(db, logger) - + // Add test appearances app1 := NewAppearance(100, "Test1", 1000) app2 := NewAppearance(200, "Test2", 1096) baseManager.AddAppearance(app1) baseManager.AddAppearance(app2) - + cache := NewSimpleAppearanceCache() cachedManager := NewCachedAppearanceManager(baseManager, cache) - + // Test FindAppearanceByID with caching found := cachedManager.FindAppearanceByID(100) if found == nil || found.GetID() != 100 { t.Error("FindAppearanceByID(100) failed") } - + // Verify it was cached if cache.GetSize() != 1 { t.Error("Appearance was not cached") } - + // Test cache hit found2 := cachedManager.FindAppearanceByID(100) if found2 != found { t.Error("Should have returned cached appearance") } - + // Test AddAppearance updates cache app3 := NewAppearance(300, "Test3", 1200) err := cachedManager.AddAppearance(app3) if err != nil { t.Errorf("AddAppearance failed: %v", err) } - + if cache.Get(300) == nil { t.Error("Added appearance was not cached") } - + // Test UpdateAppearance updates cache updated := NewAppearance(100, "Updated", 1100) err = cachedManager.UpdateAppearance(updated) if err != nil { t.Errorf("UpdateAppearance failed: %v", err) } - + cached := cache.Get(100) if cached == nil || cached.GetName() != "Updated" { t.Error("Cache was not updated") } - + // Test RemoveAppearance updates cache err = cachedManager.RemoveAppearance(200) if err != nil { t.Errorf("RemoveAppearance failed: %v", err) } - + if cache.Get(200) != nil { t.Error("Removed appearance still in cache") } - + // Test ClearCache cachedManager.ClearCache() if cache.GetSize() != 0 { @@ -1339,33 +1339,33 @@ func TestCachedAppearanceManager(t *testing.T) { func TestCacheConcurrency(t *testing.T) { cache := NewSimpleAppearanceCache() var wg sync.WaitGroup - + // Concurrent operations for i := 0; i < 100; i++ { wg.Add(3) - + // Writer go func(id int32) { defer wg.Done() app := NewAppearance(id, "Test", 1000) cache.Set(id, app) }(int32(i)) - + // Reader go func(id int32) { defer wg.Done() _ = cache.Get(id) }(int32(i)) - + // Size checker go func() { defer wg.Done() _ = cache.GetSize() }() } - + wg.Wait() - + // Final size should be predictable if size := cache.GetSize(); size > 100 { t.Errorf("Cache size %v is too large", size) @@ -1376,23 +1376,23 @@ func TestCacheConcurrency(t *testing.T) { func TestEntityAppearanceAdapterErrorCases(t *testing.T) { entity := &MockEntity{id: 1, name: "TestEntity"} logger := &MockLogger{} - + // Test with nil manager adapter := NewEntityAppearanceAdapter(entity, nil, logger) adapter.SetAppearanceID(100) - + // GetAppearance should return nil and log error appearance := adapter.GetAppearance() if appearance != nil { t.Error("GetAppearance should return nil with nil manager") } - + // Test UpdateAppearance with nil manager err := adapter.UpdateAppearance(100) if err == nil { t.Error("UpdateAppearance should fail with nil manager") } - + // Test UpdateAppearance with non-existent appearance manager := NewManager(nil, logger) adapter = NewEntityAppearanceAdapter(entity, manager, logger) @@ -1400,7 +1400,7 @@ func TestEntityAppearanceAdapterErrorCases(t *testing.T) { if err == nil { t.Error("UpdateAppearance should fail for non-existent appearance") } - + // Test SendAppearanceToClient with no appearance set client := &MockClient{version: 1100} adapter.SetAppearanceID(0) @@ -1412,12 +1412,12 @@ func TestEntityAppearanceAdapterErrorCases(t *testing.T) { func TestManagerGetAppearances(t *testing.T) { manager := NewManager(nil, nil) - + appearances := manager.GetAppearances() if appearances == nil { t.Error("GetAppearances should not return nil") } - + if appearances != manager.appearances { t.Error("GetAppearances should return internal appearances collection") } @@ -1425,19 +1425,19 @@ func TestManagerGetAppearances(t *testing.T) { func TestManagerCompatibleAndSearch(t *testing.T) { manager := NewManager(nil, nil) - + // Add test appearances app1 := NewAppearance(100, "Human Male", 1000) app2 := NewAppearance(200, "Human Female", 1096) manager.AddAppearance(app1) manager.AddAppearance(app2) - + // Test GetCompatibleAppearances compatible := manager.GetCompatibleAppearances(1050) if len(compatible) != 1 { t.Errorf("GetCompatibleAppearances(1050) returned %v results, want 1", len(compatible)) } - + // Test SearchAppearancesByName results := manager.SearchAppearancesByName("Human") if len(results) != 2 { @@ -1452,7 +1452,7 @@ func TestManagerReloadFromDatabase(t *testing.T) { if err == nil { t.Error("ReloadFromDatabase should fail with nil database") } - + // Test successful reload db := &MockDatabase{ appearances: []*Appearance{ @@ -1460,24 +1460,24 @@ func TestManagerReloadFromDatabase(t *testing.T) { }, } manager = NewManager(db, nil) - + // Add some appearances first manager.AddAppearance(NewAppearance(200, "Existing", 1000)) - + err = manager.ReloadFromDatabase() if err != nil { t.Errorf("ReloadFromDatabase failed: %v", err) } - + // Should only have the database appearance now if count := manager.GetAppearanceCount(); count != 1 { t.Errorf("After reload, count = %v, want 1", count) } - + if !manager.appearances.HasAppearance(100) { t.Error("Reloaded appearance not found") } - + if manager.appearances.HasAppearance(200) { t.Error("Previous appearance should be cleared") } @@ -1485,19 +1485,19 @@ func TestManagerReloadFromDatabase(t *testing.T) { func TestManagerCommandEdgeCases(t *testing.T) { manager := NewManager(nil, nil) - + // Test reload command without database result, err := manager.ProcessCommand("reload", nil) if err == nil { t.Error("Reload command should fail without database") } - + // Test info command with invalid ID _, err = manager.ProcessCommand("info", []string{"invalid"}) if err == nil { t.Error("Info command should fail with invalid ID") } - + // Test info command with non-existent ID result, err = manager.ProcessCommand("info", []string{"999"}) if err != nil { @@ -1506,7 +1506,7 @@ func TestManagerCommandEdgeCases(t *testing.T) { if !contains(result, "not found") { t.Error("Info command should indicate appearance not found") } - + // Test search with no results result, err = manager.ProcessCommand("search", []string{"nonexistent"}) if err != nil { @@ -1519,10 +1519,10 @@ func TestManagerCommandEdgeCases(t *testing.T) { func TestManagerValidateAllAppearances(t *testing.T) { manager := NewManager(nil, nil) - + // Add valid appearance manager.AddAppearance(NewAppearance(100, "Valid", 1000)) - + issues := manager.ValidateAllAppearances() if len(issues) != 0 { t.Errorf("ValidateAllAppearances returned issues for valid data: %v", issues) @@ -1533,18 +1533,18 @@ func TestEntityAppearanceAdapterGetAppearanceName(t *testing.T) { entity := &MockEntity{id: 1, name: "TestEntity"} manager := NewManager(nil, nil) adapter := NewEntityAppearanceAdapter(entity, manager, nil) - + // Test with no appearance if name := adapter.GetAppearanceName(); name != "" { t.Errorf("GetAppearanceName() with no appearance = %v, want empty string", name) } - + // Test with appearance app := NewAppearance(100, "TestName", 1000) manager.AddAppearance(app) adapter.SetAppearanceID(100) - + if name := adapter.GetAppearanceName(); name != "TestName" { t.Errorf("GetAppearanceName() = %v, want TestName", name) } -} \ No newline at end of file +} diff --git a/internal/appearances/interfaces.go b/internal/appearances/interfaces.go index 734a65d..ad7700f 100644 --- a/internal/appearances/interfaces.go +++ b/internal/appearances/interfaces.go @@ -15,10 +15,10 @@ type Database interface { // Logger interface for appearance logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // AppearanceProvider interface for entities that provide appearances diff --git a/internal/appearances/manager.go b/internal/appearances/manager.go index 8576c11..f692b1a 100644 --- a/internal/appearances/manager.go +++ b/internal/appearances/manager.go @@ -201,7 +201,7 @@ func (m *Manager) SearchAppearancesByName(nameSubstring string) []*Appearance { } // GetStatistics returns appearance system statistics -func (m *Manager) GetStatistics() map[string]interface{} { +func (m *Manager) GetStatistics() map[string]any { m.mutex.RLock() defer m.mutex.RUnlock() diff --git a/internal/classes/classes.go b/internal/classes/classes.go index 869d8e2..50a492b 100644 --- a/internal/classes/classes.go +++ b/internal/classes/classes.go @@ -329,11 +329,11 @@ func (c *Classes) GetClassCount() int { } // GetClassInfo returns comprehensive information about a class -func (c *Classes) GetClassInfo(classID int8) map[string]interface{} { +func (c *Classes) GetClassInfo(classID int8) map[string]any { c.mutex.RLock() defer c.mutex.RUnlock() - info := make(map[string]interface{}) + info := make(map[string]any) if !c.IsValidClassID(classID) { info["valid"] = false diff --git a/internal/classes/classes_test.go b/internal/classes/classes_test.go index a888eda..29d1b45 100644 --- a/internal/classes/classes_test.go +++ b/internal/classes/classes_test.go @@ -1185,7 +1185,7 @@ func TestGetClassSelectionData(t *testing.T) { t.Error("Selection data should include statistics") } - adventureClasses := selectionData["adventure_classes"].([]map[string]interface{}) + adventureClasses := selectionData["adventure_classes"].([]map[string]any) if len(adventureClasses) == 0 { t.Error("Selection data should include adventure classes") } @@ -1442,7 +1442,7 @@ func TestGetClassRecommendations(t *testing.T) { manager := NewClassManager() // Test recommendations by class type - preferences := map[string]interface{}{ + preferences := map[string]any{ "class_type": ClassTypeAdventure, } @@ -1459,7 +1459,7 @@ func TestGetClassRecommendations(t *testing.T) { } // Test recommendations by base class - basePreferences := map[string]interface{}{ + basePreferences := map[string]any{ "base_class": ClassFighter, } @@ -1469,7 +1469,7 @@ func TestGetClassRecommendations(t *testing.T) { } // Test recommendations by preferred stats - statPreferences := map[string]interface{}{ + statPreferences := map[string]any{ "preferred_stats": []string{"strength", "stamina"}, } @@ -1479,7 +1479,7 @@ func TestGetClassRecommendations(t *testing.T) { } // Test empty preferences (should get defaults) - emptyPreferences := map[string]interface{}{} + emptyPreferences := map[string]any{} defaultRecommendations := manager.GetClassRecommendations(emptyPreferences) if len(defaultRecommendations) == 0 { t.Error("Should get default recommendations when no preferences given") diff --git a/internal/classes/integration.go b/internal/classes/integration.go index 38e6662..11a0ffa 100644 --- a/internal/classes/integration.go +++ b/internal/classes/integration.go @@ -33,7 +33,7 @@ func NewClassIntegration() *ClassIntegration { } // ValidateEntityClass validates an entity's class and provides detailed information -func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string, map[string]interface{}) { +func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string, map[string]any) { classID := entity.GetClass() if !ci.classes.IsValidClassID(classID) { @@ -45,8 +45,8 @@ func (ci *ClassIntegration) ValidateEntityClass(entity ClassAware) (bool, string } // GetEntityClassInfo returns comprehensive class information for an entity -func (ci *ClassIntegration) GetEntityClassInfo(entity EntityWithClass) map[string]interface{} { - info := make(map[string]interface{}) +func (ci *ClassIntegration) GetEntityClassInfo(entity EntityWithClass) map[string]any { + info := make(map[string]any) // Basic entity info info["entity_id"] = entity.GetID() @@ -281,12 +281,12 @@ func (ci *ClassIntegration) GetClassStartingStats(classID int8) map[string]int16 } // CreateClassSpecificEntity creates entity data with class-specific properties -func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]interface{} { +func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]any { if !ci.classes.IsValidClassID(classID) { return nil } - entityData := make(map[string]interface{}) + entityData := make(map[string]any) // Basic class info entityData["class_id"] = classID @@ -310,16 +310,16 @@ func (ci *ClassIntegration) CreateClassSpecificEntity(classID int8) map[string]i } // GetClassSelectionData returns data for class selection UI -func (ci *ClassIntegration) GetClassSelectionData() map[string]interface{} { - data := make(map[string]interface{}) +func (ci *ClassIntegration) GetClassSelectionData() map[string]any { + data := make(map[string]any) // All available adventure classes (exclude tradeskill for character creation) allClasses := ci.classes.GetAllClasses() - adventureClasses := make([]map[string]interface{}, 0) + adventureClasses := make([]map[string]any, 0) for classID, displayName := range allClasses { if ci.classes.IsAdventureClass(classID) { - classData := map[string]interface{}{ + classData := map[string]any{ "id": classID, "name": displayName, "type": ci.classes.GetClassType(classID), diff --git a/internal/classes/manager.go b/internal/classes/manager.go index 108de71..dc4e177 100644 --- a/internal/classes/manager.go +++ b/internal/classes/manager.go @@ -330,8 +330,8 @@ func (cm *ClassManager) handleProgressionCommand(args []string) (string, error) } // ValidateEntityClasses validates classes for a collection of entities -func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]interface{} { - validationResults := make(map[string]interface{}) +func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string]any { + validationResults := make(map[string]any) validCount := 0 invalidCount := 0 @@ -351,11 +351,11 @@ func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string] // Track invalid entities if !isValid { if validationResults["invalid_entities"] == nil { - validationResults["invalid_entities"] = make([]map[string]interface{}, 0) + validationResults["invalid_entities"] = make([]map[string]any, 0) } - invalidList := validationResults["invalid_entities"].([]map[string]interface{}) - invalidList = append(invalidList, map[string]interface{}{ + invalidList := validationResults["invalid_entities"].([]map[string]any) + invalidList = append(invalidList, map[string]any{ "index": i, "class_id": classID, }) @@ -372,7 +372,7 @@ func (cm *ClassManager) ValidateEntityClasses(entities []ClassAware) map[string] } // GetClassRecommendations returns class recommendations for character creation -func (cm *ClassManager) GetClassRecommendations(preferences map[string]interface{}) []int8 { +func (cm *ClassManager) GetClassRecommendations(preferences map[string]any) []int8 { recommendations := make([]int8, 0) // Check for class type preference diff --git a/internal/classes/utils.go b/internal/classes/utils.go index f14d0a1..f350b15 100644 --- a/internal/classes/utils.go +++ b/internal/classes/utils.go @@ -366,8 +366,8 @@ func (cu *ClassUtils) GetClassAliases(classID int8) []string { } // GetClassStatistics returns statistics about the class system -func (cu *ClassUtils) GetClassStatistics() map[string]interface{} { - stats := make(map[string]interface{}) +func (cu *ClassUtils) GetClassStatistics() map[string]any { + stats := make(map[string]any) allClasses := cu.classes.GetAllClasses() stats["total_classes"] = len(allClasses) diff --git a/internal/collections/interfaces.go b/internal/collections/interfaces.go index f47c339..a31e916 100644 --- a/internal/collections/interfaces.go +++ b/internal/collections/interfaces.go @@ -163,14 +163,14 @@ type CollectionEventHandler interface { // LogHandler provides logging functionality type LogHandler interface { // LogDebug logs debug messages - LogDebug(category, message string, args ...interface{}) + LogDebug(category, message string, args ...any) // LogInfo logs informational messages - LogInfo(category, message string, args ...interface{}) + LogInfo(category, message string, args ...any) // LogError logs error messages - LogError(category, message string, args ...interface{}) + LogError(category, message string, args ...any) // LogWarning logs warning messages - LogWarning(category, message string, args ...interface{}) + LogWarning(category, message string, args ...any) } diff --git a/internal/entity/entity.go b/internal/entity/entity.go index 1c48192..1b534ea 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -151,7 +151,7 @@ func (e *Entity) SetInfoStruct(info *InfoStruct) { } // GetClient returns the client for this entity (overridden by Player) -func (e *Entity) GetClient() interface{} { +func (e *Entity) GetClient() any { return nil } diff --git a/internal/factions/interfaces.go b/internal/factions/interfaces.go index ce4a618..c5e66b2 100644 --- a/internal/factions/interfaces.go +++ b/internal/factions/interfaces.go @@ -18,10 +18,10 @@ type Database interface { // Logger interface for faction logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // FactionRelation represents a relationship between two factions diff --git a/internal/factions/manager.go b/internal/factions/manager.go index 3853e98..e6e57d7 100644 --- a/internal/factions/manager.go +++ b/internal/factions/manager.go @@ -237,11 +237,11 @@ func (m *Manager) RecordFactionDecrease(factionID int32) { } // GetStatistics returns faction system statistics -func (m *Manager) GetStatistics() map[string]interface{} { +func (m *Manager) GetStatistics() map[string]any { m.mutex.RLock() defer m.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_factions"] = m.masterFactionList.GetFactionCount() stats["total_faction_changes"] = m.totalFactionChanges stats["faction_increases"] = m.factionIncreases diff --git a/internal/ground_spawn/benchmark_test.go b/internal/ground_spawn/benchmark_test.go new file mode 100644 index 0000000..ff4a8a5 --- /dev/null +++ b/internal/ground_spawn/benchmark_test.go @@ -0,0 +1,562 @@ +package ground_spawn + +import ( + "testing" +) + +// Mock implementations are in test_utils.go + +// Benchmark GroundSpawn operations +func BenchmarkGroundSpawn(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 2, + RandomizeHeading: true, + Location: SpawnLocation{ + X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1, + }, + Name: "Benchmark Node", + Description: "A benchmark harvestable node", + } + + gs := NewGroundSpawn(config) + + b.Run("GetNumberHarvests", func(b *testing.B) { + for b.Loop() { + _ = gs.GetNumberHarvests() + } + }) + + b.Run("SetNumberHarvests", func(b *testing.B) { + b.ResetTimer() + for i := 0; b.Loop(); i++ { + gs.SetNumberHarvests(int8(i % 10)) + } + }) + + b.Run("GetCollectionSkill", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = gs.GetCollectionSkill() + } + }) + + b.Run("SetCollectionSkill", func(b *testing.B) { + skills := []string{SkillGathering, SkillMining, SkillFishing, SkillTrapping} + b.ResetTimer() + for i := 0; b.Loop(); i++ { + gs.SetCollectionSkill(skills[i%len(skills)]) + } + }) + + b.Run("IsAvailable", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = gs.IsAvailable() + } + }) + + b.Run("IsDepleted", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = gs.IsDepleted() + } + }) + + b.Run("GetHarvestMessageName", func(b *testing.B) { + b.ResetTimer() + for i := 0; b.Loop(); i++ { + _ = gs.GetHarvestMessageName(i%2 == 0, i%4 == 0) + } + }) + + b.Run("GetHarvestSpellType", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = gs.GetHarvestSpellType() + } + }) + + b.Run("Copy", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + copy := gs.Copy() + _ = copy + } + }) + + b.Run("Respawn", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + gs.Respawn() + } + }) +} + +// Benchmark Manager operations +func BenchmarkManager(b *testing.B) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate with ground spawns + for i := int32(1); i <= 100; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: float32(i * 45), GridID: 1, + }, + Name: "Benchmark Node", + Description: "Benchmark node", + } + gs := manager.CreateGroundSpawn(config) + _ = gs + } + + b.Run("GetGroundSpawn", func(b *testing.B) { + b.ResetTimer() + for i := 0; b.Loop(); i++ { + spawnID := int32((i % 100) + 1) + _ = manager.GetGroundSpawn(spawnID) + } + }) + + b.Run("CreateGroundSpawn", func(b *testing.B) { + b.ResetTimer() + for i := 0; b.Loop(); i++ { + config := GroundSpawnConfig{ + GroundSpawnID: int32(2000 + i), + CollectionSkill: SkillMining, + NumberHarvests: 3, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i), Y: float32(i * 2), Z: float32(i * 3), + Heading: 0, GridID: 1, + }, + Name: "New Node", + Description: "New benchmark node", + } + _ = manager.CreateGroundSpawn(config) + } + }) + + b.Run("GetGroundSpawnsByZone", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = manager.GetGroundSpawnsByZone(1) + } + }) + + b.Run("GetGroundSpawnCount", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = manager.GetGroundSpawnCount() + } + }) + + b.Run("GetActiveGroundSpawns", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = manager.GetActiveGroundSpawns() + } + }) + + b.Run("GetDepletedGroundSpawns", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = manager.GetDepletedGroundSpawns() + } + }) + + b.Run("GetStatistics", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _ = manager.GetStatistics() + } + }) + + b.Run("ResetStatistics", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + manager.ResetStatistics() + } + }) + + b.Run("ProcessRespawns", func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + manager.ProcessRespawns() + } + }) +} + +// Benchmark concurrent operations +func BenchmarkConcurrentOperations(b *testing.B) { + b.Run("GroundSpawnConcurrentReads", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Concurrent Node", + Description: "Concurrent benchmark node", + } + gs := NewGroundSpawn(config) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + switch i % 4 { + case 0: + _ = gs.GetNumberHarvests() + case 1: + _ = gs.GetCollectionSkill() + case 2: + _ = gs.IsAvailable() + case 3: + _ = gs.GetHarvestSpellType() + } + i++ + } + }) + }) + + b.Run("GroundSpawnConcurrentWrites", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Concurrent Node", + Description: "Concurrent benchmark node", + } + gs := NewGroundSpawn(config) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + switch i % 3 { + case 0: + gs.SetNumberHarvests(int8(i % 10)) + case 1: + gs.SetCollectionSkill(SkillMining) + case 2: + gs.SetRandomizeHeading(i%2 == 0) + } + i++ + } + }) + }) + + b.Run("ManagerConcurrentOperations", func(b *testing.B) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate + for i := int32(1); i <= 10; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Manager Node", + Description: "Manager benchmark node", + } + manager.CreateGroundSpawn(config) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + spawnID := int32((i % 10) + 1) + switch i % 5 { + case 0: + _ = manager.GetGroundSpawn(spawnID) + case 1: + _ = manager.GetGroundSpawnsByZone(1) + case 2: + _ = manager.GetStatistics() + case 3: + _ = manager.GetActiveGroundSpawns() + case 4: + manager.ProcessRespawns() + } + i++ + } + }) + }) +} + +// Memory allocation benchmarks +func BenchmarkMemoryAllocations(b *testing.B) { + b.Run("GroundSpawnCreation", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Memory Test Node", + Description: "Memory test node", + } + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = NewGroundSpawn(config) + } + }) + + b.Run("ManagerCreation", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = NewManager(nil, &mockLogger{}) + } + }) + + b.Run("GroundSpawnCopy", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Copy Test Node", + Description: "Copy test node", + } + gs := NewGroundSpawn(config) + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = gs.Copy() + } + }) + + b.Run("StatisticsGeneration", func(b *testing.B) { + manager := NewManager(nil, &mockLogger{}) + + // Add some data for meaningful statistics + for i := int32(1); i <= 10; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Stats Node", + Description: "Stats test node", + } + manager.CreateGroundSpawn(config) + } + + // Add some harvest statistics + manager.mutex.Lock() + manager.totalHarvests = 1000 + manager.successfulHarvests = 850 + manager.rareItemsHarvested = 50 + manager.skillUpsGenerated = 200 + manager.harvestsBySkill[SkillGathering] = 600 + manager.harvestsBySkill[SkillMining] = 400 + manager.mutex.Unlock() + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = manager.GetStatistics() + } + }) +} + +// Contention benchmarks +func BenchmarkContention(b *testing.B) { + b.Run("HighContentionReads", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Contention Node", + Description: "Contention test node", + } + gs := NewGroundSpawn(config) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = gs.GetNumberHarvests() + } + }) + }) + + b.Run("HighContentionWrites", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Contention Node", + Description: "Contention test node", + } + gs := NewGroundSpawn(config) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + gs.SetNumberHarvests(int8(i % 10)) + i++ + } + }) + }) + + b.Run("MixedReadWrite", func(b *testing.B) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Mixed Node", + Description: "Mixed operations test node", + } + gs := NewGroundSpawn(config) + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + if i%10 == 0 { + // 10% writes + gs.SetNumberHarvests(int8(i % 5)) + } else { + // 90% reads + _ = gs.GetNumberHarvests() + } + i++ + } + }) + }) + + b.Run("ManagerHighContention", func(b *testing.B) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate + for i := int32(1); i <= 5; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Contention Node", + Description: "Manager contention test", + } + manager.CreateGroundSpawn(config) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = manager.GetGroundSpawn(1) + } + }) + }) +} + +// Scalability benchmarks +func BenchmarkScalability(b *testing.B) { + sizes := []int{10, 100, 1000} + + for _, size := range sizes { + b.Run("GroundSpawnLookup_"+string(rune(size)), func(b *testing.B) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate with varying sizes + for i := int32(1); i <= int32(size); i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Scale Node", + Description: "Scalability test node", + } + manager.CreateGroundSpawn(config) + } + + b.ResetTimer() + for i := 0; b.Loop(); i++ { + spawnID := int32((i % size) + 1) + _ = manager.GetGroundSpawn(spawnID) + } + }) + } + + for _, size := range sizes { + b.Run("ZoneLookup_"+string(rune(size)), func(b *testing.B) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate with varying sizes + for i := int32(1); i <= int32(size); i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Zone Node", + Description: "Zone scalability test", + } + manager.CreateGroundSpawn(config) + } + + b.ResetTimer() + for b.Loop() { + _ = manager.GetGroundSpawnsByZone(1) + } + }) + } +} diff --git a/internal/ground_spawn/concurrency_test.go b/internal/ground_spawn/concurrency_test.go new file mode 100644 index 0000000..e4481f1 --- /dev/null +++ b/internal/ground_spawn/concurrency_test.go @@ -0,0 +1,523 @@ +package ground_spawn + +import ( + "sync" + "testing" + "time" +) + +// Mock implementations are in test_utils.go + +// Stress test GroundSpawn with concurrent operations +func TestGroundSpawnConcurrency(t *testing.T) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 2, + RandomizeHeading: true, + Location: SpawnLocation{ + X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1, + }, + Name: "Test Node", + Description: "A test harvestable node", + } + + gs := NewGroundSpawn(config) + + const numGoroutines = 100 + const operationsPerGoroutine = 100 + + var wg sync.WaitGroup + + t.Run("ConcurrentGetterSetterOperations", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + switch j % 8 { + case 0: + gs.SetNumberHarvests(int8(goroutineID % 10)) + case 1: + _ = gs.GetNumberHarvests() + case 2: + gs.SetAttemptsPerHarvest(int8(goroutineID % 5)) + case 3: + _ = gs.GetAttemptsPerHarvest() + case 4: + gs.SetCollectionSkill(SkillMining) + case 5: + _ = gs.GetCollectionSkill() + case 6: + gs.SetRandomizeHeading(goroutineID%2 == 0) + case 7: + _ = gs.GetRandomizeHeading() + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentStateChecks", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + switch j % 4 { + case 0: + _ = gs.IsDepleted() + case 1: + _ = gs.IsAvailable() + case 2: + _ = gs.GetHarvestMessageName(true, false) + case 3: + _ = gs.GetHarvestSpellType() + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentCopyOperations", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + // Test concurrent copying while modifying + if j%2 == 0 { + copy := gs.Copy() + if copy == nil { + t.Errorf("Goroutine %d: Copy returned nil", goroutineID) + } + } else { + gs.SetNumberHarvests(int8(goroutineID % 5)) + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentRespawnOperations", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + if j%10 == 0 { + gs.Respawn() + } else { + // Mix of reads and writes during respawn + switch j % 4 { + case 0: + _ = gs.GetNumberHarvests() + case 1: + gs.SetNumberHarvests(int8(goroutineID % 3)) + case 2: + _ = gs.IsAvailable() + case 3: + _ = gs.IsDepleted() + } + } + } + }(i) + } + + wg.Wait() + }) +} + +// Stress test Manager with concurrent operations +func TestManagerConcurrency(t *testing.T) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate with some ground spawns + for i := int32(1); i <= 10; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: float32(i * 45), GridID: 1, + }, + Name: "Test Node", + Description: "Test node", + } + gs := manager.CreateGroundSpawn(config) + if gs == nil { + t.Fatalf("Failed to create ground spawn %d", i) + } + } + + const numGoroutines = 100 + const operationsPerGoroutine = 100 + + var wg sync.WaitGroup + + t.Run("ConcurrentGroundSpawnAccess", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + spawnID := int32((goroutineID % 10) + 1) + + switch j % 5 { + case 0: + _ = manager.GetGroundSpawn(spawnID) + case 1: + _ = manager.GetGroundSpawnsByZone(1) + case 2: + _ = manager.GetGroundSpawnCount() + case 3: + _ = manager.GetActiveGroundSpawns() + case 4: + _ = manager.GetDepletedGroundSpawns() + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentStatisticsOperations", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + switch j % 3 { + case 0: + _ = manager.GetStatistics() + case 1: + manager.ResetStatistics() + case 2: + // Simulate harvest statistics updates + manager.mutex.Lock() + manager.totalHarvests++ + manager.successfulHarvests++ + skill := SkillGathering + manager.harvestsBySkill[skill]++ + manager.mutex.Unlock() + } + } + }(i) + } + + wg.Wait() + + // Verify statistics consistency + stats := manager.GetStatistics() + if stats.TotalHarvests < 0 || stats.SuccessfulHarvests < 0 { + t.Errorf("Invalid statistics: total=%d, successful=%d", + stats.TotalHarvests, stats.SuccessfulHarvests) + } + }) + + t.Run("ConcurrentGroundSpawnModification", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + // Use a more unique ID generation strategy to avoid conflicts + // Start at 10000 and use goroutine*1000 + iteration to ensure uniqueness + newID := int32(10000 + goroutineID*1000 + j) + + config := GroundSpawnConfig{ + GroundSpawnID: newID, + CollectionSkill: SkillMining, + NumberHarvests: 3, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(j), Y: float32(j * 2), Z: float32(j * 3), + Heading: float32(j * 10), GridID: 1, + }, + Name: "Concurrent Node", + Description: "Concurrent test node", + } + + // Add ground spawn - note that CreateGroundSpawn overwrites the ID + gs := manager.CreateGroundSpawn(config) + if gs == nil { + t.Errorf("Goroutine %d: Failed to create ground spawn", goroutineID) + continue + } + + // Since CreateGroundSpawn assigns its own ID, we need to get the actual ID + actualID := gs.GetID() + + // Verify it was added with the manager-assigned ID + retrieved := manager.GetGroundSpawn(actualID) + if retrieved == nil { + t.Errorf("Goroutine %d: Failed to retrieve ground spawn %d", goroutineID, actualID) + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentRespawnProcessing", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + if j%50 == 0 { + // Process respawns occasionally + manager.ProcessRespawns() + } else { + // Schedule respawns + spawnID := int32((goroutineID % 10) + 1) + if gs := manager.GetGroundSpawn(spawnID); gs != nil { + if gs.IsDepleted() { + manager.scheduleRespawn(gs) + } + } + } + } + }(i) + } + + wg.Wait() + }) +} + +// Test for potential deadlocks +func TestDeadlockPrevention(t *testing.T) { + manager := NewManager(nil, &mockLogger{}) + + // Create test ground spawns + for i := int32(1); i <= 10; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Deadlock Test Node", + Description: "Test node", + } + gs := manager.CreateGroundSpawn(config) + if gs == nil { + t.Fatalf("Failed to create ground spawn %d", i) + } + } + + const numGoroutines = 50 + var wg sync.WaitGroup + + // Test potential deadlock scenarios + t.Run("MixedOperations", func(t *testing.T) { + done := make(chan bool, 1) + + // Set a timeout to detect deadlocks + go func() { + time.Sleep(10 * time.Second) + select { + case <-done: + return + default: + t.Error("Potential deadlock detected - test timed out") + } + }() + + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range 100 { + spawnID := int32((goroutineID % 10) + 1) + + // Mix operations that could potentially deadlock + switch j % 8 { + case 0: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil { + _ = gs.GetNumberHarvests() + } + case 1: + _ = manager.GetStatistics() + case 2: + _ = manager.GetGroundSpawnsByZone(1) + case 3: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil { + gs.SetNumberHarvests(int8(j % 5)) + } + case 4: + manager.ProcessRespawns() + case 5: + _ = manager.GetActiveGroundSpawns() + case 6: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil { + _ = gs.Copy() + } + case 7: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil && gs.IsDepleted() { + manager.scheduleRespawn(gs) + } + } + } + }(i) + } + + wg.Wait() + done <- true + }) +} + +// Race condition detection test - run with -race flag +func TestRaceConditions(t *testing.T) { + if testing.Short() { + t.Skip("Skipping race condition test in short mode") + } + + manager := NewManager(nil, &mockLogger{}) + + // Rapid concurrent operations to trigger race conditions + const numGoroutines = 200 + const operationsPerGoroutine = 50 + + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range operationsPerGoroutine { + // Use unique IDs to avoid conflicts in rapid creation + uniqueID := int32(20000 + goroutineID*1000 + j) + + // Rapid-fire operations + config := GroundSpawnConfig{ + GroundSpawnID: uniqueID, + CollectionSkill: SkillGathering, + NumberHarvests: 3, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(j), Y: float32(j * 2), Z: float32(j * 3), + Heading: 0, GridID: 1, + }, + Name: "Race Test Node", + Description: "Race test", + } + + gs := manager.CreateGroundSpawn(config) + if gs != nil { + actualID := gs.GetID() // Get the manager-assigned ID + gs.SetNumberHarvests(int8(j%5 + 1)) + _ = gs.GetNumberHarvests() + _ = gs.IsAvailable() + copy := gs.Copy() + if copy != nil { + copy.SetCollectionSkill(SkillMining) + } + + _ = manager.GetGroundSpawn(actualID) + } + + _ = manager.GetStatistics() + manager.ProcessRespawns() + } + }(i) + } + + wg.Wait() +} + +// Specific test for Copy() method mutex safety +func TestCopyMutexSafety(t *testing.T) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Copy Test Node", + Description: "Test node for copy safety", + } + + original := NewGroundSpawn(config) + + const numGoroutines = 100 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Test copying while modifying + for i := range numGoroutines { + go func(goroutineID int) { + defer wg.Done() + + for j := range 100 { + if j%2 == 0 { + // Copy operations + copy := original.Copy() + if copy == nil { + t.Errorf("Goroutine %d: Copy returned nil", goroutineID) + continue + } + + // Verify copy is independent by setting a unique value + expectedValue := int8(goroutineID%5 + 1) // Ensure non-zero value + copy.SetNumberHarvests(expectedValue) + + // Verify the copy has the value we set + if copy.GetNumberHarvests() != expectedValue { + t.Errorf("Goroutine %d: Copy failed to set value correctly, expected %d got %d", + goroutineID, expectedValue, copy.GetNumberHarvests()) + } + + // Copy independence is verified by the fact that we can set different values + // We don't check against original since other goroutines are modifying it concurrently + } else { + // Modify original + original.SetNumberHarvests(int8(goroutineID % 10)) + original.SetCollectionSkill(SkillMining) + _ = original.GetRandomizeHeading() + } + } + }(i) + } + + wg.Wait() +} diff --git a/internal/ground_spawn/core_concurrency_test.go b/internal/ground_spawn/core_concurrency_test.go new file mode 100644 index 0000000..43eefc2 --- /dev/null +++ b/internal/ground_spawn/core_concurrency_test.go @@ -0,0 +1,333 @@ +package ground_spawn + +import ( + "sync" + "testing" + "time" +) + +// Mock implementations are in test_utils.go + +// Test core GroundSpawn concurrency patterns without dependencies +func TestGroundSpawnCoreConcurrency(t *testing.T) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 10, + AttemptsPerHarvest: 2, + RandomizeHeading: true, + Location: SpawnLocation{ + X: 100.0, Y: 200.0, Z: 300.0, Heading: 45.0, GridID: 1, + }, + Name: "Test Node", + Description: "A test harvestable node", + } + + gs := NewGroundSpawn(config) + + const numGoroutines = 100 + const operationsPerGoroutine = 100 + + var wg sync.WaitGroup + + t.Run("ConcurrentAccessors", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(goroutineID int) { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + switch j % 8 { + case 0: + gs.SetNumberHarvests(int8(goroutineID % 10)) + case 1: + _ = gs.GetNumberHarvests() + case 2: + gs.SetAttemptsPerHarvest(int8(goroutineID % 5)) + case 3: + _ = gs.GetAttemptsPerHarvest() + case 4: + gs.SetCollectionSkill(SkillMining) + case 5: + _ = gs.GetCollectionSkill() + case 6: + gs.SetRandomizeHeading(goroutineID%2 == 0) + case 7: + _ = gs.GetRandomizeHeading() + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentStateChecks", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(goroutineID int) { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + switch j % 4 { + case 0: + _ = gs.IsDepleted() + case 1: + _ = gs.IsAvailable() + case 2: + _ = gs.GetHarvestMessageName(true, false) + case 3: + _ = gs.GetHarvestSpellType() + } + } + }(i) + } + + wg.Wait() + }) +} + +// Test Manager core concurrency patterns +func TestManagerCoreConcurrency(t *testing.T) { + manager := NewManager(nil, &mockLogger{}) + + // Pre-populate with some ground spawns + for i := int32(1); i <= 10; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: float32(i * 45), GridID: 1, + }, + Name: "Test Node", + Description: "Test node", + } + gs := manager.CreateGroundSpawn(config) + if gs == nil { + t.Fatalf("Failed to create ground spawn %d", i) + } + } + + const numGoroutines = 50 + const operationsPerGoroutine = 50 + + var wg sync.WaitGroup + + t.Run("ConcurrentAccess", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(goroutineID int) { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + spawnID := int32((goroutineID % 10) + 1) + + switch j % 5 { + case 0: + _ = manager.GetGroundSpawn(spawnID) + case 1: + _ = manager.GetGroundSpawnsByZone(1) + case 2: + _ = manager.GetGroundSpawnCount() + case 3: + _ = manager.GetActiveGroundSpawns() + case 4: + _ = manager.GetDepletedGroundSpawns() + } + } + }(i) + } + + wg.Wait() + }) + + t.Run("ConcurrentStatistics", func(t *testing.T) { + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(goroutineID int) { + defer wg.Done() + + for j := 0; j < operationsPerGoroutine; j++ { + switch j % 3 { + case 0: + _ = manager.GetStatistics() + case 1: + manager.ResetStatistics() + case 2: + // Simulate statistics updates + manager.mutex.Lock() + manager.totalHarvests++ + manager.successfulHarvests++ + skill := SkillGathering + manager.harvestsBySkill[skill]++ + manager.mutex.Unlock() + } + } + }(i) + } + + wg.Wait() + + // Verify statistics consistency + stats := manager.GetStatistics() + if stats.TotalHarvests < 0 || stats.SuccessfulHarvests < 0 { + t.Errorf("Invalid statistics: total=%d, successful=%d", + stats.TotalHarvests, stats.SuccessfulHarvests) + } + }) +} + +// Test Copy() method thread safety +func TestCopyThreadSafety(t *testing.T) { + config := GroundSpawnConfig{ + GroundSpawnID: 1, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: 100, Y: 200, Z: 300, Heading: 45, GridID: 1, + }, + Name: "Copy Test Node", + Description: "Test node for copy safety", + } + + original := NewGroundSpawn(config) + + const numGoroutines = 50 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Test copying while modifying + for i := 0; i < numGoroutines; i++ { + go func(goroutineID int) { + defer wg.Done() + + for j := 0; j < 100; j++ { + if j%2 == 0 { + // Copy operations + copy := original.Copy() + if copy == nil { + t.Errorf("Goroutine %d: Copy returned nil", goroutineID) + continue + } + + // Verify copy is independent by setting different values + newValue := int8(goroutineID%5 + 1) // Ensure non-zero value + copy.SetNumberHarvests(newValue) + + // Copy should have the new value we just set + if copy.GetNumberHarvests() != newValue { + t.Errorf("Goroutine %d: Copy failed to set value correctly, expected %d got %d", + goroutineID, newValue, copy.GetNumberHarvests()) + } + // Note: We can't reliably test that original is unchanged due to concurrent modifications + } else { + // Modify original + original.SetNumberHarvests(int8(goroutineID % 10)) + original.SetCollectionSkill(SkillMining) + _ = original.GetRandomizeHeading() + } + } + }(i) + } + + wg.Wait() +} + +// Test core deadlock prevention +func TestCoreDeadlockPrevention(t *testing.T) { + manager := NewManager(nil, &mockLogger{}) + + // Create test ground spawns + for i := int32(1); i <= 5; i++ { + config := GroundSpawnConfig{ + GroundSpawnID: i, + CollectionSkill: SkillGathering, + NumberHarvests: 5, + AttemptsPerHarvest: 1, + Location: SpawnLocation{ + X: float32(i * 10), Y: float32(i * 20), Z: float32(i * 30), + Heading: 0, GridID: 1, + }, + Name: "Deadlock Test Node", + Description: "Test node", + } + gs := manager.CreateGroundSpawn(config) + if gs == nil { + t.Fatalf("Failed to create ground spawn %d", i) + } + } + + const numGoroutines = 25 + var wg sync.WaitGroup + + // Test potential deadlock scenarios + t.Run("MixedOperations", func(t *testing.T) { + done := make(chan bool, 1) + + // Set a timeout to detect deadlocks + go func() { + time.Sleep(5 * time.Second) + select { + case <-done: + return + default: + t.Error("Potential deadlock detected - test timed out") + } + }() + + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(goroutineID int) { + defer wg.Done() + + for j := 0; j < 50; j++ { + spawnID := int32((goroutineID % 5) + 1) + + // Mix operations that could potentially deadlock + switch j % 8 { + case 0: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil { + _ = gs.GetNumberHarvests() + } + case 1: + _ = manager.GetStatistics() + case 2: + _ = manager.GetGroundSpawnsByZone(1) + case 3: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil { + gs.SetNumberHarvests(int8(j % 5)) + } + case 4: + manager.ProcessRespawns() + case 5: + _ = manager.GetActiveGroundSpawns() + case 6: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil { + _ = gs.Copy() + } + case 7: + gs := manager.GetGroundSpawn(spawnID) + if gs != nil && gs.IsDepleted() { + manager.scheduleRespawn(gs) + } + } + } + }(i) + } + + wg.Wait() + done <- true + }) +} diff --git a/internal/ground_spawn/database.go b/internal/ground_spawn/database.go new file mode 100644 index 0000000..9337d57 --- /dev/null +++ b/internal/ground_spawn/database.go @@ -0,0 +1,249 @@ +package ground_spawn + +import ( + "fmt" + + "eq2emu/internal/database" +) + +// DatabaseAdapter implements the Database interface using the internal database wrapper +type DatabaseAdapter struct { + db *database.DB +} + +// NewDatabaseAdapter creates a new database adapter using the database wrapper +func NewDatabaseAdapter(db *database.DB) *DatabaseAdapter { + return &DatabaseAdapter{ + db: db, + } +} + +// LoadGroundSpawnEntries loads harvest entries for a ground spawn +func (da *DatabaseAdapter) LoadGroundSpawnEntries(groundspawnID int32) ([]*GroundSpawnEntry, error) { + query := ` + SELECT min_skill_level, min_adventure_level, bonus_table, harvest_1, harvest_3, + harvest_5, harvest_imbue, harvest_rare, harvest_10, harvest_coin + FROM groundspawn_entries + WHERE groundspawn_id = ? + ORDER BY min_skill_level ASC + ` + + var entries []*GroundSpawnEntry + + err := da.db.Query(query, func(row *database.Row) error { + entry := &GroundSpawnEntry{} + var bonusTable int32 + + entry.MinSkillLevel = int16(row.Int(0)) + entry.MinAdventureLevel = int16(row.Int(1)) + bonusTable = int32(row.Int(2)) + entry.Harvest1 = float32(row.Float(3)) + entry.Harvest3 = float32(row.Float(4)) + entry.Harvest5 = float32(row.Float(5)) + entry.HarvestImbue = float32(row.Float(6)) + entry.HarvestRare = float32(row.Float(7)) + entry.Harvest10 = float32(row.Float(8)) + entry.HarvestCoin = float32(row.Float(9)) + + entry.BonusTable = bonusTable == 1 + entries = append(entries, entry) + + return nil + }, groundspawnID) + + if err != nil { + return nil, fmt.Errorf("failed to query groundspawn entries: %w", err) + } + + return entries, nil +} + +// LoadGroundSpawnItems loads harvest items for a ground spawn +func (da *DatabaseAdapter) LoadGroundSpawnItems(groundspawnID int32) ([]*GroundSpawnEntryItem, error) { + query := ` + SELECT item_id, is_rare, grid_id, quantity + FROM groundspawn_items + WHERE groundspawn_id = ? + ORDER BY item_id ASC + ` + + var items []*GroundSpawnEntryItem + + err := da.db.Query(query, func(row *database.Row) error { + item := &GroundSpawnEntryItem{} + + item.ItemID = int32(row.Int(0)) + item.IsRare = int8(row.Int(1)) + item.GridID = int32(row.Int(2)) + item.Quantity = int16(row.Int(3)) + + items = append(items, item) + + return nil + }, groundspawnID) + + if err != nil { + return nil, fmt.Errorf("failed to query groundspawn items: %w", err) + } + + return items, nil +} + +// SaveGroundSpawn saves a ground spawn to the database +func (da *DatabaseAdapter) SaveGroundSpawn(gs *GroundSpawn) error { + if gs == nil { + return fmt.Errorf("ground spawn cannot be nil") + } + + query := ` + INSERT OR REPLACE INTO groundspawns ( + id, name, x, y, z, heading, respawn_timer, collection_skill, + number_harvests, attempts_per_harvest, groundspawn_entry_id, + randomize_heading, zone_id, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) + ` + + randomizeHeading := 0 + if gs.GetRandomizeHeading() { + randomizeHeading = 1 + } + + // TODO: Get actual zone ID from spawn + zoneID := int32(1) + + err := da.db.Exec(query, + gs.GetID(), + gs.GetName(), + gs.GetX(), + gs.GetY(), + gs.GetZ(), + int16(gs.GetHeading()), + 300, // Default 5 minutes respawn timer + gs.GetCollectionSkill(), + gs.GetNumberHarvests(), + gs.GetAttemptsPerHarvest(), + gs.GetGroundSpawnEntryID(), + randomizeHeading, + zoneID, + ) + + if err != nil { + return fmt.Errorf("failed to save ground spawn %d: %w", gs.GetID(), err) + } + + return nil +} + +// LoadAllGroundSpawns loads all ground spawns from the database +func (da *DatabaseAdapter) LoadAllGroundSpawns() ([]*GroundSpawn, error) { + query := ` + SELECT id, name, x, y, z, heading, collection_skill, number_harvests, + attempts_per_harvest, groundspawn_entry_id, randomize_heading + FROM groundspawns + WHERE zone_id = ? + ORDER BY id ASC + ` + + // TODO: Support multiple zones + zoneID := int32(1) + + var groundSpawns []*GroundSpawn + + err := da.db.Query(query, func(row *database.Row) error { + id := int32(row.Int(0)) + name := row.Text(1) + x := float32(row.Float(2)) + y := float32(row.Float(3)) + z := float32(row.Float(4)) + heading := float32(row.Float(5)) + collectionSkill := row.Text(6) + numberHarvests := int8(row.Int(7)) + attemptsPerHarvest := int8(row.Int(8)) + groundspawnEntryID := int32(row.Int(9)) + randomizeHeading := int32(row.Int(10)) + + config := GroundSpawnConfig{ + GroundSpawnID: groundspawnEntryID, + CollectionSkill: collectionSkill, + NumberHarvests: numberHarvests, + AttemptsPerHarvest: attemptsPerHarvest, + RandomizeHeading: randomizeHeading == 1, + Location: SpawnLocation{ + X: x, + Y: y, + Z: z, + Heading: heading, + GridID: 1, // TODO: Load from database + }, + Name: name, + Description: fmt.Sprintf("A %s node", collectionSkill), + } + + gs := NewGroundSpawn(config) + gs.SetID(id) + + groundSpawns = append(groundSpawns, gs) + + return nil + }, zoneID) + + if err != nil { + return nil, fmt.Errorf("failed to query groundspawns: %w", err) + } + + return groundSpawns, nil +} + +// DeleteGroundSpawn deletes a ground spawn from the database +func (da *DatabaseAdapter) DeleteGroundSpawn(id int32) error { + query := `DELETE FROM groundspawns WHERE id = ?` + + err := da.db.Exec(query, id) + if err != nil { + return fmt.Errorf("failed to delete ground spawn %d: %w", id, err) + } + + return nil +} + +// LoadPlayerHarvestStatistics loads harvest statistics for a player +func (da *DatabaseAdapter) LoadPlayerHarvestStatistics(playerID int32) (map[string]int64, error) { + query := ` + SELECT skill_name, harvest_count + FROM player_harvest_stats + WHERE player_id = ? + ` + + stats := make(map[string]int64) + + err := da.db.Query(query, func(row *database.Row) error { + skillName := row.Text(0) + harvestCount := row.Int64(1) + + stats[skillName] = harvestCount + + return nil + }, playerID) + + if err != nil { + return nil, fmt.Errorf("failed to query player harvest stats: %w", err) + } + + return stats, nil +} + +// SavePlayerHarvestStatistic saves a player's harvest statistic +func (da *DatabaseAdapter) SavePlayerHarvestStatistic(playerID int32, skillName string, count int64) error { + query := ` + INSERT OR REPLACE INTO player_harvest_stats (player_id, skill_name, harvest_count, updated_at) + VALUES (?, ?, ?, datetime('now')) + ` + + err := da.db.Exec(query, playerID, skillName, count) + if err != nil { + return fmt.Errorf("failed to save player harvest stat: %w", err) + } + + return nil +} diff --git a/internal/ground_spawn/ground_spawn.go b/internal/ground_spawn/ground_spawn.go index c23c9a7..5a35619 100644 --- a/internal/ground_spawn/ground_spawn.go +++ b/internal/ground_spawn/ground_spawn.go @@ -24,18 +24,20 @@ func NewGroundSpawn(config GroundSpawnConfig) *GroundSpawn { // Configure base spawn properties gs.SetName(config.Name) gs.SetSpawnType(DefaultSpawnType) - gs.SetDifficulty(DefaultDifficulty) - gs.SetState(DefaultState) + // Note: SetDifficulty and SetState methods not available in spawn interface // Set position gs.SetX(config.Location.X) - gs.SetY(config.Location.Y) + gs.SetY(config.Location.Y, false) gs.SetZ(config.Location.Z) if config.RandomizeHeading { - gs.SetHeading(rand.Float32() * 360.0) + // Convert float32 to int16 for heading + heading := int16(rand.Float32() * 360.0) + gs.SetHeading(heading, heading) } else { - gs.SetHeading(config.Location.Heading) + heading := int16(config.Location.Heading) + gs.SetHeading(heading, heading) } return gs @@ -47,12 +49,14 @@ func (gs *GroundSpawn) Copy() *GroundSpawn { defer gs.harvestMutex.Unlock() newSpawn := &GroundSpawn{ - Spawn: gs.Spawn.Copy().(*spawn.Spawn), + Spawn: gs.Spawn, // TODO: Implement proper copy when spawn.Copy() is available numberHarvests: gs.numberHarvests, numAttemptsPerHarvest: gs.numAttemptsPerHarvest, groundspawnID: gs.groundspawnID, collectionSkill: gs.collectionSkill, randomizeHeading: gs.randomizeHeading, + // Reset mutexes in the copy to avoid sharing the same mutex instances + // harvestMutex and harvestUseMutex are zero-initialized (correct behavior) } return newSpawn @@ -252,14 +256,14 @@ func (gs *GroundSpawn) ProcessHarvest(context *HarvestContext) (*HarvestResult, } // Validate harvest data - if context.GroundSpawnEntries == nil || len(context.GroundSpawnEntries) == 0 { + if len(context.GroundSpawnEntries) == 0 { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: No groundspawn entries assigned to groundspawn id: %d", gs.groundspawnID), }, nil } - if context.GroundSpawnItems == nil || len(context.GroundSpawnItems) == 0 { + if len(context.GroundSpawnItems) == 0 { return &HarvestResult{ Success: false, MessageText: fmt.Sprintf("Error: No groundspawn items assigned to groundspawn id: %d", gs.groundspawnID), @@ -351,7 +355,7 @@ func (gs *GroundSpawn) filterHarvestTables(context *HarvestContext) []*GroundSpa } // Check level requirement for bonus tables - if entry.BonusTable && context.Player.GetLevel() < entry.MinAdventureLevel { + if entry.BonusTable && (*context.Player).GetLevel() < entry.MinAdventureLevel { continue } @@ -441,9 +445,9 @@ func (gs *GroundSpawn) awardHarvestItems(harvestType int8, availableItems []*Gro var items []*HarvestedItem // Filter items based on harvest type and player location - normalItems := gs.filterItems(availableItems, ItemRarityNormal, player.GetLocation()) - rareItems := gs.filterItems(availableItems, ItemRarityRare, player.GetLocation()) - imbueItems := gs.filterItems(availableItems, ItemRarityImbue, player.GetLocation()) + normalItems := gs.filterItems(availableItems, ItemRarityNormal, (*player).GetLocation()) + rareItems := gs.filterItems(availableItems, ItemRarityRare, (*player).GetLocation()) + imbueItems := gs.filterItems(availableItems, ItemRarityImbue, (*player).GetLocation()) switch harvestType { case HarvestType1Item: @@ -522,7 +526,7 @@ func (gs *GroundSpawn) handleSkillProgression(context *HarvestContext, table *Gr // Check if player skill is already at max for this node maxSkillAllowed := int16(float32(context.MaxSkillRequired) * 1.0) // TODO: Use skill multiplier rule - if context.PlayerSkill.GetCurrentValue() >= maxSkillAllowed { + if (*context.PlayerSkill).GetCurrentValue() >= maxSkillAllowed { return false } @@ -579,7 +583,7 @@ func (gs *GroundSpawn) handleHarvestUse(client Client) error { if client.GetLogger() != nil { client.GetLogger().LogDebug("Player %s attempting to harvest %s using spell %s", - client.GetPlayer().GetName(), gs.GetName(), spellName) + (*client.GetPlayer()).GetName(), gs.GetName(), spellName) } return nil @@ -595,7 +599,7 @@ func (gs *GroundSpawn) handleCommandUse(client Client, command string) error { if client.GetLogger() != nil { client.GetLogger().LogDebug("Player %s using command %s on %s", - client.GetPlayer().GetName(), command, gs.GetName()) + (*client.GetPlayer()).GetName(), command, gs.GetName()) } return nil @@ -603,8 +607,9 @@ func (gs *GroundSpawn) handleCommandUse(client Client, command string) error { // Serialize creates a packet representation of the ground spawn func (gs *GroundSpawn) Serialize(player *Player, version int16) ([]byte, error) { - // Use base spawn serialization - return gs.Spawn.Serialize(player, version) + // TODO: Implement proper ground spawn serialization when spawn.Serialize is available + // For now, return empty packet as placeholder + return make([]byte, 0), nil } // Respawn resets the ground spawn to harvestable state @@ -617,9 +622,24 @@ func (gs *GroundSpawn) Respawn() { // Randomize heading if configured if gs.randomizeHeading { - gs.SetHeading(rand.Float32() * 360.0) + heading := int16(rand.Float32() * 360.0) + gs.SetHeading(heading, heading) } // Mark as alive gs.SetAlive(true) } + +// MeetsSpawnAccessRequirements checks if a player can access this ground spawn +func (gs *GroundSpawn) MeetsSpawnAccessRequirements(player *Player) bool { + // TODO: Implement proper access requirements checking + // For now, allow all players to access ground spawns + return player != nil +} + +// HasCommandIcon returns true if this ground spawn has command interactions +func (gs *GroundSpawn) HasCommandIcon() bool { + // TODO: Implement command icon checking based on spawn configuration + // For now, ground spawns don't have command icons (only harvest) + return false +} diff --git a/internal/ground_spawn/interfaces.go b/internal/ground_spawn/interfaces.go index ddd9e1f..d37b851 100644 --- a/internal/ground_spawn/interfaces.go +++ b/internal/ground_spawn/interfaces.go @@ -11,10 +11,10 @@ type Database interface { // Logger interface for ground spawn logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // Player interface for ground spawn interactions @@ -35,7 +35,7 @@ type Client interface { GetVersion() int16 GetLogger() Logger GetCurrentZoneID() int32 - Message(channel int32, message string, args ...interface{}) + Message(channel int32, message string, args ...any) SimpleMessage(channel int32, message string) SendPopupMessage(type_ int32, message string, sound string, duration float32, r, g, b int32) AddItem(item *Item, itemDeleted *bool) error @@ -126,8 +126,8 @@ type SkillProvider interface { // SpawnProvider interface for spawn system integration type SpawnProvider interface { - CreateSpawn() interface{} - GetSpawn(id int32) interface{} + CreateSpawn() any + GetSpawn(id int32) any RegisterGroundSpawn(gs *GroundSpawn) error UnregisterGroundSpawn(id int32) error } @@ -168,7 +168,7 @@ func (pgsa *PlayerGroundSpawnAdapter) CanHarvest(gs *GroundSpawn) bool { } // Check if player has required skill - skill := pgsa.player.GetSkillByName(gs.GetCollectionSkill()) + skill := (*pgsa.player).GetSkillByName(gs.GetCollectionSkill()) if skill == nil { return false } @@ -184,7 +184,7 @@ func (pgsa *PlayerGroundSpawnAdapter) GetHarvestSkill(skillName string) *Skill { return nil } - return pgsa.player.GetSkillByName(skillName) + return (*pgsa.player).GetSkillByName(skillName) } // GetHarvestModifiers returns harvest modifiers for the player @@ -207,7 +207,7 @@ func (pgsa *PlayerGroundSpawnAdapter) OnHarvestResult(result *HarvestResult) { if result.Success && len(result.ItemsAwarded) > 0 { if pgsa.logger != nil { pgsa.logger.LogDebug("Player %s successfully harvested %d items", - pgsa.player.GetName(), len(result.ItemsAwarded)) + (*pgsa.player).GetName(), len(result.ItemsAwarded)) } } } @@ -227,7 +227,7 @@ func NewHarvestEventAdapter(handler HarvestHandler, logger Logger) *HarvestEvent } // ProcessHarvestEvent processes a harvest event -func (hea *HarvestEventAdapter) ProcessHarvestEvent(eventType string, gs *GroundSpawn, player *Player, data interface{}) { +func (hea *HarvestEventAdapter) ProcessHarvestEvent(eventType string, gs *GroundSpawn, player *Player, data any) { if hea.handler == nil { return } diff --git a/internal/ground_spawn/manager.go b/internal/ground_spawn/manager.go index 0ac304e..58e6662 100644 --- a/internal/ground_spawn/manager.go +++ b/internal/ground_spawn/manager.go @@ -13,8 +13,11 @@ func NewManager(database Database, logger Logger) *Manager { entriesByID: make(map[int32][]*GroundSpawnEntry), itemsByID: make(map[int32][]*GroundSpawnEntryItem), respawnQueue: make(map[int32]time.Time), + activeSpawns: make(map[int32]*GroundSpawn), + depletedSpawns: make(map[int32]*GroundSpawn), database: database, logger: logger, + nextSpawnID: 1, harvestsBySkill: make(map[string]int64), } } @@ -41,8 +44,22 @@ func (m *Manager) Initialize() error { m.mutex.Lock() defer m.mutex.Unlock() + var maxID int32 for _, gs := range groundSpawns { - m.groundSpawns[gs.GetID()] = gs + spawnID := gs.GetID() + m.groundSpawns[spawnID] = gs + + // Track max ID for nextSpawnID initialization + if spawnID > maxID { + maxID = spawnID + } + + // Populate active/depleted caches based on current state + if gs.IsAvailable() { + m.activeSpawns[spawnID] = gs + } else if gs.IsDepleted() { + m.depletedSpawns[spawnID] = gs + } // Group by zone (placeholder - zone ID would come from spawn location) zoneID := int32(1) // TODO: Get actual zone ID from spawn @@ -50,10 +67,13 @@ func (m *Manager) Initialize() error { // Load harvest entries and items if err := m.loadGroundSpawnData(gs); err != nil && m.logger != nil { - m.logger.LogWarning("Failed to load data for ground spawn %d: %v", gs.GetID(), err) + m.logger.LogWarning("Failed to load data for ground spawn %d: %v", spawnID, err) } } + // Set nextSpawnID to avoid collisions + m.nextSpawnID = maxID + 1 + if m.logger != nil { m.logger.LogInfo("Loaded %d ground spawns from database", len(groundSpawns)) } @@ -89,15 +109,24 @@ func (m *Manager) CreateGroundSpawn(config GroundSpawnConfig) *GroundSpawn { m.mutex.Lock() defer m.mutex.Unlock() - // Generate ID (placeholder implementation) - newID := int32(len(m.groundSpawns) + 1) + // Use efficient ID counter instead of len() + newID := m.nextSpawnID + m.nextSpawnID++ gs.SetID(newID) // Store ground spawn m.groundSpawns[newID] = gs - // Group by zone + // Add to active cache (new spawns are typically active) + if gs.IsAvailable() { + m.activeSpawns[newID] = gs + } + + // Group by zone - pre-allocate zone slice if needed zoneID := int32(1) // TODO: Get actual zone ID from config.Location + if m.spawnsByZone[zoneID] == nil { + m.spawnsByZone[zoneID] = make([]*GroundSpawn, 0, 16) // Pre-allocate with reasonable capacity + } m.spawnsByZone[zoneID] = append(m.spawnsByZone[zoneID], gs) if m.logger != nil { @@ -121,13 +150,13 @@ func (m *Manager) GetGroundSpawnsByZone(zoneID int32) []*GroundSpawn { defer m.mutex.RUnlock() spawns := m.spawnsByZone[zoneID] - if spawns == nil { + if len(spawns) == 0 { return []*GroundSpawn{} } - // Return a copy to prevent external modification - result := make([]*GroundSpawn, len(spawns)) - copy(result, spawns) + // Return a copy to prevent external modification - use append for better performance + result := make([]*GroundSpawn, 0, len(spawns)) + result = append(result, spawns...) return result } @@ -179,8 +208,11 @@ func (m *Manager) ProcessHarvest(gs *GroundSpawn, player *Player) (*HarvestResul m.mutex.Unlock() } - // Handle respawn if depleted + // Handle respawn if depleted and update cache if gs.IsDepleted() { + m.mutex.Lock() + m.updateSpawnStateCache(gs) + m.mutex.Unlock() m.scheduleRespawn(gs) } @@ -196,11 +228,11 @@ func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*Harvest items := m.itemsByID[groundspawnID] m.mutex.RUnlock() - if entries == nil || len(entries) == 0 { + if len(entries) == 0 { return nil, fmt.Errorf("no harvest entries found for groundspawn %d", groundspawnID) } - if items == nil || len(items) == 0 { + if len(items) == 0 { return nil, fmt.Errorf("no harvest items found for groundspawn %d", groundspawnID) } @@ -210,13 +242,13 @@ func (m *Manager) buildHarvestContext(gs *GroundSpawn, player *Player) (*Harvest skillName = SkillGathering // Collections use gathering skill } - playerSkill := player.GetSkillByName(skillName) + playerSkill := (*player).GetSkillByName(skillName) if playerSkill == nil { return nil, fmt.Errorf("player lacks required skill: %s", skillName) } // Calculate total skill (base + bonuses) - totalSkill := playerSkill.GetCurrentValue() + totalSkill := (*playerSkill).GetCurrentValue() // TODO: Add stat bonuses when stat system is integrated // Find max skill required @@ -276,6 +308,12 @@ func (m *Manager) ProcessRespawns() { for _, spawnID := range toRespawn { if gs := m.GetGroundSpawn(spawnID); gs != nil { gs.Respawn() + + // Update cache after respawn + m.mutex.Lock() + m.updateSpawnStateCache(gs) + m.mutex.Unlock() + if m.logger != nil { m.logger.LogDebug("Ground spawn %d respawned", spawnID) } @@ -365,6 +403,8 @@ func (m *Manager) RemoveGroundSpawn(id int32) bool { delete(m.groundSpawns, id) delete(m.respawnQueue, id) + delete(m.activeSpawns, id) + delete(m.depletedSpawns, id) // Remove from zone list // TODO: Get actual zone ID from ground spawn @@ -401,11 +441,10 @@ func (m *Manager) GetActiveGroundSpawns() []*GroundSpawn { m.mutex.RLock() defer m.mutex.RUnlock() - var active []*GroundSpawn - for _, gs := range m.groundSpawns { - if gs.IsAvailable() { - active = append(active, gs) - } + // Use cached active spawns for O(1) performance instead of O(N) iteration + active := make([]*GroundSpawn, 0, len(m.activeSpawns)) + for _, gs := range m.activeSpawns { + active = append(active, gs) } return active @@ -416,16 +455,32 @@ func (m *Manager) GetDepletedGroundSpawns() []*GroundSpawn { m.mutex.RLock() defer m.mutex.RUnlock() - var depleted []*GroundSpawn - for _, gs := range m.groundSpawns { - if gs.IsDepleted() { - depleted = append(depleted, gs) - } + // Use cached depleted spawns for O(1) performance instead of O(N) iteration + depleted := make([]*GroundSpawn, 0, len(m.depletedSpawns)) + for _, gs := range m.depletedSpawns { + depleted = append(depleted, gs) } return depleted } +// updateSpawnStateCache updates the active/depleted caches when a spawn's state changes +// IMPORTANT: This method must be called while holding the manager's mutex +func (m *Manager) updateSpawnStateCache(gs *GroundSpawn) { + spawnID := gs.GetID() + + // Remove from both caches first + delete(m.activeSpawns, spawnID) + delete(m.depletedSpawns, spawnID) + + // Add to appropriate cache based on current state + if gs.IsAvailable() { + m.activeSpawns[spawnID] = gs + } else if gs.IsDepleted() { + m.depletedSpawns[spawnID] = gs + } +} + // ProcessCommand handles ground spawn management commands func (m *Manager) ProcessCommand(command string, args []string) (string, error) { switch command { @@ -536,7 +591,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) { return fmt.Sprintf("Ground spawn %d not found.", spawnID), nil } - result := fmt.Sprintf("Ground Spawn Information:\n") + result := "Ground Spawn Information:\n" result += fmt.Sprintf("ID: %d\n", gs.GetID()) result += fmt.Sprintf("Name: %s\n", gs.GetName()) result += fmt.Sprintf("Collection Skill: %s\n", gs.GetCollectionSkill()) @@ -550,7 +605,7 @@ func (m *Manager) handleInfoCommand(args []string) (string, error) { } // handleReloadCommand reloads ground spawns from database -func (m *Manager) handleReloadCommand(args []string) (string, error) { +func (m *Manager) handleReloadCommand(_ []string) (string, error) { if m.database == nil { return "", fmt.Errorf("no database available") } @@ -562,6 +617,9 @@ func (m *Manager) handleReloadCommand(args []string) (string, error) { m.entriesByID = make(map[int32][]*GroundSpawnEntry) m.itemsByID = make(map[int32][]*GroundSpawnEntryItem) m.respawnQueue = make(map[int32]time.Time) + m.activeSpawns = make(map[int32]*GroundSpawn) + m.depletedSpawns = make(map[int32]*GroundSpawn) + m.nextSpawnID = 1 // Reset ID counter m.mutex.Unlock() // Reload from database @@ -588,4 +646,7 @@ func (m *Manager) Shutdown() { m.entriesByID = make(map[int32][]*GroundSpawnEntry) m.itemsByID = make(map[int32][]*GroundSpawnEntryItem) m.respawnQueue = make(map[int32]time.Time) + m.activeSpawns = make(map[int32]*GroundSpawn) + m.depletedSpawns = make(map[int32]*GroundSpawn) + m.nextSpawnID = 1 } diff --git a/internal/ground_spawn/test_utils.go b/internal/ground_spawn/test_utils.go new file mode 100644 index 0000000..ab79bc8 --- /dev/null +++ b/internal/ground_spawn/test_utils.go @@ -0,0 +1,8 @@ +package ground_spawn + +type mockLogger struct{} + +func (l *mockLogger) LogInfo(message string, args ...any) {} +func (l *mockLogger) LogError(message string, args ...any) {} +func (l *mockLogger) LogDebug(message string, args ...any) {} +func (l *mockLogger) LogWarning(message string, args ...any) {} diff --git a/internal/ground_spawn/types.go b/internal/ground_spawn/types.go index 1f603c7..24cfb73 100644 --- a/internal/ground_spawn/types.go +++ b/internal/ground_spawn/types.go @@ -111,10 +111,15 @@ type Manager struct { itemsByID map[int32][]*GroundSpawnEntryItem // Harvest items by groundspawn ID respawnQueue map[int32]time.Time // Respawn timestamps + // Performance optimization: cache active/depleted spawns to avoid O(N) scans + activeSpawns map[int32]*GroundSpawn // Cache of active spawns for O(1) lookups + depletedSpawns map[int32]*GroundSpawn // Cache of depleted spawns for O(1) lookups + database Database // Database interface logger Logger // Logging interface - mutex sync.RWMutex // Thread safety + mutex sync.RWMutex // Thread safety + nextSpawnID int32 // Efficient ID counter to avoid len() calls // Statistics totalHarvests int64 // Total harvest attempts diff --git a/internal/groups/README.md b/internal/groups/README.md index 5323a41..837f2f6 100644 --- a/internal/groups/README.md +++ b/internal/groups/README.md @@ -275,7 +275,7 @@ type MyGroupPacketHandler struct { // client connection management } -func (ph *MyGroupPacketHandler) SendGroupUpdate(members []*groups.GroupMemberInfo, excludeClient interface{}) error { +func (ph *MyGroupPacketHandler) SendGroupUpdate(members []*groups.GroupMemberInfo, excludeClient any) error { // Send group update packets to clients return nil } diff --git a/internal/groups/group.go b/internal/groups/group.go index fc50fb1..8cb8229 100644 --- a/internal/groups/group.go +++ b/internal/groups/group.go @@ -316,12 +316,12 @@ func (g *Group) Disband() { } // SendGroupUpdate sends an update to all group members -func (g *Group) SendGroupUpdate(excludeClient interface{}, forceRaidUpdate bool) { +func (g *Group) SendGroupUpdate(excludeClient any, forceRaidUpdate bool) { g.sendGroupUpdate(excludeClient, forceRaidUpdate) } // sendGroupUpdate internal method to send group updates -func (g *Group) sendGroupUpdate(excludeClient interface{}, forceRaidUpdate bool) { +func (g *Group) sendGroupUpdate(excludeClient any, forceRaidUpdate bool) { update := NewGroupUpdate(GROUP_UPDATE_FLAG_MEMBER_LIST, g.id) update.ExcludeClient = excludeClient update.ForceRaidUpdate = forceRaidUpdate @@ -430,7 +430,7 @@ func (g *Group) GetLeaderName() string { } // ShareQuestWithGroup shares a quest with all group members -func (g *Group) ShareQuestWithGroup(questSharer interface{}, quest interface{}) bool { +func (g *Group) ShareQuestWithGroup(questSharer any, quest any) bool { // TODO: Implement quest sharing // This would require integration with the quest system return false diff --git a/internal/groups/interfaces.go b/internal/groups/interfaces.go index bdbf9cc..5d0f62c 100644 --- a/internal/groups/interfaces.go +++ b/internal/groups/interfaces.go @@ -38,7 +38,7 @@ type GroupManagerInterface interface { RemoveGroupMemberByName(groupID int32, name string, isClient bool, charID int32) error // Group updates - SendGroupUpdate(groupID int32, excludeClient interface{}, forceRaidUpdate bool) + SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) // Invitations Invite(leader entity.Entity, member entity.Entity) int8 @@ -141,9 +141,9 @@ type GroupDatabase interface { // GroupPacketHandler interface for handling group-related packets type GroupPacketHandler interface { // Group update packets - SendGroupUpdate(members []*GroupMemberInfo, excludeClient interface{}) error - SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient interface{}) error - SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient interface{}) error + SendGroupUpdate(members []*GroupMemberInfo, excludeClient any) error + SendGroupMemberUpdate(member *GroupMemberInfo, excludeClient any) error + SendGroupOptionsUpdate(groupID int32, options *GroupOptions, excludeClient any) error // Group invitation packets SendGroupInvite(inviter, invitee entity.Entity) error @@ -154,16 +154,16 @@ type GroupPacketHandler interface { SendGroupChatMessage(members []*GroupMemberInfo, from string, message string, channel int16, language int32) error // Raid packets - SendRaidUpdate(raidGroups []*Group, excludeClient interface{}) error + SendRaidUpdate(raidGroups []*Group, excludeClient any) error SendRaidInvite(leaderGroup, targetGroup *Group) error SendRaidInviteResponse(leaderGroup, targetGroup *Group, accepted bool) error // Group UI packets - SendGroupWindowUpdate(client interface{}, group *Group) error - SendRaidWindowUpdate(client interface{}, raidGroups []*Group) error + SendGroupWindowUpdate(client any, group *Group) error + SendRaidWindowUpdate(client any, raidGroups []*Group) error // Group member packets - SendGroupMemberStats(member *GroupMemberInfo, excludeClient interface{}) error + SendGroupMemberStats(member *GroupMemberInfo, excludeClient any) error SendGroupMemberZoneChange(member *GroupMemberInfo, oldZoneID, newZoneID int32) error } @@ -244,8 +244,8 @@ type GroupStatistics interface { RecordGroupMemoryUsage(groups int32, members int32) // Statistics retrieval - GetGroupStatistics(groupID int32) map[string]interface{} - GetOverallStatistics() map[string]interface{} + GetGroupStatistics(groupID int32) map[string]any + GetOverallStatistics() map[string]any GetStatisticsSummary() *GroupManagerStats } diff --git a/internal/groups/manager.go b/internal/groups/manager.go index eaa790e..9121dbd 100644 --- a/internal/groups/manager.go +++ b/internal/groups/manager.go @@ -204,7 +204,7 @@ func (gm *GroupManager) RemoveGroupMemberByName(groupID int32, name string, isCl } // SendGroupUpdate sends an update to all members of a group -func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient interface{}, forceRaidUpdate bool) { +func (gm *GroupManager) SendGroupUpdate(groupID int32, excludeClient any, forceRaidUpdate bool) { group := gm.GetGroup(groupID) if group != nil { group.SendGroupUpdate(excludeClient, forceRaidUpdate) diff --git a/internal/groups/types.go b/internal/groups/types.go index df5565e..ad8ddf0 100644 --- a/internal/groups/types.go +++ b/internal/groups/types.go @@ -59,7 +59,7 @@ type GroupMemberInfo struct { Member entity.Entity `json:"-"` // Client reference (players only) - interface to avoid circular deps - Client interface{} `json:"-"` + Client any `json:"-"` // Timestamps JoinTime time.Time `json:"join_time"` @@ -102,13 +102,13 @@ type Group struct { // GroupMessage represents a message sent to the group type GroupMessage struct { - Type int8 `json:"type"` - Channel int16 `json:"channel"` - Message string `json:"message"` - FromName string `json:"from_name"` - Language int32 `json:"language"` - Timestamp time.Time `json:"timestamp"` - ExcludeClient interface{} `json:"-"` + Type int8 `json:"type"` + Channel int16 `json:"channel"` + Message string `json:"message"` + FromName string `json:"from_name"` + Language int32 `json:"language"` + Timestamp time.Time `json:"timestamp"` + ExcludeClient any `json:"-"` } // GroupUpdate represents a group update event @@ -119,7 +119,7 @@ type GroupUpdate struct { Options *GroupOptions `json:"options,omitempty"` RaidGroups []int32 `json:"raid_groups,omitempty"` ForceRaidUpdate bool `json:"force_raid_update"` - ExcludeClient interface{} `json:"-"` + ExcludeClient any `json:"-"` Timestamp time.Time `json:"timestamp"` } diff --git a/internal/guilds/interfaces.go b/internal/guilds/interfaces.go index 32eeccd..b446e5a 100644 --- a/internal/guilds/interfaces.go +++ b/internal/guilds/interfaces.go @@ -182,16 +182,16 @@ type GuildEventHandler interface { // LogHandler provides logging functionality type LogHandler interface { // LogDebug logs debug messages - LogDebug(category, message string, args ...interface{}) + LogDebug(category, message string, args ...any) // LogInfo logs informational messages - LogInfo(category, message string, args ...interface{}) + LogInfo(category, message string, args ...any) // LogError logs error messages - LogError(category, message string, args ...interface{}) + LogError(category, message string, args ...any) // LogWarning logs warning messages - LogWarning(category, message string, args ...interface{}) + LogWarning(category, message string, args ...any) } // PlayerInfo contains basic player information @@ -308,10 +308,10 @@ type NotificationManager interface { NotifyMemberLogout(guild *Guild, member *GuildMember) // NotifyGuildMessage sends a message to all guild members - NotifyGuildMessage(guild *Guild, eventType int8, message string, args ...interface{}) + NotifyGuildMessage(guild *Guild, eventType int8, message string, args ...any) // NotifyOfficers sends a message to officers only - NotifyOfficers(guild *Guild, message string, args ...interface{}) + NotifyOfficers(guild *Guild, message string, args ...any) // NotifyGuildUpdate notifies guild members of guild changes NotifyGuildUpdate(guild *Guild) diff --git a/internal/heroic_ops/interfaces.go b/internal/heroic_ops/interfaces.go index dd1cf33..b7699f4 100644 --- a/internal/heroic_ops/interfaces.go +++ b/internal/heroic_ops/interfaces.go @@ -117,10 +117,10 @@ type PlayerManager interface { // LogHandler defines the interface for logging operations type LogHandler interface { - LogDebug(system, format string, args ...interface{}) - LogInfo(system, format string, args ...interface{}) - LogWarning(system, format string, args ...interface{}) - LogError(system, format string, args ...interface{}) + LogDebug(system, format string, args ...any) + LogInfo(system, format string, args ...any) + LogWarning(system, format string, args ...any) + LogError(system, format string, args ...any) } // TimerManager defines the interface for timer management @@ -139,8 +139,8 @@ type TimerManager interface { // CacheManager defines the interface for caching operations type CacheManager interface { // Cache operations - Set(key string, value interface{}, expiration time.Duration) error - Get(key string) (interface{}, bool) + Set(key string, value any, expiration time.Duration) error + Get(key string) (any, bool) Delete(key string) error Clear() error @@ -213,6 +213,6 @@ type StatisticsCollector interface { type ConfigManager interface { GetHOConfig() *HeroicOPConfig UpdateHOConfig(config *HeroicOPConfig) error - GetConfigValue(key string) interface{} - SetConfigValue(key string, value interface{}) error + GetConfigValue(key string) any + SetConfigValue(key string, value any) error } diff --git a/internal/heroic_ops/master_list.go b/internal/heroic_ops/master_list.go index 3d71fb6..34aabc9 100644 --- a/internal/heroic_ops/master_list.go +++ b/internal/heroic_ops/master_list.go @@ -426,11 +426,11 @@ func (mhol *MasterHeroicOPList) SearchWheels(criteria HeroicOPSearchCriteria) [] } // GetStatistics returns usage statistics for the HO system -func (mhol *MasterHeroicOPList) GetStatistics() map[string]interface{} { +func (mhol *MasterHeroicOPList) GetStatistics() map[string]any { mhol.mu.RLock() defer mhol.mu.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) // Basic counts stats["total_starters"] = mhol.getStarterCountNoLock() diff --git a/internal/housing/interfaces.go b/internal/housing/interfaces.go index bb4b245..e01f185 100644 --- a/internal/housing/interfaces.go +++ b/internal/housing/interfaces.go @@ -159,10 +159,10 @@ type ZoneManager interface { // LogHandler defines the interface for logging operations type LogHandler interface { - LogDebug(system, format string, args ...interface{}) - LogInfo(system, format string, args ...interface{}) - LogWarning(system, format string, args ...interface{}) - LogError(system, format string, args ...interface{}) + LogDebug(system, format string, args ...any) + LogInfo(system, format string, args ...any) + LogWarning(system, format string, args ...any) + LogError(system, format string, args ...any) } // Additional integration interfaces @@ -282,8 +282,8 @@ type AccessManager interface { type ConfigManager interface { GetHousingConfig() *HousingConfig UpdateHousingConfig(config *HousingConfig) error - GetConfigValue(key string) interface{} - SetConfigValue(key string, value interface{}) error + GetConfigValue(key string) any + SetConfigValue(key string, value any) error } // NotificationManager defines interface for housing notifications @@ -298,8 +298,8 @@ type NotificationManager interface { // CacheManager defines interface for caching operations type CacheManager interface { // Cache operations - Set(key string, value interface{}, expiration time.Duration) error - Get(key string) (interface{}, bool) + Set(key string, value any, expiration time.Duration) error + Get(key string) (any, bool) Delete(key string) error Clear() error diff --git a/internal/items/interfaces.go b/internal/items/interfaces.go index dfc4aca..230c9e6 100644 --- a/internal/items/interfaces.go +++ b/internal/items/interfaces.go @@ -573,7 +573,7 @@ func (isa *ItemSystemAdapter) CraftItem(playerID uint32, itemID int32, quality i } // GetPlayerItemStats returns statistics about a player's items -func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]interface{}, error) { +func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]any, error) { inventory, err := isa.GetPlayerInventory(playerID) if err != nil { return nil, err @@ -587,7 +587,7 @@ func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]in // Calculate equipment bonuses bonuses := equipment.CalculateEquipmentBonuses() - return map[string]interface{}{ + return map[string]any{ "player_id": playerID, "total_items": inventory.GetNumberOfItems(), "equipped_items": equipment.GetNumberOfItems(), @@ -601,13 +601,13 @@ func (isa *ItemSystemAdapter) GetPlayerItemStats(playerID uint32) (map[string]in } // GetSystemStats returns comprehensive statistics about the item system -func (isa *ItemSystemAdapter) GetSystemStats() map[string]interface{} { +func (isa *ItemSystemAdapter) GetSystemStats() map[string]any { isa.mutex.RLock() defer isa.mutex.RUnlock() masterStats := isa.masterList.GetStats() - return map[string]interface{}{ + return map[string]any{ "total_item_templates": masterStats.TotalItems, "items_by_type": masterStats.ItemsByType, "items_by_tier": masterStats.ItemsByTier, diff --git a/internal/items/loot/chest.go b/internal/items/loot/chest.go index 83cb6d0..f8a1652 100644 --- a/internal/items/loot/chest.go +++ b/internal/items/loot/chest.go @@ -43,13 +43,13 @@ func (ci ChestInteraction) String() string { // ChestInteractionResult represents the result of a chest interaction type ChestInteractionResult struct { Success bool `json:"success"` - Result int8 `json:"result"` // ChestResult constant - Message string `json:"message"` // Message to display to player - Items []*items.Item `json:"items"` // Items received - Coins int32 `json:"coins"` // Coins received - Experience int32 `json:"experience"` // Experience gained (for disarming/lockpicking) - ChestEmpty bool `json:"chest_empty"` // Whether chest is now empty - ChestClosed bool `json:"chest_closed"` // Whether chest should be closed + Result int8 `json:"result"` // ChestResult constant + Message string `json:"message"` // Message to display to player + Items []*items.Item `json:"items"` // Items received + Coins int32 `json:"coins"` // Coins received + Experience int32 `json:"experience"` // Experience gained (for disarming/lockpicking) + ChestEmpty bool `json:"chest_empty"` // Whether chest is now empty + ChestClosed bool `json:"chest_closed"` // Whether chest should be closed } // ChestService handles treasure chest interactions and management @@ -73,7 +73,7 @@ type PlayerService interface { // ZoneService interface for zone-related operations type ZoneService interface { - GetZoneRule(zoneID int32, ruleName string) (interface{}, error) + GetZoneRule(zoneID int32, ruleName string) (any, error) SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error) RemoveObjectFromZone(zoneID int32, objectID int32) error GetDistanceBetweenPoints(x1, y1, z1, x2, y2, z2 float32) float32 @@ -123,7 +123,7 @@ func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32, // Don't create chest if no qualifying items and no coins if filteredResult.IsEmpty() { - log.Printf("%s No qualifying loot for treasure chest (tier >= %d) for spawn %d", + log.Printf("%s No qualifying loot for treasure chest (tier >= %d) for spawn %d", LogPrefixChest, LootTierCommon, spawnID) return nil, nil } @@ -136,7 +136,7 @@ func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32, // Spawn the chest object in the zone chestCommands := []string{"loot", "disarm"} // TODO: Add "lockpick" if chest is locked - objectID, err := cs.zoneService.SpawnObjectInZone(zoneID, chest.AppearanceID, x, y, z, heading, + objectID, err := cs.zoneService.SpawnObjectInZone(zoneID, chest.AppearanceID, x, y, z, heading, "Treasure Chest", chestCommands) if err != nil { log.Printf("%s Failed to spawn chest object in zone: %v", LogPrefixChest, err) @@ -149,7 +149,7 @@ func (cs *ChestService) CreateTreasureChestFromLoot(spawnID int32, zoneID int32, } // HandleChestInteraction processes a player's interaction with a treasure chest -func (cs *ChestService) HandleChestInteraction(chestID int32, playerID uint32, +func (cs *ChestService) HandleChestInteraction(chestID int32, playerID uint32, interaction ChestInteraction, itemUniqueID int64) *ChestInteractionResult { result := &ChestInteractionResult{ @@ -273,10 +273,10 @@ func (cs *ChestService) handleViewChest(chest *TreasureChest, playerID uint32) * return &ChestInteractionResult{ Success: true, Result: ChestResultSuccess, - Message: fmt.Sprintf("The chest contains %d items and %d coins", + Message: fmt.Sprintf("The chest contains %d items and %d coins", len(chest.LootResult.GetItems()), chest.LootResult.GetCoins()), - Items: chest.LootResult.GetItems(), - Coins: chest.LootResult.GetCoins(), + Items: chest.LootResult.GetItems(), + Coins: chest.LootResult.GetCoins(), } } @@ -400,7 +400,7 @@ func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32) // Get player's disarm skill disarmSkill := cs.playerService.GetPlayerSkillValue(playerID, "Disarm Trap") - + // Calculate success chance (simplified) successChance := float32(disarmSkill) - float32(chest.DisarmDifficulty) if successChance < 0 { @@ -410,7 +410,7 @@ func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32) } // Roll for success - roll := float32(time.Now().UnixNano()%100) // Simple random + roll := float32(time.Now().UnixNano() % 100) // Simple random if roll > successChance { // Failed disarm - could trigger trap effects here return &ChestInteractionResult{ @@ -422,7 +422,7 @@ func (cs *ChestService) handleDisarmChest(chest *TreasureChest, playerID uint32) // Success - disarm the trap chest.IsDisarmable = false - + // Give experience experience := int32(chest.DisarmDifficulty * 10) // 10 exp per difficulty point cs.playerService.AddPlayerExperience(playerID, experience, "Disarm Trap") @@ -450,7 +450,7 @@ func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint3 // Get player's lockpicking skill lockpickSkill := cs.playerService.GetPlayerSkillValue(playerID, "Pick Lock") - + // Calculate success chance (simplified) successChance := float32(lockpickSkill) - float32(chest.LockpickDifficulty) if successChance < 0 { @@ -460,7 +460,7 @@ func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint3 } // Roll for success - roll := float32(time.Now().UnixNano()%100) // Simple random + roll := float32(time.Now().UnixNano() % 100) // Simple random if roll > successChance { return &ChestInteractionResult{ Success: false, @@ -471,7 +471,7 @@ func (cs *ChestService) handleLockpickChest(chest *TreasureChest, playerID uint3 // Success - unlock the chest chest.IsLocked = false - + // Give experience experience := int32(chest.LockpickDifficulty * 10) // 10 exp per difficulty point cs.playerService.AddPlayerExperience(playerID, experience, "Pick Lock") @@ -500,12 +500,12 @@ func (cs *ChestService) handleCloseChest(chest *TreasureChest, playerID uint32) // CleanupEmptyChests removes empty chests from zones func (cs *ChestService) CleanupEmptyChests(zoneID int32) { chests := cs.lootManager.GetZoneChests(zoneID) - + for _, chest := range chests { if chest.LootResult.IsEmpty() { // Remove from zone cs.zoneService.RemoveObjectFromZone(zoneID, chest.ID) - + // Remove from loot manager cs.lootManager.RemoveTreasureChest(chest.ID) } @@ -515,4 +515,4 @@ func (cs *ChestService) CleanupEmptyChests(zoneID int32) { // GetPlayerChestList returns a list of chests a player can access func (cs *ChestService) GetPlayerChestList(playerID uint32) []*TreasureChest { return cs.lootManager.GetPlayerChests(playerID) -} \ No newline at end of file +} diff --git a/internal/items/loot/database.go b/internal/items/loot/database.go index 73180ec..7b8dddd 100644 --- a/internal/items/loot/database.go +++ b/internal/items/loot/database.go @@ -10,12 +10,12 @@ import ( // LootDatabase handles all database operations for the loot system type LootDatabase struct { - db *sql.DB - queries map[string]*sql.Stmt - lootTables map[int32]*LootTable - spawnLoot map[int32][]int32 // spawn_id -> []loot_table_id - globalLoot []*GlobalLoot - mutex sync.RWMutex + db *sql.DB + queries map[string]*sql.Stmt + lootTables map[int32]*LootTable + spawnLoot map[int32][]int32 // spawn_id -> []loot_table_id + globalLoot []*GlobalLoot + mutex sync.RWMutex } // NewLootDatabase creates a new loot database manager @@ -42,88 +42,88 @@ func (ldb *LootDatabase) prepareQueries() { FROM loottable ORDER BY id `, - + "load_loot_drops": ` SELECT loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id FROM lootdrop WHERE loot_table_id = ? ORDER BY probability DESC `, - + "load_spawn_loot": ` SELECT spawn_id, loottable_id FROM spawn_loot ORDER BY spawn_id `, - + "load_global_loot": ` SELECT type, loot_table, value1, value2, value3, value4 FROM loot_global ORDER BY type, value1 `, - + "insert_loot_table": ` INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability) VALUES (?, ?, ?, ?, ?, ?, ?) `, - + "update_loot_table": ` UPDATE loottable SET name = ?, mincoin = ?, maxcoin = ?, maxlootitems = ?, lootdrop_probability = ?, coin_probability = ? WHERE id = ? `, - + "delete_loot_table": ` DELETE FROM loottable WHERE id = ? `, - + "insert_loot_drop": ` INSERT INTO lootdrop (loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id) VALUES (?, ?, ?, ?, ?, ?) `, - + "delete_loot_drops": ` DELETE FROM lootdrop WHERE loot_table_id = ? `, - + "insert_spawn_loot": ` INSERT OR REPLACE INTO spawn_loot (spawn_id, loottable_id) VALUES (?, ?) `, - + "delete_spawn_loot": ` DELETE FROM spawn_loot WHERE spawn_id = ? `, - + "insert_global_loot": ` INSERT INTO loot_global (type, loot_table, value1, value2, value3, value4) VALUES (?, ?, ?, ?, ?, ?) `, - + "delete_global_loot": ` DELETE FROM loot_global WHERE type = ? `, - + "get_loot_table": ` SELECT id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability FROM loottable WHERE id = ? `, - + "get_spawn_loot_tables": ` SELECT loottable_id FROM spawn_loot WHERE spawn_id = ? `, - + "count_loot_tables": ` SELECT COUNT(*) FROM loottable `, - + "count_loot_drops": ` SELECT COUNT(*) FROM lootdrop `, - + "count_spawn_loot": ` SELECT COUNT(*) FROM spawn_loot `, @@ -141,36 +141,36 @@ func (ldb *LootDatabase) prepareQueries() { // LoadAllLootData loads all loot data from the database func (ldb *LootDatabase) LoadAllLootData() error { log.Printf("%s Loading loot data from database...", LogPrefixDatabase) - + // Load loot tables first if err := ldb.loadLootTables(); err != nil { return fmt.Errorf("failed to load loot tables: %v", err) } - + // Load loot drops for each table if err := ldb.loadLootDrops(); err != nil { return fmt.Errorf("failed to load loot drops: %v", err) } - + // Load spawn loot assignments if err := ldb.loadSpawnLoot(); err != nil { return fmt.Errorf("failed to load spawn loot: %v", err) } - + // Load global loot configuration if err := ldb.loadGlobalLoot(); err != nil { return fmt.Errorf("failed to load global loot: %v", err) } - + ldb.mutex.RLock() tableCount := len(ldb.lootTables) spawnCount := len(ldb.spawnLoot) globalCount := len(ldb.globalLoot) ldb.mutex.RUnlock() - - log.Printf("%s Loaded %d loot tables, %d spawn assignments, %d global loot entries", + + log.Printf("%s Loaded %d loot tables, %d spawn assignments, %d global loot entries", LogPrefixDatabase, tableCount, spawnCount, globalCount) - + return nil } @@ -189,7 +189,7 @@ func (ldb *LootDatabase) loadLootTables() error { ldb.mutex.Lock() defer ldb.mutex.Unlock() - + // Clear existing tables ldb.lootTables = make(map[int32]*LootTable) @@ -279,7 +279,7 @@ func (ldb *LootDatabase) loadSpawnLoot() error { ldb.mutex.Lock() defer ldb.mutex.Unlock() - + // Clear existing spawn loot ldb.spawnLoot = make(map[int32][]int32) @@ -313,7 +313,7 @@ func (ldb *LootDatabase) loadGlobalLoot() error { ldb.mutex.Lock() defer ldb.mutex.Unlock() - + // Clear existing global loot ldb.globalLoot = make([]*GlobalLoot, 0) @@ -361,7 +361,7 @@ func (ldb *LootDatabase) loadGlobalLoot() error { func (ldb *LootDatabase) GetLootTable(tableID int32) *LootTable { ldb.mutex.RLock() defer ldb.mutex.RUnlock() - + return ldb.lootTables[tableID] } @@ -369,12 +369,12 @@ func (ldb *LootDatabase) GetLootTable(tableID int32) *LootTable { func (ldb *LootDatabase) GetSpawnLootTables(spawnID int32) []int32 { ldb.mutex.RLock() defer ldb.mutex.RUnlock() - + tables := ldb.spawnLoot[spawnID] if tables == nil { return nil } - + // Return a copy to prevent external modification result := make([]int32, len(tables)) copy(result, tables) @@ -385,9 +385,9 @@ func (ldb *LootDatabase) GetSpawnLootTables(spawnID int32) []int32 { func (ldb *LootDatabase) GetGlobalLootTables(level int16, race int16, zoneID int32) []*GlobalLoot { ldb.mutex.RLock() defer ldb.mutex.RUnlock() - + var result []*GlobalLoot - + for _, global := range ldb.globalLoot { switch global.Type { case GlobalLootTypeLevel: @@ -404,7 +404,7 @@ func (ldb *LootDatabase) GetGlobalLootTables(level int16, race int16, zoneID int } } } - + return result } @@ -587,8 +587,8 @@ func (ldb *LootDatabase) DeleteSpawnLoot(spawnID int32) error { } // GetLootStatistics returns database statistics -func (ldb *LootDatabase) GetLootStatistics() (map[string]interface{}, error) { - stats := make(map[string]interface{}) +func (ldb *LootDatabase) GetLootStatistics() (map[string]any, error) { + stats := make(map[string]any) // Count loot tables if stmt := ldb.queries["count_loot_tables"]; stmt != nil { @@ -629,7 +629,7 @@ func (ldb *LootDatabase) GetLootStatistics() (map[string]interface{}, error) { // ReloadLootData reloads all loot data from the database func (ldb *LootDatabase) ReloadLootData() error { log.Printf("%s Reloading loot data from database...", LogPrefixDatabase) - + return ldb.LoadAllLootData() } @@ -641,4 +641,4 @@ func (ldb *LootDatabase) Close() error { } } return nil -} \ No newline at end of file +} diff --git a/internal/items/loot/integration.go b/internal/items/loot/integration.go index 84d9395..e115026 100644 --- a/internal/items/loot/integration.go +++ b/internal/items/loot/integration.go @@ -18,13 +18,13 @@ type LootSystem struct { // LootSystemConfig holds configuration for the loot system type LootSystemConfig struct { - DatabaseConnection *sql.DB - ItemMasterList items.MasterItemListService - PlayerService PlayerService - ZoneService ZoneService - ClientService ClientService - ItemPacketBuilder ItemPacketBuilder - StartCleanupTimer bool + DatabaseConnection *sql.DB + ItemMasterList items.MasterItemListService + PlayerService PlayerService + ZoneService ZoneService + ClientService ClientService + ItemPacketBuilder ItemPacketBuilder + StartCleanupTimer bool } // NewLootSystem creates a complete loot system with all components @@ -78,7 +78,7 @@ func NewLootSystem(config *LootSystemConfig) (*LootSystem, error) { } // GenerateAndCreateChest generates loot for a spawn and creates a treasure chest -func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32, +func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, z, heading float32, context *LootContext) (*TreasureChest, error) { if ls.ChestService == nil { @@ -98,7 +98,7 @@ func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, } // Create treasure chest - chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading, + chest, err := ls.ChestService.CreateTreasureChestFromLoot(spawnID, zoneID, x, y, z, heading, lootResult, context.GroupMembers) if err != nil { return nil, fmt.Errorf("failed to create treasure chest: %v", err) @@ -108,7 +108,7 @@ func (ls *LootSystem) GenerateAndCreateChest(spawnID int32, zoneID int32, x, y, } // HandlePlayerLootInteraction handles a player's interaction with a chest and sends appropriate packets -func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32, +func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32, interaction ChestInteraction, itemUniqueID int64) error { if ls.ChestService == nil { @@ -141,7 +141,7 @@ func (ls *LootSystem) HandlePlayerLootInteraction(chestID int32, playerID uint32 } // Log the interaction - log.Printf("%s Player %d %s chest %d: %s", + log.Printf("%s Player %d %s chest %d: %s", LogPrefixLoot, playerID, interaction.String(), chestID, result.Message) return nil @@ -168,8 +168,8 @@ func (ls *LootSystem) ShowChestToPlayer(chestID int32, playerID uint32) error { } // GetSystemStatistics returns comprehensive statistics about the loot system -func (ls *LootSystem) GetSystemStatistics() (map[string]interface{}, error) { - stats := make(map[string]interface{}) +func (ls *LootSystem) GetSystemStatistics() (map[string]any, error) { + stats := make(map[string]any) // Database statistics if dbStats, err := ls.Database.GetLootStatistics(); err == nil { @@ -220,7 +220,7 @@ func (ls *LootSystem) AddLootTableWithDrops(table *LootTable) error { } // CreateQuickLootTable creates a simple loot table with basic parameters -func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem, +func (ls *LootSystem) CreateQuickLootTable(tableID int32, name string, items []QuickLootItem, minCoin, maxCoin int32, maxItems int16) error { table := &LootTable{ @@ -289,9 +289,9 @@ func (ls *LootSystem) CreateGlobalLevelLoot(minLevel, maxLevel int8, tableID int ls.Database.globalLoot = append(ls.Database.globalLoot, global) ls.Database.mutex.Unlock() - log.Printf("%s Created global level loot for levels %d-%d using table %d", + log.Printf("%s Created global level loot for levels %d-%d using table %d", LogPrefixLoot, minLevel, maxLevel, tableID) - + return nil } @@ -303,10 +303,10 @@ func (ls *LootSystem) GetActiveChestsInZone(zoneID int32) []*TreasureChest { // CleanupZoneChests removes all chests from a specific zone func (ls *LootSystem) CleanupZoneChests(zoneID int32) { chests := ls.Manager.GetZoneChests(zoneID) - + for _, chest := range chests { ls.Manager.RemoveTreasureChest(chest.ID) - + // Remove from zone if chest service is available if ls.ChestService != nil { ls.ChestService.zoneService.RemoveObjectFromZone(zoneID, chest.ID) @@ -356,17 +356,17 @@ type ValidationError struct { func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*LootPreview, error) { tableIDs := ls.Database.GetSpawnLootTables(spawnID) globalLoot := ls.Database.GetGlobalLootTables(context.PlayerLevel, context.PlayerRace, context.ZoneID) - + for _, global := range globalLoot { tableIDs = append(tableIDs, global.TableID) } preview := &LootPreview{ - SpawnID: spawnID, - TableIDs: tableIDs, + SpawnID: spawnID, + TableIDs: tableIDs, PossibleItems: make([]*LootPreviewItem, 0), - MinCoins: 0, - MaxCoins: 0, + MinCoins: 0, + MaxCoins: 0, } for _, tableID := range tableIDs { @@ -400,11 +400,11 @@ func (ls *LootSystem) GetLootPreview(spawnID int32, context *LootContext) (*Loot // LootPreview represents a preview of potential loot type LootPreview struct { - SpawnID int32 `json:"spawn_id"` - TableIDs []int32 `json:"table_ids"` - PossibleItems []*LootPreviewItem `json:"possible_items"` - MinCoins int32 `json:"min_coins"` - MaxCoins int32 `json:"max_coins"` + SpawnID int32 `json:"spawn_id"` + TableIDs []int32 `json:"table_ids"` + PossibleItems []*LootPreviewItem `json:"possible_items"` + MinCoins int32 `json:"min_coins"` + MaxCoins int32 `json:"max_coins"` } // LootPreviewItem represents a potential loot item in a preview @@ -413,4 +413,4 @@ type LootPreviewItem struct { ItemName string `json:"item_name"` Probability float32 `json:"probability"` Tier int8 `json:"tier"` -} \ No newline at end of file +} diff --git a/internal/items/loot/loot_test.go b/internal/items/loot/loot_test.go index 524faae..30240e9 100644 --- a/internal/items/loot/loot_test.go +++ b/internal/items/loot/loot_test.go @@ -106,18 +106,18 @@ func (m *MockPlayerService) SetInventorySpace(playerID uint32, space int) { // MockZoneService implements ZoneService for testing type MockZoneService struct { - rules map[int32]map[string]interface{} - objects map[int32]map[int32]interface{} // zoneID -> objectID + rules map[int32]map[string]any + objects map[int32]map[int32]any // zoneID -> objectID } func NewMockZoneService() *MockZoneService { return &MockZoneService{ - rules: make(map[int32]map[string]interface{}), - objects: make(map[int32]map[int32]interface{}), + rules: make(map[int32]map[string]any), + objects: make(map[int32]map[int32]any), } } -func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (interface{}, error) { +func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (any, error) { if rules, exists := m.rules[zoneID]; exists { return rules[ruleName], nil } @@ -127,7 +127,7 @@ func (m *MockZoneService) GetZoneRule(zoneID int32, ruleName string) (interface{ func (m *MockZoneService) SpawnObjectInZone(zoneID int32, appearanceID int32, x, y, z, heading float32, name string, commands []string) (int32, error) { objectID := int32(len(m.objects[zoneID]) + 1) if m.objects[zoneID] == nil { - m.objects[zoneID] = make(map[int32]interface{}) + m.objects[zoneID] = make(map[int32]any) } m.objects[zoneID][objectID] = struct{}{} return objectID, nil @@ -382,7 +382,7 @@ func TestTreasureChestCreation(t *testing.T) { } if chest.AppearanceID != ChestAppearanceOrnate { - t.Errorf("Expected ornate chest appearance %d for legendary item, got %d", + t.Errorf("Expected ornate chest appearance %d for legendary item, got %d", ChestAppearanceOrnate, chest.AppearanceID) } @@ -530,7 +530,7 @@ func TestChestAppearanceSelection(t *testing.T) { for _, tc := range testCases { appearance := GetChestAppearance(tc.tier) if appearance.AppearanceID != tc.expected { - t.Errorf("For tier %d, expected appearance %d, got %d", + t.Errorf("For tier %d, expected appearance %d, got %d", tc.tier, tc.expected, appearance.AppearanceID) } } @@ -667,4 +667,4 @@ func BenchmarkChestInteraction(b *testing.B) { for i := 0; i < b.N; i++ { chestService.HandleChestInteraction(chest.ID, 1, ChestInteractionView, 0) } -} \ No newline at end of file +} diff --git a/internal/languages/interfaces.go b/internal/languages/interfaces.go index 5a3f31a..e349a8a 100644 --- a/internal/languages/interfaces.go +++ b/internal/languages/interfaces.go @@ -14,10 +14,10 @@ type Database interface { // Logger interface for language logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // Player interface for language-related player operations @@ -372,7 +372,7 @@ func NewLanguageEventAdapter(handler LanguageHandler, logger Logger) *LanguageEv } // ProcessLanguageEvent processes a language-related event -func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *Player, languageID int32, data interface{}) { +func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player *Player, languageID int32, data any) { if lea.handler == nil { return } diff --git a/internal/npc/ai/interfaces.go b/internal/npc/ai/interfaces.go index 0f702cc..9d068d4 100644 --- a/internal/npc/ai/interfaces.go +++ b/internal/npc/ai/interfaces.go @@ -7,10 +7,10 @@ import ( // Logger interface for AI logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // NPC interface defines the required NPC functionality for AI @@ -144,7 +144,7 @@ type LuaInterface interface { type Zone interface { GetSpawnByID(int32) Spawn ProcessSpell(spell Spell, caster NPC, target Spawn) error - CallSpawnScript(npc NPC, scriptType string, args ...interface{}) error + CallSpawnScript(npc NPC, scriptType string, args ...any) error } // AIManager provides high-level management of the AI system @@ -209,7 +209,7 @@ func (am *AIManager) GetBrain(npcID int32) Brain { } // CreateBrainForNPC creates and adds the appropriate brain for an NPC -func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...interface{}) error { +func (am *AIManager) CreateBrainForNPC(npc NPC, brainType int8, options ...any) error { if npc == nil { return fmt.Errorf("NPC cannot be nil") } diff --git a/internal/npc/ai/variants.go b/internal/npc/ai/variants.go index 85c8c46..925604a 100644 --- a/internal/npc/ai/variants.go +++ b/internal/npc/ai/variants.go @@ -289,7 +289,7 @@ func (dfpb *DumbFirePetBrain) ExtendExpireTime(durationMS int32) { // Brain factory functions // CreateBrain creates the appropriate brain type for an NPC -func CreateBrain(npc NPC, brainType int8, logger Logger, options ...interface{}) Brain { +func CreateBrain(npc NPC, brainType int8, logger Logger, options ...any) Brain { switch brainType { case BrainTypeCombatPet: return NewCombatPetBrain(npc, logger) diff --git a/internal/npc/interfaces.go b/internal/npc/interfaces.go index d449148..5820b9f 100644 --- a/internal/npc/interfaces.go +++ b/internal/npc/interfaces.go @@ -1,5 +1,7 @@ package npc +import "fmt" + // Database interface for NPC persistence type Database interface { LoadAllNPCs() ([]*NPC, error) @@ -13,10 +15,10 @@ type Database interface { // Logger interface for NPC logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // Client interface for NPC-related client operations @@ -50,15 +52,15 @@ type Zone interface { RemoveNPC(npcID int32) error GetPlayersInRange(x, y, z, radius float32) []Player ProcessEntityCommand(command string, client Client, target *NPC) error - CallSpawnScript(npc *NPC, scriptType string, args ...interface{}) error + CallSpawnScript(npc *NPC, scriptType string, args ...any) error } // SpellManager interface for spell system integration type SpellManager interface { GetSpell(spellID int32, tier int8) Spell - CastSpell(caster *NPC, target interface{}, spell Spell) error - GetSpellEffect(entity interface{}, spellID int32) SpellEffect - ProcessSpell(spell Spell, caster *NPC, target interface{}) error + CastSpell(caster *NPC, target any, spell Spell) error + GetSpellEffect(entity any, spellID int32) SpellEffect + ProcessSpell(spell Spell, caster *NPC, target any) error } // Spell interface for spell data @@ -88,8 +90,8 @@ type SpellEffect interface { type SkillManager interface { GetSkill(skillID int32) MasterSkill GetSkillByName(name string) MasterSkill - ApplySkillBonus(entity interface{}, skillID int32, bonus float32) error - RemoveSkillBonus(entity interface{}, skillID int32, bonus float32) error + ApplySkillBonus(entity any, skillID int32, bonus float32) error + RemoveSkillBonus(entity any, skillID int32, bonus float32) error } // MasterSkill interface for skill definitions @@ -127,11 +129,11 @@ type MovementManager interface { // CombatManager interface for combat system integration type CombatManager interface { - StartCombat(npc *NPC, target interface{}) error + StartCombat(npc *NPC, target any) error EndCombat(npc *NPC) error ProcessCombatRound(npc *NPC) error - CalculateDamage(attacker *NPC, target interface{}) int32 - ApplyDamage(target interface{}, damage int32) error + CalculateDamage(attacker *NPC, target any) int32 + ApplyDamage(target any, damage int32) error } // NPCAware interface for entities that can interact with NPCs @@ -287,7 +289,7 @@ func NewSpellCasterAdapter(npc *NPC, spellManager SpellManager, logger Logger) * } // GetNextSpell selects the next spell to cast based on AI strategy -func (sca *SpellCasterAdapter) GetNextSpell(target interface{}, distance float32) Spell { +func (sca *SpellCasterAdapter) GetNextSpell(target any, distance float32) Spell { if sca.npc == nil || sca.spellManager == nil { return nil } @@ -307,7 +309,7 @@ func (sca *SpellCasterAdapter) GetNextSpell(target interface{}, distance float32 } // GetNextBuffSpell selects the next buff spell to cast -func (sca *SpellCasterAdapter) GetNextBuffSpell(target interface{}) Spell { +func (sca *SpellCasterAdapter) GetNextBuffSpell(target any) Spell { if sca.npc == nil || sca.spellManager == nil { return nil } @@ -347,7 +349,7 @@ func (sca *SpellCasterAdapter) GetNextBuffSpell(target interface{}) Spell { } // CastSpell attempts to cast a spell -func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error { +func (sca *SpellCasterAdapter) CastSpell(target any, spell Spell) error { if sca.npc == nil || sca.spellManager == nil || spell == nil { return fmt.Errorf("invalid parameters for spell casting") } @@ -371,7 +373,7 @@ func (sca *SpellCasterAdapter) CastSpell(target interface{}, spell Spell) error } // getNextCastOnAggroSpell selects cast-on-aggro spells -func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell { +func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target any) Spell { castOnSpells := sca.npc.castOnSpells[CastOnAggro] for _, npcSpell := range castOnSpells { spell := sca.spellManager.GetSpell(npcSpell.GetSpellID(), npcSpell.GetTier()) @@ -386,7 +388,7 @@ func (sca *SpellCasterAdapter) getNextCastOnAggroSpell(target interface{}) Spell } // getNextSpellByStrategy selects spells based on AI strategy -func (sca *SpellCasterAdapter) getNextSpellByStrategy(target interface{}, distance float32, strategy int8) Spell { +func (sca *SpellCasterAdapter) getNextSpellByStrategy(target any, distance float32, strategy int8) Spell { // TODO: Implement more sophisticated spell selection based on strategy for _, npcSpell := range sca.npc.spells { @@ -446,7 +448,7 @@ func NewCombatAdapter(npc *NPC, combatManager CombatManager, logger Logger) *Com } // EnterCombat handles entering combat state -func (ca *CombatAdapter) EnterCombat(target interface{}) error { +func (ca *CombatAdapter) EnterCombat(target any) error { if ca.npc == nil { return fmt.Errorf("NPC is nil") } diff --git a/internal/object/integration.go b/internal/object/integration.go index 6c7bb5c..b381185 100644 --- a/internal/object/integration.go +++ b/internal/object/integration.go @@ -114,8 +114,8 @@ func (os *ObjectSpawn) ShowsCommandIcon() bool { } // GetObjectInfo returns comprehensive information about the object spawn -func (os *ObjectSpawn) GetObjectInfo() map[string]interface{} { - info := make(map[string]interface{}) +func (os *ObjectSpawn) GetObjectInfo() map[string]any { + info := make(map[string]any) // Add spawn info info["spawn_id"] = os.GetID() @@ -246,7 +246,7 @@ func ConvertSpawnToObject(spawn *spawn.Spawn) *ObjectSpawn { // LoadObjectSpawnFromData loads object spawn data from database/config // This would be called when loading spawns from the database -func LoadObjectSpawnFromData(spawnData map[string]interface{}) *ObjectSpawn { +func LoadObjectSpawnFromData(spawnData map[string]any) *ObjectSpawn { objectSpawn := NewObjectSpawn() // Load basic spawn data diff --git a/internal/object/manager.go b/internal/object/manager.go index 3a8e7cd..ee51676 100644 --- a/internal/object/manager.go +++ b/internal/object/manager.go @@ -315,11 +315,11 @@ func (om *ObjectManager) ClearZone(zoneName string) int { } // GetStatistics returns statistics about objects in the manager -func (om *ObjectManager) GetStatistics() map[string]interface{} { +func (om *ObjectManager) GetStatistics() map[string]any { om.mutex.RLock() defer om.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_objects"] = len(om.objects) stats["zones_with_objects"] = len(om.objectsByZone) stats["interactive_objects"] = len(om.interactiveObjects) diff --git a/internal/object/object.go b/internal/object/object.go index 83a8667..9a7b226 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -581,11 +581,11 @@ func (o *Object) ShowsCommandIcon() bool { } // GetObjectInfo returns comprehensive information about the object -func (o *Object) GetObjectInfo() map[string]interface{} { +func (o *Object) GetObjectInfo() map[string]any { o.mutex.RLock() defer o.mutex.RUnlock() - info := make(map[string]interface{}) + info := make(map[string]any) info["clickable"] = o.clickable info["zone_name"] = o.zoneName info["device_id"] = o.deviceID diff --git a/internal/player/interfaces.go b/internal/player/interfaces.go index 46900db..f3fb3f7 100644 --- a/internal/player/interfaces.go +++ b/internal/player/interfaces.go @@ -40,10 +40,10 @@ type PlayerManager interface { GetPlayersInZone(zoneID int32) []*Player // SendToAll sends a message to all players - SendToAll(message interface{}) error + SendToAll(message any) error // SendToZone sends a message to all players in a zone - SendToZone(zoneID int32, message interface{}) error + SendToZone(zoneID int32, message any) error } // PlayerDatabase interface for database operations @@ -85,13 +85,13 @@ type PlayerDatabase interface { // PlayerPacketHandler interface for handling player packets type PlayerPacketHandler interface { // HandlePacket handles a packet from a player - HandlePacket(player *Player, packet interface{}) error + HandlePacket(player *Player, packet any) error // SendPacket sends a packet to a player - SendPacket(player *Player, packet interface{}) error + SendPacket(player *Player, packet any) error // BroadcastPacket broadcasts a packet to multiple players - BroadcastPacket(players []*Player, packet interface{}) error + BroadcastPacket(players []*Player, packet any) error } // PlayerEventHandler interface for player events @@ -181,7 +181,7 @@ type PlayerStatistics interface { RecordSpellCast(player *Player, spell *spells.Spell) // GetStatistics returns player statistics - GetStatistics(playerID int32) map[string]interface{} + GetStatistics(playerID int32) map[string]any } // PlayerNotifier interface for player notifications diff --git a/internal/player/manager.go b/internal/player/manager.go index 077e157..75bedf1 100644 --- a/internal/player/manager.go +++ b/internal/player/manager.go @@ -274,7 +274,7 @@ func (m *Manager) GetPlayersInZone(zoneID int32) []*Player { } // SendToAll sends a message to all players -func (m *Manager) SendToAll(message interface{}) error { +func (m *Manager) SendToAll(message any) error { if m.packetHandler == nil { return fmt.Errorf("no packet handler configured") } @@ -284,7 +284,7 @@ func (m *Manager) SendToAll(message interface{}) error { } // SendToZone sends a message to all players in a zone -func (m *Manager) SendToZone(zoneID int32, message interface{}) error { +func (m *Manager) SendToZone(zoneID int32, message any) error { if m.packetHandler == nil { return fmt.Errorf("no packet handler configured") } diff --git a/internal/quests/interfaces.go b/internal/quests/interfaces.go index 78fd2c2..189b7a5 100644 --- a/internal/quests/interfaces.go +++ b/internal/quests/interfaces.go @@ -114,12 +114,12 @@ type Packet interface { // PacketStruct interface defines packet structure functionality type PacketStruct interface { // Packet building methods - SetDataByName(name string, value interface{}, index ...int) + SetDataByName(name string, value any, index ...int) SetArrayLengthByName(name string, length int) - SetArrayDataByName(name string, value interface{}, index int) + SetArrayDataByName(name string, value any, index int) SetSubArrayLengthByName(name string, length int, index int) - SetSubArrayDataByName(name string, value interface{}, index1, index2 int) - SetSubstructArrayDataByName(substruct, name string, value interface{}, index int) + SetSubArrayDataByName(name string, value any, index1, index2 int) + SetSubstructArrayDataByName(substruct, name string, value any, index int) SetItemArrayDataByName(name string, item Item, player Player, index int, flag ...int) Serialize() Packet GetVersion() int16 @@ -197,10 +197,10 @@ type Database interface { // Logger interface defines logging functionality for quest system type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // Configuration interface defines quest system configuration diff --git a/internal/races/integration.go b/internal/races/integration.go index bf11112..9c22e10 100644 --- a/internal/races/integration.go +++ b/internal/races/integration.go @@ -32,7 +32,7 @@ func NewRaceIntegration() *RaceIntegration { } // ValidateEntityRace validates an entity's race and provides detailed information -func (ri *RaceIntegration) ValidateEntityRace(entity RaceAware) (bool, string, map[string]interface{}) { +func (ri *RaceIntegration) ValidateEntityRace(entity RaceAware) (bool, string, map[string]any) { raceID := entity.GetRace() if !ri.races.IsValidRaceID(raceID) { @@ -63,8 +63,8 @@ func (ri *RaceIntegration) ApplyRacialBonuses(entity RaceAware, stats map[string } // GetEntityRaceInfo returns comprehensive race information for an entity -func (ri *RaceIntegration) GetEntityRaceInfo(entity EntityWithRace) map[string]interface{} { - info := make(map[string]interface{}) +func (ri *RaceIntegration) GetEntityRaceInfo(entity EntityWithRace) map[string]any { + info := make(map[string]any) // Basic entity info info["entity_id"] = entity.GetID() @@ -230,12 +230,12 @@ func (ri *RaceIntegration) GetRaceStartingStats(raceID int8) map[string]int16 { } // CreateRaceSpecificEntity creates entity data with race-specific properties -func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]interface{} { +func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]any { if !ri.races.IsValidRaceID(raceID) { return nil } - entityData := make(map[string]interface{}) + entityData := make(map[string]any) // Basic race info entityData["race_id"] = raceID @@ -255,15 +255,15 @@ func (ri *RaceIntegration) CreateRaceSpecificEntity(raceID int8) map[string]inte } // GetRaceSelectionData returns data for race selection UI -func (ri *RaceIntegration) GetRaceSelectionData() map[string]interface{} { - data := make(map[string]interface{}) +func (ri *RaceIntegration) GetRaceSelectionData() map[string]any { + data := make(map[string]any) // All available races allRaces := ri.races.GetAllRaces() - raceList := make([]map[string]interface{}, 0, len(allRaces)) + raceList := make([]map[string]any, 0, len(allRaces)) for raceID, friendlyName := range allRaces { - raceData := map[string]interface{}{ + raceData := map[string]any{ "id": raceID, "name": friendlyName, "alignment": ri.races.GetRaceAlignment(raceID), diff --git a/internal/races/manager.go b/internal/races/manager.go index d0f8a3e..0c88476 100644 --- a/internal/races/manager.go +++ b/internal/races/manager.go @@ -267,8 +267,8 @@ func (rm *RaceManager) handleSearchCommand(args []string) (string, error) { } // ValidateEntityRaces validates races for a collection of entities -func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]interface{} { - validationResults := make(map[string]interface{}) +func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]any { + validationResults := make(map[string]any) validCount := 0 invalidCount := 0 @@ -288,11 +288,11 @@ func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]inte // Track invalid entities if !isValid { if validationResults["invalid_entities"] == nil { - validationResults["invalid_entities"] = make([]map[string]interface{}, 0) + validationResults["invalid_entities"] = make([]map[string]any, 0) } - invalidList := validationResults["invalid_entities"].([]map[string]interface{}) - invalidList = append(invalidList, map[string]interface{}{ + invalidList := validationResults["invalid_entities"].([]map[string]any) + invalidList = append(invalidList, map[string]any{ "index": i, "race_id": raceID, }) @@ -309,7 +309,7 @@ func (rm *RaceManager) ValidateEntityRaces(entities []RaceAware) map[string]inte } // GetRaceRecommendations returns race recommendations for character creation -func (rm *RaceManager) GetRaceRecommendations(preferences map[string]interface{}) []int8 { +func (rm *RaceManager) GetRaceRecommendations(preferences map[string]any) []int8 { recommendations := make([]int8, 0) // Check for alignment preference diff --git a/internal/races/races.go b/internal/races/races.go index 24b083f..9866185 100644 --- a/internal/races/races.go +++ b/internal/races/races.go @@ -351,11 +351,11 @@ func (r *Races) GetEvilRaceCount() int { } // GetRaceInfo returns comprehensive information about a race -func (r *Races) GetRaceInfo(raceID int8) map[string]interface{} { +func (r *Races) GetRaceInfo(raceID int8) map[string]any { r.mutex.RLock() defer r.mutex.RUnlock() - info := make(map[string]interface{}) + info := make(map[string]any) if !r.IsValidRaceID(raceID) { info["valid"] = false diff --git a/internal/races/utils.go b/internal/races/utils.go index 5cabcad..3787e9e 100644 --- a/internal/races/utils.go +++ b/internal/races/utils.go @@ -321,8 +321,8 @@ func (ru *RaceUtils) GetRaceAliases(raceID int8) []string { } // GetRaceStatistics returns statistics about the race system -func (ru *RaceUtils) GetRaceStatistics() map[string]interface{} { - stats := make(map[string]interface{}) +func (ru *RaceUtils) GetRaceStatistics() map[string]any { + stats := make(map[string]any) stats["total_races"] = ru.races.GetRaceCount() stats["good_races"] = ru.races.GetGoodRaceCount() diff --git a/internal/recipes/recipe.go b/internal/recipes/recipe.go index 6405c45..51a8e60 100644 --- a/internal/recipes/recipe.go +++ b/internal/recipes/recipe.go @@ -320,11 +320,11 @@ func (r *Recipe) GetComponentQuantityForSlot(slot int8) int16 { } // GetInfo returns comprehensive information about the recipe -func (r *Recipe) GetInfo() map[string]interface{} { +func (r *Recipe) GetInfo() map[string]any { r.mutex.RLock() defer r.mutex.RUnlock() - info := make(map[string]interface{}) + info := make(map[string]any) info["id"] = r.ID info["soe_id"] = r.SoeID diff --git a/internal/rules/interfaces.go b/internal/rules/interfaces.go index 6589810..470a4f0 100644 --- a/internal/rules/interfaces.go +++ b/internal/rules/interfaces.go @@ -86,12 +86,12 @@ type RuleValidator interface { // ValidationRule defines validation criteria for a rule type ValidationRule struct { - Required bool // Whether the rule is required - MinValue interface{} // Minimum allowed value (for numeric types) - MaxValue interface{} // Maximum allowed value (for numeric types) - ValidValues []string // List of valid string values - Pattern string // Regex pattern for validation - Description string // Description of the rule + Required bool // Whether the rule is required + MinValue any // Minimum allowed value (for numeric types) + MaxValue any // Maximum allowed value (for numeric types) + ValidValues []string // List of valid string values + Pattern string // Regex pattern for validation + Description string // Description of the rule } // RuleEventHandler defines the interface for rule change events diff --git a/internal/sign/interfaces.go b/internal/sign/interfaces.go index a884dee..d96309b 100644 --- a/internal/sign/interfaces.go +++ b/internal/sign/interfaces.go @@ -21,7 +21,7 @@ type Client interface { GetCurrentZone() Zone SetTemporaryTransportID(id int32) SimpleMessage(channel int32, message string) - Message(channel int32, format string, args ...interface{}) + Message(channel int32, format string, args ...any) CheckZoneAccess(zoneName string) bool TryZoneInstance(zoneID int32, useDefaults bool) bool Zone(zoneName string, useDefaults bool) error @@ -66,10 +66,10 @@ type EntityCommand struct { // Logger interface for sign logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) - LogWarning(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) + LogWarning(message string, args ...any) } // SignSpawn provides sign functionality for spawn entities diff --git a/internal/sign/manager.go b/internal/sign/manager.go index 01c1260..46845f2 100644 --- a/internal/sign/manager.go +++ b/internal/sign/manager.go @@ -265,11 +265,11 @@ func (m *Manager) HandleSignUse(sign *Sign, client Client, command string) error } // GetStatistics returns sign system statistics -func (m *Manager) GetStatistics() map[string]interface{} { +func (m *Manager) GetStatistics() map[string]any { m.mutex.RLock() defer m.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_signs"] = m.totalSigns stats["sign_interactions"] = m.signInteractions stats["zone_transports"] = m.zoneTransports diff --git a/internal/skills/integration.go b/internal/skills/integration.go index c642a22..268ea62 100644 --- a/internal/skills/integration.go +++ b/internal/skills/integration.go @@ -27,9 +27,9 @@ type Database interface { // Logger interface for skill system logging type Logger interface { - LogInfo(message string, args ...interface{}) - LogError(message string, args ...interface{}) - LogDebug(message string, args ...interface{}) + LogInfo(message string, args ...any) + LogError(message string, args ...any) + LogDebug(message string, args ...any) } // EntitySkillAdapter provides skill functionality for entities diff --git a/internal/skills/manager.go b/internal/skills/manager.go index a348bf2..5288fad 100644 --- a/internal/skills/manager.go +++ b/internal/skills/manager.go @@ -73,11 +73,11 @@ func (m *Manager) RecordSkillUp(skillID int32, skillType int32) { } // GetStatistics returns skill system statistics -func (m *Manager) GetStatistics() map[string]interface{} { +func (m *Manager) GetStatistics() map[string]any { m.mutex.RLock() defer m.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_skill_ups"] = m.totalSkillUps stats["players_with_skills"] = m.playersWithSkills stats["total_skills_in_master"] = m.masterSkillList.GetSkillCount() diff --git a/internal/spawn/spawn.go b/internal/spawn/spawn.go index 4ebc7f6..2274f79 100644 --- a/internal/spawn/spawn.go +++ b/internal/spawn/spawn.go @@ -1074,7 +1074,7 @@ func (s *Spawn) SetBasicInfo(info *BasicInfoStruct) { } // GetClient returns the client associated with this spawn (overridden by Player) -func (s *Spawn) GetClient() interface{} { +func (s *Spawn) GetClient() any { return nil // Base spawns have no client } diff --git a/internal/spells/spell_manager.go b/internal/spells/spell_manager.go index ec99f11..97fc08f 100644 --- a/internal/spells/spell_manager.go +++ b/internal/spells/spell_manager.go @@ -555,13 +555,13 @@ func (sm *SpellManager) CanCastSpell(casterID, targetID, spellID int32) (bool, s } // GetSpellInfo returns comprehensive information about a spell -func (sm *SpellManager) GetSpellInfo(spellID int32) map[string]interface{} { +func (sm *SpellManager) GetSpellInfo(spellID int32) map[string]any { spell := sm.masterList.GetSpell(spellID) if spell == nil { return nil } - info := make(map[string]interface{}) + info := make(map[string]any) // Basic spell info info["id"] = spell.GetSpellID() diff --git a/internal/spells/spell_targeting.go b/internal/spells/spell_targeting.go index cc96dba..e4f1784 100644 --- a/internal/spells/spell_targeting.go +++ b/internal/spells/spell_targeting.go @@ -155,7 +155,7 @@ func (st *SpellTargeting) getAETargets(luaSpell *LuaSpell, result *TargetingResu // For now, implement basic logic _ = luaSpell.Spell.GetSpellData() // TODO: Use spell data when needed - maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists + maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists if maxTargets <= 0 { maxTargets = 10 // Default limit } @@ -416,8 +416,8 @@ func (st *SpellTargeting) RequiresTarget(spell *Spell) bool { } // GetTargetingInfo returns information about spell targeting requirements -func (st *SpellTargeting) GetTargetingInfo(spell *Spell) map[string]interface{} { - info := make(map[string]interface{}) +func (st *SpellTargeting) GetTargetingInfo(spell *Spell) map[string]any { + info := make(map[string]any) if spell == nil { return info diff --git a/internal/titles/master_list.go b/internal/titles/master_list.go index 3c49df1..5282ad7 100644 --- a/internal/titles/master_list.go +++ b/internal/titles/master_list.go @@ -339,11 +339,11 @@ func (mtl *MasterTitlesList) UpdateTitle(title *Title) error { categorySlice := mtl.categorized[existing.Category] mtl.removeFromSlice(&categorySlice, existing) mtl.categorized[existing.Category] = categorySlice - + sourceSlice := mtl.bySource[existing.Source] mtl.removeFromSlice(&sourceSlice, existing) mtl.bySource[existing.Source] = sourceSlice - + raritySlice := mtl.byRarity[existing.Rarity] mtl.removeFromSlice(&raritySlice, existing) mtl.byRarity[existing.Rarity] = raritySlice @@ -487,7 +487,7 @@ func (mtl *MasterTitlesList) SaveToDatabase(db *database.DB) error { // Insert all current titles for _, title := range mtl.titles { - var achievementID interface{} + var achievementID any if title.AchievementID != 0 { achievementID = title.AchievementID } @@ -495,9 +495,9 @@ func (mtl *MasterTitlesList) SaveToDatabase(db *database.DB) error { err := txDB.Exec(` INSERT INTO titles (id, name, description, category, position, source, rarity, flags, achievement_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, title.ID, title.Name, title.Description, title.Category, - int(title.Position), int(title.Source), int(title.Rarity), - int64(title.Flags), achievementID) + `, title.ID, title.Name, title.Description, title.Category, + int(title.Position), int(title.Source), int(title.Rarity), + int64(title.Flags), achievementID) if err != nil { return fmt.Errorf("failed to insert title %d: %w", title.ID, err) diff --git a/internal/titles/player_titles.go b/internal/titles/player_titles.go index eb84b2f..e008d9f 100644 --- a/internal/titles/player_titles.go +++ b/internal/titles/player_titles.go @@ -451,7 +451,7 @@ func (ptl *PlayerTitlesList) LoadFromDatabase(db *database.DB) error { } // Load all titles for this player - err := db.Query("SELECT title_id, achievement_id, granted_date, expiration_date, is_active FROM player_titles WHERE player_id = ?", + err := db.Query("SELECT title_id, achievement_id, granted_date, expiration_date, is_active FROM player_titles WHERE player_id = ?", func(row *database.Row) error { playerTitle := &PlayerTitle{ TitleID: int32(row.Int64(0)), @@ -499,12 +499,12 @@ func (ptl *PlayerTitlesList) SaveToDatabase(db *database.DB) error { // Insert all current titles for _, playerTitle := range ptl.titles { - var achievementID interface{} + var achievementID any if playerTitle.AchievementID != 0 { achievementID = playerTitle.AchievementID } - var expirationDate interface{} + var expirationDate any if !playerTitle.ExpiresAt.IsZero() { expirationDate = playerTitle.ExpiresAt.Unix() } @@ -519,8 +519,8 @@ func (ptl *PlayerTitlesList) SaveToDatabase(db *database.DB) error { err := txDB.Exec(` INSERT INTO player_titles (player_id, title_id, achievement_id, granted_date, expiration_date, is_active) VALUES (?, ?, ?, ?, ?, ?) - `, ptl.playerID, playerTitle.TitleID, achievementID, - playerTitle.EarnedDate.Unix(), expirationDate, isActive) + `, ptl.playerID, playerTitle.TitleID, achievementID, + playerTitle.EarnedDate.Unix(), expirationDate, isActive) if err != nil { return fmt.Errorf("failed to insert player title %d: %w", playerTitle.TitleID, err) diff --git a/internal/titles/title_manager.go b/internal/titles/title_manager.go index b659c53..d9353f7 100644 --- a/internal/titles/title_manager.go +++ b/internal/titles/title_manager.go @@ -319,11 +319,11 @@ func (tm *TitleManager) cleanupExpiredTitles() { } // GetStatistics returns title system statistics -func (tm *TitleManager) GetStatistics() map[string]interface{} { +func (tm *TitleManager) GetStatistics() map[string]any { tm.mutex.RLock() defer tm.mutex.RUnlock() - return map[string]interface{}{ + return map[string]any{ "total_titles": tm.masterList.GetTitleCount(), "total_players": len(tm.playerLists), "titles_granted": tm.totalTitlesGranted, diff --git a/internal/trade/manager.go b/internal/trade/manager.go index 83361c4..ad2049c 100644 --- a/internal/trade/manager.go +++ b/internal/trade/manager.go @@ -146,7 +146,7 @@ func (ts *TradeService) CancelTrade(entityID int32) error { } // GetTradeInfo returns comprehensive information about a trade -func (ts *TradeService) GetTradeInfo(entityID int32) (map[string]interface{}, error) { +func (ts *TradeService) GetTradeInfo(entityID int32) (map[string]any, error) { trade := ts.tradeManager.GetTrade(entityID) if trade == nil { return nil, fmt.Errorf("entity is not in a trade") @@ -174,8 +174,8 @@ func (ts *TradeService) ProcessTrades() { } // GetTradeStatistics returns statistics about trade activity -func (ts *TradeService) GetTradeStatistics() map[string]interface{} { - stats := make(map[string]interface{}) +func (ts *TradeService) GetTradeStatistics() map[string]any { + stats := make(map[string]any) stats["active_trades"] = ts.tradeManager.GetActiveTradeCount() stats["max_trade_duration_minutes"] = ts.maxTradeDuration.Minutes() diff --git a/internal/trade/trade.go b/internal/trade/trade.go index 967af4f..dededa1 100644 --- a/internal/trade/trade.go +++ b/internal/trade/trade.go @@ -392,11 +392,11 @@ func (t *Trade) GetTraderSlot(entityID int32, slot int8) Item { } // GetTradeInfo returns comprehensive information about the trade -func (t *Trade) GetTradeInfo() map[string]interface{} { +func (t *Trade) GetTradeInfo() map[string]any { t.mutex.RLock() defer t.mutex.RUnlock() - info := make(map[string]interface{}) + info := make(map[string]any) info["state"] = t.state info["start_time"] = t.startTime info["trader1_id"] = t.trader1.EntityID diff --git a/internal/trade/utils.go b/internal/trade/utils.go index 7218a43..2b63f35 100644 --- a/internal/trade/utils.go +++ b/internal/trade/utils.go @@ -127,7 +127,7 @@ func IsValidTradeState(state TradeState, operation string) bool { } // GenerateTradeLogEntry creates a log entry for trade operations -func GenerateTradeLogEntry(tradeID string, operation string, entityID int32, details interface{}) string { +func GenerateTradeLogEntry(tradeID string, operation string, entityID int32, details any) string { return fmt.Sprintf("[Trade:%s] %s by entity %d: %v", tradeID, operation, entityID, details) } @@ -147,8 +147,8 @@ func CompareTradeItems(item1, item2 TradeItemInfo) bool { // CalculateTradeValue estimates the total value of items and coins in a trade // This is a helper function for trade balancing and analysis -func CalculateTradeValue(participant *TradeParticipant) map[string]interface{} { - value := make(map[string]interface{}) +func CalculateTradeValue(participant *TradeParticipant) map[string]any { + value := make(map[string]any) // Add coin value value["coins"] = participant.Coins @@ -159,9 +159,9 @@ func CalculateTradeValue(participant *TradeParticipant) map[string]interface{} { value["item_count"] = itemCount if itemCount > 0 { - items := make([]map[string]interface{}, 0, itemCount) + items := make([]map[string]any, 0, itemCount) for slot, itemInfo := range participant.Items { - itemData := make(map[string]interface{}) + itemData := make(map[string]any) itemData["slot"] = slot itemData["quantity"] = itemInfo.Quantity if itemInfo.Item != nil { diff --git a/internal/tradeskills/interfaces.go b/internal/tradeskills/interfaces.go index ee01ac3..e1417ac 100644 --- a/internal/tradeskills/interfaces.go +++ b/internal/tradeskills/interfaces.go @@ -596,11 +596,11 @@ func (tsa *TradeskillSystemAdapter) updatePlayerRecipeStage(currentStage int8, c } // GetSystemStats returns comprehensive statistics about the tradeskill system. -func (tsa *TradeskillSystemAdapter) GetSystemStats() map[string]interface{} { +func (tsa *TradeskillSystemAdapter) GetSystemStats() map[string]any { managerStats := tsa.manager.GetStats() eventsStats := tsa.eventsList.GetStats() - return map[string]interface{}{ + return map[string]any{ "active_sessions": managerStats.ActiveSessions, "recent_completions": managerStats.RecentCompletions, "average_session_time": managerStats.AverageSessionTime, diff --git a/internal/traits/interfaces.go b/internal/traits/interfaces.go index 6c59cda..8be97a1 100644 --- a/internal/traits/interfaces.go +++ b/internal/traits/interfaces.go @@ -348,7 +348,7 @@ func (tsa *TraitSystemAdapter) IsPlayerAllowedTrait(playerID uint32, spellID uin } // GetPlayerTraitStats returns statistics about a player's trait selections. -func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]interface{}, error) { +func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string]any, error) { // Get player information player, err := tsa.playerManager.GetPlayer(playerID) if err != nil { @@ -381,7 +381,7 @@ func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string] tsa.masterList.getSpellCount(playerState, playerState.TraitLists.InnateRaceTraits, false) focusEffects := tsa.masterList.getSpellCount(playerState, playerState.TraitLists.FocusEffects, false) - return map[string]interface{}{ + return map[string]any{ "player_id": playerID, "level": playerState.Level, "character_traits": characterTraits, @@ -394,11 +394,11 @@ func (tsa *TraitSystemAdapter) GetPlayerTraitStats(playerID uint32) (map[string] } // GetSystemStats returns comprehensive statistics about the trait system. -func (tsa *TraitSystemAdapter) GetSystemStats() map[string]interface{} { +func (tsa *TraitSystemAdapter) GetSystemStats() map[string]any { masterStats := tsa.masterList.GetStats() managerStats := tsa.traitManager.GetManagerStats() - return map[string]interface{}{ + return map[string]any{ "total_traits": masterStats.TotalTraits, "traits_by_type": masterStats.TraitsByType, "traits_by_group": masterStats.TraitsByGroup, diff --git a/internal/traits/manager.go b/internal/traits/manager.go index fde9094..bca07be 100644 --- a/internal/traits/manager.go +++ b/internal/traits/manager.go @@ -325,7 +325,7 @@ func (mtl *MasterTraitList) getClassicAvailability(levelLimits []int16, totalUse } // getSpellCount counts how many spells from a trait map the player has selected. -func (mtl *MasterTraitList) getSpellCount(playerState *PlayerTraitState, traitMap interface{}, onlyCharTraits bool) int16 { +func (mtl *MasterTraitList) getSpellCount(playerState *PlayerTraitState, traitMap any, onlyCharTraits bool) int16 { count := int16(0) switch tm := traitMap.(type) { diff --git a/internal/transmute/manager.go b/internal/transmute/manager.go index 760a1bd..f10fe3b 100644 --- a/internal/transmute/manager.go +++ b/internal/transmute/manager.go @@ -94,11 +94,11 @@ func (m *Manager) ReloadTransmutingTiers() error { } // GetStatistics returns transmutation statistics -func (m *Manager) GetStatistics() map[string]interface{} { +func (m *Manager) GetStatistics() map[string]any { m.mutex.RLock() defer m.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_transmutes"] = m.totalTransmutes stats["successful_transmutes"] = m.successfulTransmutes stats["failed_transmutes"] = m.failedTransmutes diff --git a/internal/transmute/types.go b/internal/transmute/types.go index dc5f9e5..08158f2 100644 --- a/internal/transmute/types.go +++ b/internal/transmute/types.go @@ -71,7 +71,7 @@ type Client interface { SetTransmuteID(id int32) QueuePacket(packet []byte) SimpleMessage(channel int32, message string) - Message(channel int32, format string, args ...interface{}) + Message(channel int32, format string, args ...any) AddItem(item Item, itemDeleted *bool) error } diff --git a/internal/widget/interfaces.go b/internal/widget/interfaces.go index f1d3cde..d4472b1 100644 --- a/internal/widget/interfaces.go +++ b/internal/widget/interfaces.go @@ -8,7 +8,7 @@ import ( type ClientInterface interface { GetPlayer() *spawn.Spawn SetTemporaryTransportID(id int32) - ProcessTeleport(widget *Widget, destinations []interface{}, transporterID int32) + ProcessTeleport(widget *Widget, destinations []any, transporterID int32) GetVersion() int32 GetCurrentZone() ZoneInterface } @@ -21,8 +21,8 @@ type ZoneInterface interface { PlaySoundFile(unknown int32, soundFile string, x, y, z float32) CallSpawnScript(s *spawn.Spawn, scriptType string, caller *spawn.Spawn, extra string, state bool) bool GetSpawnByDatabaseID(id int32) *spawn.Spawn - GetTransporters(client ClientInterface, transporterID int32) []interface{} - ProcessEntityCommand(command interface{}, player *spawn.Spawn, target *spawn.Spawn) + GetTransporters(client ClientInterface, transporterID int32) []any + ProcessEntityCommand(command any, player *spawn.Spawn, target *spawn.Spawn) GetInstanceID() int32 GetInstanceType() int32 SendHouseItems(client ClientInterface) diff --git a/internal/widget/manager.go b/internal/widget/manager.go index 30eba78..401b571 100644 --- a/internal/widget/manager.go +++ b/internal/widget/manager.go @@ -295,11 +295,11 @@ func (m *Manager) Clear() { } // GetStatistics returns widget statistics -func (m *Manager) GetStatistics() map[string]interface{} { +func (m *Manager) GetStatistics() map[string]any { m.mutex.RLock() defer m.mutex.RUnlock() - stats := make(map[string]interface{}) + stats := make(map[string]any) stats["total_widgets"] = len(m.widgets) stats["door_count"] = len(m.GetDoorWidgets()) stats["lift_count"] = len(m.GetLiftWidgets()) diff --git a/internal/zone/interfaces.go b/internal/zone/interfaces.go index 460b29e..242036a 100644 --- a/internal/zone/interfaces.go +++ b/internal/zone/interfaces.go @@ -97,8 +97,8 @@ type NPC interface { SetRespawnTime(seconds int32) GetSpawnGroupID() int32 SetSpawnGroupID(groupID int32) - GetRandomizedFeatures() map[string]interface{} - SetRandomizedFeatures(features map[string]interface{}) + GetRandomizedFeatures() map[string]any + SetRandomizedFeatures(features map[string]any) } // Object interface represents an interactive world object @@ -337,12 +337,12 @@ type Recipe interface { // MovementLocation represents a movement waypoint for NPCs type MovementLocation struct { - X float32 - Y float32 - Z float32 - Heading float32 - Speed float32 - Delay int32 + X float32 + Y float32 + Z float32 + Heading float32 + Speed float32 + Delay int32 MovementType int8 } @@ -357,7 +357,7 @@ type PathNode struct { type SpawnLocation struct { ID int32 X float32 - Y float32 + Y float32 Z float32 Heading float32 Pitch float32 @@ -375,46 +375,46 @@ type SpawnLocation struct { // SpawnEntry contains the template data for spawns type SpawnEntry struct { - ID int32 - SpawnType int8 - SpawnEntryID int32 - Name string - Level int16 - EncounterLevel int16 - Model string - Size float32 - HP int32 - Power int32 - Heroic int8 - Gender int8 - Race int16 - AdventureClass int16 + ID int32 + SpawnType int8 + SpawnEntryID int32 + Name string + Level int16 + EncounterLevel int16 + Model string + Size float32 + HP int32 + Power int32 + Heroic int8 + Gender int8 + Race int16 + AdventureClass int16 TradeskillClass int16 - AttackType int8 - MinLevel int16 - MaxLevel int16 - EncounterType int8 - ShowName int8 - Targetable int8 - ShowLevel int8 - Command string - LootTier int8 - MinGold int32 - MaxGold int32 - HarvestType string - Icon int32 + AttackType int8 + MinLevel int16 + MaxLevel int16 + EncounterType int8 + ShowName int8 + Targetable int8 + ShowLevel int8 + Command string + LootTier int8 + MinGold int32 + MaxGold int32 + HarvestType string + Icon int32 } // EntityCommand represents an available command for an entity type EntityCommand struct { - ID int32 - Name string - Distance float32 - ErrorText string - CastTime int16 - SpellVisual int32 - Command string - DisplayText string + ID int32 + Name string + Distance float32 + ErrorText string + CastTime int16 + SpellVisual int32 + Command string + DisplayText string } // LootTable represents a loot table configuration @@ -433,9 +433,9 @@ type LootTable struct { type LootDrop struct { LootTableID int32 ItemID int32 - ItemCharges int16 - EquipItem bool - Probability float32 + ItemCharges int16 + EquipItem bool + Probability float32 NoDropQuestCompletedID int32 } @@ -452,51 +452,51 @@ type GlobalLoot struct { // TransportDestination represents a transport destination type TransportDestination struct { - ID int32 - Type int8 - Name string - Message string - DestinationZoneID int32 - DestinationX float32 - DestinationY float32 - DestinationZ float32 + ID int32 + Type int8 + Name string + Message string + DestinationZoneID int32 + DestinationX float32 + DestinationY float32 + DestinationZ float32 DestinationHeading float32 - Cost int32 - UniqueID int32 - MinLevel int8 - MaxLevel int8 - QuestRequired int32 - QuestStepRequired int16 - QuestCompleted int32 - MapX int32 - MapY int32 - ExpansionFlag int32 - HolidayFlag int32 - MinClientVersion int32 - MaxClientVersion int32 - FlightPathID int32 - MountID int16 - MountRedColor int8 - MountGreenColor int8 - MountBlueColor int8 + Cost int32 + UniqueID int32 + MinLevel int8 + MaxLevel int8 + QuestRequired int32 + QuestStepRequired int16 + QuestCompleted int32 + MapX int32 + MapY int32 + ExpansionFlag int32 + HolidayFlag int32 + MinClientVersion int32 + MaxClientVersion int32 + FlightPathID int32 + MountID int16 + MountRedColor int8 + MountGreenColor int8 + MountBlueColor int8 } // LocationTransportDestination represents a location-based transport trigger type LocationTransportDestination struct { - ZoneID int32 - Message string - TriggerX float32 - TriggerY float32 - TriggerZ float32 - TriggerRadius float32 - DestinationZoneID int32 - DestinationX float32 - DestinationY float32 - DestinationZ float32 + ZoneID int32 + Message string + TriggerX float32 + TriggerY float32 + TriggerZ float32 + TriggerRadius float32 + DestinationZoneID int32 + DestinationX float32 + DestinationY float32 + DestinationZ float32 DestinationHeading float32 - Cost int32 - UniqueID int32 - ForceZone bool + Cost int32 + UniqueID int32 + ForceZone bool } // Location represents a discoverable location @@ -509,4 +509,4 @@ type Location struct { } // Item placeholder - should import from items package -type Item = items.Item \ No newline at end of file +type Item = items.Item