diff --git a/cmd/login_server/README.md b/cmd/login_server/README.md deleted file mode 100644 index f7e1a7b..0000000 --- a/cmd/login_server/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# EQ2Go Login Server - -A modern Go implementation of the EverQuest II login server, providing client authentication, character management, and world server coordination. - -## Features - -- **Client Authentication**: MD5-hashed password authentication with account management -- **Character Management**: Character list, creation, deletion, and play requests -- **World Server Coordination**: Registration and status tracking of world servers -- **Web Administration**: HTTP interface for monitoring and management -- **Database Integration**: SQLite database with automatic table initialization -- **UDP Protocol**: EverQuest II compatible UDP protocol implementation - -## Quick Start - -### Building - -```bash -go build ./cmd/login_server -``` - -### Running - -```bash -# Run with defaults (creates login_config.json if missing) -./login_server - -# Run with custom configuration -./login_server -config custom_login.json - -# Run with overrides -./login_server -listen-port 6000 -web-port 8082 -db custom.db -``` - -### Configuration - -On first run, a default `login_config.json` will be created: - -```json -{ - "listen_addr": "0.0.0.0", - "listen_port": 5999, - "max_clients": 1000, - "web_addr": "0.0.0.0", - "web_port": 8081, - "database_path": "login.db", - "server_name": "EQ2Go Login Server", - "log_level": "info", - "world_servers": [] -} -``` - -## Web Interface - -Access the web administration interface at `http://localhost:8081` (or configured web_port). - -Features: -- Real-time server statistics -- Connected client monitoring -- World server status -- Client management (kick clients) - -## Database - -The login server uses SQLite by default with the following tables: - -- `login_accounts` - User account information -- `characters` - Character data for character selection -- `server_stats` - Server statistics and monitoring data - -## Command Line Options - -- `-config` - Path to configuration file (default: login_config.json) -- `-listen-addr` - Override listen address -- `-listen-port` - Override listen port -- `-web-port` - Override web interface port -- `-db` - Override database path -- `-log-level` - Override log level (debug, info, warn, error) -- `-name` - Override server name -- `-version` - Show version information - -## Architecture - -The login server follows the EQ2Go architecture patterns: - -- **Server**: Main server instance managing UDP connections and web interface -- **ClientList**: Thread-safe management of connected clients -- **WorldList**: Management of registered world servers -- **Database Integration**: Uses zombiezen SQLite with proper connection pooling -- **UDP Protocol**: Compatible with EverQuest II client expectations - -## Development - -The login server integrates with the broader EQ2Go ecosystem: - -- Uses `internal/udp` for EverQuest II protocol handling -- Uses `internal/database` for data persistence -- Follows Go concurrency patterns with proper synchronization -- Implements comprehensive error handling and logging - -## Next Steps - -To complete the login server implementation: - -1. Add character creation functionality -2. Add character deletion functionality -3. Implement world server communication protocol -4. Add user registration/account creation -5. Add password reset functionality -6. Add account management features \ No newline at end of file diff --git a/internal/achievements/achievements_test.go b/internal/achievements/achievements_test.go deleted file mode 100644 index b082f36..0000000 --- a/internal/achievements/achievements_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package achievements - -import ( - "testing" -) - -func TestNew(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestNewAchievement(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterList(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/achievements/benchmark_test.go b/internal/achievements/benchmark_test.go deleted file mode 100644 index 1533d90..0000000 --- a/internal/achievements/benchmark_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package achievements - -import ( - "testing" -) - -func BenchmarkAchievementCreation(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkMasterListOperations(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkConcurrentAccess(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} \ No newline at end of file diff --git a/internal/alt_advancement/alt_advancement_test.go b/internal/alt_advancement/alt_advancement_test.go deleted file mode 100644 index 1158a21..0000000 --- a/internal/alt_advancement/alt_advancement_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package alt_advancement - -import ( - "testing" -) - -func TestNew(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestNewAltAdvancement(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestAltAdvancementOperations(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestAltAdvancementValidation(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestAltAdvancementConcurrency(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/alt_advancement/benchmark_test.go b/internal/alt_advancement/benchmark_test.go deleted file mode 100644 index a749a38..0000000 --- a/internal/alt_advancement/benchmark_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package alt_advancement - -import ( - "testing" -) - -func BenchmarkAltAdvancementCreation(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkMasterListOperations(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkConcurrentAccess(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} \ No newline at end of file diff --git a/internal/appearances/appearance_test.go b/internal/appearances/appearance_test.go deleted file mode 100644 index 7462457..0000000 --- a/internal/appearances/appearance_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package appearances - -import ( - "testing" -) - -func TestNew(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestNewWithData(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestAppearanceGetters(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestAppearanceSetters(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestIsCompatibleWithClient(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestAppearanceClone(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestGetAppearanceTypeName(t *testing.T) { - // This test doesn't require database, so it can run - testCases := []struct { - typeConst int8 - expected string - }{ - {0, "Unknown"}, - {1, "Hair"}, - {2, "Face"}, - {3, "Wing"}, - {4, "Chest"}, - {5, "Legs"}, - {-1, "Unknown"}, - {100, "Unknown"}, - } - - for _, tt := range testCases { - t.Run("", func(t *testing.T) { - result := GetAppearanceTypeName(tt.typeConst) - if result != tt.expected { - t.Errorf("GetAppearanceTypeName(%d) = %q, want %q", tt.typeConst, result, tt.expected) - } - }) - } -} - -func TestMasterList(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListConcurrency(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/appearances/benchmark_test.go b/internal/appearances/benchmark_test.go deleted file mode 100644 index a87d226..0000000 --- a/internal/appearances/benchmark_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package appearances - -import ( - "testing" -) - -func BenchmarkAppearanceCreation(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkMasterListOperations(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkConcurrentAccess(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} \ No newline at end of file diff --git a/internal/chat/benchmark_test.go b/internal/chat/benchmark_test.go deleted file mode 100644 index 8a24051..0000000 --- a/internal/chat/benchmark_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package chat - -import ( - "testing" -) - -func BenchmarkChannelCreation(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkMasterListOperations(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkMessageRouting(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkConcurrentAccess(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkChannelMemory(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} \ No newline at end of file diff --git a/internal/chat/channel_test.go b/internal/chat/channel_test.go deleted file mode 100644 index 1b2ef0c..0000000 --- a/internal/chat/channel_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package chat - -import ( - "testing" -) - -func TestNew(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestChannelOperations(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestChannelMembers(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestChannelMessage(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestChannelPermissions(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestChannelConcurrency(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestChannelBatch(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/chat/master_test.go b/internal/chat/master_test.go deleted file mode 100644 index 609b58a..0000000 --- a/internal/chat/master_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package chat - -import ( - "testing" -) - -func TestNewMasterList(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListOperations(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListConcurrency(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListChannelManagement(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListUserManagement(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListMessageRouting(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListPermissions(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListEdgeCases(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListPerformance(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/classes/class_test.go b/internal/classes/class_test.go deleted file mode 100644 index 9825a17..0000000 --- a/internal/classes/class_test.go +++ /dev/null @@ -1,372 +0,0 @@ -package classes - -import ( - "testing" -) - -func TestGetClassID(t *testing.T) { - tests := []struct { - name string - input string - expected int8 - }{ - {"Uppercase", "WARRIOR", ClassWarrior}, - {"Lowercase", "warrior", ClassWarrior}, - {"Mixed case", "WaRrIoR", ClassWarrior}, - {"With spaces", " WARRIOR ", ClassWarrior}, - {"Invalid", "INVALID_CLASS", -1}, - {"Empty", "", -1}, - {"Tradeskill", "CARPENTER", ClassCarpenter}, - {"Special", "CHANNELER", ClassChanneler}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := GetClassID(tt.input) - if result != tt.expected { - t.Errorf("GetClassID(%q) = %d, want %d", tt.input, result, tt.expected) - } - }) - } -} - -func TestGetClassName(t *testing.T) { - tests := []struct { - classID int8 - expected string - }{ - {ClassWarrior, "WARRIOR"}, - {ClassPriest, "PRIEST"}, - {ClassCarpenter, "CARPENTER"}, - {ClassChanneler, "CHANNELER"}, - {-1, ""}, - {100, ""}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetClassName(tt.classID) - if result != tt.expected { - t.Errorf("GetClassName(%d) = %q, want %q", tt.classID, result, tt.expected) - } - }) - } -} - -func TestGetClassNameCase(t *testing.T) { - tests := []struct { - classID int8 - expected string - }{ - {ClassWarrior, "Warrior"}, - {ClassShadowknight, "Shadowknight"}, - {ClassTroubador, "Troubador"}, - {ClassCarpenter, "Carpenter"}, - {-1, ""}, - {100, ""}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetClassNameCase(tt.classID) - if result != tt.expected { - t.Errorf("GetClassNameCase(%d) = %q, want %q", tt.classID, result, tt.expected) - } - }) - } -} - -func TestGetBaseClass(t *testing.T) { - tests := []struct { - classID int8 - expected int8 - }{ - {ClassGuardian, ClassFighter}, - {ClassBerserker, ClassFighter}, - {ClassTemplar, ClassPriest}, - {ClassWizard, ClassMage}, - {ClassRanger, ClassScout}, - {ClassChanneler, ClassPriest}, - {ClassCommoner, ClassCommoner}, - {ClassFighter, ClassCommoner}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetBaseClass(tt.classID) - if result != tt.expected { - t.Errorf("GetBaseClass(%d) = %d, want %d", tt.classID, result, tt.expected) - } - }) - } -} - -func TestGetSecondaryBaseClass(t *testing.T) { - tests := []struct { - classID int8 - expected int8 - }{ - {ClassGuardian, ClassWarrior}, - {ClassBerserker, ClassWarrior}, - {ClassMonk, ClassBrawler}, - {ClassTemplar, ClassCleric}, - {ClassWizard, ClassSorcerer}, - {ClassRanger, ClassPredator}, - {ClassBeastlord, ClassAnimalist}, - {ClassChanneler, ClassShaper}, - {ClassWarrior, ClassCommoner}, // No secondary - {ClassFighter, ClassCommoner}, // No secondary - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetSecondaryBaseClass(tt.classID) - if result != tt.expected { - t.Errorf("GetSecondaryBaseClass(%d) = %d, want %d", tt.classID, result, tt.expected) - } - }) - } -} - -func TestGetTSBaseClass(t *testing.T) { - tests := []struct { - classID int8 - expected int8 - }{ - {3, 1}, // Guardian (3+42=45 >= ClassArtisan) returns ClassArtisan-44 = 1 - {10, 1}, // Paladin (10+42=52 >= ClassArtisan) returns ClassArtisan-44 = 1 - {0, 0}, // Commoner (0+42=42 < ClassArtisan) returns 0 - {1, 1}, // Fighter (1+42=43 < ClassArtisan) returns 1 - {2, 2}, // Warrior (2+42=44 < ClassArtisan) returns 2 - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetTSBaseClass(tt.classID) - if result != tt.expected { - t.Errorf("GetTSBaseClass(%d) = %d, want %d", tt.classID, result, tt.expected) - } - }) - } -} - -func TestIsValidClassID(t *testing.T) { - tests := []struct { - classID int8 - expected bool - }{ - {ClassCommoner, true}, - {ClassWarrior, true}, - {ClassAlchemist, true}, - {-1, false}, - {100, false}, - {58, false}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := IsValidClassID(tt.classID) - if result != tt.expected { - t.Errorf("IsValidClassID(%d) = %v, want %v", tt.classID, result, tt.expected) - } - }) - } -} - -func TestIsAdventureClass(t *testing.T) { - tests := []struct { - classID int8 - expected bool - }{ - {ClassCommoner, true}, - {ClassWarrior, true}, - {ClassChanneler, true}, - {ClassArtisan, false}, - {ClassCarpenter, false}, - {-1, false}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := IsAdventureClass(tt.classID) - if result != tt.expected { - t.Errorf("IsAdventureClass(%d) = %v, want %v", tt.classID, result, tt.expected) - } - }) - } -} - -func TestIsTradeskillClass(t *testing.T) { - tests := []struct { - classID int8 - expected bool - }{ - {ClassArtisan, true}, - {ClassCarpenter, true}, - {ClassAlchemist, true}, - {ClassWarrior, false}, - {ClassCommoner, false}, - {-1, false}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := IsTradeskillClass(tt.classID) - if result != tt.expected { - t.Errorf("IsTradeskillClass(%d) = %v, want %v", tt.classID, result, tt.expected) - } - }) - } -} - -func TestGetClassType(t *testing.T) { - tests := []struct { - classID int8 - expected string - }{ - {ClassWarrior, ClassTypeAdventure}, - {ClassChanneler, ClassTypeAdventure}, - {ClassArtisan, ClassTypeTradeskill}, - {ClassCarpenter, ClassTypeTradeskill}, - {-1, ClassTypeSpecial}, - {100, ClassTypeSpecial}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetClassType(tt.classID) - if result != tt.expected { - t.Errorf("GetClassType(%d) = %q, want %q", tt.classID, result, tt.expected) - } - }) - } -} - -func TestGetAllClasses(t *testing.T) { - classes := GetAllClasses() - - // Check we have the right number of classes - if len(classes) != 58 { - t.Errorf("GetAllClasses() returned %d classes, want 58", len(classes)) - } - - // Check a few specific classes - if name, ok := classes[ClassWarrior]; !ok || name != "Warrior" { - t.Errorf("GetAllClasses()[ClassWarrior] = %q, %v; want 'Warrior', true", name, ok) - } - - if name, ok := classes[ClassCarpenter]; !ok || name != "Carpenter" { - t.Errorf("GetAllClasses()[ClassCarpenter] = %q, %v; want 'Carpenter', true", name, ok) - } -} - -func TestGetClassInfo(t *testing.T) { - // Test valid class - info := GetClassInfo(ClassGuardian) - if !info["valid"].(bool) { - t.Error("GetClassInfo(ClassGuardian) should be valid") - } - if info["class_id"].(int8) != ClassGuardian { - t.Errorf("GetClassInfo(ClassGuardian).class_id = %v, want %d", info["class_id"], ClassGuardian) - } - if info["name"].(string) != "GUARDIAN" { - t.Errorf("GetClassInfo(ClassGuardian).name = %v, want 'GUARDIAN'", info["name"]) - } - if info["display_name"].(string) != "Guardian" { - t.Errorf("GetClassInfo(ClassGuardian).display_name = %v, want 'Guardian'", info["display_name"]) - } - if info["base_class"].(int8) != ClassFighter { - t.Errorf("GetClassInfo(ClassGuardian).base_class = %v, want %d", info["base_class"], ClassFighter) - } - if info["secondary_base_class"].(int8) != ClassWarrior { - t.Errorf("GetClassInfo(ClassGuardian).secondary_base_class = %v, want %d", info["secondary_base_class"], ClassWarrior) - } - if !info["is_adventure"].(bool) { - t.Error("GetClassInfo(ClassGuardian).is_adventure should be true") - } - if info["is_tradeskill"].(bool) { - t.Error("GetClassInfo(ClassGuardian).is_tradeskill should be false") - } - - // Test invalid class - info = GetClassInfo(-1) - if info["valid"].(bool) { - t.Error("GetClassInfo(-1) should be invalid") - } -} - -func TestGetClassHierarchy(t *testing.T) { - tests := []struct { - classID int8 - expected []int8 - }{ - {ClassGuardian, []int8{ClassGuardian, ClassWarrior, ClassFighter, ClassCommoner}}, - {ClassWizard, []int8{ClassWizard, ClassSorcerer, ClassMage, ClassCommoner}}, - {ClassFighter, []int8{ClassFighter, ClassCommoner}}, - {ClassCommoner, []int8{ClassCommoner}}, - {-1, nil}, - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := GetClassHierarchy(tt.classID) - if len(result) != len(tt.expected) { - t.Errorf("GetClassHierarchy(%d) = %v, want %v", tt.classID, result, tt.expected) - return - } - for i, id := range result { - if id != tt.expected[i] { - t.Errorf("GetClassHierarchy(%d)[%d] = %d, want %d", tt.classID, i, id, tt.expected[i]) - } - } - }) - } -} - -func TestIsSameArchetype(t *testing.T) { - tests := []struct { - classID1 int8 - classID2 int8 - expected bool - }{ - {ClassGuardian, ClassBerserker, true}, // Both Fighter archetype - {ClassGuardian, ClassMonk, true}, // Both Fighter archetype - {ClassWizard, ClassWarlock, true}, // Both Mage archetype - {ClassGuardian, ClassWizard, false}, // Different archetypes - {ClassCommoner, ClassCommoner, true}, // Same class - {-1, ClassGuardian, false}, // Invalid class - {ClassGuardian, -1, false}, // Invalid class - } - - for _, tt := range tests { - t.Run("", func(t *testing.T) { - result := IsSameArchetype(tt.classID1, tt.classID2) - if result != tt.expected { - t.Errorf("IsSameArchetype(%d, %d) = %v, want %v", tt.classID1, tt.classID2, result, tt.expected) - } - }) - } -} - -func TestGetArchetypeClasses(t *testing.T) { - // Test Fighter archetype - fighterClasses := GetArchetypeClasses(ClassFighter) - expectedFighterCount := 9 // Warrior through Paladin (2-10) - if len(fighterClasses) != expectedFighterCount { - t.Errorf("GetArchetypeClasses(ClassFighter) returned %d classes, want %d", len(fighterClasses), expectedFighterCount) - } - - // Verify all returned classes are fighters - for _, classID := range fighterClasses { - if GetBaseClass(classID) != ClassFighter { - t.Errorf("GetArchetypeClasses(ClassFighter) returned non-fighter class %d", classID) - } - } - - // Test Mage archetype - mageClasses := GetArchetypeClasses(ClassMage) - expectedMageCount := 9 // Sorcerer through Necromancer (22-30) - if len(mageClasses) != expectedMageCount { - t.Errorf("GetArchetypeClasses(ClassMage) returned %d classes, want %d", len(mageClasses), expectedMageCount) - } -} \ No newline at end of file diff --git a/internal/collections/benchmark_test.go b/internal/collections/benchmark_test.go deleted file mode 100644 index c2fd9bf..0000000 --- a/internal/collections/benchmark_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package collections - -import ( - "testing" -) - -func BenchmarkCollectionCreation(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkMasterListOperations(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkCollectionMemory(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkConcurrentAccess(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} - -func BenchmarkCollectionSearch(b *testing.B) { - b.Skip("Skipping benchmark - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement benchmarks -} \ No newline at end of file diff --git a/internal/collections/collection_test.go b/internal/collections/collection_test.go deleted file mode 100644 index fe4f23c..0000000 --- a/internal/collections/collection_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package collections - -import ( - "testing" -) - -func TestNew(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestNewWithData(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestCollectionGetters(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestCollectionSetters(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestCollectionConcurrency(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestCollectionThreadSafety(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestCollectionBatch(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/collections/master_test.go b/internal/collections/master_test.go deleted file mode 100644 index 0ac47f0..0000000 --- a/internal/collections/master_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package collections - -import ( - "testing" -) - -func TestNewMasterList(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListOperations(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListConcurrency(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListFiltering(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListBatchOperations(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListSearch(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListMemoryUsage(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListPerformance(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} - -func TestMasterListEdgeCases(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database and implement tests -} \ No newline at end of file diff --git a/internal/commands/README.md b/internal/commands/README.md deleted file mode 100644 index 2908fe1..0000000 --- a/internal/commands/README.md +++ /dev/null @@ -1,327 +0,0 @@ -# EQ2Go Command System - -The EQ2Go command system provides a comprehensive framework for handling player commands, admin commands, and console commands in the EverQuest II server emulator. This system is converted from the original C++ EQ2EMu codebase while leveraging modern Go patterns and practices. - -## Architecture Overview - -The command system consists of several key components: - -- **CommandManager**: Central registry for all commands with thread-safe operations -- **CommandContext**: Execution context providing access to players, zones, arguments, and messaging -- **Command Types**: Different command categories (Player, Admin, Console, etc.) -- **Subcommand Support**: Hierarchical command structure for complex operations - -## Core Components - -### CommandManager - -The `CommandManager` is the central hub that: -- Registers commands by name with case-insensitive lookup -- Manages subcommands under parent commands -- Executes commands with proper admin level checking -- Parses raw command strings into structured contexts - -### CommandContext - -The `CommandContext` provides: -- Type-safe argument parsing (string, int, float, bool) -- Message queuing with channel and color support -- Validation helpers for argument counts and requirements -- Access to game objects (player, zone, target) -- Result storage for complex operations - -### Command Types - -Commands are categorized by type: - -- **Player Commands** (`CommandTypePlayer`): Basic player actions like `/say`, `/tell`, `/who` -- **Admin Commands** (`CommandTypeAdmin`): GM/Admin tools like `/kick`, `/ban`, `/summon` -- **Console Commands** (`CommandTypeConsole`): Server management from console -- **Specialized Types**: Spawn, Zone, Guild, Item, and Quest commands - -## Admin Levels - -The system supports four admin levels: - -- **Player** (0): Basic player commands -- **Guide** (100): Helper/guide commands -- **GM** (200): Game master commands -- **Admin** (300): Full administrative access - -## Usage Examples - -### Basic Command Registration - -```go -// Initialize the command system -cm, err := InitializeCommands() -if err != nil { - log.Fatalf("Failed to initialize commands: %v", err) -} - -// Register a custom command -customCmd := &Command{ - Name: "custom", - Type: CommandTypePlayer, - Description: "A custom command", - Usage: "/custom ", - RequiredLevel: AdminLevelPlayer, - Handler: func(ctx *CommandContext) error { - if err := ctx.ValidateArgumentCount(1, 1); err != nil { - ctx.AddErrorMessage("Usage: /custom ") - return nil - } - - arg := ctx.Arguments[0] - ctx.AddStatusMessage(fmt.Sprintf("You used custom command with: %s", arg)) - return nil - }, -} - -err = cm.Register(customCmd) -if err != nil { - log.Printf("Failed to register custom command: %v", err) -} -``` - -### Executing Commands - -```go -// Parse and execute a command from a client -err := ExecuteCommand(cm, "/say Hello everyone!", client) -if err != nil { - log.Printf("Command execution failed: %v", err) -} -``` - -### Subcommand Registration - -```go -// Register a parent command -parentCmd := &Command{ - Name: "manage", - Type: CommandTypeAdmin, - RequiredLevel: AdminLevelGM, - Handler: func(ctx *CommandContext) error { - ctx.AddErrorMessage("Usage: /manage [options]") - return nil - }, -} -cm.Register(parentCmd) - -// Register subcommands -playerSubCmd := &Command{ - Name: "player", - Type: CommandTypeAdmin, - RequiredLevel: AdminLevelGM, - Handler: func(ctx *CommandContext) error { - // Handle player management - return nil - }, -} -cm.RegisterSubCommand("manage", playerSubCmd) -``` - -## Command Implementation - -### Player Commands - -Located in `player.go`, these include: - -- **Communication**: `/say`, `/tell`, `/yell`, `/shout`, `/ooc`, `/emote` -- **Group Management**: `/group`, `/groupsay`, `/gsay` -- **Guild Management**: `/guild`, `/guildsay`, `/officersay` -- **Information**: `/who`, `/time`, `/location`, `/consider` -- **Character**: `/afk`, `/anon`, `/lfg`, `/inventory` -- **Utilities**: `/help`, `/quit`, `/trade`, `/quest` - -### Admin Commands - -Located in `admin.go`, these include: - -- **Player Management**: `/kick`, `/ban`, `/unban`, `/summon`, `/goto` -- **Communication**: `/broadcast`, `/announce` -- **Spawn Management**: `/spawn`, `/npc` -- **Item Management**: `/item`, `/giveitem` -- **Character Modification**: `/modify`, `/setlevel`, `/setclass` -- **Server Management**: `/reload`, `/shutdown`, `/cancelshutdown` -- **GM Utilities**: `/invisible`, `/invulnerable`, `/speed`, `/flymode` -- **Information**: `/info`, `/version` - -### Console Commands - -Located in `console.go`, these include prefixed versions of admin commands for console use: - -- **Player Management**: `console_ban`, `console_kick`, `console_unban` -- **Communication**: `console_broadcast`, `console_announce`, `console_tell` -- **Information**: `console_guild`, `player`, `console_zone`, `console_who` -- **Server Management**: `console_reload`, `console_shutdown`, `exit` -- **MOTD Management**: `getmotd`, `setmotd` -- **Rules Management**: `rules` - -## Message Channels and Colors - -Commands can send messages through various channels: - -### Channels -- `ChannelSay` (8): Local say messages -- `ChannelTell` (28): Private messages -- `ChannelBroadcast` (92): Server-wide broadcasts -- `ChannelError` (3): Error messages -- `ChannelStatus` (4): Status updates - -### Colors -- `ColorWhite` (254): Standard text -- `ColorRed` (3): Errors and warnings -- `ColorYellow` (5): Status messages -- `ColorChatRelation` (4): Relationship text - -## Error Handling - -The command system provides comprehensive error handling: - -```go -func HandleExampleCommand(ctx *CommandContext) error { - // Validate player is present - if err := ctx.RequirePlayer(); err != nil { - return err - } - - // Validate argument count - if err := ctx.ValidateArgumentCount(1, 3); err != nil { - ctx.AddErrorMessage("Usage: /example [optional1] [optional2]") - return nil - } - - // Validate admin level - if err := ctx.RequireAdminLevel(AdminLevelGM); err != nil { - return err - } - - // Command logic here... - return nil -} -``` - -## Testing - -The command system includes comprehensive tests in `commands_test.go`: - -```bash -# Run all command tests -go test ./internal/commands -v - -# Run specific test -go test ./internal/commands -run TestCommandManager_Execute -``` - -Tests cover: -- Command registration and retrieval -- Argument parsing and validation -- Message handling -- Admin level checking -- Subcommand functionality -- Error conditions - -## Integration with Server - -To integrate the command system with the server: - -```go -// Initialize commands during server startup -commandManager, err := commands.InitializeCommands() -if err != nil { - log.Fatalf("Failed to initialize commands: %v", err) -} - -// Handle incoming command from client -func handleClientCommand(client ClientInterface, rawCommand string) { - err := commands.ExecuteCommand(commandManager, rawCommand, client) - if err != nil { - log.Printf("Command execution error: %v", err) - } -} -``` - -## Thread Safety - -The command system is designed to be thread-safe: -- CommandManager uses read/write mutexes for concurrent access -- CommandContext uses mutexes for message and result storage -- All operations are safe for concurrent use - -## Extension Points - -The system is designed for easy extension: - -### Custom Command Types -```go -const CommandTypeCustom CommandType = 100 - -func (ct CommandType) String() string { - switch ct { - case CommandTypeCustom: - return "custom" - default: - return CommandType(ct).String() - } -} -``` - -### Custom Handlers -```go -func MyCustomHandler(ctx *CommandContext) error { - // Custom command logic - ctx.AddStatusMessage("Custom command executed!") - return nil -} -``` - -## Performance Considerations - -- Commands are indexed by name in a hash map for O(1) lookup -- Case-insensitive matching uses normalized lowercase keys -- Message batching reduces client communication overhead -- Argument parsing is lazy and type-safe - -## Migration from C++ - -Key differences from the original C++ implementation: - -1. **Type Safety**: Go's type system prevents many runtime errors -2. **Memory Management**: Automatic garbage collection eliminates memory leaks -3. **Concurrency**: Native goroutine support for concurrent operations -4. **Error Handling**: Explicit error returns instead of exceptions -5. **Testing**: Built-in testing framework with comprehensive coverage - -## Future Enhancements - -Planned improvements: - -- Lua script integration for dynamic commands -- Command aliasing system -- Advanced permission system -- Command cooldowns and rate limiting -- Audit logging for admin commands -- Dynamic command loading/unloading - -## Troubleshooting - -### Common Issues - -**Command Not Found**: Ensure command is registered and name matches exactly -**Insufficient Privileges**: Check admin level requirements -**Argument Validation**: Use proper argument count validation -**Interface Errors**: Ensure client implements ClientInterface correctly - -### Debug Output - -Enable debug output for command execution: -```go -ctx.AddDefaultMessage(fmt.Sprintf("Debug: Command %s executed with %d args", - ctx.CommandName, ctx.ArgumentCount())) -``` - -## Conclusion - -The EQ2Go command system provides a robust, extensible framework for handling all types of game commands while maintaining compatibility with the original EverQuest II protocol. The system's modular design and comprehensive testing ensure reliability and ease of maintenance. \ No newline at end of file diff --git a/internal/commands/commands_test.go b/internal/commands/commands_test.go deleted file mode 100644 index 74e4234..0000000 --- a/internal/commands/commands_test.go +++ /dev/null @@ -1,614 +0,0 @@ -package commands - -import ( - "testing" - - "eq2emu/internal/entity" - "eq2emu/internal/spawn" -) - -// Mock implementations for testing - -type mockClient struct { - player *entity.Entity - accountID int32 - characterID int32 - adminLevel int - name string - zone *mockZone - messages []mockMessage -} - -type mockMessage struct { - channel int - color int - message string -} - -func (mc *mockClient) GetPlayer() *entity.Entity { return mc.player } -func (mc *mockClient) GetAccountID() int32 { return mc.accountID } -func (mc *mockClient) GetCharacterID() int32 { return mc.characterID } -func (mc *mockClient) GetAdminLevel() int { return mc.adminLevel } -func (mc *mockClient) GetName() string { return mc.name } -func (mc *mockClient) IsInZone() bool { return mc.zone != nil } -func (mc *mockClient) GetZone() ZoneInterface { return mc.zone } -func (mc *mockClient) SendMessage(channel, color int, message string) { - mc.messages = append(mc.messages, mockMessage{channel, color, message}) -} -func (mc *mockClient) SendPopupMessage(message string) {} -func (mc *mockClient) Disconnect() {} - -type mockZone struct { - id int32 - name string - description string - players []*entity.Entity -} - -func (mz *mockZone) GetID() int32 { return mz.id } -func (mz *mockZone) GetName() string { return mz.name } -func (mz *mockZone) GetDescription() string { return mz.description } -func (mz *mockZone) GetPlayers() []*entity.Entity { return mz.players } -func (mz *mockZone) Shutdown() {} -func (mz *mockZone) SendZoneMessage(channel, color int, message string) {} -func (mz *mockZone) GetSpawnByName(name string) *spawn.Spawn { return nil } -func (mz *mockZone) GetSpawnByID(id int32) *spawn.Spawn { return nil } - -func TestCommandManager_Register(t *testing.T) { - cm := NewCommandManager() - - // Test registering a valid command - cmd := &Command{ - Name: "test", - Type: CommandTypePlayer, - Description: "Test command", - Usage: "/test", - RequiredLevel: AdminLevelPlayer, - Handler: func(ctx *CommandContext) error { - ctx.AddStatusMessage("Test executed") - return nil - }, - } - - err := cm.Register(cmd) - if err != nil { - t.Errorf("Failed to register valid command: %v", err) - } - - // Test registering duplicate command - err = cm.Register(cmd) - if err == nil { - t.Error("Expected error when registering duplicate command") - } - - // Test registering nil command - err = cm.Register(nil) - if err == nil { - t.Error("Expected error when registering nil command") - } - - // Test registering command with empty name - invalidCmd := &Command{ - Name: "", - Handler: func(ctx *CommandContext) error { return nil }, - } - err = cm.Register(invalidCmd) - if err == nil { - t.Error("Expected error when registering command with empty name") - } - - // Test registering command with nil handler - invalidCmd2 := &Command{ - Name: "invalid", - Handler: nil, - } - err = cm.Register(invalidCmd2) - if err == nil { - t.Error("Expected error when registering command with nil handler") - } -} - -func TestCommandManager_HasCommand(t *testing.T) { - cm := NewCommandManager() - - cmd := &Command{ - Name: "testcmd", - Type: CommandTypePlayer, - RequiredLevel: AdminLevelPlayer, - Handler: func(ctx *CommandContext) error { return nil }, - } - - // Test command doesn't exist initially - if cm.HasCommand("testcmd") { - t.Error("Command should not exist initially") - } - - // Register command - err := cm.Register(cmd) - if err != nil { - t.Fatalf("Failed to register command: %v", err) - } - - // Test command exists after registration - if !cm.HasCommand("testcmd") { - t.Error("Command should exist after registration") - } - - // Test case insensitivity - if !cm.HasCommand("TESTCMD") { - t.Error("Command lookup should be case insensitive") - } -} - -func TestCommandManager_GetCommand(t *testing.T) { - cm := NewCommandManager() - - cmd := &Command{ - Name: "getcmd", - Type: CommandTypeAdmin, - RequiredLevel: AdminLevelGM, - Handler: func(ctx *CommandContext) error { return nil }, - } - - // Test getting non-existent command - _, exists := cm.GetCommand("getcmd") - if exists { - t.Error("Command should not exist initially") - } - - // Register command - err := cm.Register(cmd) - if err != nil { - t.Fatalf("Failed to register command: %v", err) - } - - // Test getting existing command - retrievedCmd, exists := cm.GetCommand("getcmd") - if !exists { - t.Error("Command should exist after registration") - } - - if retrievedCmd.Name != "getcmd" { - t.Errorf("Expected command name 'getcmd', got '%s'", retrievedCmd.Name) - } - - if retrievedCmd.Type != CommandTypeAdmin { - t.Errorf("Expected command type Admin, got %v", retrievedCmd.Type) - } - - // Test case insensitivity - _, exists = cm.GetCommand("GETCMD") - if !exists { - t.Error("Command lookup should be case insensitive") - } -} - -func TestCommandManager_Execute(t *testing.T) { - cm := NewCommandManager() - - executed := false - cmd := &Command{ - Name: "exectest", - Type: CommandTypePlayer, - RequiredLevel: AdminLevelPlayer, - Handler: func(ctx *CommandContext) error { - executed = true - ctx.AddStatusMessage("Command executed successfully") - return nil - }, - } - - err := cm.Register(cmd) - if err != nil { - t.Fatalf("Failed to register command: %v", err) - } - - // Test successful execution - ctx := NewCommandContext(CommandTypePlayer, "exectest", []string{}) - ctx.AdminLevel = AdminLevelPlayer - - err = cm.Execute(ctx) - if err != nil { - t.Errorf("Command execution failed: %v", err) - } - - if !executed { - t.Error("Command handler was not executed") - } - - // Test insufficient admin level - ctx2 := NewCommandContext(CommandTypePlayer, "exectest", []string{}) - ctx2.AdminLevel = 0 - - // Register admin-only command - adminCmd := &Command{ - Name: "admintest", - Type: CommandTypeAdmin, - RequiredLevel: AdminLevelAdmin, - Handler: func(ctx *CommandContext) error { - return nil - }, - } - - err = cm.Register(adminCmd) - if err != nil { - t.Fatalf("Failed to register admin command: %v", err) - } - - ctx3 := NewCommandContext(CommandTypeAdmin, "admintest", []string{}) - ctx3.AdminLevel = AdminLevelPlayer - - err = cm.Execute(ctx3) - if err == nil { - t.Error("Expected error for insufficient admin level") - } - - // Test unknown command - ctx4 := NewCommandContext(CommandTypePlayer, "unknown", []string{}) - err = cm.Execute(ctx4) - if err == nil { - t.Error("Expected error for unknown command") - } -} - -func TestCommandManager_RegisterSubCommand(t *testing.T) { - cm := NewCommandManager() - - // Register parent command - parentCmd := &Command{ - Name: "parent", - Type: CommandTypeAdmin, - RequiredLevel: AdminLevelGM, - Handler: func(ctx *CommandContext) error { - return nil - }, - } - - err := cm.Register(parentCmd) - if err != nil { - t.Fatalf("Failed to register parent command: %v", err) - } - - // Register subcommand - subCmd := &Command{ - Name: "sub", - Type: CommandTypeAdmin, - RequiredLevel: AdminLevelGM, - Handler: func(ctx *CommandContext) error { - ctx.AddStatusMessage("Subcommand executed") - return nil - }, - } - - err = cm.RegisterSubCommand("parent", subCmd) - if err != nil { - t.Errorf("Failed to register subcommand: %v", err) - } - - // Test registering subcommand for non-existent parent - err = cm.RegisterSubCommand("nonexistent", subCmd) - if err == nil { - t.Error("Expected error when registering subcommand for non-existent parent") - } - - // Test registering duplicate subcommand - err = cm.RegisterSubCommand("parent", subCmd) - if err == nil { - t.Error("Expected error when registering duplicate subcommand") - } -} - -func TestCommandManager_ParseCommand(t *testing.T) { - cm := NewCommandManager() - - client := &mockClient{ - name: "TestPlayer", - adminLevel: AdminLevelPlayer, - } - - // Test parsing valid command - ctx := cm.ParseCommand("/say Hello world", client) - if ctx == nil { - t.Error("Expected non-nil context for valid command") - } - - if ctx.CommandName != "say" { - t.Errorf("Expected command name 'say', got '%s'", ctx.CommandName) - } - - if len(ctx.Arguments) != 2 { - t.Errorf("Expected 2 arguments, got %d", len(ctx.Arguments)) - } - - if ctx.Arguments[0] != "Hello" || ctx.Arguments[1] != "world" { - t.Errorf("Unexpected arguments: %v", ctx.Arguments) - } - - if ctx.RawArguments != "Hello world" { - t.Errorf("Expected raw arguments 'Hello world', got '%s'", ctx.RawArguments) - } - - // Test parsing command without slash - ctx2 := cm.ParseCommand("tell Player Hi there", client) - if ctx2 == nil { - t.Error("Expected non-nil context for command without slash") - } - - if ctx2.CommandName != "tell" { - t.Errorf("Expected command name 'tell', got '%s'", ctx2.CommandName) - } - - // Test parsing empty command - ctx3 := cm.ParseCommand("", client) - if ctx3 != nil { - t.Error("Expected nil context for empty command") - } - - // Test parsing whitespace-only command - ctx4 := cm.ParseCommand(" ", client) - if ctx4 != nil { - t.Error("Expected nil context for whitespace-only command") - } -} - -func TestCommandContext_ValidateArgumentCount(t *testing.T) { - ctx := NewCommandContext(CommandTypePlayer, "test", []string{"arg1", "arg2", "arg3"}) - - // Test valid argument count (exactly 3) - err := ctx.ValidateArgumentCount(3, 3) - if err != nil { - t.Errorf("Expected no error for exact argument count, got: %v", err) - } - - // Test valid argument count (range 2-4) - err = ctx.ValidateArgumentCount(2, 4) - if err != nil { - t.Errorf("Expected no error for argument count in range, got: %v", err) - } - - // Test too few arguments - err = ctx.ValidateArgumentCount(5, 6) - if err == nil { - t.Error("Expected error for too few arguments") - } - - // Test too many arguments - err = ctx.ValidateArgumentCount(1, 2) - if err == nil { - t.Error("Expected error for too many arguments") - } - - // Test unlimited max arguments (-1) - err = ctx.ValidateArgumentCount(2, -1) - if err != nil { - t.Errorf("Expected no error for unlimited max arguments, got: %v", err) - } -} - -func TestCommandContext_GetArgument(t *testing.T) { - ctx := NewCommandContext(CommandTypePlayer, "test", []string{"first", "second", "third"}) - - // Test valid indices - arg, exists := ctx.GetArgument(0) - if !exists || arg != "first" { - t.Errorf("Expected 'first', got '%s' (exists: %v)", arg, exists) - } - - arg, exists = ctx.GetArgument(2) - if !exists || arg != "third" { - t.Errorf("Expected 'third', got '%s' (exists: %v)", arg, exists) - } - - // Test invalid indices - _, exists = ctx.GetArgument(-1) - if exists { - t.Error("Expected false for negative index") - } - - _, exists = ctx.GetArgument(3) - if exists { - t.Error("Expected false for out-of-bounds index") - } -} - -func TestCommandContext_GetArgumentInt(t *testing.T) { - ctx := NewCommandContext(CommandTypePlayer, "test", []string{"123", "not_a_number", "-456"}) - - // Test valid integer - val := ctx.GetArgumentInt(0, 999) - if val != 123 { - t.Errorf("Expected 123, got %d", val) - } - - // Test invalid integer (should return default) - val = ctx.GetArgumentInt(1, 999) - if val != 999 { - t.Errorf("Expected default value 999, got %d", val) - } - - // Test negative integer - val = ctx.GetArgumentInt(2, 999) - if val != -456 { - t.Errorf("Expected -456, got %d", val) - } - - // Test out-of-bounds index (should return default) - val = ctx.GetArgumentInt(5, 999) - if val != 999 { - t.Errorf("Expected default value 999, got %d", val) - } -} - -func TestCommandContext_GetArgumentFloat(t *testing.T) { - ctx := NewCommandContext(CommandTypePlayer, "test", []string{"12.34", "not_a_number", "-56.78"}) - - // Test valid float - val := ctx.GetArgumentFloat(0, 999.0) - if val != 12.34 { - t.Errorf("Expected 12.34, got %f", val) - } - - // Test invalid float (should return default) - val = ctx.GetArgumentFloat(1, 999.0) - if val != 999.0 { - t.Errorf("Expected default value 999.0, got %f", val) - } - - // Test negative float - val = ctx.GetArgumentFloat(2, 999.0) - if val != -56.78 { - t.Errorf("Expected -56.78, got %f", val) - } -} - -func TestCommandContext_GetArgumentBool(t *testing.T) { - ctx := NewCommandContext(CommandTypePlayer, "test", - []string{"true", "false", "yes", "no", "on", "off", "1", "0", "invalid"}) - - tests := []struct { - index int - expected bool - }{ - {0, true}, // "true" - {1, false}, // "false" - {2, true}, // "yes" - {3, false}, // "no" - {4, true}, // "on" - {5, false}, // "off" - {6, true}, // "1" - {7, false}, // "0" - } - - for _, test := range tests { - val := ctx.GetArgumentBool(test.index, !test.expected) // Use opposite as default - if val != test.expected { - t.Errorf("Index %d: expected %v, got %v", test.index, test.expected, val) - } - } - - // Test invalid boolean (should return default) - val := ctx.GetArgumentBool(8, true) - if val != true { - t.Errorf("Expected default value true for invalid boolean, got %v", val) - } -} - -func TestCommandContext_Messages(t *testing.T) { - ctx := NewCommandContext(CommandTypePlayer, "test", []string{}) - - // Test adding messages - ctx.AddDefaultMessage("Default message") - ctx.AddErrorMessage("Error message") - ctx.AddStatusMessage("Status message") - ctx.AddMessage(ChannelSay, ColorWhite, "Custom message") - - if len(ctx.Messages) != 4 { - t.Errorf("Expected 4 messages, got %d", len(ctx.Messages)) - } - - // Check message types - expectedMessages := []struct { - channel int - color int - message string - }{ - {ChannelDefault, ColorWhite, "Default message"}, - {ChannelError, ColorRed, "Error message"}, - {ChannelStatus, ColorYellow, "Status message"}, - {ChannelSay, ColorWhite, "Custom message"}, - } - - for i, expected := range expectedMessages { - msg := ctx.Messages[i] - if msg.Channel != expected.channel { - t.Errorf("Message %d: expected channel %d, got %d", i, expected.channel, msg.Channel) - } - if msg.Color != expected.color { - t.Errorf("Message %d: expected color %d, got %d", i, expected.color, msg.Color) - } - if msg.Message != expected.message { - t.Errorf("Message %d: expected message '%s', got '%s'", i, expected.message, msg.Message) - } - } - - // Test clearing messages - ctx.ClearMessages() - if len(ctx.Messages) != 0 { - t.Errorf("Expected 0 messages after clearing, got %d", len(ctx.Messages)) - } -} - -func TestInitializeCommands(t *testing.T) { - cm, err := InitializeCommands() - if err != nil { - t.Fatalf("Failed to initialize commands: %v", err) - } - - if cm == nil { - t.Fatal("Expected non-nil command manager") - } - - // Test that some basic commands are registered - expectedCommands := []string{"say", "tell", "kick", "ban", "reload", "shutdown", "console_ban", "console_kick"} - - for _, cmdName := range expectedCommands { - if !cm.HasCommand(cmdName) { - t.Errorf("Expected command '%s' to be registered", cmdName) - } - } - - // Test command counts by type - playerCommands := cm.ListCommandsByType(CommandTypePlayer) - adminCommands := cm.ListCommandsByType(CommandTypeAdmin) - consoleCommands := cm.ListCommandsByType(CommandTypeConsole) - - if len(playerCommands) == 0 { - t.Error("Expected player commands to be registered") - } - - if len(adminCommands) == 0 { - t.Error("Expected admin commands to be registered") - } - - if len(consoleCommands) == 0 { - t.Error("Expected console commands to be registered") - } -} - -func TestGetAvailableCommands(t *testing.T) { - cm, err := InitializeCommands() - if err != nil { - t.Fatalf("Failed to initialize commands: %v", err) - } - - // Test player level commands - playerCommands := GetAvailableCommands(cm, AdminLevelPlayer) - if len(playerCommands) == 0 { - t.Error("Expected player-level commands to be available") - } - - // Test admin level commands - adminCommands := GetAvailableCommands(cm, AdminLevelAdmin) - if len(adminCommands) <= len(playerCommands) { - t.Error("Expected admin to have more commands available than player") - } - - // Verify all player commands are also available to admin - playerCommandMap := make(map[string]bool) - for _, cmd := range playerCommands { - playerCommandMap[cmd] = true - } - - for _, cmd := range playerCommands { - found := false - for _, adminCmd := range adminCommands { - if adminCmd == cmd { - found = true - break - } - } - if !found { - t.Errorf("Player command '%s' should be available to admin", cmd) - } - } -} \ No newline at end of file diff --git a/internal/database/database_test.go b/internal/database/database_test.go deleted file mode 100644 index fea0b47..0000000 --- a/internal/database/database_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package database - -import ( - "testing" - - _ "github.com/go-sql-driver/mysql" -) - -func TestNewMySQL(t *testing.T) { - // Skip this test if no MySQL test database is available - t.Skip("Skipping MySQL test - requires MySQL test database") - - // Example test for when MySQL is available: - // db, err := NewMySQL("test_user:test_pass@tcp(localhost:3306)/test_db") - // if err != nil { - // t.Fatalf("Failed to create MySQL database: %v", err) - // } - // defer db.Close() - // - // // Test database type - // if db.GetType() != MySQL { - // t.Errorf("Expected MySQL database type, got %v", db.GetType()) - // } -} - -func TestConfigValidation(t *testing.T) { - tests := []struct { - name string - config Config - wantErr bool - }{ - { - name: "valid_mysql_config", - config: Config{ - DSN: "user:password@tcp(localhost:3306)/database", - }, - wantErr: false, // Will fail without actual MySQL, but config is valid - }, - { - name: "empty_dsn", - config: Config{ - DSN: "", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - db, _ := New(tt.config) - // We expect connection errors since we don't have a test MySQL - // but we can test that the configuration is handled properly - if db != nil { - db.Close() - } - // For now, just ensure no panics occur - }) - } -} - -func TestDatabaseTypeMethods(t *testing.T) { - // Test with mock config (will fail to connect but won't panic) - config := Config{ - DSN: "test:test@tcp(localhost:3306)/test", - } - - db, err := New(config) - if err != nil { - // Expected - no actual MySQL server - t.Logf("Expected connection error: %v", err) - return - } - defer db.Close() - - if db.GetType() != MySQL { - t.Errorf("Expected MySQL type, got %v", db.GetType()) - } -} - -func TestDatabaseMethods(t *testing.T) { - // Skip actual database tests without MySQL - t.Skip("Skipping database method tests - requires MySQL test database") - - // Example tests for when MySQL is available: - // db, err := NewMySQL("test_user:test_pass@tcp(localhost:3306)/test_db") - // if err != nil { - // t.Fatalf("Failed to create database: %v", err) - // } - // defer db.Close() - // - // // Test basic operations - // _, err = db.Exec("CREATE TEMPORARY TABLE test (id INT PRIMARY KEY, name VARCHAR(255))") - // if err != nil { - // t.Fatalf("Failed to create test table: %v", err) - // } - // - // // Test insert - // result, err := db.Exec("INSERT INTO test (id, name) VALUES (?, ?)", 1, "test_value") - // if err != nil { - // t.Fatalf("Failed to insert test data: %v", err) - // } - // - // // Test query - // rows, err := db.Query("SELECT name FROM test WHERE id = ?", 1) - // if err != nil { - // t.Fatalf("Failed to query test data: %v", err) - // } - // defer rows.Close() - // - // if !rows.Next() { - // t.Fatal("No rows returned from query") - // } - // - // var name string - // if err := rows.Scan(&name); err != nil { - // t.Fatalf("Failed to scan result: %v", err) - // } - // - // if name != "test_value" { - // t.Errorf("Expected 'test_value', got '%s'", name) - // } -} \ No newline at end of file diff --git a/internal/entity/benchmark_test.go b/internal/entity/benchmark_test.go deleted file mode 100644 index d77555a..0000000 --- a/internal/entity/benchmark_test.go +++ /dev/null @@ -1,505 +0,0 @@ -package entity - -import ( - "fmt" - "math/rand" - "testing" - "time" -) - -// BenchmarkEntityCreation measures entity creation performance -func BenchmarkEntityCreation(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - entity := NewEntity() - _ = entity - } - }) -} - -// BenchmarkEntityCombatState measures combat state operations -func BenchmarkEntityCombatState(b *testing.B) { - entity := NewEntity() - - b.Run("Sequential", func(b *testing.B) { - for i := 0; i < b.N; i++ { - entity.SetInCombat(true) - _ = entity.IsInCombat() - entity.SetInCombat(false) - } - }) - - b.Run("Parallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - entity.SetInCombat(true) - _ = entity.IsInCombat() - entity.SetInCombat(false) - } - }) - }) -} - -// BenchmarkEntityCastingState measures casting state operations -func BenchmarkEntityCastingState(b *testing.B) { - entity := NewEntity() - - b.Run("Sequential", func(b *testing.B) { - for i := 0; i < b.N; i++ { - entity.SetCasting(true) - _ = entity.IsCasting() - entity.SetCasting(false) - } - }) - - b.Run("Parallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - entity.SetCasting(true) - _ = entity.IsCasting() - entity.SetCasting(false) - } - }) - }) -} - -// BenchmarkEntityStatCalculations measures stat calculation performance -func BenchmarkEntityStatCalculations(b *testing.B) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Set up some base stats - info.SetStr(100.0) - info.SetSta(100.0) - info.SetAgi(100.0) - info.SetWis(100.0) - info.SetIntel(100.0) - - b.Run("GetStats", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = entity.GetStr() - _ = entity.GetSta() - _ = entity.GetAgi() - _ = entity.GetWis() - _ = entity.GetIntel() - } - }) - - b.Run("GetStatsParallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = entity.GetStr() - _ = entity.GetSta() - _ = entity.GetAgi() - _ = entity.GetWis() - _ = entity.GetIntel() - } - }) - }) - - b.Run("GetPrimaryStat", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = entity.GetPrimaryStat() - } - }) - - b.Run("CalculateBonuses", func(b *testing.B) { - for i := 0; i < b.N; i++ { - entity.CalculateBonuses() - } - }) -} - -// BenchmarkEntitySpellEffects measures spell effect operations -func BenchmarkEntitySpellEffects(b *testing.B) { - entity := NewEntity() - - b.Run("AddRemoveSpellEffect", func(b *testing.B) { - for i := 0; i < b.N; i++ { - spellID := int32(i + 1000) - entity.AddSpellEffect(spellID, 123, 30.0) - entity.RemoveSpellEffect(spellID) - } - }) - - b.Run("AddRemoveSpellEffectParallel", func(b *testing.B) { - var counter int64 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - spellID := int32(counter + 1000) - counter++ - entity.AddSpellEffect(spellID, 123, 30.0) - entity.RemoveSpellEffect(spellID) - } - }) - }) -} - -// BenchmarkEntityMaintainedSpells measures maintained spell operations -func BenchmarkEntityMaintainedSpells(b *testing.B) { - entity := NewEntity() - info := entity.GetInfoStruct() - info.SetMaxConcentration(1000) // Large pool for benchmarking - - b.Run("AddRemoveMaintainedSpell", func(b *testing.B) { - for i := 0; i < b.N; i++ { - spellID := int32(i + 2000) - if entity.AddMaintainedSpell("Benchmark Spell", spellID, 60.0, 1) { - entity.RemoveMaintainedSpell(spellID) - } - } - }) - - b.Run("AddRemoveMaintainedSpellParallel", func(b *testing.B) { - var counter int64 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - spellID := int32(counter + 2000) - counter++ - if entity.AddMaintainedSpell("Benchmark Spell", spellID, 60.0, 1) { - entity.RemoveMaintainedSpell(spellID) - } - } - }) - }) -} - -// BenchmarkInfoStructBasicOps measures basic InfoStruct operations -func BenchmarkInfoStructBasicOps(b *testing.B) { - info := NewInfoStruct() - - b.Run("SetGetName", func(b *testing.B) { - for i := 0; i < b.N; i++ { - info.SetName("BenchmarkCharacter") - _ = info.GetName() - } - }) - - b.Run("SetGetNameParallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - info.SetName("BenchmarkCharacter") - _ = info.GetName() - } - }) - }) - - b.Run("SetGetLevel", func(b *testing.B) { - for i := 0; i < b.N; i++ { - info.SetLevel(int16(i % 100)) - _ = info.GetLevel() - } - }) - - b.Run("SetGetStats", func(b *testing.B) { - for i := 0; i < b.N; i++ { - val := float32(i % 1000) - info.SetStr(val) - info.SetSta(val) - info.SetAgi(val) - info.SetWis(val) - info.SetIntel(val) - - _ = info.GetStr() - _ = info.GetSta() - _ = info.GetAgi() - _ = info.GetWis() - _ = info.GetIntel() - } - }) -} - -// BenchmarkInfoStructConcentration measures concentration operations -func BenchmarkInfoStructConcentration(b *testing.B) { - info := NewInfoStruct() - info.SetMaxConcentration(1000) - - b.Run("AddRemoveConcentration", func(b *testing.B) { - for i := 0; i < b.N; i++ { - amount := int16(i%10 + 1) - if info.AddConcentration(amount) { - info.RemoveConcentration(amount) - } - } - }) - - b.Run("AddRemoveConcentrationParallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - amount := int16(1) // Use small amount to reduce contention - if info.AddConcentration(amount) { - info.RemoveConcentration(amount) - } - } - }) - }) -} - -// BenchmarkInfoStructCoins measures coin operations -func BenchmarkInfoStructCoins(b *testing.B) { - info := NewInfoStruct() - - b.Run("AddRemoveCoins", func(b *testing.B) { - for i := 0; i < b.N; i++ { - amount := int32(i%10000 + 1) - info.AddCoins(amount) - info.RemoveCoins(amount / 2) - } - }) - - b.Run("AddRemoveCoinsParallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - amount := int32(100) - info.AddCoins(amount) - info.RemoveCoins(amount / 2) - } - }) - }) - - b.Run("GetCoins", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = info.GetCoins() - } - }) -} - -// BenchmarkInfoStructResistances measures resistance operations -func BenchmarkInfoStructResistances(b *testing.B) { - info := NewInfoStruct() - resistTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"} - - b.Run("SetGetResistances", func(b *testing.B) { - for i := 0; i < b.N; i++ { - resistType := resistTypes[i%len(resistTypes)] - value := int16(i % 100) - info.SetResistance(resistType, value) - _ = info.GetResistance(resistType) - } - }) - - b.Run("SetGetResistancesParallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - resistType := resistTypes[rand.Intn(len(resistTypes))] - value := int16(rand.Intn(100)) - info.SetResistance(resistType, value) - _ = info.GetResistance(resistType) - } - }) - }) -} - -// BenchmarkInfoStructClone measures clone operations -func BenchmarkInfoStructClone(b *testing.B) { - info := NewInfoStruct() - - // Set up some state to clone - info.SetName("Original Character") - info.SetLevel(50) - info.SetStr(100.0) - info.SetSta(120.0) - info.SetAgi(90.0) - info.SetWis(110.0) - info.SetIntel(105.0) - info.AddConcentration(5) - info.AddCoins(50000) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - clone := info.Clone() - _ = clone - } -} - -// BenchmarkEntityPetManagement measures pet management operations -func BenchmarkEntityPetManagement(b *testing.B) { - entity := NewEntity() - - b.Run("SetGetPet", func(b *testing.B) { - for i := 0; i < b.N; i++ { - pet := NewEntity() - entity.SetPet(pet) - _ = entity.GetPet() - entity.SetPet(nil) - } - }) - - b.Run("SetGetPetParallel", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - pet := NewEntity() - entity.SetPet(pet) - _ = entity.GetPet() - entity.SetPet(nil) - } - }) - }) - - b.Run("AllPetTypes", func(b *testing.B) { - for i := 0; i < b.N; i++ { - pet := NewEntity() - - entity.SetPet(pet) - _ = entity.GetPet() - - entity.SetCharmedPet(pet) - _ = entity.GetCharmedPet() - - entity.SetDeityPet(pet) - _ = entity.GetDeityPet() - - entity.SetCosmeticPet(pet) - _ = entity.GetCosmeticPet() - - // Clear all pets - entity.SetPet(nil) - entity.SetCharmedPet(nil) - entity.SetDeityPet(nil) - entity.SetCosmeticPet(nil) - } - }) -} - -// BenchmarkConcurrentWorkload simulates a realistic concurrent workload -func BenchmarkConcurrentWorkload(b *testing.B) { - numEntities := 100 - entities := make([]*Entity, numEntities) - - // Create entities - for i := 0; i < numEntities; i++ { - entities[i] = NewEntity() - info := entities[i].GetInfoStruct() - info.SetMaxConcentration(50) - info.SetName("Entity" + string(rune('A'+i%26))) - info.SetLevel(int16(i%100 + 1)) - } - - b.ResetTimer() - - b.Run("MixedOperations", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - entityIdx := rand.Intn(numEntities) - entity := entities[entityIdx] - - switch rand.Intn(10) { - case 0, 1: // Combat state changes (20%) - entity.SetInCombat(rand.Intn(2) == 1) - _ = entity.IsInCombat() - case 2, 3: // Stat reads (20%) - _ = entity.GetStr() - _ = entity.GetSta() - _ = entity.GetPrimaryStat() - case 4: // Spell effects (10%) - spellID := int32(rand.Intn(1000) + 10000) - entity.AddSpellEffect(spellID, int32(entityIdx), 30.0) - entity.RemoveSpellEffect(spellID) - case 5: // Maintained spells (10%) - spellID := int32(rand.Intn(100) + 20000) - if entity.AddMaintainedSpell("Workload Spell", spellID, 60.0, 1) { - entity.RemoveMaintainedSpell(spellID) - } - case 6, 7: // InfoStruct operations (20%) - info := entity.GetInfoStruct() - info.SetStr(float32(rand.Intn(200) + 50)) - _ = info.GetStr() - case 8: // Coin operations (10%) - info := entity.GetInfoStruct() - info.AddCoins(int32(rand.Intn(1000))) - _ = info.GetCoins() - case 9: // Resistance operations (10%) - info := entity.GetInfoStruct() - resistTypes := []string{"heat", "cold", "magic", "mental"} - resistType := resistTypes[rand.Intn(len(resistTypes))] - info.SetResistance(resistType, int16(rand.Intn(100))) - _ = info.GetResistance(resistType) - } - } - }) - }) -} - -// BenchmarkMemoryAllocation measures memory allocation patterns -func BenchmarkMemoryAllocation(b *testing.B) { - b.Run("EntityAllocation", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - entity := NewEntity() - _ = entity - } - }) - - b.Run("InfoStructAllocation", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - info := NewInfoStruct() - _ = info - } - }) - - b.Run("CloneAllocation", func(b *testing.B) { - info := NewInfoStruct() - info.SetName("Test") - info.SetLevel(50) - - b.ReportAllocs() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - clone := info.Clone() - _ = clone - } - }) -} - -// BenchmarkContention measures performance under high contention -func BenchmarkContention(b *testing.B) { - entity := NewEntity() - info := entity.GetInfoStruct() - info.SetMaxConcentration(10) // Low limit to create contention - - b.Run("HighContentionConcentration", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - if info.AddConcentration(1) { - // Hold for a brief moment to increase contention - time.Sleep(time.Nanosecond) - info.RemoveConcentration(1) - } - } - }) - }) - - b.Run("HighContentionSpellEffects", func(b *testing.B) { - var spellCounter int64 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - spellID := int32(spellCounter % 100) // Reuse spell IDs to create contention - spellCounter++ - entity.AddSpellEffect(spellID, 123, 30.0) - entity.RemoveSpellEffect(spellID) - } - }) - }) -} - -// BenchmarkScalability tests performance as load increases -func BenchmarkScalability(b *testing.B) { - goroutineCounts := []int{1, 2, 4, 8, 16, 32, 64} - - for _, numGoroutines := range goroutineCounts { - b.Run(fmt.Sprintf("Goroutines_%d", numGoroutines), func(b *testing.B) { - entity := NewEntity() - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - entity.SetInCombat(true) - _ = entity.IsInCombat() - entity.SetInCombat(false) - } - }) - }) - } -} diff --git a/internal/entity/concurrency_test.go b/internal/entity/concurrency_test.go deleted file mode 100644 index c288e19..0000000 --- a/internal/entity/concurrency_test.go +++ /dev/null @@ -1,445 +0,0 @@ -package entity - -import ( - "sync" - "sync/atomic" - "testing" - "time" -) - -// TestEntityConcurrencyStress performs intensive concurrent operations to identify race conditions -func TestEntityConcurrencyStress(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - info.SetMaxConcentration(1000) // Large concentration pool - - var wg sync.WaitGroup - numGoroutines := 100 - operationsPerGoroutine := 100 - - // Test 1: Concurrent combat and casting state changes - t.Run("CombatCastingStates", func(t *testing.T) { - var combatOps, castingOps int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Combat state operations - entity.SetInCombat(true) - if entity.IsInCombat() { - atomic.AddInt64(&combatOps, 1) - } - entity.SetInCombat(false) - - // Casting state operations - entity.SetCasting(true) - if entity.IsCasting() { - atomic.AddInt64(&castingOps, 1) - } - entity.SetCasting(false) - } - }() - } - wg.Wait() - - t.Logf("Combat operations: %d, Casting operations: %d", combatOps, castingOps) - }) - - // Test 2: Concurrent spell effect operations - t.Run("SpellEffects", func(t *testing.T) { - var addOps, removeOps int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - spellID := int32(goroutineID*1000 + j) - - if entity.AddSpellEffect(spellID, int32(goroutineID), 30.0) { - atomic.AddInt64(&addOps, 1) - } - - if entity.RemoveSpellEffect(spellID) { - atomic.AddInt64(&removeOps, 1) - } - } - }(i) - } - wg.Wait() - - t.Logf("Spell effect adds: %d, removes: %d", addOps, removeOps) - }) - - // Test 3: Concurrent maintained spell operations with concentration management - t.Run("MaintainedSpells", func(t *testing.T) { - var addOps, removeOps, concentrationFailures int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine/10; j++ { // Fewer ops due to concentration limits - spellID := int32(goroutineID*100 + j + 10000) - - if entity.AddMaintainedSpell("Stress Test Spell", spellID, 60.0, 1) { - atomic.AddInt64(&addOps, 1) - - // Small delay to increase contention - time.Sleep(time.Microsecond) - - if entity.RemoveMaintainedSpell(spellID) { - atomic.AddInt64(&removeOps, 1) - } - } else { - atomic.AddInt64(&concentrationFailures, 1) - } - } - }(i) - } - wg.Wait() - - t.Logf("Maintained spell adds: %d, removes: %d, concentration failures: %d", - addOps, removeOps, concentrationFailures) - - // Verify concentration was properly managed - currentConc := info.GetCurConcentration() - if currentConc != 0 { - t.Errorf("Expected concentration to be 0 after all operations, got %d", currentConc) - } - }) - - // Test 4: Concurrent stat calculations with bonuses - t.Run("StatCalculations", func(t *testing.T) { - var statReads int64 - - // Set some base stats - info.SetStr(100.0) - info.SetSta(100.0) - info.SetAgi(100.0) - info.SetWis(100.0) - info.SetIntel(100.0) - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Add some stat bonuses - entity.AddStatBonus(int32(goroutineID*1000+j), 1, float32(j%10)) - - // Read stats (these involve bonus calculations) - _ = entity.GetStr() - _ = entity.GetSta() - _ = entity.GetAgi() - _ = entity.GetWis() - _ = entity.GetIntel() - _ = entity.GetPrimaryStat() - - atomic.AddInt64(&statReads, 6) - - // Trigger bonus recalculation - entity.CalculateBonuses() - } - }(i) - } - wg.Wait() - - t.Logf("Stat reads: %d", statReads) - }) - - // Test 5: Concurrent pet management - t.Run("PetManagement", func(t *testing.T) { - var petOps int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine/10; j++ { - pet := NewEntity() - - // Test different pet types - switch j % 4 { - case 0: - entity.SetPet(pet) - _ = entity.GetPet() - entity.SetPet(nil) - case 1: - entity.SetCharmedPet(pet) - _ = entity.GetCharmedPet() - entity.SetCharmedPet(nil) - case 2: - entity.SetDeityPet(pet) - _ = entity.GetDeityPet() - entity.SetDeityPet(nil) - case 3: - entity.SetCosmeticPet(pet) - _ = entity.GetCosmeticPet() - entity.SetCosmeticPet(nil) - } - - atomic.AddInt64(&petOps, 1) - } - }() - } - wg.Wait() - - t.Logf("Pet operations: %d", petOps) - }) -} - -// TestInfoStructConcurrencyStress performs intensive concurrent operations on InfoStruct -func TestInfoStructConcurrencyStress(t *testing.T) { - info := NewInfoStruct() - info.SetMaxConcentration(1000) - - var wg sync.WaitGroup - numGoroutines := 100 - operationsPerGoroutine := 100 - - // Test 1: Concurrent basic property access - t.Run("BasicProperties", func(t *testing.T) { - var nameOps, levelOps, statOps int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - // Name operations - info.SetName("TestChar" + string(rune('A'+goroutineID%26))) - _ = info.GetName() - atomic.AddInt64(&nameOps, 1) - - // Level operations - info.SetLevel(int16(j % 100)) - _ = info.GetLevel() - atomic.AddInt64(&levelOps, 1) - - // Stat operations - info.SetStr(float32(j)) - _ = info.GetStr() - atomic.AddInt64(&statOps, 1) - } - }(i) - } - wg.Wait() - - t.Logf("Name ops: %d, Level ops: %d, Stat ops: %d", nameOps, levelOps, statOps) - }) - - // Test 2: Concurrent concentration management - t.Run("ConcentrationManagement", func(t *testing.T) { - var addSuccesses, addFailures, removes int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - amount := int16(j%5 + 1) // 1-5 concentration points - - if info.AddConcentration(amount) { - atomic.AddInt64(&addSuccesses, 1) - - // Small delay to increase contention - time.Sleep(time.Microsecond) - - info.RemoveConcentration(amount) - atomic.AddInt64(&removes, 1) - } else { - atomic.AddInt64(&addFailures, 1) - } - } - }() - } - wg.Wait() - - t.Logf("Concentration adds: %d, failures: %d, removes: %d", - addSuccesses, addFailures, removes) - - // Verify final state - finalConc := info.GetCurConcentration() - if finalConc < 0 || finalConc > info.GetMaxConcentration() { - t.Errorf("Invalid final concentration: %d (max: %d)", finalConc, info.GetMaxConcentration()) - } - }) - - // Test 3: Concurrent coin operations - t.Run("CoinOperations", func(t *testing.T) { - var addOps, removeSuccesses, removeFailures int64 - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - amount := int32((goroutineID*1000 + j) % 10000) - - // Add coins - info.AddCoins(amount) - atomic.AddInt64(&addOps, 1) - - // Try to remove some coins - removeAmount := amount / 2 - if info.RemoveCoins(removeAmount) { - atomic.AddInt64(&removeSuccesses, 1) - } else { - atomic.AddInt64(&removeFailures, 1) - } - - // Read total coins - _ = info.GetCoins() - } - }(i) - } - wg.Wait() - - t.Logf("Coin adds: %d, remove successes: %d, failures: %d", - addOps, removeSuccesses, removeFailures) - - // Verify coins are non-negative - finalCoins := info.GetCoins() - if finalCoins < 0 { - t.Errorf("Coins became negative: %d", finalCoins) - } - }) - - // Test 4: Concurrent resistance operations - t.Run("ResistanceOperations", func(t *testing.T) { - var resistOps int64 - resistTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"} - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - resistType := resistTypes[j%len(resistTypes)] - value := int16(j % 100) - - info.SetResistance(resistType, value) - _ = info.GetResistance(resistType) - atomic.AddInt64(&resistOps, 1) - } - }(i) - } - wg.Wait() - - t.Logf("Resistance operations: %d", resistOps) - }) - - // Test 5: Concurrent clone operations - t.Run("CloneOperations", func(t *testing.T) { - var cloneOps int64 - - // Set some initial state - info.SetName("Original") - info.SetLevel(50) - info.SetStr(100.0) - info.AddConcentration(5) - - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine/10; j++ { // Fewer clones as they're expensive - clone := info.Clone() - if clone != nil { - // Verify clone independence - clone.SetName("Clone") - if info.GetName() == "Clone" { - t.Errorf("Clone modified original") - } - atomic.AddInt64(&cloneOps, 1) - } - } - }() - } - wg.Wait() - - t.Logf("Clone operations: %d", cloneOps) - }) -} - -// TestRaceConditionDetection uses the race detector to find potential issues -func TestRaceConditionDetection(t *testing.T) { - if testing.Short() { - t.Skip("Skipping race condition test in short mode") - } - - entity := NewEntity() - info := entity.GetInfoStruct() - info.SetMaxConcentration(100) - - var wg sync.WaitGroup - numGoroutines := 50 - - // Create a scenario designed to trigger race conditions - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - - for j := 0; j < 50; j++ { - // Mix of read and write operations that could race - entity.SetInCombat(id%2 == 0) - isInCombat := entity.IsInCombat() - - entity.SetCasting(j%2 == 0) - isCasting := entity.IsCasting() - - // Stats with bonus calculations - info.SetStr(float32(id + j)) - str := entity.GetStr() - - // Concentration with potential for contention - if info.AddConcentration(1) { - info.RemoveConcentration(1) - } - - // Use the values to prevent optimization - _ = isInCombat - _ = isCasting - _ = str - } - }(i) - } - wg.Wait() -} - -// Cleanup test to run after stress tests -func TestConcurrencyCleanup(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Verify entity is in clean state after stress tests - if entity.IsInCombat() { - t.Error("Entity should not be in combat after tests") - } - - if entity.IsCasting() { - t.Error("Entity should not be casting after tests") - } - - if info.GetCurConcentration() < 0 { - t.Error("Concentration should not be negative") - } - - if info.GetCoins() < 0 { - t.Error("Coins should not be negative") - } -} diff --git a/internal/entity/entity_test.go b/internal/entity/entity_test.go deleted file mode 100644 index d4b127f..0000000 --- a/internal/entity/entity_test.go +++ /dev/null @@ -1,659 +0,0 @@ -package entity - -import ( - "sync" - "testing" - "time" - - "eq2emu/internal/spells" -) - -func TestNewEntity(t *testing.T) { - entity := NewEntity() - if entity == nil { - t.Fatal("NewEntity returned nil") - } - - if entity.Spawn == nil { - t.Error("Expected Spawn to be initialized") - } - - if entity.infoStruct == nil { - t.Error("Expected InfoStruct to be initialized") - } - - if entity.spellEffectManager == nil { - t.Error("Expected SpellEffectManager to be initialized") - } - - // Check default values - if entity.GetMaxSpeed() != 6.0 { - t.Errorf("Expected max speed 6.0, got %f", entity.GetMaxSpeed()) - } - - if entity.GetBaseSpeed() != 0.0 { - t.Errorf("Expected base speed 0.0, got %f", entity.GetBaseSpeed()) - } - - if entity.GetSpeedMultiplier() != 1.0 { - t.Errorf("Expected speed multiplier 1.0, got %f", entity.GetSpeedMultiplier()) - } - - // Check initial states - if entity.IsInCombat() { - t.Error("Expected entity to not be in combat initially") - } - - if entity.IsCasting() { - t.Error("Expected entity to not be casting initially") - } - - if entity.IsPetDismissing() { - t.Error("Expected pet to not be dismissing initially") - } - - if entity.HasSeeInvisSpell() { - t.Error("Expected entity to not have see invisible spell initially") - } - - if entity.HasSeeHideSpell() { - t.Error("Expected entity to not have see hidden spell initially") - } -} - -func TestEntityIsEntity(t *testing.T) { - entity := NewEntity() - if !entity.IsEntity() { - t.Error("Expected IsEntity to return true") - } -} - -func TestEntityInfoStruct(t *testing.T) { - entity := NewEntity() - - stats := entity.GetInfoStruct() - if stats == nil { - t.Error("Expected InfoStruct to be initialized") - } - - // Test setting a new info struct - newInfo := NewInfoStruct() - newInfo.SetName("Test Entity") - entity.SetInfoStruct(newInfo) - - if entity.GetInfoStruct().GetName() != "Test Entity" { - t.Error("Expected info struct to be updated") - } - - // Test that nil info struct is ignored - entity.SetInfoStruct(nil) - if entity.GetInfoStruct().GetName() != "Test Entity" { - t.Error("Expected info struct to remain unchanged when setting nil") - } -} - -func TestEntityClient(t *testing.T) { - entity := NewEntity() - - // Base Entity should return nil for GetClient - client := entity.GetClient() - if client != nil { - t.Error("Expected GetClient to return nil for base Entity") - } -} - -func TestEntityCombatState(t *testing.T) { - entity := NewEntity() - - // Initial state should be false - if entity.IsInCombat() { - t.Error("Expected entity to not be in combat initially") - } - - // Set combat state to true - entity.SetInCombat(true) - if !entity.IsInCombat() { - t.Error("Expected entity to be in combat after setting to true") - } - - // Set combat state to false - entity.SetInCombat(false) - if entity.IsInCombat() { - t.Error("Expected entity to not be in combat after setting to false") - } -} - -func TestEntityCastingState(t *testing.T) { - entity := NewEntity() - - // Initial state should be false - if entity.IsCasting() { - t.Error("Expected entity to not be casting initially") - } - - // Set casting state to true - entity.SetCasting(true) - if !entity.IsCasting() { - t.Error("Expected entity to be casting after setting to true") - } - - // Set casting state to false - entity.SetCasting(false) - if entity.IsCasting() { - t.Error("Expected entity to not be casting after setting to false") - } -} - -func TestEntitySpeedMethods(t *testing.T) { - entity := NewEntity() - - // Test max speed - entity.SetMaxSpeed(10.0) - if entity.GetMaxSpeed() != 10.0 { - t.Errorf("Expected max speed 10.0, got %f", entity.GetMaxSpeed()) - } - - // Test base speed - entity.SetBaseSpeed(5.0) - if entity.GetBaseSpeed() != 5.0 { - t.Errorf("Expected base speed 5.0, got %f", entity.GetBaseSpeed()) - } - - // Test speed multiplier - entity.SetSpeedMultiplier(2.0) - if entity.GetSpeedMultiplier() != 2.0 { - t.Errorf("Expected speed multiplier 2.0, got %f", entity.GetSpeedMultiplier()) - } - - // Test effective speed calculation with base speed set - effectiveSpeed := entity.CalculateEffectiveSpeed() - if effectiveSpeed != 10.0 { // 5.0 * 2.0 - t.Errorf("Expected effective speed 10.0, got %f", effectiveSpeed) - } - - // Test effective speed calculation with zero base speed (should use max speed) - entity.SetBaseSpeed(0.0) - effectiveSpeed = entity.CalculateEffectiveSpeed() - if effectiveSpeed != 20.0 { // 10.0 * 2.0 - t.Errorf("Expected effective speed 20.0, got %f", effectiveSpeed) - } -} - -func TestEntityPositionTracking(t *testing.T) { - entity := NewEntity() - - // Test setting and getting last position - entity.SetLastPosition(100.0, 200.0, 300.0, 1.5) - x, y, z, heading := entity.GetLastPosition() - - if x != 100.0 || y != 200.0 || z != 300.0 || heading != 1.5 { - t.Errorf("Expected position (100.0, 200.0, 300.0, 1.5), got (%f, %f, %f, %f)", x, y, z, heading) - } - - // Test HasMoved with different positions - // Note: This would require setting the spawn's actual position, which depends on the spawn implementation - // For now, we just test that the method doesn't panic - moved := entity.HasMoved() - _ = moved // We can't easily test the actual movement detection without setting up spawn positions -} - -func TestEntityStatCalculation(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Set base stats - info.SetStr(10.0) - info.SetSta(12.0) - info.SetAgi(8.0) - info.SetWis(15.0) - info.SetIntel(20.0) - - // Test individual stat getters (these include bonuses from spell effects) - str := entity.GetStr() - if str < 10 { - t.Errorf("Expected strength >= 10, got %d", str) - } - - sta := entity.GetSta() - if sta < 12 { - t.Errorf("Expected stamina >= 12, got %d", sta) - } - - agi := entity.GetAgi() - if agi < 8 { - t.Errorf("Expected agility >= 8, got %d", agi) - } - - wis := entity.GetWis() - if wis < 15 { - t.Errorf("Expected wisdom >= 15, got %d", wis) - } - - intel := entity.GetIntel() - if intel < 20 { - t.Errorf("Expected intelligence >= 20, got %d", intel) - } - - // Test primary stat calculation - primaryStat := entity.GetPrimaryStat() - if primaryStat < 20 { - t.Errorf("Expected primary stat >= 20 (intelligence is highest), got %d", primaryStat) - } -} - -func TestEntityResistances(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Set base resistances - info.SetResistance("heat", 10) - info.SetResistance("cold", 15) - info.SetResistance("magic", 20) - info.SetResistance("mental", 25) - info.SetResistance("divine", 30) - info.SetResistance("disease", 35) - info.SetResistance("poison", 40) - - // Test resistance getters - if entity.GetHeatResistance() != 10 { - t.Errorf("Expected heat resistance 10, got %d", entity.GetHeatResistance()) - } - - if entity.GetColdResistance() != 15 { - t.Errorf("Expected cold resistance 15, got %d", entity.GetColdResistance()) - } - - if entity.GetMagicResistance() != 20 { - t.Errorf("Expected magic resistance 20, got %d", entity.GetMagicResistance()) - } - - if entity.GetMentalResistance() != 25 { - t.Errorf("Expected mental resistance 25, got %d", entity.GetMentalResistance()) - } - - if entity.GetDivineResistance() != 30 { - t.Errorf("Expected divine resistance 30, got %d", entity.GetDivineResistance()) - } - - if entity.GetDiseaseResistance() != 35 { - t.Errorf("Expected disease resistance 35, got %d", entity.GetDiseaseResistance()) - } - - if entity.GetPoisonResistance() != 40 { - t.Errorf("Expected poison resistance 40, got %d", entity.GetPoisonResistance()) - } -} - -func TestEntityMaintainedSpells(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Set max concentration - info.SetMaxConcentration(10) - - // Test adding maintained spell - success := entity.AddMaintainedSpell("Test Spell", 123, 60.0, 3) - if !success { - t.Error("Expected AddMaintainedSpell to succeed") - } - - // Check concentration usage - if info.GetCurConcentration() != 3 { - t.Errorf("Expected current concentration 3, got %d", info.GetCurConcentration()) - } - - // Test getting maintained spell - effect := entity.GetMaintainedSpell(123) - if effect == nil { - t.Error("Expected to find maintained spell effect") - } - - // Test adding spell that would exceed concentration - success = entity.AddMaintainedSpell("Another Spell", 124, 60.0, 8) - if success { - t.Error("Expected AddMaintainedSpell to fail when concentration exceeded") - } - - // Test removing maintained spell - success = entity.RemoveMaintainedSpell(123) - if !success { - t.Error("Expected RemoveMaintainedSpell to succeed") - } - - // Check concentration was returned - if info.GetCurConcentration() != 0 { - t.Errorf("Expected current concentration 0, got %d", info.GetCurConcentration()) - } - - // Test removing non-existent spell - success = entity.RemoveMaintainedSpell(999) - if success { - t.Error("Expected RemoveMaintainedSpell to fail for non-existent spell") - } -} - -func TestEntitySpellEffects(t *testing.T) { - entity := NewEntity() - - // Test adding spell effect - success := entity.AddSpellEffect(456, 789, 30.0) - if !success { - t.Error("Expected AddSpellEffect to succeed") - } - - // Test removing spell effect - success = entity.RemoveSpellEffect(456) - if !success { - t.Error("Expected RemoveSpellEffect to succeed") - } - - // Test removing non-existent spell effect - success = entity.RemoveSpellEffect(999) - if success { - t.Error("Expected RemoveSpellEffect to fail for non-existent spell") - } -} - -func TestEntityDetrimentalSpells(t *testing.T) { - entity := NewEntity() - - // Test adding detrimental spell - entity.AddDetrimentalSpell(789, 456, 45.0, 1) - - // Test getting detrimental effect - effect := entity.GetDetrimentalEffect(789, 456) - if effect == nil { - t.Error("Expected to find detrimental spell effect") - } - - // Test removing detrimental spell - success := entity.RemoveDetrimentalSpell(789, 456) - if !success { - t.Error("Expected RemoveDetrimentalSpell to succeed") - } - - // Test removing non-existent detrimental spell - success = entity.RemoveDetrimentalSpell(999, 888) - if success { - t.Error("Expected RemoveDetrimentalSpell to fail for non-existent spell") - } -} - -func TestEntityBonusSystem(t *testing.T) { - entity := NewEntity() - - // Test adding skill bonus - entity.AddSkillBonus(123, 5, 15.0) // Skill ID 5, value 15.0 - - // Test adding stat bonus - entity.AddStatBonus(124, 1, 5.0) // Stat type 1 (STR), value 5.0 - - // Test calculating bonuses - entity.CalculateBonuses() - - // The actual bonus calculation depends on the spell effect manager implementation - // We just test that the method doesn't panic -} - -func TestEntityPetManagement(t *testing.T) { - entity := NewEntity() - pet := NewEntity() - - // Test setting and getting summon pet - entity.SetPet(pet) - if entity.GetPet() != pet { - t.Error("Expected pet to be set correctly") - } - - if pet.GetOwner() != entity.GetID() { - t.Error("Expected pet owner to be set correctly") - } - - if pet.GetPetType() != PetTypeSummon { - t.Error("Expected pet type to be summon") - } - - // Test charmed pet - charmedPet := NewEntity() - entity.SetCharmedPet(charmedPet) - if entity.GetCharmedPet() != charmedPet { - t.Error("Expected charmed pet to be set correctly") - } - - if charmedPet.GetPetType() != PetTypeCharm { - t.Error("Expected pet type to be charm") - } - - // Test deity pet - deityPet := NewEntity() - entity.SetDeityPet(deityPet) - if entity.GetDeityPet() != deityPet { - t.Error("Expected deity pet to be set correctly") - } - - if deityPet.GetPetType() != PetTypeDeity { - t.Error("Expected pet type to be deity") - } - - // Test cosmetic pet - cosmeticPet := NewEntity() - entity.SetCosmeticPet(cosmeticPet) - if entity.GetCosmeticPet() != cosmeticPet { - t.Error("Expected cosmetic pet to be set correctly") - } - - if cosmeticPet.GetPetType() != PetTypeCosmetic { - t.Error("Expected pet type to be cosmetic") - } - - // Test pet dismissing state - entity.SetPetDismissing(true) - if !entity.IsPetDismissing() { - t.Error("Expected pet to be dismissing") - } - - entity.SetPetDismissing(false) - if entity.IsPetDismissing() { - t.Error("Expected pet to not be dismissing") - } -} - -func TestEntityDeity(t *testing.T) { - entity := NewEntity() - - // Test setting and getting deity - entity.SetDeity(5) - if entity.GetDeity() != 5 { - t.Errorf("Expected deity 5, got %d", entity.GetDeity()) - } -} - -func TestEntityDodgeChance(t *testing.T) { - entity := NewEntity() - - // Test base dodge chance - dodgeChance := entity.GetDodgeChance() - if dodgeChance != 5.0 { - t.Errorf("Expected base dodge chance 5.0, got %f", dodgeChance) - } -} - -func TestEntitySeeSpells(t *testing.T) { - entity := NewEntity() - - // Test see invisible spell - entity.SetSeeInvisSpell(true) - if !entity.HasSeeInvisSpell() { - t.Error("Expected entity to have see invisible spell") - } - - entity.SetSeeInvisSpell(false) - if entity.HasSeeInvisSpell() { - t.Error("Expected entity to not have see invisible spell") - } - - // Test see hidden spell - entity.SetSeeHideSpell(true) - if !entity.HasSeeHideSpell() { - t.Error("Expected entity to have see hidden spell") - } - - entity.SetSeeHideSpell(false) - if entity.HasSeeHideSpell() { - t.Error("Expected entity to not have see hidden spell") - } -} - -func TestEntityCleanupMethods(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Set up some spell effects and concentration usage - info.SetMaxConcentration(10) - entity.AddMaintainedSpell("Test Spell 1", 123, 60.0, 3) - entity.AddMaintainedSpell("Test Spell 2", 124, 60.0, 2) - entity.AddSpellEffect(456, 789, 30.0) - - // Verify concentration is used - if info.GetCurConcentration() != 5 { - t.Errorf("Expected current concentration 5, got %d", info.GetCurConcentration()) - } - - // Test deleting all spell effects - entity.DeleteSpellEffects(false) - - // Verify concentration was returned - if info.GetCurConcentration() != 0 { - t.Errorf("Expected current concentration 0 after cleanup, got %d", info.GetCurConcentration()) - } - - // Test RemoveSpells - entity.AddMaintainedSpell("Test Spell 3", 125, 60.0, 2) - entity.RemoveSpells(false) // Remove all spells - - if info.GetCurConcentration() != 0 { - t.Errorf("Expected current concentration 0 after RemoveSpells, got %d", info.GetCurConcentration()) - } -} - -func TestEntityProcessEffects(t *testing.T) { - entity := NewEntity() - - // Test that ProcessEffects doesn't panic - entity.ProcessEffects() -} - -func TestEntityClassSystemIntegration(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - - // Test class getter/setter - info.SetClass1(5) - if entity.GetClass() != 5 { - t.Errorf("Expected class 5, got %d", entity.GetClass()) - } - - entity.SetClass(10) - if entity.GetClass() != 10 { - t.Errorf("Expected class 10, got %d", entity.GetClass()) - } - - // Test level getter - info.SetLevel(25) - if entity.GetLevel() != 25 { - t.Errorf("Expected level 25, got %d", entity.GetLevel()) - } -} - -func TestEntityConcurrency(t *testing.T) { - entity := NewEntity() - info := entity.GetInfoStruct() - info.SetMaxConcentration(20) - - var wg sync.WaitGroup - numGoroutines := 10 - - // Test concurrent access to combat state - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - entity.SetInCombat(true) - _ = entity.IsInCombat() - entity.SetInCombat(false) - }() - } - wg.Wait() - - // Test concurrent access to casting state - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - entity.SetCasting(true) - _ = entity.IsCasting() - entity.SetCasting(false) - }() - } - wg.Wait() - - // Test concurrent spell effect operations - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - spellID := int32(1000 + i) - go func(id int32) { - defer wg.Done() - entity.AddSpellEffect(id, 123, 30.0) - time.Sleep(time.Millisecond) // Small delay to ensure some overlap - entity.RemoveSpellEffect(id) - }(spellID) - } - wg.Wait() - - // Test concurrent maintained spell operations - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - spellID := int32(2000 + i) - go func(id int32) { - defer wg.Done() - if entity.AddMaintainedSpell("Concurrent Spell", id, 60.0, 1) { - time.Sleep(time.Millisecond) - entity.RemoveMaintainedSpell(id) - } - }(spellID) - } - wg.Wait() -} - -func TestEntityControlEffects(t *testing.T) { - entity := NewEntity() - - // Test has control effect - should work without panicking - // The actual implementation depends on the spell effect manager - hasStun := entity.HasControlEffect(spells.ControlEffectStun) - _ = hasStun // We can't easily test the actual value without setting up effects -} - -func TestEntityConstants(t *testing.T) { - // Test pet type constants - if PetTypeSummon != 1 { - t.Errorf("Expected PetTypeSummon to be 1, got %d", PetTypeSummon) - } - - if PetTypeCharm != 2 { - t.Errorf("Expected PetTypeCharm to be 2, got %d", PetTypeCharm) - } - - if PetTypeDeity != 3 { - t.Errorf("Expected PetTypeDeity to be 3, got %d", PetTypeDeity) - } - - if PetTypeCosmetic != 4 { - t.Errorf("Expected PetTypeCosmetic to be 4, got %d", PetTypeCosmetic) - } - - // Test control effect constants (re-exported from spells package) - if spells.ControlEffectStun == 0 && spells.ControlEffectRoot == 0 { - t.Error("Control effect constants should be non-zero") - } -} diff --git a/internal/entity/info_struct_test.go b/internal/entity/info_struct_test.go deleted file mode 100644 index 93ba60c..0000000 --- a/internal/entity/info_struct_test.go +++ /dev/null @@ -1,514 +0,0 @@ -package entity - -import ( - "sync" - "testing" - "time" -) - -func TestNewInfoStruct(t *testing.T) { - info := NewInfoStruct() - if info == nil { - t.Fatal("NewInfoStruct returned nil") - } - - // Test default values - if info.GetName() != "" { - t.Errorf("Expected empty name, got %s", info.GetName()) - } - - if info.GetLevel() != 0 { - t.Errorf("Expected level 0, got %d", info.GetLevel()) - } - - if info.GetMaxConcentration() != 5 { - t.Errorf("Expected max concentration 5, got %d", info.GetMaxConcentration()) - } - - if info.GetCurConcentration() != 0 { - t.Errorf("Expected current concentration 0, got %d", info.GetCurConcentration()) - } -} - -func TestInfoStructBasicProperties(t *testing.T) { - info := NewInfoStruct() - - // Test name - info.SetName("Test Character") - if info.GetName() != "Test Character" { - t.Errorf("Expected name 'Test Character', got %s", info.GetName()) - } - - // Test level - info.SetLevel(25) - if info.GetLevel() != 25 { - t.Errorf("Expected level 25, got %d", info.GetLevel()) - } - - // Test effective level - info.SetEffectiveLevel(30) - if info.GetEffectiveLevel() != 30 { - t.Errorf("Expected effective level 30, got %d", info.GetEffectiveLevel()) - } - - // Test class - info.SetClass1(5) - if info.GetClass1() != 5 { - t.Errorf("Expected class 5, got %d", info.GetClass1()) - } - - // Test race - info.SetRace(3) - if info.GetRace() != 3 { - t.Errorf("Expected race 3, got %d", info.GetRace()) - } - - // Test gender - info.SetGender(1) - if info.GetGender() != 1 { - t.Errorf("Expected gender 1, got %d", info.GetGender()) - } -} - -func TestInfoStructStats(t *testing.T) { - info := NewInfoStruct() - - // Test strength - info.SetStr(15.5) - if info.GetStr() != 15.5 { - t.Errorf("Expected strength 15.5, got %f", info.GetStr()) - } - - // Test stamina - info.SetSta(20.0) - if info.GetSta() != 20.0 { - t.Errorf("Expected stamina 20.0, got %f", info.GetSta()) - } - - // Test agility - info.SetAgi(12.75) - if info.GetAgi() != 12.75 { - t.Errorf("Expected agility 12.75, got %f", info.GetAgi()) - } - - // Test wisdom - info.SetWis(18.25) - if info.GetWis() != 18.25 { - t.Errorf("Expected wisdom 18.25, got %f", info.GetWis()) - } - - // Test intelligence - info.SetIntel(22.5) - if info.GetIntel() != 22.5 { - t.Errorf("Expected intelligence 22.5, got %f", info.GetIntel()) - } -} - -func TestInfoStructConcentration(t *testing.T) { - info := NewInfoStruct() - - // Test setting max concentration - info.SetMaxConcentration(15) - if info.GetMaxConcentration() != 15 { - t.Errorf("Expected max concentration 15, got %d", info.GetMaxConcentration()) - } - - // Test adding concentration - success := info.AddConcentration(5) - if !success { - t.Error("Expected AddConcentration to succeed") - } - - if info.GetCurConcentration() != 5 { - t.Errorf("Expected current concentration 5, got %d", info.GetCurConcentration()) - } - - // Test adding concentration that would exceed maximum - success = info.AddConcentration(12) - if success { - t.Error("Expected AddConcentration to fail when exceeding maximum") - } - - if info.GetCurConcentration() != 5 { - t.Errorf("Expected current concentration to remain 5, got %d", info.GetCurConcentration()) - } - - // Test adding concentration that exactly reaches maximum - success = info.AddConcentration(10) - if !success { - t.Error("Expected AddConcentration to succeed when exactly reaching maximum") - } - - if info.GetCurConcentration() != 15 { - t.Errorf("Expected current concentration 15, got %d", info.GetCurConcentration()) - } - - // Test removing concentration - info.RemoveConcentration(7) - if info.GetCurConcentration() != 8 { - t.Errorf("Expected current concentration 8, got %d", info.GetCurConcentration()) - } - - // Test removing more concentration than available - info.RemoveConcentration(20) - if info.GetCurConcentration() != 0 { - t.Errorf("Expected current concentration to be clamped to 0, got %d", info.GetCurConcentration()) - } -} - -func TestInfoStructCoins(t *testing.T) { - info := NewInfoStruct() - - // Test initial coins - if info.GetCoins() != 0 { - t.Errorf("Expected initial coins 0, got %d", info.GetCoins()) - } - - // Test adding copper - info.AddCoins(150) // 1 silver and 50 copper - totalCopper := info.GetCoins() - if totalCopper != 150 { - t.Errorf("Expected total coins 150, got %d", totalCopper) - } - - // Test adding large amount that converts to higher denominations - info.AddCoins(1234567) // Should convert to plat, gold, silver, copper - expectedTotal := 150 + 1234567 - totalCopper = info.GetCoins() - if totalCopper != int32(expectedTotal) { - t.Errorf("Expected total coins %d, got %d", expectedTotal, totalCopper) - } - - // Test removing coins - success := info.RemoveCoins(100) - if !success { - t.Error("Expected RemoveCoins to succeed") - } - - expectedTotal -= 100 - totalCopper = info.GetCoins() - if totalCopper != int32(expectedTotal) { - t.Errorf("Expected total coins %d after removal, got %d", expectedTotal, totalCopper) - } - - // Test removing more coins than available - success = info.RemoveCoins(9999999) - if success { - t.Error("Expected RemoveCoins to fail when removing more than available") - } - - // Total should remain unchanged - totalCopper = info.GetCoins() - if totalCopper != int32(expectedTotal) { - t.Errorf("Expected total coins to remain %d, got %d", expectedTotal, totalCopper) - } -} - -func TestInfoStructResistances(t *testing.T) { - info := NewInfoStruct() - - // Test all resistance types - resistanceTypes := []string{"heat", "cold", "magic", "mental", "divine", "disease", "poison"} - expectedValues := []int16{10, 15, 20, 25, 30, 35, 40} - - for i, resistType := range resistanceTypes { - expectedValue := expectedValues[i] - - // Set resistance - info.SetResistance(resistType, expectedValue) - - // Get resistance - actualValue := info.GetResistance(resistType) - if actualValue != expectedValue { - t.Errorf("Expected %s resistance %d, got %d", resistType, expectedValue, actualValue) - } - } - - // Test invalid resistance type - unknownResist := info.GetResistance("unknown") - if unknownResist != 0 { - t.Errorf("Expected unknown resistance type to return 0, got %d", unknownResist) - } - - // Setting invalid resistance type should not panic - info.SetResistance("unknown", 50) // Should be ignored -} - -func TestInfoStructResetEffects(t *testing.T) { - info := NewInfoStruct() - - // Set some base values first (these would normally be set during character creation) - info.str = 10.0 - info.strBase = 10.0 - info.sta = 12.0 - info.staBase = 12.0 - info.heat = 15 - info.heatBase = 15 - - // Modify current values to simulate bonuses - info.SetStr(20.0) - info.SetSta(25.0) - info.SetResistance("heat", 30) - - // Reset effects - info.ResetEffects() - - // Check that values were reset to base - if info.GetStr() != 10.0 { - t.Errorf("Expected strength to reset to 10.0, got %f", info.GetStr()) - } - - if info.GetSta() != 12.0 { - t.Errorf("Expected stamina to reset to 12.0, got %f", info.GetSta()) - } - - if info.GetResistance("heat") != 15 { - t.Errorf("Expected heat resistance to reset to 15, got %d", info.GetResistance("heat")) - } -} - -func TestInfoStructCalculatePrimaryStat(t *testing.T) { - info := NewInfoStruct() - - // Set all stats to different values - info.SetStr(10.0) - info.SetSta(15.0) - info.SetAgi(8.0) - info.SetWis(20.0) // This should be the highest - info.SetIntel(12.0) - - primaryStat := info.CalculatePrimaryStat() - if primaryStat != 20.0 { - t.Errorf("Expected primary stat 20.0 (wisdom), got %f", primaryStat) - } - - // Test with intelligence being highest - info.SetIntel(25.0) - primaryStat = info.CalculatePrimaryStat() - if primaryStat != 25.0 { - t.Errorf("Expected primary stat 25.0 (intelligence), got %f", primaryStat) - } - - // Test with all stats equal - info.SetStr(30.0) - info.SetSta(30.0) - info.SetAgi(30.0) - info.SetWis(30.0) - info.SetIntel(30.0) - - primaryStat = info.CalculatePrimaryStat() - if primaryStat != 30.0 { - t.Errorf("Expected primary stat 30.0 (all equal), got %f", primaryStat) - } -} - -func TestInfoStructClone(t *testing.T) { - info := NewInfoStruct() - - // Set some values - info.SetName("Original Character") - info.SetLevel(15) - info.SetStr(20.0) - info.SetResistance("magic", 25) - info.AddConcentration(3) - - // Clone the info struct - clone := info.Clone() - if clone == nil { - t.Fatal("Clone returned nil") - } - - // Verify clone has same values - if clone.GetName() != "Original Character" { - t.Errorf("Expected cloned name 'Original Character', got %s", clone.GetName()) - } - - if clone.GetLevel() != 15 { - t.Errorf("Expected cloned level 15, got %d", clone.GetLevel()) - } - - if clone.GetStr() != 20.0 { - t.Errorf("Expected cloned strength 20.0, got %f", clone.GetStr()) - } - - if clone.GetResistance("magic") != 25 { - t.Errorf("Expected cloned magic resistance 25, got %d", clone.GetResistance("magic")) - } - - if clone.GetCurConcentration() != 3 { - t.Errorf("Expected cloned concentration 3, got %d", clone.GetCurConcentration()) - } - - // Verify that modifying the clone doesn't affect the original - clone.SetName("Cloned Character") - clone.SetLevel(20) - - if info.GetName() != "Original Character" { - t.Error("Original name was modified when clone was changed") - } - - if info.GetLevel() != 15 { - t.Error("Original level was modified when clone was changed") - } -} - -func TestInfoStructGetUptime(t *testing.T) { - info := NewInfoStruct() - - // Test uptime (currently returns 0 as it's not implemented) - uptime := info.GetUptime() - if uptime != time.Duration(0) { - t.Errorf("Expected uptime to be 0 (not implemented), got %v", uptime) - } -} - -func TestInfoStructConcurrency(t *testing.T) { - info := NewInfoStruct() - info.SetMaxConcentration(100) - - var wg sync.WaitGroup - numGoroutines := 20 - - // Test concurrent access to basic properties - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(index int) { - defer wg.Done() - - // Each goroutine sets unique values - info.SetName("Character" + string(rune('A'+index))) - _ = info.GetName() - - info.SetLevel(int16(10 + index)) - _ = info.GetLevel() - - info.SetStr(float32(10.0 + float32(index))) - _ = info.GetStr() - }(i) - } - wg.Wait() - - // Test concurrent concentration operations - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - - // Try to add concentration - if info.AddConcentration(1) { - // If successful, remove it after a short delay - time.Sleep(time.Microsecond) - info.RemoveConcentration(1) - } - }() - } - wg.Wait() - - // After all operations, concentration should be back to 0 - // (This might not always be true due to race conditions, but shouldn't crash) - _ = info.GetCurConcentration() - - // Test concurrent coin operations - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(amount int32) { - defer wg.Done() - - info.AddCoins(amount) - _ = info.GetCoins() - - // Try to remove some coins - info.RemoveCoins(amount / 2) - }(int32(100 + i)) - } - wg.Wait() - - // Test concurrent resistance operations - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(value int16) { - defer wg.Done() - - info.SetResistance("heat", value) - _ = info.GetResistance("heat") - - info.SetResistance("cold", value+1) - _ = info.GetResistance("cold") - }(int16(i)) - } - wg.Wait() -} - -func TestInfoStructLargeValues(t *testing.T) { - info := NewInfoStruct() - - // Test with large coin amounts - largeCoinAmount := int32(2000000000) // 2 billion copper - info.AddCoins(largeCoinAmount) - - totalCoins := info.GetCoins() - if totalCoins != largeCoinAmount { - t.Errorf("Expected large coin amount %d, got %d", largeCoinAmount, totalCoins) - } - - // Test removing large amounts - success := info.RemoveCoins(largeCoinAmount) - if !success { - t.Error("Expected to be able to remove large coin amount") - } - - if info.GetCoins() != 0 { - t.Errorf("Expected coins to be 0 after removing all, got %d", info.GetCoins()) - } - - // Test with maximum values - info.SetMaxConcentration(32767) // Max int16 - info.SetLevel(32767) - - if info.GetMaxConcentration() != 32767 { - t.Errorf("Expected max concentration 32767, got %d", info.GetMaxConcentration()) - } - - if info.GetLevel() != 32767 { - t.Errorf("Expected level 32767, got %d", info.GetLevel()) - } -} - -func TestInfoStructEdgeCases(t *testing.T) { - info := NewInfoStruct() - - // Test negative values - info.SetStr(-5.0) - if info.GetStr() != -5.0 { - t.Errorf("Expected negative strength -5.0, got %f", info.GetStr()) - } - - // Test zero values - info.SetMaxConcentration(0) - success := info.AddConcentration(1) - if success { - t.Error("Expected AddConcentration to fail with max concentration 0") - } - - // Test very small float values - info.SetAgi(0.001) - if info.GetAgi() != 0.001 { - t.Errorf("Expected small agility 0.001, got %f", info.GetAgi()) - } - - // Test empty string name - info.SetName("") - if info.GetName() != "" { - t.Errorf("Expected empty name, got '%s'", info.GetName()) - } - - // Test very long name - longName := string(make([]byte, 1000)) - for i := range longName { - longName = longName[:i] + "A" + longName[i+1:] - } - info.SetName(longName) - if info.GetName() != longName { - t.Error("Expected to handle very long names") - } -} diff --git a/internal/events/README.md b/internal/events/README.md deleted file mode 100644 index 94e066d..0000000 --- a/internal/events/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# EQ2Go Event System - -A simplified event-driven system for handling game logic without the complexity of a full scripting engine. - -## Overview - -The event system provides: -- Simple event registration and execution -- Context-based parameter passing -- 100+ built-in EQ2 game functions organized by domain -- Thread-safe operations -- Minimal overhead -- Domain-specific function organization - -## Basic Usage - -```go -// Create event handler -handler := NewEventHandler() - -// Register all EQ2 functions (100+ functions organized by domain) -err := functions.RegisterAllEQ2Functions(handler) - -// Create context and execute event -ctx := NewEventContext(EventTypeSpawn, "SetCurrentHP", "heal_spell"). - WithSpawn(player). - WithParameter("hp", 150.0) - -err = handler.Execute(ctx) -``` - -## Event Context - -The `EventContext` provides: -- Game objects: `Caster`, `Target`, `Spawn`, `Quest` -- Parameters: Type-safe parameter access -- Results: Return values from event functions -- Logging: Built-in debug/info/warn/error logging - -### Fluent API - -```go -ctx := NewEventContext(EventTypeSpell, "heal", "cast"). - WithCaster(caster). - WithTarget(target). - WithParameter("spell_id", 123). - WithParameter("power_cost", 50) -``` - -### Parameter Access - -```go -func MyEvent(ctx *EventContext) error { - spellID := ctx.GetParameterInt("spell_id", 0) - message := ctx.GetParameterString("message", "default") - amount := ctx.GetParameterFloat("amount", 0.0) - enabled := ctx.GetParameterBool("enabled", false) - - // Set results - ctx.SetResult("damage_dealt", 150) - - return nil -} -``` - -## Available EQ2 Functions - -The system provides 100+ functions organized by domain: - -### Health Domain (23 functions) -- **HP Management**: `SetCurrentHP`, `SetMaxHP`, `SetMaxHPBase`, `GetCurrentHP`, `GetMaxHP`, `GetMaxHPBase` -- **Power Management**: `SetCurrentPower`, `SetMaxPower`, `SetMaxPowerBase`, `GetCurrentPower`, `GetMaxPower`, `GetMaxPowerBase` -- **Modifiers**: `ModifyHP`, `ModifyPower`, `ModifyMaxHP`, `ModifyMaxPower`, `ModifyTotalHP`, `ModifyTotalPower` -- **Percentages**: `GetPCTOfHP`, `GetPCTOfPower` -- **Healing**: `SpellHeal`, `SpellHealPct` -- **State**: `IsAlive` - -### Attributes Domain (24 functions) -- **Stats**: `SetInt`, `SetWis`, `SetSta`, `SetStr`, `SetAgi`, `GetInt`, `GetWis`, `GetSta`, `GetStr`, `GetAgi` -- **Base Stats**: `SetIntBase`, `SetWisBase`, `SetStaBase`, `SetStrBase`, `SetAgiBase`, `GetIntBase`, `GetWisBase`, `GetStaBase`, `GetStrBase`, `GetAgiBase` -- **Character Info**: `GetLevel`, `SetLevel`, `SetPlayerLevel`, `GetDifficulty`, `GetClass`, `SetClass`, `SetAdventureClass` -- **Classes**: `GetTradeskillClass`, `SetTradeskillClass`, `GetTradeskillLevel`, `SetTradeskillLevel` -- **Identity**: `GetRace`, `GetGender`, `GetModelType`, `SetModelType`, `GetDeity`, `SetDeity`, `GetAlignment`, `SetAlignment` -- **Bonuses**: `AddSpellBonus`, `RemoveSpellBonus`, `AddSkillBonus`, `RemoveSkillBonus` - -### Movement Domain (27 functions) -- **Position**: `SetPosition`, `GetPosition`, `GetX`, `GetY`, `GetZ`, `GetHeading`, `SetHeading` -- **Original Position**: `GetOrigX`, `GetOrigY`, `GetOrigZ` -- **Distance & Facing**: `GetDistance`, `FaceTarget` -- **Speed**: `GetSpeed`, `SetSpeed`, `SetSpeedMultiplier`, `HasMoved`, `IsRunning` -- **Movement**: `MoveToLocation`, `ClearRunningLocations`, `SpawnMove`, `MovementLoopAdd`, `PauseMovement`, `StopMovement` -- **Mounts**: `SetMount`, `GetMount`, `SetMountColor`, `StartAutoMount`, `EndAutoMount`, `IsOnAutoMount` -- **Waypoints**: `AddWaypoint`, `RemoveWaypoint`, `SendWaypoints` -- **Transport**: `Evac`, `Bind`, `Gate` - -### Combat Domain (36 functions) -- **Basic Combat**: `Attack`, `AddHate`, `ClearHate`, `GetMostHated`, `SetTarget`, `GetTarget` -- **Combat State**: `IsInCombat`, `SetInCombat`, `IsCasting`, `HasRecovered` -- **Damage**: `SpellDamage`, `SpellDamageExt`, `DamageSpawn`, `ProcDamage`, `ProcHate` -- **Effects**: `Knockback`, `Interrupt` -- **Processing**: `ProcessMelee`, `ProcessSpell`, `LastSpellAttackHit` -- **Positioning**: `IsBehind`, `IsFlanking`, `InFront` -- **Encounters**: `GetEncounterSize`, `GetEncounter`, `GetHateList`, `ClearEncounter` -- **AI**: `ClearRunback`, `Runback`, `GetRunbackDistance`, `CompareSpawns` -- **Life/Death**: `KillSpawn`, `KillSpawnByDistance`, `Resurrect` -- **Invulnerability**: `IsInvulnerable`, `SetInvulnerable`, `SetAttackable` - -### Miscellaneous Domain (27 functions) -- **Messaging**: `SendMessage`, `LogMessage` -- **Utility**: `MakeRandomInt`, `MakeRandomFloat`, `ParseInt` -- **Identity**: `GetName`, `GetID`, `GetSpawnID`, `IsPlayer`, `IsNPC`, `IsEntity`, `IsDead`, `GetCharacterID` -- **Spawning**: `Despawn`, `Spawn`, `SpawnByLocationID`, `SpawnGroupByID`, `DespawnByLocationID` -- **Groups**: `GetSpawnByLocationID`, `GetSpawnByGroupID`, `GetSpawnGroupID`, `SetSpawnGroupID`, `AddSpawnToGroup`, `IsSpawnGroupAlive` -- **Location**: `GetSpawnLocationID`, `GetSpawnLocationPlacementID`, `SetGridID` -- **Spawn Management**: `SpawnSet`, `SpawnSetByDistance` -- **Variables**: `GetVariableValue`, `SetServerVariable`, `GetServerVariable`, `SetTempVariable`, `GetTempVariable` -- **Line of Sight**: `CheckLOS`, `CheckLOSByCoordinates` - -## Function Organization - -Access functions by domain using the `functions` package: - -```go -import "eq2emu/internal/events/functions" - -// Register all functions at once -handler := events.NewEventHandler() -err := functions.RegisterAllEQ2Functions(handler) - -// Get functions organized by domain -domains := functions.GetFunctionsByDomain() -healthFunctions := domains["health"] // 23 functions -combatFunctions := domains["combat"] // 36 functions -movementFunctions := domains["movement"] // 27 functions -// ... etc -``` - -## Custom Events - -```go -// Register custom event -handler.Register("my_custom_event", func(ctx *events.EventContext) error { - spawn := ctx.GetSpawn() - if spawn == nil { - return fmt.Errorf("no spawn provided") - } - - // Custom logic here - ctx.Debug("Custom event executed for %s", spawn.GetName()) - return nil -}) - -// Execute custom event -ctx := events.NewEventContext(events.EventTypeSpawn, "my_custom_event", "trigger"). - WithSpawn(someSpawn) - -err := handler.Execute(ctx) -``` - -## Event Types - -- `EventTypeSpell` - Spell-related events -- `EventTypeSpawn` - Spawn-related events -- `EventTypeQuest` - Quest-related events -- `EventTypeCombat` - Combat-related events -- `EventTypeZone` - Zone-related events -- `EventTypeItem` - Item-related events - -## Thread Safety - -All operations are thread-safe: -- Event registration/unregistration -- Context parameter/result access -- Event execution - -## Performance - -The event system is designed for minimal overhead: -- No complex registry or statistics -- Direct function calls -- Simple context passing -- Optional timeout support \ No newline at end of file diff --git a/internal/events/functions/functions_test.go b/internal/events/functions/functions_test.go deleted file mode 100644 index 20d8709..0000000 --- a/internal/events/functions/functions_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package functions - -import ( - "testing" - - "eq2emu/internal/events" - "eq2emu/internal/spawn" -) - -func TestAllEQ2Functions(t *testing.T) { - // Create event handler - handler := events.NewEventHandler() - - // Register all EQ2 functions - err := RegisterAllEQ2Functions(handler) - if err != nil { - t.Fatalf("Failed to register all EQ2 functions: %v", err) - } - - // Verify we have a substantial number of functions registered - events := handler.ListEvents() - if len(events) < 100 { - t.Errorf("Expected at least 100 functions, got %d", len(events)) - } - - // Test some key functions exist - requiredFunctions := []string{ - "SetCurrentHP", "GetCurrentHP", "SetMaxHP", "GetMaxHP", - "SetLevel", "GetLevel", "SetClass", "GetClass", - "SetPosition", "GetPosition", "GetX", "GetY", "GetZ", - "Attack", "AddHate", "SpellDamage", "IsInCombat", - "GetName", "GetID", "IsPlayer", "IsNPC", - "MakeRandomInt", "ParseInt", "LogMessage", - } - - for _, funcName := range requiredFunctions { - if !handler.HasEvent(funcName) { - t.Errorf("Required function %s not registered", funcName) - } - } -} - -func TestHealthFunctions(t *testing.T) { - handler := events.NewEventHandler() - err := RegisterAllEQ2Functions(handler) - if err != nil { - t.Fatalf("Failed to register functions: %v", err) - } - - // Create test spawn - testSpawn := &spawn.Spawn{} - - // Test SetCurrentHP - ctx := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test"). - WithSpawn(testSpawn). - WithParameter("hp", 250.0) - - err = handler.Execute(ctx) - if err != nil { - t.Fatalf("SetCurrentHP failed: %v", err) - } - - // Test GetCurrentHP - ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetCurrentHP", "test"). - WithSpawn(testSpawn) - - err = handler.Execute(ctx2) - if err != nil { - t.Fatalf("GetCurrentHP failed: %v", err) - } - - // Verify result - if hp, exists := ctx2.GetResult("hp"); !exists { - t.Error("GetCurrentHP should return hp result") - } else if hp != int32(250) { - t.Errorf("Expected HP 250, got %v", hp) - } -} - -func TestAttributeFunctions(t *testing.T) { - handler := events.NewEventHandler() - err := RegisterAllEQ2Functions(handler) - if err != nil { - t.Fatalf("Failed to register functions: %v", err) - } - - // Create test spawn - testSpawn := &spawn.Spawn{} - - // Test SetLevel - ctx := events.NewEventContext(events.EventTypeSpawn, "SetLevel", "test"). - WithSpawn(testSpawn). - WithParameter("level", 50) - - err = handler.Execute(ctx) - if err != nil { - t.Fatalf("SetLevel failed: %v", err) - } - - // Test GetLevel - ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetLevel", "test"). - WithSpawn(testSpawn) - - err = handler.Execute(ctx2) - if err != nil { - t.Fatalf("GetLevel failed: %v", err) - } - - // Verify result - if level, exists := ctx2.GetResult("level"); !exists { - t.Error("GetLevel should return level result") - } else if level != int16(50) { - t.Errorf("Expected level 50, got %v", level) - } -} - -func TestMovementFunctions(t *testing.T) { - handler := events.NewEventHandler() - err := RegisterAllEQ2Functions(handler) - if err != nil { - t.Fatalf("Failed to register functions: %v", err) - } - - // Create test spawn - testSpawn := &spawn.Spawn{} - - // Test SetPosition - ctx := events.NewEventContext(events.EventTypeSpawn, "SetPosition", "test"). - WithSpawn(testSpawn). - WithParameter("x", 100.0). - WithParameter("y", 200.0). - WithParameter("z", 300.0). - WithParameter("heading", 180.0) - - err = handler.Execute(ctx) - if err != nil { - t.Fatalf("SetPosition failed: %v", err) - } - - // Test GetPosition - ctx2 := events.NewEventContext(events.EventTypeSpawn, "GetPosition", "test"). - WithSpawn(testSpawn) - - err = handler.Execute(ctx2) - if err != nil { - t.Fatalf("GetPosition failed: %v", err) - } - - // Verify results - if x, exists := ctx2.GetResult("x"); !exists || x != float32(100.0) { - t.Errorf("Expected X=100.0, got %v (exists: %t)", x, exists) - } - if y, exists := ctx2.GetResult("y"); !exists || y != float32(200.0) { - t.Errorf("Expected Y=200.0, got %v (exists: %t)", y, exists) - } - if z, exists := ctx2.GetResult("z"); !exists || z != float32(300.0) { - t.Errorf("Expected Z=300.0, got %v (exists: %t)", z, exists) - } -} - -func TestMiscFunctions(t *testing.T) { - handler := events.NewEventHandler() - err := RegisterAllEQ2Functions(handler) - if err != nil { - t.Fatalf("Failed to register functions: %v", err) - } - - // Create test spawn - testSpawn := &spawn.Spawn{} - - // Test GetName - ctx := events.NewEventContext(events.EventTypeSpawn, "GetName", "test"). - WithSpawn(testSpawn) - - err = handler.Execute(ctx) - if err != nil { - t.Fatalf("GetName failed: %v", err) - } - - // Test MakeRandomInt - ctx2 := events.NewEventContext(events.EventTypeSpawn, "MakeRandomInt", "test"). - WithParameter("min", 10). - WithParameter("max", 20) - - err = handler.Execute(ctx2) - if err != nil { - t.Fatalf("MakeRandomInt failed: %v", err) - } - - if result, exists := ctx2.GetResult("random_int"); !exists { - t.Error("MakeRandomInt should return random_int result") - } else if randInt, ok := result.(int); !ok || randInt < 10 || randInt > 20 { - t.Errorf("Expected random int between 10-20, got %v", result) - } -} - -func TestFunctionsByDomain(t *testing.T) { - domains := GetFunctionsByDomain() - - // Verify we have expected domains - expectedDomains := []string{"health", "attributes", "movement", "combat", "misc"} - for _, domain := range expectedDomains { - if functions, exists := domains[domain]; !exists { - t.Errorf("Domain %s not found", domain) - } else if len(functions) == 0 { - t.Errorf("Domain %s has no functions", domain) - } - } - - // Verify health domain has expected functions - healthFunctions := domains["health"] - expectedHealthFunctions := []string{"SetCurrentHP", "GetCurrentHP", "SetMaxHP", "GetMaxHP"} - for _, funcName := range expectedHealthFunctions { - found := false - for _, f := range healthFunctions { - if f == funcName { - found = true - break - } - } - if !found { - t.Errorf("Health domain missing function %s", funcName) - } - } -} - -func TestErrorHandling(t *testing.T) { - handler := events.NewEventHandler() - err := RegisterAllEQ2Functions(handler) - if err != nil { - t.Fatalf("Failed to register functions: %v", err) - } - - // Test function with no spawn context - ctx := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test"). - WithParameter("hp", 100.0) - // No spawn set - - err = handler.Execute(ctx) - if err == nil { - t.Error("SetCurrentHP should fail without spawn context") - } - - // Test function with invalid parameters - testSpawn := &spawn.Spawn{} - ctx2 := events.NewEventContext(events.EventTypeSpawn, "SetCurrentHP", "test"). - WithSpawn(testSpawn). - WithParameter("hp", -50.0) // Negative HP - - err = handler.Execute(ctx2) - if err == nil { - t.Error("SetCurrentHP should fail with negative HP") - } -} \ No newline at end of file diff --git a/internal/factions/benchmark_test.go b/internal/factions/benchmark_test.go deleted file mode 100644 index d774c8f..0000000 --- a/internal/factions/benchmark_test.go +++ /dev/null @@ -1,836 +0,0 @@ -package factions - -import ( - "fmt" - "testing" -) - -// Benchmark MasterFactionList operations -func BenchmarkMasterFactionList(b *testing.B) { - b.Run("GetFaction", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 1000) + 1) - _ = mfl.GetFaction(factionID) - } - }) - - b.Run("GetFactionByName", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetFactionByName("Benchmark Faction") - } - }) - - b.Run("AddFaction", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32(2000 + i) - faction := NewFaction(factionID, "New Faction", "Test", "New test") - mfl.AddFaction(faction) - } - }) - - b.Run("GetFactionCount", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetFactionCount() - } - }) - - b.Run("AddHostileFaction", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 1000) + 1) - hostileID := int32(((i + 1) % 1000) + 1) - mfl.AddHostileFaction(factionID, hostileID) - } - }) - - b.Run("GetHostileFactions", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions and some hostile relationships - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - // Add a reasonable number of hostile relationships - for i := int32(1); i <= 100; i++ { - for j := int32(1); j <= 5; j++ { - mfl.AddHostileFaction(i, i+j) - } - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - _ = mfl.GetHostileFactions(factionID) - } - }) - - b.Run("ValidateFactions", func(b *testing.B) { - mfl := NewMasterList() - // Pre-populate with factions - for i := int32(1); i <= 1000; i++ { - faction := NewFaction(i, "Benchmark Faction", "Test", "Benchmark test") - mfl.AddFaction(faction) - } - // Add some hostile relationships for realistic validation - for i := int32(1); i <= 100; i++ { - mfl.AddHostileFaction(i, i+1) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.ValidateFactions() - } - }) -} - -// Benchmark PlayerFaction operations -func BenchmarkPlayerFaction(b *testing.B) { - mfl := NewMasterList() - - // Setup factions with proper values - for i := int32(1); i <= 100; i++ { - faction := NewFaction(i, "Player Faction", "Test", "Player test") - faction.PositiveChange = 100 - faction.NegativeChange = 50 - mfl.AddFaction(faction) - } - - pf := NewPlayerFaction(mfl) - - // Pre-populate some faction values - for i := int32(1); i <= 100; i++ { - pf.SetFactionValue(i, int32(i*1000)) - } - - b.Run("GetFactionValue", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - _ = pf.GetFactionValue(factionID) - } - }) - - b.Run("SetFactionValue", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - pf.SetFactionValue(factionID, int32(i)) - } - }) - - b.Run("IncreaseFaction", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - pf.IncreaseFaction(factionID, 10) - } - }) - - b.Run("DecreaseFaction", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - pf.DecreaseFaction(factionID, 5) - } - }) - - b.Run("GetCon", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - _ = pf.GetCon(factionID) - } - }) - - b.Run("GetPercent", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - _ = pf.GetPercent(factionID) - } - }) - - b.Run("ShouldAttack", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - _ = pf.ShouldAttack(factionID) - } - }) - - b.Run("FactionUpdate", func(b *testing.B) { - // Trigger some updates first - for i := int32(1); i <= 10; i++ { - pf.IncreaseFaction(i, 1) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - pf.FactionUpdate(int16(i % 10)) - } - }) -} - -// Benchmark Manager operations -func BenchmarkManager(b *testing.B) { - manager := NewManager(nil, nil) - - // Pre-populate with factions - for i := int32(1); i <= 100; i++ { - faction := NewFaction(i, "Manager Faction", "Test", "Manager test") - manager.AddFaction(faction) - } - - b.Run("GetFaction", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - _ = manager.GetFaction(factionID) - } - }) - - b.Run("CreatePlayerFaction", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.CreatePlayerFaction() - } - }) - - b.Run("RecordFactionIncrease", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - manager.RecordFactionIncrease(factionID) - } - }) - - b.Run("RecordFactionDecrease", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 100) + 1) - manager.RecordFactionDecrease(factionID) - } - }) - - b.Run("GetStatistics", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.GetStatistics() - } - }) - - b.Run("ValidateAllFactions", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = manager.ValidateAllFactions() - } - }) -} - -// Benchmark EntityFactionAdapter operations -func BenchmarkEntityFactionAdapter(b *testing.B) { - manager := NewManager(nil, nil) - entity := &mockEntity{id: 123, name: "Benchmark Entity", dbID: 456} - adapter := NewEntityFactionAdapter(entity, manager, nil) - - // Set up factions and relationships - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Entity Faction", "Test", "Entity test") - manager.AddFaction(faction) - } - - mfl := manager.GetMasterFactionList() - mfl.AddHostileFaction(1, 2) - mfl.AddFriendlyFaction(1, 3) - - b.Run("GetFactionID", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = adapter.GetFactionID() - } - }) - - b.Run("SetFactionID", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 10) + 1) - adapter.SetFactionID(factionID) - } - }) - - b.Run("GetFaction", func(b *testing.B) { - adapter.SetFactionID(1) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = adapter.GetFaction() - } - }) - - b.Run("IsHostileToFaction", func(b *testing.B) { - adapter.SetFactionID(1) - b.ResetTimer() - for i := 0; i < b.N; i++ { - targetID := int32((i % 10) + 1) - _ = adapter.IsHostileToFaction(targetID) - } - }) - - b.Run("IsFriendlyToFaction", func(b *testing.B) { - adapter.SetFactionID(1) - b.ResetTimer() - for i := 0; i < b.N; i++ { - targetID := int32((i % 10) + 1) - _ = adapter.IsFriendlyToFaction(targetID) - } - }) - - b.Run("ValidateFaction", func(b *testing.B) { - adapter.SetFactionID(1) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = adapter.ValidateFaction() - } - }) -} - -// Benchmark concurrent operations -func BenchmarkConcurrentOperations(b *testing.B) { - b.Run("MasterFactionListConcurrent", func(b *testing.B) { - mfl := NewMasterList() - - // Pre-populate - for i := int32(1); i <= 100; i++ { - faction := NewFaction(i, "Concurrent Faction", "Test", "Concurrent test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - factionID := int32((i % 100) + 1) - _ = mfl.GetFaction(factionID) - i++ - } - }) - }) - - b.Run("PlayerFactionConcurrent", func(b *testing.B) { - mfl := NewMasterList() - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Player Faction", "Test", "Player test") - faction.PositiveChange = 100 - mfl.AddFaction(faction) - } - - pf := NewPlayerFaction(mfl) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - factionID := int32((i % 10) + 1) - switch i % 4 { - case 0: - _ = pf.GetFactionValue(factionID) - case 1: - pf.IncreaseFaction(factionID, 1) - case 2: - _ = pf.GetCon(factionID) - case 3: - _ = pf.GetPercent(factionID) - } - i++ - } - }) - }) - - b.Run("ManagerConcurrent", func(b *testing.B) { - manager := NewManager(nil, nil) - - // Pre-populate - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Manager Faction", "Test", "Manager test") - manager.AddFaction(faction) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - factionID := int32((i % 10) + 1) - switch i % 4 { - case 0: - _ = manager.GetFaction(factionID) - case 1: - manager.RecordFactionIncrease(factionID) - case 2: - _ = manager.GetStatistics() - case 3: - _ = manager.CreatePlayerFaction() - } - i++ - } - }) - }) -} - -// Memory allocation benchmarks -func BenchmarkMemoryAllocations(b *testing.B) { - b.Run("FactionCreation", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = NewFaction(int32(i), "Memory Test", "Test", "Memory test") - } - }) - - b.Run("MasterFactionListCreation", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = NewMasterList() - } - }) - - b.Run("PlayerFactionCreation", func(b *testing.B) { - mfl := NewMasterList() - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = NewPlayerFaction(mfl) - } - }) - - b.Run("ManagerCreation", func(b *testing.B) { - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = NewManager(nil, nil) - } - }) - - b.Run("EntityAdapterCreation", func(b *testing.B) { - manager := NewManager(nil, nil) - entity := &mockEntity{id: 123, name: "Memory Entity", dbID: 456} - - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = NewEntityFactionAdapter(entity, manager, nil) - } - }) -} - -// Contention benchmarks -func BenchmarkContention(b *testing.B) { - b.Run("HighContentionReads", func(b *testing.B) { - mfl := NewMasterList() - - // Add a single faction that will be accessed heavily - faction := NewFaction(1, "Contention Test", "Test", "Contention test") - mfl.AddFaction(faction) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = mfl.GetFaction(1) - } - }) - }) - - b.Run("HighContentionWrites", func(b *testing.B) { - mfl := NewMasterList() - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - factionID := int32(1000000 + i) // Unique IDs to avoid conflicts - faction := NewFaction(factionID, "Write Test", "Test", "Write test") - mfl.AddFaction(faction) - i++ - } - }) - }) - - b.Run("MixedReadWrite", func(b *testing.B) { - mfl := NewMasterList() - - // Pre-populate - for i := int32(1); i <= 100; i++ { - faction := NewFaction(i, "Mixed Test", "Test", "Mixed test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - if i%10 == 0 { - // 10% writes - factionID := int32(2000000 + i) - faction := NewFaction(factionID, "New Mixed", "Test", "New mixed") - mfl.AddFaction(faction) - } else { - // 90% reads - factionID := int32((i % 100) + 1) - _ = mfl.GetFaction(factionID) - } - i++ - } - }) - }) -} - -// Scalability benchmarks -func BenchmarkScalability(b *testing.B) { - sizes := []int{10, 100, 1000, 10000} - - for _, size := range sizes { - b.Run("FactionLookup_"+string(rune(size)), func(b *testing.B) { - mfl := NewMasterList() - - // Pre-populate with varying sizes - for i := int32(1); i <= int32(size); i++ { - faction := NewFaction(i, "Scale Test", "Test", "Scale test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % size) + 1) - _ = mfl.GetFaction(factionID) - } - }) - } -} - -// Benchmark bespoke MasterList features -func BenchmarkMasterListBespokeFeatures(b *testing.B) { - // Setup function for consistent test data - setupMasterList := func() *MasterList { - mfl := NewMasterList() - - // Add factions across different types - types := []string{"City", "Guild", "Religion", "Race", "Organization"} - - for i := 1; i <= 1000; i++ { - factionType := types[i%len(types)] - faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") - mfl.AddFaction(faction) - } - return mfl - } - - b.Run("GetFactionSafe", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 1000) + 1) - _, _ = mfl.GetFactionSafe(factionID) - } - }) - - b.Run("GetFactionByName", func(b *testing.B) { - mfl := setupMasterList() - names := []string{"faction1", "faction50", "faction100", "faction500", "faction1000"} - b.ResetTimer() - for i := 0; i < b.N; i++ { - name := names[i%len(names)] - _ = mfl.GetFactionByName(name) - } - }) - - b.Run("GetFactionsByType", func(b *testing.B) { - mfl := setupMasterList() - types := []string{"City", "Guild", "Religion", "Race", "Organization"} - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionType := types[i%len(types)] - _ = mfl.GetFactionsByType(factionType) - } - }) - - b.Run("GetSpecialFactions", func(b *testing.B) { - mfl := setupMasterList() - // Add some special factions - for i := int32(1); i <= 5; i++ { - faction := NewFaction(i, fmt.Sprintf("Special%d", i), "Special", "Special faction") - mfl.AddFaction(faction) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetSpecialFactions() - } - }) - - b.Run("GetRegularFactions", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetRegularFactions() - } - }) - - b.Run("GetTypes", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetTypes() - } - }) - - b.Run("GetAllFactionsList", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetAllFactionsList() - } - }) - - b.Run("GetFactionIDs", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetFactionIDs() - } - }) - - b.Run("UpdateFaction", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - factionID := int32((i % 1000) + 1) - updatedFaction := &Faction{ - ID: factionID, - Name: fmt.Sprintf("Updated%d", i), - Type: "Updated", - Description: "Updated faction", - } - mfl.UpdateFaction(updatedFaction) - } - }) - - b.Run("ForEach", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - count := 0 - mfl.ForEach(func(id int32, faction *Faction) { - count++ - }) - } - }) - - b.Run("GetStatistics", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = mfl.GetStatistics() - } - }) - - b.Run("RemoveFaction", func(b *testing.B) { - b.StopTimer() - mfl := setupMasterList() - initialCount := mfl.GetFactionCount() - - // Pre-populate with factions we'll remove - for i := 0; i < b.N; i++ { - faction := NewFaction(int32(20000+i), fmt.Sprintf("ToRemove%d", i), "Temporary", "Temporary faction") - mfl.AddFaction(faction) - } - - b.StartTimer() - for i := 0; i < b.N; i++ { - mfl.RemoveFaction(int32(20000 + i)) - } - - b.StopTimer() - if mfl.GetFactionCount() != initialCount { - b.Errorf("Expected %d factions after removal, got %d", initialCount, mfl.GetFactionCount()) - } - }) -} - -// Memory allocation benchmarks for bespoke features -func BenchmarkMasterListBespokeFeatures_Allocs(b *testing.B) { - setupMasterList := func() *MasterList { - mfl := NewMasterList() - types := []string{"City", "Guild", "Religion", "Race", "Organization"} - - for i := 1; i <= 100; i++ { - factionType := types[i%len(types)] - faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") - mfl.AddFaction(faction) - } - return mfl - } - - b.Run("GetFactionByName_Allocs", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = mfl.GetFactionByName("faction1") - } - }) - - b.Run("GetFactionsByType_Allocs", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = mfl.GetFactionsByType("City") - } - }) - - b.Run("GetTypes_Allocs", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = mfl.GetTypes() - } - }) - - b.Run("GetSpecialFactions_Allocs", func(b *testing.B) { - mfl := setupMasterList() - // Add some special factions - for i := int32(1); i <= 5; i++ { - faction := NewFaction(i, fmt.Sprintf("Special%d", i), "Special", "Special faction") - mfl.AddFaction(faction) - } - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = mfl.GetSpecialFactions() - } - }) - - b.Run("GetStatistics_Allocs", func(b *testing.B) { - mfl := setupMasterList() - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = mfl.GetStatistics() - } - }) -} - -// Concurrent benchmarks for bespoke features -func BenchmarkMasterListBespokeConcurrent(b *testing.B) { - b.Run("ConcurrentReads", func(b *testing.B) { - mfl := NewMasterList() - - // Setup test data - types := []string{"City", "Guild", "Religion", "Race", "Organization"} - for i := 1; i <= 100; i++ { - factionType := types[i%len(types)] - faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - // Mix of read operations - switch i % 6 { - case 0: - mfl.GetFaction(int32(i%100 + 1)) - case 1: - mfl.GetFactionsByType("City") - case 2: - mfl.GetFactionByName("faction1") - case 3: - mfl.GetSpecialFactions() - case 4: - mfl.GetRegularFactions() - case 5: - mfl.GetTypes() - } - i++ - } - }) - }) - - b.Run("ConcurrentMixed", func(b *testing.B) { - mfl := NewMasterList() - - // Setup test data - types := []string{"City", "Guild", "Religion", "Race", "Organization"} - for i := 1; i <= 100; i++ { - factionType := types[i%len(types)] - faction := NewFaction(int32(i), fmt.Sprintf("Faction%d", i), factionType, "Benchmark test") - mfl.AddFaction(faction) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - // Mix of read and write operations (mostly reads) - switch i % 10 { - case 0: // 10% writes - faction := NewFaction(int32(i+50000), fmt.Sprintf("Concurrent%d", i), "Concurrent", "Concurrent test") - mfl.AddFaction(faction) - default: // 90% reads - switch i % 5 { - case 0: - mfl.GetFaction(int32(i%100 + 1)) - case 1: - mfl.GetFactionsByType("City") - case 2: - mfl.GetFactionByName("faction1") - case 3: - mfl.GetSpecialFactions() - case 4: - mfl.GetTypes() - } - } - i++ - } - }) - }) -} diff --git a/internal/factions/concurrency_test.go b/internal/factions/concurrency_test.go deleted file mode 100644 index 68a0d89..0000000 --- a/internal/factions/concurrency_test.go +++ /dev/null @@ -1,524 +0,0 @@ -package factions - -import ( - "sync" - "testing" - "time" -) - -// Stress test MasterFactionList with concurrent operations -func TestMasterFactionListConcurrency(t *testing.T) { - mfl := NewMasterList() - - // Pre-populate with some factions - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Test Faction", "Test", "Test faction") - mfl.AddFaction(faction) - } - - const numGoroutines = 100 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - - t.Run("ConcurrentFactionAccess", 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++ { - factionID := int32((goroutineID % 10) + 1) - - // Mix of read operations - faction := mfl.GetFaction(factionID) - if faction == nil { - t.Errorf("Goroutine %d: Expected faction %d to exist", goroutineID, factionID) - } - - _ = mfl.GetFactionByName("Test Faction") - _ = mfl.HasFaction(factionID) - _ = mfl.GetFactionCount() - _ = mfl.GetDefaultFactionValue(factionID) - _ = mfl.GetIncreaseAmount(factionID) - _ = mfl.GetDecreaseAmount(factionID) - } - }(i) - } - - wg.Wait() - }) - - t.Run("ConcurrentRelationshipManagement", 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++ { - factionID := int32((goroutineID % 10) + 1) - targetID := int32(((goroutineID + 1) % 10) + 1) - - // Concurrent relationship operations - if goroutineID%2 == 0 { - mfl.AddHostileFaction(factionID, targetID) - _ = mfl.GetHostileFactions(factionID) - } else { - mfl.AddFriendlyFaction(factionID, targetID) - _ = mfl.GetFriendlyFactions(factionID) - } - } - }(i) - } - - wg.Wait() - }) - - t.Run("ConcurrentFactionModification", 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++ { - newFactionID := int32(1000 + goroutineID*operationsPerGoroutine + j) - faction := NewFaction(newFactionID, "Concurrent Faction", "Test", "Concurrent test") - - err := mfl.AddFaction(faction) - if err != nil { - t.Errorf("Goroutine %d: Failed to add faction %d: %v", goroutineID, newFactionID, err) - } - - // Verify it was added - retrieved := mfl.GetFaction(newFactionID) - if retrieved == nil { - t.Errorf("Goroutine %d: Failed to retrieve faction %d", goroutineID, newFactionID) - } - } - }(i) - } - - wg.Wait() - }) -} - -// Stress test PlayerFaction with concurrent operations -func TestPlayerFactionConcurrency(t *testing.T) { - mfl := NewMasterList() - - // Add test factions with proper values - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Test Faction", "Test", "Test faction") - faction.PositiveChange = 100 - faction.NegativeChange = 50 - mfl.AddFaction(faction) - } - - pf := NewPlayerFaction(mfl) - - const numGoroutines = 100 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - - t.Run("ConcurrentFactionValueOperations", 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++ { - factionID := int32((goroutineID % 10) + 1) - - // Mix of faction value operations - switch j % 4 { - case 0: - pf.IncreaseFaction(factionID, 10) - case 1: - pf.DecreaseFaction(factionID, 5) - case 2: - pf.SetFactionValue(factionID, int32(goroutineID*100)) - case 3: - _ = pf.GetFactionValue(factionID) - _ = pf.GetCon(factionID) - _ = pf.GetPercent(factionID) - } - } - }(i) - } - - wg.Wait() - }) - - t.Run("ConcurrentUpdateManagement", 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++ { - factionID := int32((goroutineID % 10) + 1) - - // Operations that trigger updates - pf.IncreaseFaction(factionID, 1) - - // Check for pending updates - _ = pf.HasPendingUpdates() - _ = pf.GetPendingUpdates() - - // Occasionally clear updates - if j%10 == 0 { - pf.ClearPendingUpdates() - } - } - }(i) - } - - wg.Wait() - }) - - t.Run("ConcurrentPacketGeneration", 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++ { - // Trigger faction updates - factionID := int32((goroutineID % 10) + 1) - pf.IncreaseFaction(factionID, 1) - - // Generate packets concurrently - _, err := pf.FactionUpdate(int16(goroutineID % 10)) - if err != nil { - t.Errorf("Goroutine %d: FactionUpdate failed: %v", goroutineID, err) - } - } - }(i) - } - - wg.Wait() - }) -} - -// Stress test Manager with concurrent operations -func TestManagerConcurrency(t *testing.T) { - manager := NewManager(nil, nil) // No database or logger for testing - - const numGoroutines = 100 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - - t.Run("ConcurrentStatisticsOperations", 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++ { - factionID := int32((goroutineID % 10) + 1) - - // Mix of statistics operations - switch j % 4 { - case 0: - manager.RecordFactionIncrease(factionID) - case 1: - manager.RecordFactionDecrease(factionID) - case 2: - _ = manager.GetStatistics() - case 3: - _ = manager.GetFactionCount() - } - } - }(i) - } - - wg.Wait() - - // Verify statistics consistency - stats := manager.GetStatistics() - totalChanges := stats["total_faction_changes"].(int64) - increases := stats["faction_increases"].(int64) - decreases := stats["faction_decreases"].(int64) - - if totalChanges != increases+decreases { - t.Errorf("Statistics inconsistency: total %d != increases %d + decreases %d", - totalChanges, increases, decreases) - } - }) - - t.Run("ConcurrentFactionManagement", 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++ { - factionID := int32(2000 + goroutineID*operationsPerGoroutine + j) - faction := NewFaction(factionID, "Manager Test", "Test", "Manager test") - - // Add faction - err := manager.AddFaction(faction) - if err != nil { - t.Errorf("Goroutine %d: Failed to add faction %d: %v", goroutineID, factionID, err) - continue - } - - // Read operations - _ = manager.GetFaction(factionID) - _ = manager.GetFactionByName("Manager Test") - } - }(i) - } - - wg.Wait() - }) - - t.Run("ConcurrentPlayerFactionCreation", 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++ { - // Create player factions concurrently - pf := manager.CreatePlayerFaction() - if pf == nil { - t.Errorf("Goroutine %d: CreatePlayerFaction returned nil", goroutineID) - } - } - }(i) - } - - wg.Wait() - - // Check statistics - stats := manager.GetStatistics() - expectedPlayers := int64(numGoroutines * operationsPerGoroutine) - actualPlayers := stats["players_with_factions"].(int64) - - if actualPlayers != expectedPlayers { - t.Errorf("Expected %d players with factions, got %d", expectedPlayers, actualPlayers) - } - }) -} - -// Stress test EntityFactionAdapter with concurrent operations -func TestEntityFactionAdapterConcurrency(t *testing.T) { - manager := NewManager(nil, nil) - - // Mock entity - entity := &mockEntity{id: 123, name: "Test Entity", dbID: 456} - adapter := NewEntityFactionAdapter(entity, manager, nil) - - const numGoroutines = 100 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - - t.Run("ConcurrentFactionIDOperations", 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++ { - factionID := int32((goroutineID % 10) + 1) - - // Mix of faction ID operations - switch j % 3 { - case 0: - adapter.SetFactionID(factionID) - case 1: - _ = adapter.GetFactionID() - case 2: - _ = adapter.GetFaction() - _ = adapter.GetFactionName() - } - } - }(i) - } - - wg.Wait() - }) - - t.Run("ConcurrentRelationshipChecks", func(t *testing.T) { - // Set up some factions first - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Test Faction", "Test", "Test faction") - manager.AddFaction(faction) - } - - // Set up relationships - mfl := manager.GetMasterFactionList() - mfl.AddHostileFaction(1, 2) - mfl.AddFriendlyFaction(1, 3) - - adapter.SetFactionID(1) - - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - otherFactionID := int32((goroutineID % 10) + 1) - - // Concurrent relationship checks - _ = adapter.IsHostileToFaction(otherFactionID) - _ = adapter.IsFriendlyToFaction(otherFactionID) - - // Validation - _ = adapter.ValidateFaction() - } - }(i) - } - - wg.Wait() - }) -} - -// Mock entity for testing -type mockEntity struct { - id int32 - name string - dbID int32 -} - -func (e *mockEntity) GetID() int32 { - return e.id -} - -func (e *mockEntity) GetName() string { - return e.name -} - -func (e *mockEntity) GetDatabaseID() int32 { - return e.dbID -} - -// Test for potential deadlocks -func TestDeadlockPrevention(t *testing.T) { - mfl := NewMasterList() - manager := NewManager(nil, nil) - - // Add test factions - for i := int32(1); i <= 10; i++ { - faction := NewFaction(i, "Test Faction", "Test", "Test faction") - manager.AddFaction(faction) - } - - 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 := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < 100; j++ { - factionID := int32((goroutineID % 10) + 1) - - // Mix operations that could potentially deadlock - switch j % 6 { - case 0: - _ = mfl.GetFaction(factionID) - _ = manager.GetFaction(factionID) - case 1: - _ = mfl.GetFactionCount() - _ = manager.GetFactionCount() - case 2: - mfl.AddHostileFaction(factionID, factionID+1) - _ = manager.GetStatistics() - case 3: - _ = mfl.GetAllFactions() - manager.RecordFactionIncrease(factionID) - case 4: - _ = mfl.ValidateFactions() - _ = manager.ValidateAllFactions() - case 5: - pf := manager.CreatePlayerFaction() - pf.IncreaseFaction(factionID, 10) - _ = pf.GetFactionValue(factionID) - } - } - }(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") - } - - // This test is designed to be run with: go test -race - mfl := NewMasterList() - manager := NewManager(nil, nil) - - // Rapid concurrent operations to trigger race conditions - const numGoroutines = 200 - const operationsPerGoroutine = 50 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(goroutineID int) { - defer wg.Done() - - for j := 0; j < operationsPerGoroutine; j++ { - factionID := int32((goroutineID % 20) + 1) - - // Rapid-fire operations - faction := NewFaction(factionID, "Race Test", "Test", "Race test") - mfl.AddFaction(faction) - _ = mfl.GetFaction(factionID) - mfl.AddHostileFaction(factionID, factionID+1) - _ = mfl.GetHostileFactions(factionID) - - manager.AddFaction(faction) - manager.RecordFactionIncrease(factionID) - _ = manager.GetStatistics() - - pf := manager.CreatePlayerFaction() - pf.IncreaseFaction(factionID, 1) - _ = pf.GetFactionValue(factionID) - } - }(i) - } - - wg.Wait() -} diff --git a/internal/factions/factions_test.go b/internal/factions/factions_test.go deleted file mode 100644 index 8fab74b..0000000 --- a/internal/factions/factions_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package factions - -import ( - "testing" -) - -func TestNewFaction(t *testing.T) { - faction := NewFaction(1, "Test Faction", "TestType", "A test faction") - if faction == nil { - t.Fatal("NewFaction returned nil") - } - - if faction.ID != 1 { - t.Errorf("Expected ID 1, got %d", faction.ID) - } - - if faction.Name != "Test Faction" { - t.Errorf("Expected name 'Test Faction', got '%s'", faction.Name) - } - - if faction.Type != "TestType" { - t.Errorf("Expected type 'TestType', got '%s'", faction.Type) - } - - if faction.Description != "A test faction" { - t.Errorf("Expected description 'A test faction', got '%s'", faction.Description) - } -} - -func TestMasterFactionList(t *testing.T) { - mfl := NewMasterList() - if mfl == nil { - t.Fatal("NewMasterFactionList returned nil") - } - - // Test adding faction - faction := NewFaction(100, "Test Faction", "Test", "Test faction") - err := mfl.AddFaction(faction) - if err != nil { - t.Fatalf("Failed to add faction: %v", err) - } - - // Test getting faction - retrieved := mfl.GetFaction(100) - if retrieved == nil { - t.Error("Failed to retrieve added faction") - } else if retrieved.Name != "Test Faction" { - t.Errorf("Expected name 'Test Faction', got '%s'", retrieved.Name) - } - - // Test getting all factions - factions := mfl.GetAllFactions() - if len(factions) == 0 { - t.Error("Expected at least one faction") - } -} - -func TestPlayerFaction(t *testing.T) { - mfl := NewMasterList() - pf := NewPlayerFaction(mfl) - if pf == nil { - t.Fatal("NewPlayerFaction returned nil") - } - - // Use faction ID > 10 since IDs <= 10 are special factions that always return 0 - testFactionID := int32(100) - - // Test setting faction value - pf.SetFactionValue(testFactionID, 1000) - value := pf.GetFactionValue(testFactionID) - if value != 1000 { - t.Errorf("Expected faction value 1000, got %d", value) - } - - // Test faction modification - pf.IncreaseFaction(testFactionID, 500) - value = pf.GetFactionValue(testFactionID) - if value != 1500 { - t.Errorf("Expected faction value 1500 after increase, got %d", value) - } - - pf.DecreaseFaction(testFactionID, 200) - value = pf.GetFactionValue(testFactionID) - if value != 1300 { - t.Errorf("Expected faction value 1300 after decrease, got %d", value) - } - - // Test consideration calculation - consideration := pf.GetCon(testFactionID) - if consideration < -4 || consideration > 4 { - t.Errorf("Consideration %d is out of valid range [-4, 4]", consideration) - } -} - -func TestFactionRelations(t *testing.T) { - mfl := NewMasterList() - - // Add test factions - faction1 := NewFaction(1, "Faction 1", "Test", "Test faction 1") - faction2 := NewFaction(2, "Faction 2", "Test", "Test faction 2") - faction3 := NewFaction(3, "Faction 3", "Test", "Test faction 3") - - mfl.AddFaction(faction1) - mfl.AddFaction(faction2) - mfl.AddFaction(faction3) - - // Test hostile relations - mfl.AddHostileFaction(1, 2) - - // Check if faction 2 is in the hostile list for faction 1 - hostiles := mfl.GetHostileFactions(1) - isHostile := false - for _, hostileID := range hostiles { - if hostileID == 2 { - isHostile = true - break - } - } - if !isHostile { - t.Error("Expected faction 2 to be hostile to faction 1") - } - - // Test friendly relations - mfl.AddFriendlyFaction(1, 3) - - // Check if faction 3 is in the friendly list for faction 1 - friendlies := mfl.GetFriendlyFactions(1) - isFriendly := false - for _, friendlyID := range friendlies { - if friendlyID == 3 { - isFriendly = true - break - } - } - if !isFriendly { - t.Error("Expected faction 3 to be friendly to faction 1") - } - - // Test removing relations - need to implement RemoveHostileFaction first - // For now, just verify current state - hostiles = mfl.GetHostileFactions(1) - isHostile = false - for _, hostileID := range hostiles { - if hostileID == 2 { - isHostile = true - break - } - } - if !isHostile { - t.Error("Expected faction 2 to still be hostile to faction 1 (removal not implemented)") - } -} - -func TestFactionValidation(t *testing.T) { - mfl := NewMasterList() - - // Test nil faction - err := mfl.AddFaction(nil) - if err == nil { - t.Error("Expected error when adding nil faction") - } - - // Test invalid faction ID - faction := NewFaction(0, "Invalid", "Test", "Invalid faction") - err = mfl.AddFaction(faction) - if err == nil { - t.Error("Expected error when adding faction with ID 0") - } - - // Test empty name - faction = NewFaction(1, "", "Test", "Empty name faction") - err = mfl.AddFaction(faction) - if err == nil { - t.Error("Expected error when adding faction with empty name") - } -} - -func TestMasterListBespokeFeatures(t *testing.T) { - mfl := NewMasterList() - - // Create test factions with different properties - faction1 := NewFaction(1, "Special Faction", "Special", "A special faction") - faction2 := NewFaction(20, "City Faction", "City", "A city faction") - faction3 := NewFaction(21, "Guild Faction", "Guild", "A guild faction") - faction4 := NewFaction(30, "Another City", "City", "Another city faction") - - // Add factions - mfl.AddFaction(faction1) - mfl.AddFaction(faction2) - mfl.AddFaction(faction3) - mfl.AddFaction(faction4) - - // Test GetFactionSafe - retrieved, exists := mfl.GetFactionSafe(20) - if !exists || retrieved == nil { - t.Error("GetFactionSafe should return existing faction and true") - } - - _, exists = mfl.GetFactionSafe(9999) - if exists { - t.Error("GetFactionSafe should return false for non-existent ID") - } - - // Test GetFactionByName (case-insensitive) - found := mfl.GetFactionByName("city faction") - if found == nil || found.ID != 20 { - t.Error("GetFactionByName should find 'City Faction' (case insensitive)") - } - - found = mfl.GetFactionByName("GUILD FACTION") - if found == nil || found.ID != 21 { - t.Error("GetFactionByName should find 'Guild Faction' (uppercase)") - } - - found = mfl.GetFactionByName("NonExistent") - if found != nil { - t.Error("GetFactionByName should return nil for non-existent faction") - } - - // Test GetFactionsByType - cityFactions := mfl.GetFactionsByType("City") - if len(cityFactions) != 2 { - t.Errorf("GetFactionsByType('City') returned %v results, want 2", len(cityFactions)) - } - - guildFactions := mfl.GetFactionsByType("Guild") - if len(guildFactions) != 1 { - t.Errorf("GetFactionsByType('Guild') returned %v results, want 1", len(guildFactions)) - } - - // Test GetSpecialFactions and GetRegularFactions - specialFactions := mfl.GetSpecialFactions() - if len(specialFactions) != 1 { - t.Errorf("GetSpecialFactions() returned %v results, want 1", len(specialFactions)) - } - - regularFactions := mfl.GetRegularFactions() - if len(regularFactions) != 3 { - t.Errorf("GetRegularFactions() returned %v results, want 3", len(regularFactions)) - } - - // Test GetTypes - types := mfl.GetTypes() - if len(types) < 3 { - t.Errorf("GetTypes() returned %v types, want at least 3", len(types)) - } - - // Verify types contains expected values - typeMap := make(map[string]bool) - for _, factionType := range types { - typeMap[factionType] = true - } - if !typeMap["Special"] || !typeMap["City"] || !typeMap["Guild"] { - t.Error("GetTypes() should contain 'Special', 'City', and 'Guild'") - } - - // Test UpdateFaction - updatedFaction := &Faction{ - ID: 20, - Name: "Updated City Faction", - Type: "UpdatedCity", - Description: "An updated city faction", - } - - err := mfl.UpdateFaction(updatedFaction) - if err != nil { - t.Errorf("UpdateFaction failed: %v", err) - } - - // Verify the update worked - retrieved = mfl.GetFaction(20) - if retrieved.Name != "Updated City Faction" { - t.Errorf("Expected updated name 'Updated City Faction', got '%s'", retrieved.Name) - } - - if retrieved.Type != "UpdatedCity" { - t.Errorf("Expected updated type 'UpdatedCity', got '%s'", retrieved.Type) - } - - // Test updating non-existent faction - nonExistentFaction := &Faction{ID: 9999, Name: "Non-existent"} - err = mfl.UpdateFaction(nonExistentFaction) - if err == nil { - t.Error("UpdateFaction should fail for non-existent faction") - } - - // Test GetAllFactionsList - allList := mfl.GetAllFactionsList() - if len(allList) != 4 { - t.Errorf("GetAllFactionsList() returned %v factions, want 4", len(allList)) - } - - // Test GetFactionIDs - ids := mfl.GetFactionIDs() - if len(ids) != 4 { - t.Errorf("GetFactionIDs() returned %v IDs, want 4", len(ids)) - } -} - -func TestMasterListConcurrency(t *testing.T) { - mfl := NewMasterList() - - // Add initial factions - for i := 1; i <= 50; i++ { - faction := NewFaction(int32(i+100), "Faction", "Test", "Test faction") - mfl.AddFaction(faction) - } - - // Test concurrent access - done := make(chan bool, 10) - - // Concurrent readers - for i := 0; i < 5; i++ { - go func() { - defer func() { done <- true }() - for j := 0; j < 100; j++ { - mfl.GetFaction(int32(j%50 + 101)) - mfl.GetFactionsByType("Test") - mfl.GetFactionByName("faction") - mfl.HasFaction(int32(j%50 + 101)) - } - }() - } - - // Concurrent writers - for i := 0; i < 5; i++ { - go func(workerID int) { - defer func() { done <- true }() - for j := 0; j < 10; j++ { - factionID := int32(workerID*1000 + j + 1000) - faction := NewFaction(factionID, "Worker Faction", "Worker", "Worker test faction") - mfl.AddFaction(faction) // Some may fail due to concurrent additions - } - }(i) - } - - // Wait for all goroutines - for i := 0; i < 10; i++ { - <-done - } - - // Verify final state - should have at least 50 initial factions - finalCount := mfl.GetFactionCount() - if finalCount < 50 { - t.Errorf("Expected at least 50 factions after concurrent operations, got %d", finalCount) - } - if finalCount > 100 { - t.Errorf("Expected at most 100 factions after concurrent operations, got %d", finalCount) - } -} diff --git a/internal/ground_spawn/benchmark_test.go b/internal/ground_spawn/benchmark_test.go deleted file mode 100644 index 4df8373..0000000 --- a/internal/ground_spawn/benchmark_test.go +++ /dev/null @@ -1,524 +0,0 @@ -package ground_spawn - -import ( - "fmt" - "math/rand" - "sync" - "testing" -) - -// Mock implementations for benchmarking - -// mockPlayer implements Player interface for benchmarks -type mockPlayer struct { - level int16 - location int32 - name string -} - -func (p *mockPlayer) GetLevel() int16 { return p.level } -func (p *mockPlayer) GetLocation() int32 { return p.location } -func (p *mockPlayer) GetName() string { return p.name } - -// mockSkill implements Skill interface for benchmarks -type mockSkill struct { - current int16 - max int16 -} - -func (s *mockSkill) GetCurrentValue() int16 { return s.current } -func (s *mockSkill) GetMaxValue() int16 { return s.max } - -// createTestGroundSpawn creates a ground spawn for benchmarking -func createTestGroundSpawn(b *testing.B, id int32) *GroundSpawn { - b.Helper() - - // Don't create a database for every ground spawn - that's extremely expensive! - // Pass nil for the database since we're just benchmarking the in-memory operations - gs := New(nil) - gs.GroundSpawnID = id - gs.Name = fmt.Sprintf("Benchmark Node %d", id) - gs.CollectionSkill = "Mining" - gs.NumberHarvests = 5 - gs.AttemptsPerHarvest = 1 - gs.CurrentHarvests = 5 - gs.X, gs.Y, gs.Z = float32(rand.Intn(1000)), float32(rand.Intn(1000)), float32(rand.Intn(1000)) - gs.ZoneID = int32(rand.Intn(10) + 1) - gs.GridID = int32(rand.Intn(100)) - - // Add mock harvest entries for realistic benchmarking - gs.HarvestEntries = []*HarvestEntry{ - { - GroundSpawnID: id, - MinSkillLevel: 10, - MinAdventureLevel: 1, - BonusTable: false, - Harvest1: 80.0, - Harvest3: 20.0, - Harvest5: 10.0, - HarvestImbue: 5.0, - HarvestRare: 2.0, - Harvest10: 1.0, - }, - { - GroundSpawnID: id, - MinSkillLevel: 50, - MinAdventureLevel: 10, - BonusTable: true, - Harvest1: 90.0, - Harvest3: 30.0, - Harvest5: 15.0, - HarvestImbue: 8.0, - HarvestRare: 5.0, - Harvest10: 2.0, - }, - } - - // Add mock harvest items - gs.HarvestItems = []*HarvestEntryItem{ - {GroundSpawnID: id, ItemID: 1001, IsRare: ItemRarityNormal, GridID: 0, Quantity: 1}, - {GroundSpawnID: id, ItemID: 1002, IsRare: ItemRarityNormal, GridID: 0, Quantity: 1}, - {GroundSpawnID: id, ItemID: 1003, IsRare: ItemRarityRare, GridID: 0, Quantity: 1}, - {GroundSpawnID: id, ItemID: 1004, IsRare: ItemRarityImbue, GridID: 0, Quantity: 1}, - } - - return gs -} - -// BenchmarkGroundSpawnCreation measures ground spawn creation performance -func BenchmarkGroundSpawnCreation(b *testing.B) { - b.Skip("Skipping benchmark test - requires MySQL database connection") - // TODO: Set up proper MySQL test database for benchmarks - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // b.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - b.ResetTimer() - - // b.Run("Sequential", func(b *testing.B) { - // for i := 0; i < b.N; i++ { - // gs := New(db) - // gs.GroundSpawnID = int32(i) - // gs.Name = fmt.Sprintf("Node %d", i) - // _ = gs - // } - // }) - - // b.Run("Parallel", func(b *testing.B) { - // b.RunParallel(func(pb *testing.PB) { - // id := int32(0) - // for pb.Next() { - // gs := New(db) - // gs.GroundSpawnID = id - // gs.Name = fmt.Sprintf("Node %d", id) - // id++ - // _ = gs - // } - // }) - // }) -} - -// BenchmarkGroundSpawnState measures state operations -func BenchmarkGroundSpawnState(b *testing.B) { - gs := createTestGroundSpawn(b, 1001) - - b.Run("IsAvailable", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = gs.IsAvailable() - } - }) - }) - - b.Run("IsDepleted", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = gs.IsDepleted() - } - }) - }) - - b.Run("GetHarvestMessageName", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = gs.GetHarvestMessageName(true, false) - } - }) - }) - - b.Run("GetHarvestSpellType", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = gs.GetHarvestSpellType() - } - }) - }) -} - -// BenchmarkHarvestAlgorithm measures the core harvest processing performance -func BenchmarkHarvestAlgorithm(b *testing.B) { - gs := createTestGroundSpawn(b, 1001) - - player := &mockPlayer{level: 50, location: 1, name: "BenchmarkPlayer"} - skill := &mockSkill{current: 75, max: 100} - - b.Run("ProcessHarvest", func(b *testing.B) { - for b.Loop() { - // Reset harvest count for consistent benchmarking - gs.CurrentHarvests = gs.NumberHarvests - - _, err := gs.ProcessHarvest(player, skill, 75) - if err != nil { - b.Fatalf("Harvest failed: %v", err) - } - } - }) - - b.Run("FilterHarvestTables", func(b *testing.B) { - for b.Loop() { - _ = gs.filterHarvestTables(player, 75) - } - }) - - b.Run("SelectHarvestTable", func(b *testing.B) { - tables := gs.filterHarvestTables(player, 75) - for b.Loop() { - _ = gs.selectHarvestTable(tables, 75) - } - }) - - b.Run("DetermineHarvestType", func(b *testing.B) { - tables := gs.filterHarvestTables(player, 75) - table := gs.selectHarvestTable(tables, 75) - for b.Loop() { - _ = gs.determineHarvestType(table, false) - } - }) - - b.Run("AwardHarvestItems", func(b *testing.B) { - for b.Loop() { - _ = gs.awardHarvestItems(HarvestType3Items, player) - } - }) -} - -// Global shared master list for benchmarks to avoid repeated setup -var ( - sharedMasterList *MasterList - sharedSpawns []*GroundSpawn - setupOnce sync.Once -) - -// setupSharedMasterList creates the shared master list once -func setupSharedMasterList(b *testing.B) { - setupOnce.Do(func() { - sharedMasterList = NewMasterList() - - // Pre-populate with ground spawns for realistic testing - const numSpawns = 1000 - sharedSpawns = make([]*GroundSpawn, numSpawns) - - for i := range numSpawns { - sharedSpawns[i] = createTestGroundSpawn(b, int32(i+1)) - sharedSpawns[i].ZoneID = int32(i%10 + 1) // Distribute across 10 zones - sharedSpawns[i].CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4] - - // Create realistic spatial clusters within each zone - zoneBase := float32(sharedSpawns[i].ZoneID * 1000) - cluster := float32((i % 100) * 50) // Clusters of ~25 spawns - sharedSpawns[i].X = zoneBase + cluster + float32(rand.Intn(100)-50) // Add some spread - sharedSpawns[i].Y = zoneBase + cluster + float32(rand.Intn(100)-50) - sharedSpawns[i].Z = float32(rand.Intn(100)) - - sharedMasterList.AddGroundSpawn(sharedSpawns[i]) - } - }) -} - -// BenchmarkMasterListOperations measures master list performance -func BenchmarkMasterListOperations(b *testing.B) { - setupSharedMasterList(b) - ml := sharedMasterList - - b.Run("GetGroundSpawn", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - id := int32((rand.Intn(1000) + 1)) - _ = ml.GetGroundSpawn(id) - } - }) - }) - - b.Run("AddGroundSpawn", func(b *testing.B) { - // Create a separate master list for add operations to avoid contaminating shared list - addML := NewMasterList() - startID := int32(10000) - // Pre-create ground spawns to measure just the Add operation - spawnsToAdd := make([]*GroundSpawn, b.N) - for i := 0; i < b.N; i++ { - spawnsToAdd[i] = createTestGroundSpawn(b, startID+int32(i)) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - addML.AddGroundSpawn(spawnsToAdd[i]) - } - }) - - b.Run("GetByZone", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - zoneID := int32(rand.Intn(10) + 1) - _ = ml.GetByZone(zoneID) - } - }) - }) - - b.Run("GetBySkill", func(b *testing.B) { - skills := []string{"Mining", "Gathering", "Fishing", "Trapping"} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - skill := skills[rand.Intn(len(skills))] - _ = ml.GetBySkill(skill) - } - }) - }) - - b.Run("GetAvailableSpawns", func(b *testing.B) { - for b.Loop() { - _ = ml.GetAvailableSpawns() - } - }) - - b.Run("GetDepletedSpawns", func(b *testing.B) { - for b.Loop() { - _ = ml.GetDepletedSpawns() - } - }) - - b.Run("GetStatistics", func(b *testing.B) { - for b.Loop() { - _ = ml.GetStatistics() - } - }) -} - -// BenchmarkConcurrentHarvesting measures concurrent harvest performance -func BenchmarkConcurrentHarvesting(b *testing.B) { - const numSpawns = 100 - spawns := make([]*GroundSpawn, numSpawns) - - for i := 0; i < numSpawns; i++ { - spawns[i] = createTestGroundSpawn(b, int32(i+1)) - } - - player := &mockPlayer{level: 50, location: 1, name: "BenchmarkPlayer"} - skill := &mockSkill{current: 75, max: 100} - - b.Run("ConcurrentHarvesting", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - spawnIdx := rand.Intn(numSpawns) - gs := spawns[spawnIdx] - - // Reset if depleted - if gs.IsDepleted() { - gs.Respawn() - } - - _, _ = gs.ProcessHarvest(player, skill, 75) - } - }) - }) -} - -// BenchmarkMemoryAllocation measures memory allocation patterns -func BenchmarkMemoryAllocation(b *testing.B) { - b.Skip("Skipping benchmark test - requires MySQL database connection") - // TODO: Set up proper MySQL test database for benchmarks - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // b.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - // b.Run("GroundSpawnAllocation", func(b *testing.B) { - // b.ReportAllocs() - // for i := 0; i < b.N; i++ { - // gs := New(db) - // gs.GroundSpawnID = int32(i) - // gs.HarvestEntries = make([]*HarvestEntry, 2) - // gs.HarvestItems = make([]*HarvestEntryItem, 4) - // _ = gs - // } - // }) - - b.Run("MasterListAllocation", func(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - ml := NewMasterList() - _ = ml - } - }) - - b.Run("HarvestResultAllocation", func(b *testing.B) { - b.ReportAllocs() - for b.Loop() { - result := &HarvestResult{ - Success: true, - HarvestType: HarvestType3Items, - ItemsAwarded: make([]*HarvestedItem, 3), - MessageText: "You harvested 3 items", - SkillGained: false, - } - _ = result - } - }) - - b.Run("SpatialAddGroundSpawn_Allocations", func(b *testing.B) { - b.ReportAllocs() - ml := NewMasterList() - for i := 0; i < b.N; i++ { - gs := createTestGroundSpawn(b, int32(i+1)) - gs.ZoneID = int32(i%10 + 1) - gs.CollectionSkill = []string{"Mining", "Gathering", "Fishing", "Trapping"}[i%4] - gs.X = float32(rand.Intn(1000)) - gs.Y = float32(rand.Intn(1000)) - ml.AddGroundSpawn(gs) - } - }) - - b.Run("SpatialGetNearby_Allocations", func(b *testing.B) { - setupSharedMasterList(b) - ml := sharedMasterList - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - x := float32(rand.Intn(10000)) - y := float32(rand.Intn(10000)) - _ = ml.GetNearby(x, y, 100.0) - } - }) -} - -// BenchmarkRespawnOperations measures respawn performance -func BenchmarkRespawnOperations(b *testing.B) { - gs := createTestGroundSpawn(b, 1001) - - b.Run("Respawn", func(b *testing.B) { - for b.Loop() { - gs.CurrentHarvests = 0 // Deplete - gs.Respawn() - } - }) - - b.Run("RespawnWithRandomHeading", func(b *testing.B) { - gs.RandomizeHeading = true - for b.Loop() { - gs.CurrentHarvests = 0 // Deplete - gs.Respawn() - } - }) -} - -// BenchmarkStringOperations measures string processing performance -func BenchmarkStringOperations(b *testing.B) { - skills := []string{"Mining", "Gathering", "Collecting", "Fishing", "Trapping", "Foresting", "Unknown"} - - b.Run("HarvestMessageGeneration", func(b *testing.B) { - gs := createTestGroundSpawn(b, 1001) - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - skill := skills[rand.Intn(len(skills))] - gs.CollectionSkill = skill - _ = gs.GetHarvestMessageName(rand.Intn(2) == 1, rand.Intn(2) == 1) - } - }) - }) - - b.Run("SpellTypeGeneration", func(b *testing.B) { - gs := createTestGroundSpawn(b, 1001) - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - skill := skills[rand.Intn(len(skills))] - gs.CollectionSkill = skill - _ = gs.GetHarvestSpellType() - } - }) - }) -} - -// BenchmarkSpatialFeatures tests features unique to spatial implementation -func BenchmarkSpatialFeatures(b *testing.B) { - setupSharedMasterList(b) - ml := sharedMasterList - - b.Run("GetByZoneAndSkill", func(b *testing.B) { - skills := []string{"Mining", "Gathering", "Fishing", "Trapping"} - for b.Loop() { - zoneID := int32(rand.Intn(10) + 1) - skill := skills[rand.Intn(len(skills))] - _ = ml.GetByZoneAndSkill(zoneID, skill) - } - }) - - b.Run("GetNearby_Small", func(b *testing.B) { - for b.Loop() { - x := float32(rand.Intn(10000)) - y := float32(rand.Intn(10000)) - _ = ml.GetNearby(x, y, 50.0) // Small radius - } - }) - - b.Run("GetNearby_Medium", func(b *testing.B) { - for b.Loop() { - x := float32(rand.Intn(10000)) - y := float32(rand.Intn(10000)) - _ = ml.GetNearby(x, y, 200.0) // Medium radius - } - }) - - b.Run("GetNearby_Large", func(b *testing.B) { - for b.Loop() { - x := float32(rand.Intn(10000)) - y := float32(rand.Intn(10000)) - _ = ml.GetNearby(x, y, 500.0) // Large radius - } - }) -} - -// BenchmarkConcurrentSpatialOperations tests thread safety and mixed workloads -func BenchmarkConcurrentSpatialOperations(b *testing.B) { - setupSharedMasterList(b) - ml := sharedMasterList - - b.Run("MixedSpatialOperations", func(b *testing.B) { - skills := []string{"Mining", "Gathering", "Fishing", "Trapping"} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - switch rand.Intn(6) { - case 0: - id := int32(rand.Intn(1000) + 1) - _ = ml.GetGroundSpawn(id) - case 1: - zoneID := int32(rand.Intn(10) + 1) - _ = ml.GetByZone(zoneID) - case 2: - skill := skills[rand.Intn(len(skills))] - _ = ml.GetBySkill(skill) - case 3: - zoneID := int32(rand.Intn(10) + 1) - skill := skills[rand.Intn(len(skills))] - _ = ml.GetByZoneAndSkill(zoneID, skill) - case 4: - x := float32(rand.Intn(10000)) - y := float32(rand.Intn(10000)) - _ = ml.GetNearby(x, y, 100.0) - case 5: - _ = ml.GetAvailableSpawns() - } - } - }) - }) -} diff --git a/internal/ground_spawn/ground_spawn_test.go b/internal/ground_spawn/ground_spawn_test.go deleted file mode 100644 index cad2001..0000000 --- a/internal/ground_spawn/ground_spawn_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package ground_spawn - -import ( - "testing" -) - -func TestNew(t *testing.T) { - // Test creating a new ground spawn - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // t.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - // gs := New(db) - // if gs == nil { - // t.Fatal("Expected non-nil ground spawn") - // } - - // if gs.db != db { - // t.Error("Database connection not set correctly") - // } - - // if !gs.isNew { - // t.Error("New ground spawn should be marked as new") - // } - - // if !gs.IsAlive { - // t.Error("New ground spawn should be alive") - // } - - // if gs.RandomizeHeading != true { - // t.Error("Default RandomizeHeading should be true") - // } -} - -func TestGroundSpawnGetID(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // t.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - // gs := New(db) - // gs.GroundSpawnID = 12345 - - // if gs.GetID() != 12345 { - // t.Errorf("Expected GetID() to return 12345, got %d", gs.GetID()) - // } -} - -func TestGroundSpawnState(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // t.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - // gs := New(db) - // gs.NumberHarvests = 5 - // gs.CurrentHarvests = 3 - - // if gs.IsDepleted() { - // t.Error("Ground spawn with harvests should not be depleted") - // } - - // if !gs.IsAvailable() { - // t.Error("Ground spawn with harvests should be available") - // } - - // gs.CurrentHarvests = 0 - // if !gs.IsDepleted() { - // t.Error("Ground spawn with no harvests should be depleted") - // } - - // if gs.IsAvailable() { - // t.Error("Depleted ground spawn should not be available") - // } -} - -func TestHarvestMessageName(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // t.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - // testCases := []struct { - // skill string - // presentTense bool - // failure bool - // expectedVerb string - // }{ - // {"Mining", true, false, "mine"}, - // {"Mining", false, false, "mined"}, - // {"Gathering", true, false, "gather"}, - // {"Gathering", false, false, "gathered"}, - // {"Fishing", true, false, "fish"}, - // {"Fishing", false, false, "fished"}, - // {"Unknown", true, false, "collect"}, - // {"Unknown", false, false, "collected"}, - // } - - // for _, tc := range testCases { - // gs := New(db) - // gs.CollectionSkill = tc.skill - // - // result := gs.GetHarvestMessageName(tc.presentTense, tc.failure) - // if result != tc.expectedVerb { - // t.Errorf("For skill %s (present=%v, failure=%v), expected %s, got %s", - // tc.skill, tc.presentTense, tc.failure, tc.expectedVerb, result) - // } - // } -} - -func TestNewMasterList(t *testing.T) { - ml := NewMasterList() - if ml == nil { - t.Fatal("Expected non-nil master list") - } - - if ml.Size() != 0 { - t.Error("New master list should be empty") - } -} - -func TestMasterListOperations(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection") - // TODO: Set up proper MySQL test database - // db, err := database.NewMySQL("user:pass@tcp(localhost:3306)/test") - // if err != nil { - // t.Fatalf("Failed to create test database: %v", err) - // } - // defer db.Close() - - // ml := NewMasterList() - // - // // Create test ground spawn - // gs := New(db) - // gs.GroundSpawnID = 1001 - // gs.Name = "Test Node" - // gs.CollectionSkill = "Mining" - // gs.ZoneID = 1 - // gs.CurrentHarvests = 5 - - // // Test add - // if !ml.AddGroundSpawn(gs) { - // t.Error("Should be able to add new ground spawn") - // } - - // // Test get - // retrieved := ml.GetGroundSpawn(1001) - // if retrieved == nil { - // t.Fatal("Should be able to retrieve added ground spawn") - // } - - // if retrieved.Name != "Test Node" { - // t.Errorf("Expected name 'Test Node', got '%s'", retrieved.Name) - // } - - // // Test zone filter - // zoneSpawns := ml.GetByZone(1) - // if len(zoneSpawns) != 1 { - // t.Errorf("Expected 1 spawn in zone 1, got %d", len(zoneSpawns)) - // } - - // // Test skill filter - // miningSpawns := ml.GetBySkill("Mining") - // if len(miningSpawns) != 1 { - // t.Errorf("Expected 1 mining spawn, got %d", len(miningSpawns)) - // } - - // // Test available spawns - // available := ml.GetAvailableSpawns() - // if len(available) != 1 { - // t.Errorf("Expected 1 available spawn, got %d", len(available)) - // } - - // // Test depleted spawns (should be none) - // depleted := ml.GetDepletedSpawns() - // if len(depleted) != 0 { - // t.Errorf("Expected 0 depleted spawns, got %d", len(depleted)) - // } -} \ No newline at end of file diff --git a/internal/groups/README.md b/internal/groups/README.md deleted file mode 100644 index 837f2f6..0000000 --- a/internal/groups/README.md +++ /dev/null @@ -1,446 +0,0 @@ -# Groups System - -The groups system (`internal/groups`) provides comprehensive player group and raid management for the EQ2Go server emulator. This system is converted from the original C++ EQ2EMu PlayerGroups implementation with modern Go concurrency patterns and clean architecture principles. - -## Overview - -The groups system manages all aspects of player groups and raids including: - -- **Group Management**: Creation, disbanding, member management -- **Raid Functionality**: Multi-group coordination with up to 4 groups (24 players) -- **Cross-Server Groups**: Peer-to-peer group coordination across server instances -- **Group Invitations**: Invitation system with timeouts and validation -- **Group Communication**: Chat, messaging, and broadcast systems -- **Group Options**: Loot distribution, auto-split, leadership settings -- **Quest Sharing**: Share quests with group members -- **Group Buffs**: Coordinated buff management across group members -- **Statistics**: Comprehensive group activity tracking - -## Architecture - -### Core Components - -**Group** - Individual group with up to 6 members, options, and raid functionality -**GroupManager** - Global group management, invitations, and coordination -**Service** - High-level service interface with validation and configuration -**GroupMemberInfo** - Detailed member information and statistics -**GroupOptions** - Group behavior and loot configuration - -### Key Files - -- `group.go` - Core Group struct and member management -- `manager.go` - GroupManager with global group coordination -- `service.go` - High-level Service interface with validation -- `types.go` - Data structures and type definitions -- `interfaces.go` - System integration interfaces and adapters -- `constants.go` - All group system constants and limits -- `README.md` - This documentation - -## Group Creation and Management - -```go -// Create group service -config := groups.DefaultServiceConfig() -service := groups.NewService(config) -service.Start() - -// Create a new group -leader := &entity.Player{...} -options := &groups.GroupOptions{ - LootMethod: groups.LOOT_METHOD_ROUND_ROBIN, - AutoSplit: groups.AUTO_SPLIT_ENABLED, -} -groupID, err := service.CreateGroup(leader, options) - -// Get group information -groupInfo, err := service.GetGroupInfo(groupID) -fmt.Printf("Group %d has %d members\n", groupInfo.GroupID, groupInfo.Size) -``` - -## Group Invitations - -```go -// Invite a player to the group -leader := &entity.Player{...} -member := &entity.Player{...} - -// Send invitation -err := service.InviteToGroup(leader, member) -if err != nil { - fmt.Printf("Invitation failed: %v\n", err) -} - -// Accept invitation -err = service.AcceptGroupInvite(member) -if err != nil { - fmt.Printf("Failed to accept: %v\n", err) -} - -// Decline invitation -service.DeclineGroupInvite(member) -``` - -## Group Member Management - -```go -// Get group manager directly -manager := service.GetManager() - -// Add member to existing group -err := manager.AddGroupMember(groupID, member, false) - -// Remove member from group -err := manager.RemoveGroupMember(groupID, member) - -// Transfer leadership -err := service.TransferLeadership(groupID, newLeader) - -// Check if entity is in group -isInGroup := manager.IsInGroup(groupID, member) - -// Get group leader -leader := manager.GetGroupLeader(groupID) -``` - -## Group Communication - -```go -// Send simple message to group -manager.SimpleGroupMessage(groupID, "Welcome to the group!") - -// Send system message -manager.SendGroupMessage(groupID, groups.GROUP_MESSAGE_TYPE_SYSTEM, "Group is ready!") - -// Send chat message from member -manager.GroupChatMessage(groupID, fromEntity, 0, "Hello everyone!", groups.CHANNEL_GROUP_SAY) - -// Send formatted chat message -manager.SendGroupChatMessage(groupID, groups.CHANNEL_GROUP_CHAT, "Raid starting in 5 minutes") -``` - -## Group Options Configuration - -```go -// Configure group options -options := groups.GroupOptions{ - LootMethod: groups.LOOT_METHOD_NEED_BEFORE_GREED, - LootItemsRarity: groups.LOOT_RARITY_RARE, - AutoSplit: groups.AUTO_SPLIT_ENABLED, - GroupLockMethod: groups.LOCK_METHOD_INVITE_ONLY, - GroupAutolock: groups.AUTO_LOCK_ENABLED, - AutoLootMethod: groups.AUTO_LOOT_ENABLED, -} - -// Apply options to group -err := manager.SetGroupOptions(groupID, &options) - -// Get current options -currentOptions, exists := manager.GetDefaultGroupOptions(groupID) -if exists { - fmt.Printf("Loot method: %d\n", currentOptions.LootMethod) -} -``` - -## Raid Management - -```go -// Form a raid from multiple groups -leaderGroupID := int32(1) -targetGroups := []int32{2, 3, 4} - -err := service.FormRaid(leaderGroupID, targetGroups) -if err != nil { - fmt.Printf("Failed to form raid: %v\n", err) -} - -// Check if groups are in same raid -isInRaid := manager.IsInRaidGroup(groupID1, groupID2, false) - -// Get all raid groups for a group -raidGroups := manager.GetRaidGroups(groupID) -fmt.Printf("Raid has %d groups\n", len(raidGroups)) - -// Disband raid -err := service.DisbandRaid(leaderGroupID) -``` - -## Cross-Server Group Management - -```go -// Add member from peer server -memberInfo := &groups.GroupMemberInfo{ - Name: "RemotePlayer", - Leader: false, - IsClient: true, - ClassID: 1, - HPCurrent: 1500, - HPMax: 1500, - PowerCurrent: 800, - PowerMax: 800, - LevelCurrent: 50, - LevelMax: 50, - RaceID: 0, - Zone: "commonlands", - ZoneID: 220, - InstanceID: 0, - ClientPeerAddress: "192.168.1.10", - ClientPeerPort: 9000, - IsRaidLooter: false, -} - -err := manager.AddGroupMemberFromPeer(groupID, memberInfo) - -// Remove peer member by name -err = manager.RemoveGroupMemberByName(groupID, "RemotePlayer", true, 12345) -``` - -## Group Statistics and Information - -```go -// Get service statistics -stats := service.GetServiceStats() -fmt.Printf("Active groups: %d\n", stats.ManagerStats.ActiveGroups) -fmt.Printf("Total invites: %d\n", stats.ManagerStats.TotalInvites) -fmt.Printf("Average group size: %.1f\n", stats.ManagerStats.AverageGroupSize) - -// Get all groups in a zone -zoneGroups := service.GetGroupsByZone(zoneID) -for _, group := range zoneGroups { - fmt.Printf("Group %d has %d members in zone\n", group.GroupID, group.Size) -} - -// Get groups containing specific members -members := []entity.Entity{player1, player2} -memberGroups := service.GetMemberGroups(members) -``` - -## Event Handling - -```go -// Create custom event handler -type MyGroupEventHandler struct{} - -func (h *MyGroupEventHandler) OnGroupCreated(group *groups.Group, leader entity.Entity) error { - fmt.Printf("Group %d created by %s\n", group.GetID(), leader.GetName()) - return nil -} - -func (h *MyGroupEventHandler) OnGroupMemberJoined(group *groups.Group, member entity.Entity) error { - fmt.Printf("%s joined group %d\n", member.GetName(), group.GetID()) - return nil -} - -func (h *MyGroupEventHandler) OnGroupDisbanded(group *groups.Group) error { - fmt.Printf("Group %d disbanded\n", group.GetID()) - return nil -} - -// ... implement other required methods - -// Register event handler -handler := &MyGroupEventHandler{} -service.AddEventHandler(handler) -``` - -## Database Integration - -```go -// Implement database interface -type MyGroupDatabase struct { - // database connection -} - -func (db *MyGroupDatabase) SaveGroup(group *groups.Group) error { - // Save group to database - return nil -} - -func (db *MyGroupDatabase) LoadGroup(groupID int32) (*groups.Group, error) { - // Load group from database - return nil, nil -} - -// ... implement other required methods - -// Set database interface -database := &MyGroupDatabase{} -service.SetDatabase(database) -``` - -## Packet Handling Integration - -```go -// Implement packet handler interface -type MyGroupPacketHandler struct { - // client connection management -} - -func (ph *MyGroupPacketHandler) SendGroupUpdate(members []*groups.GroupMemberInfo, excludeClient any) error { - // Send group update packets to clients - return nil -} - -func (ph *MyGroupPacketHandler) SendGroupInvite(inviter, invitee entity.Entity) error { - // Send invitation packet to client - return nil -} - -// ... implement other required methods - -// Set packet handler -packetHandler := &MyGroupPacketHandler{} -service.SetPacketHandler(packetHandler) -``` - -## Validation and Security - -```go -// Implement custom validator -type MyGroupValidator struct{} - -func (v *MyGroupValidator) ValidateGroupCreation(leader entity.Entity, options *groups.GroupOptions) error { - // Custom validation logic - if leader.GetLevel() < 10 { - return fmt.Errorf("must be level 10+ to create groups") - } - return nil -} - -func (v *MyGroupValidator) ValidateGroupInvite(leader, member entity.Entity) error { - // Custom invitation validation - if leader.GetZone() != member.GetZone() { - return fmt.Errorf("cross-zone invites not allowed") - } - return nil -} - -// ... implement other required methods - -// Set validator -validator := &MyGroupValidator{} -service.SetValidator(validator) -``` - -## Configuration - -### Service Configuration - -```go -config := groups.ServiceConfig{ - ManagerConfig: groups.GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 1 * time.Second, - BuffUpdateInterval: 5 * time.Second, - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: true, - }, - AutoCreateGroups: true, - AllowCrossZoneGroups: true, - AllowBotMembers: true, - AllowNPCMembers: false, - MaxInviteDistance: 100.0, - GroupLevelRange: 10, - EnableGroupPvP: false, - EnableGroupBuffs: true, - DatabaseEnabled: true, - EventsEnabled: true, - StatisticsEnabled: true, - ValidationEnabled: true, -} - -service := groups.NewService(config) -``` - -### Group Options - -Available group options for loot and behavior management: - -- **Loot Methods**: Leader only, round robin, need before greed, lotto -- **Loot Rarity**: Common, uncommon, rare, legendary, fabled -- **Auto Split**: Enable/disable automatic coin splitting -- **Group Lock**: Open, invite only, closed -- **Auto Lock**: Automatic group locking settings -- **Auto Loot**: Automatic loot distribution - -## Constants and Limits - -### Group Limits -- **MAX_GROUP_SIZE**: 6 members per group -- **MAX_RAID_GROUPS**: 4 groups per raid -- **MAX_RAID_SIZE**: 24 total raid members - -### Invitation System -- **Default invite timeout**: 30 seconds -- **Invitation error codes**: Success, already in group, group full, declined, etc. - -### Communication Channels -- **CHANNEL_GROUP_SAY**: Group say channel (11) -- **CHANNEL_GROUP_CHAT**: Group chat channel (31) -- **CHANNEL_RAID_SAY**: Raid say channel (35) - -## Thread Safety - -All group operations are thread-safe using appropriate synchronization: - -- **RWMutex** for read-heavy operations (member lists, group lookups) -- **Atomic operations** for simple counters and flags -- **Channel-based communication** for message and update processing -- **Proper lock ordering** to prevent deadlocks -- **Background goroutines** for periodic processing - -## Integration with Other Systems - -The groups system integrates with: - -- **Entity System** - Groups work with any entity (players, NPCs, bots) -- **Player System** - Player-specific group functionality and client handling -- **Quest System** - Quest sharing within groups -- **Spell System** - Group buffs and spell coordination -- **Zone System** - Cross-zone group management -- **Chat System** - Group communication channels -- **Database System** - Group persistence and recovery -- **Network System** - Cross-server group coordination - -## Performance Considerations - -- **Efficient member tracking** with hash maps for O(1) lookups -- **Batched message processing** to reduce overhead -- **Background processing** for periodic updates and cleanup -- **Memory-efficient data structures** with proper cleanup -- **Statistics collection** with minimal performance impact -- **Channel buffering** to prevent blocking on message queues - -## Migration from C++ - -This Go implementation maintains compatibility with the original C++ EQ2EMu groups system while providing: - -- **Modern concurrency** with goroutines and channels -- **Better error handling** with Go's error interface -- **Cleaner architecture** with interface-based design -- **Improved maintainability** with package organization -- **Enhanced testing** capabilities -- **Type safety** with Go's type system - -## TODO Items - -The conversion includes TODO comments marking areas for future implementation: - -- **Quest sharing integration** with the quest system -- **Complete spell/buff integration** for group buffs -- **Advanced packet handling** for all client communication -- **Complete database schema** implementation -- **Cross-server peer management** completion -- **Bot and NPC integration** improvements -- **Advanced raid mechanics** (raid loot, raid targeting) -- **Group PvP functionality** implementation -- **Performance optimizations** for large-scale deployments - -## Usage Examples - -See the code examples throughout this documentation for detailed usage patterns. The system is designed to be used alongside the existing EQ2Go server infrastructure with proper initialization and configuration. - -The groups system provides a solid foundation for MMO group mechanics while maintaining the flexibility to extend and customize behavior through the comprehensive interface system. \ No newline at end of file diff --git a/internal/groups/benchmark_test.go b/internal/groups/benchmark_test.go deleted file mode 100644 index 0ad02f0..0000000 --- a/internal/groups/benchmark_test.go +++ /dev/null @@ -1,624 +0,0 @@ -package groups - -import ( - "fmt" - "math/rand" - "sync/atomic" - "testing" - "time" -) - -// Mock implementations for benchmarking use the existing mock entities from groups_test.go - -// Helper functions for creating test data - -// createTestEntity creates a mock entity for benchmarking -func createTestEntity(id int32, name string, isPlayer bool) *mockEntity { - entity := createMockEntity(id, name, isPlayer) - // Randomize some properties for more realistic benchmarking - entity.level = int8(rand.Intn(80) + 1) - entity.class = int8(rand.Intn(25) + 1) - entity.race = int8(rand.Intn(18) + 1) - entity.hp = int32(rand.Intn(5000) + 1000) - entity.maxHP = int32(rand.Intn(5000) + 1000) - entity.power = int32(rand.Intn(3000) + 500) - entity.maxPower = int32(rand.Intn(3000) + 500) - entity.isBot = !isPlayer && rand.Intn(2) == 1 - entity.isNPC = !isPlayer && rand.Intn(2) == 0 - // Use fewer zones to reduce indexing complexity in benchmarks - entity.zone = &mockZone{ - zoneID: int32(rand.Intn(5) + 1), - instanceID: int32(rand.Intn(3)), - zoneName: fmt.Sprintf("Zone %d", rand.Intn(5)+1), - } - return entity -} - -// createTestGroup creates a group with test data for benchmarking -func createTestGroup(b *testing.B, groupID int32, memberCount int) *Group { - b.Helper() - - group := NewGroup(groupID, nil, nil) - - // Add test members - for i := 0; i < memberCount; i++ { - entity := createTestEntity( - int32(i+1), - fmt.Sprintf("Player%d", i+1), - true, - ) - - isLeader := (i == 0) - err := group.AddMember(entity, isLeader) - if err != nil { - b.Fatalf("Failed to add member to group: %v", err) - } - } - - return group -} - -// BenchmarkGroupCreation measures group creation performance -func BenchmarkGroupCreation(b *testing.B) { - b.Run("NewGroup", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group := NewGroup(int32(i+1), nil, nil) - group.Disband() // Clean up background goroutine - } - }) - - b.Run("NewGroupWithOptions", func(b *testing.B) { - options := DefaultGroupOptions() - for i := 0; i < b.N; i++ { - group := NewGroup(int32(i+1), &options, nil) - group.Disband() // Clean up background goroutine - } - }) - - b.Run("NewGroupParallel", func(b *testing.B) { - var idCounter int64 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - id := atomic.AddInt64(&idCounter, 1) - group := NewGroup(int32(id), nil, nil) - group.Disband() // Clean up background goroutine - } - }) - }) -} - -// BenchmarkGroupMemberOperations measures member management performance -func BenchmarkGroupMemberOperations(b *testing.B) { - group := createTestGroup(b, 1001, 3) - defer group.Disband() // Clean up background goroutine - - b.Run("AddMember", func(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - // Create a new group for each iteration to avoid full group - testGroup := NewGroup(int32(i+1000), nil, nil) - entity := createTestEntity(int32(i+1), fmt.Sprintf("BenchPlayer%d", i), true) - testGroup.AddMember(entity, false) - testGroup.Disband() // Clean up background goroutine - } - }) - - b.Run("GetSize", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetSize() - } - }) - }) - - b.Run("GetMembers", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = group.GetMembers() - } - }) - - b.Run("GetLeaderName", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetLeaderName() - } - }) - }) - - b.Run("UpdateGroupMemberInfo", func(b *testing.B) { - members := group.GetMembers() - if len(members) == 0 { - b.Skip("No members to update") - } - - for i := 0; i < b.N; i++ { - member := members[i%len(members)] - if member.Member != nil { - group.UpdateGroupMemberInfo(member.Member, false) - } - } - }) -} - -// BenchmarkGroupOptions measures group options performance -func BenchmarkGroupOptions(b *testing.B) { - group := createTestGroup(b, 1001, 3) - defer group.Disband() // Clean up background goroutine - - b.Run("GetGroupOptions", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetGroupOptions() - } - }) - }) - - b.Run("SetGroupOptions", func(b *testing.B) { - options := DefaultGroupOptions() - for i := 0; i < b.N; i++ { - options.LootMethod = int8(i % 4) - group.SetGroupOptions(&options) - } - }) - - b.Run("GetLastLooterIndex", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetLastLooterIndex() - } - }) - }) - - b.Run("SetNextLooterIndex", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group.SetNextLooterIndex(int8(i % 6)) - } - }) -} - -// BenchmarkGroupRaidOperations measures raid functionality performance -func BenchmarkGroupRaidOperations(b *testing.B) { - group := createTestGroup(b, 1001, 6) - defer group.Disband() // Clean up background goroutine - - // Setup some raid groups - raidGroups := []int32{1001, 1002, 1003, 1004} - group.ReplaceRaidGroups(raidGroups) - - b.Run("GetRaidGroups", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetRaidGroups() - } - }) - }) - - b.Run("IsGroupRaid", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.IsGroupRaid() - } - }) - }) - - b.Run("IsInRaidGroup", func(b *testing.B) { - for i := 0; i < b.N; i++ { - targetID := int32((i % 4) + 1001) - _ = group.IsInRaidGroup(targetID, false) - } - }) - - b.Run("AddGroupToRaid", func(b *testing.B) { - for i := 0; i < b.N; i++ { - // Cycle through a limited set of group IDs to avoid infinite growth - groupID := int32(2000 + (i % 10)) - group.AddGroupToRaid(groupID) - } - }) - - b.Run("ReplaceRaidGroups", func(b *testing.B) { - newGroups := []int32{2001, 2002, 2003} - for i := 0; i < b.N; i++ { - group.ReplaceRaidGroups(newGroups) - } - }) - - b.Run("ClearGroupRaid", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group.ClearGroupRaid() - // Re-add groups for next iteration - group.ReplaceRaidGroups(raidGroups) - } - }) -} - -// BenchmarkGroupMessaging measures messaging performance -func BenchmarkGroupMessaging(b *testing.B) { - group := createTestGroup(b, 1001, 6) - defer group.Disband() // Clean up background goroutine - - b.Run("SimpleGroupMessage", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group.SimpleGroupMessage(fmt.Sprintf("Benchmark message %d", i)) - } - }) - - b.Run("SendGroupMessage", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group.SendGroupMessage(GROUP_MESSAGE_TYPE_SYSTEM, fmt.Sprintf("System message %d", i)) - } - }) - - b.Run("GroupChatMessageFromName", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group.GroupChatMessageFromName( - fmt.Sprintf("Player%d", i%6+1), - 0, - fmt.Sprintf("Chat message %d", i), - CHANNEL_GROUP_CHAT, - ) - } - }) -} - -// BenchmarkGroupState measures group state operations -func BenchmarkGroupState(b *testing.B) { - group := createTestGroup(b, 1001, 6) - defer group.Disband() // Clean up background goroutine - - b.Run("GetID", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetID() - } - }) - }) - - b.Run("IsDisbanded", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.IsDisbanded() - } - }) - }) - - b.Run("GetCreatedTime", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetCreatedTime() - } - }) - }) - - b.Run("GetLastActivity", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetLastActivity() - } - }) - }) -} - -// BenchmarkMasterListOperations measures master list performance -func BenchmarkMasterListOperations(b *testing.B) { - ml := NewMasterList() - - // Pre-populate with groups - reduced from 1000 to avoid goroutine exhaustion - const numGroups = 100 - groups := make([]*Group, numGroups) - - b.StopTimer() - for i := 0; i < numGroups; i++ { - groups[i] = createTestGroup(b, int32(i+1), rand.Intn(6)+1) - ml.AddGroup(groups[i]) - } - // Cleanup all groups when benchmark is done - defer func() { - for _, group := range groups { - if group != nil { - group.Disband() - } - } - }() - b.StartTimer() - - b.Run("GetGroup", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - id := int32(rand.Intn(numGroups) + 1) - _ = ml.GetGroup(id) - } - }) - }) - - b.Run("AddGroup", func(b *testing.B) { - startID := int32(numGroups + 1) - var addedGroups []*Group - b.ResetTimer() - for i := 0; i < b.N; i++ { - group := createTestGroup(b, startID+int32(i), 3) - ml.AddGroup(group) - addedGroups = append(addedGroups, group) - } - // Immediate cleanup to prevent goroutine exhaustion - b.StopTimer() - for _, group := range addedGroups { - if group != nil { - group.Disband() - } - } - b.StartTimer() - }) - - b.Run("GetAllGroups", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.GetAllGroups() - } - }) - - b.Run("GetActiveGroups", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.GetActiveGroups() - } - }) - - b.Run("GetGroupsByZone", func(b *testing.B) { - for i := 0; i < b.N; i++ { - zoneID := int32(rand.Intn(5) + 1) // Match reduced zone range - _ = ml.GetGroupsByZone(zoneID) - } - }) - - b.Run("GetGroupsBySize", func(b *testing.B) { - for i := 0; i < b.N; i++ { - size := int32(rand.Intn(6) + 1) - _ = ml.GetGroupsBySize(size) - } - }) - - b.Run("GetRaidGroups", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.GetRaidGroups() - } - }) - - b.Run("GetGroupStatistics", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = ml.GetGroupStatistics() - } - }) -} - -// BenchmarkManagerOperations measures manager performance -func BenchmarkManagerOperations(b *testing.B) { - config := GroupManagerConfig{ - MaxGroups: 1000, - InviteTimeout: 30 * time.Second, - UpdateInterval: 1 * time.Second, - BuffUpdateInterval: 5 * time.Second, - EnableStatistics: true, - } - - manager := NewManager(config, nil) - - // Pre-populate with groups - b.StopTimer() - for i := 0; i < 100; i++ { - leader := createTestEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) - manager.NewGroup(leader, nil, 0) - } - b.StartTimer() - - b.Run("NewGroup", func(b *testing.B) { - startID := int32(1000) - for i := 0; i < b.N; i++ { - leader := createTestEntity(startID+int32(i), fmt.Sprintf("BenchLeader%d", i), true) - _, err := manager.NewGroup(leader, nil, 0) - if err != nil { - b.Fatalf("Failed to create group: %v", err) - } - } - }) - - b.Run("GetGroup", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - groupID := int32(rand.Intn(100) + 1) - _ = manager.GetGroup(groupID) - } - }) - }) - - b.Run("IsGroupIDValid", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - groupID := int32(rand.Intn(100) + 1) - _ = manager.IsGroupIDValid(groupID) - } - }) - }) - - b.Run("GetGroupCount", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = manager.GetGroupCount() - } - }) - }) - - b.Run("GetAllGroups", func(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = manager.GetAllGroups() - } - }) - - b.Run("GetStats", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = manager.GetStats() - } - }) - }) -} - -// BenchmarkInviteSystem measures invitation system performance -func BenchmarkInviteSystem(b *testing.B) { - config := GroupManagerConfig{ - InviteTimeout: 30 * time.Second, - EnableStatistics: true, - } - - manager := NewManager(config, nil) - - b.Run("AddInvite", func(b *testing.B) { - for i := 0; i < b.N; i++ { - leader := createTestEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) - member := createTestEntity(int32(i+1001), fmt.Sprintf("Member%d", i+1), true) - manager.AddInvite(leader, member) - } - }) - - b.Run("HasPendingInvite", func(b *testing.B) { - // Add some invites first - for i := 0; i < 100; i++ { - leader := createTestEntity(int32(i+1), fmt.Sprintf("TestLeader%d", i+1), true) - member := createTestEntity(int32(i+2001), fmt.Sprintf("TestMember%d", i+1), true) - manager.AddInvite(leader, member) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - member := createTestEntity(int32(rand.Intn(100)+2001), fmt.Sprintf("TestMember%d", rand.Intn(100)+1), true) - _ = manager.HasPendingInvite(member) - } - }) - }) - - b.Run("Invite", func(b *testing.B) { - for i := 0; i < b.N; i++ { - leader := createTestEntity(int32(i+3001), fmt.Sprintf("InviteLeader%d", i+1), true) - member := createTestEntity(int32(i+4001), fmt.Sprintf("InviteMember%d", i+1), true) - _ = manager.Invite(leader, member) - } - }) - - b.Run("DeclineInvite", func(b *testing.B) { - // Add invites to decline - for i := 0; i < b.N; i++ { - leader := createTestEntity(int32(i+5001), fmt.Sprintf("DeclineLeader%d", i+1), true) - member := createTestEntity(int32(i+6001), fmt.Sprintf("DeclineMember%d", i+1), true) - manager.AddInvite(leader, member) - manager.DeclineInvite(member) - } - }) -} - -// BenchmarkConcurrentOperations measures concurrent access performance -func BenchmarkConcurrentOperations(b *testing.B) { - config := GroupManagerConfig{ - MaxGroups: 1000, - EnableStatistics: true, - } - - manager := NewManager(config, nil) - - // Pre-populate - for i := 0; i < 50; i++ { - leader := createTestEntity(int32(i+1), fmt.Sprintf("ConcurrentLeader%d", i+1), true) - manager.NewGroup(leader, nil, 0) - } - - b.Run("ConcurrentGroupAccess", func(b *testing.B) { - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - groupID := int32(rand.Intn(50) + 1) - - switch rand.Intn(4) { - case 0: - _ = manager.GetGroup(groupID) - case 1: - _ = manager.GetGroupSize(groupID) - case 2: - _ = manager.IsGroupIDValid(groupID) - case 3: - member := createTestEntity(int32(rand.Intn(1000)+10000), "ConcurrentMember", true) - manager.AddGroupMember(groupID, member, false) - } - } - }) - }) - - b.Run("ConcurrentInviteOperations", func(b *testing.B) { - var counter int64 - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - i := atomic.AddInt64(&counter, 1) - leader := createTestEntity(int32(i+20000), fmt.Sprintf("ConcurrentInviteLeader%d", i), true) - member := createTestEntity(int32(i+30000), fmt.Sprintf("ConcurrentInviteMember%d", i), true) - - switch rand.Intn(3) { - case 0: - manager.AddInvite(leader, member) - case 1: - _ = manager.HasPendingInvite(member) - case 2: - manager.DeclineInvite(member) - } - } - }) - }) -} - -// BenchmarkMemoryAllocation measures memory allocation patterns -func BenchmarkMemoryAllocation(b *testing.B) { - b.Run("GroupAllocation", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - group := NewGroup(int32(i+1), nil, nil) - group.Disband() // Clean up background goroutine - } - }) - - b.Run("MasterListAllocation", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - ml := NewMasterList() - _ = ml - } - }) - - b.Run("ManagerAllocation", func(b *testing.B) { - config := GroupManagerConfig{} - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager := NewManager(config, nil) - _ = manager - } - }) - - b.Run("GroupMemberInfoAllocation", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - gmi := &GroupMemberInfo{ - GroupID: int32(i + 1), - Name: fmt.Sprintf("Member%d", i+1), - Zone: "TestZone", - HPCurrent: 1000, - HPMax: 1000, - PowerCurrent: 500, - PowerMax: 500, - LevelCurrent: 50, - LevelMax: 80, - RaceID: 1, - ClassID: 1, - Leader: false, - IsClient: true, - JoinTime: time.Now(), - LastUpdate: time.Now(), - } - _ = gmi - } - }) -} diff --git a/internal/groups/groups_test.go b/internal/groups/groups_test.go deleted file mode 100644 index 9ef589b..0000000 --- a/internal/groups/groups_test.go +++ /dev/null @@ -1,1222 +0,0 @@ -package groups - -import ( - "fmt" - "sync" - "testing" - "time" -) - -// Mock entity implementation for testing -type mockEntity struct { - id int32 - name string - level int8 - class int8 - race int8 - hp int32 - maxHP int32 - power int32 - maxPower int32 - isPlayer bool - isNPC bool - isBot bool - isDead bool - zone *mockZone - groupID int32 - groupInfo *GroupMemberInfo -} - -func (m *mockEntity) GetID() int32 { return m.id } -func (m *mockEntity) GetName() string { return m.name } -func (m *mockEntity) GetLevel() int8 { return m.level } -func (m *mockEntity) GetClass() int8 { return m.class } -func (m *mockEntity) GetRace() int8 { return m.race } -func (m *mockEntity) GetHP() int32 { return m.hp } -func (m *mockEntity) GetTotalHP() int32 { return m.maxHP } -func (m *mockEntity) GetPower() int32 { return m.power } -func (m *mockEntity) GetTotalPower() int32 { return m.maxPower } -func (m *mockEntity) IsPlayer() bool { return m.isPlayer } -func (m *mockEntity) IsNPC() bool { return m.isNPC } -func (m *mockEntity) IsBot() bool { return m.isBot } -func (m *mockEntity) IsDead() bool { return m.isDead } -func (m *mockEntity) GetZone() Zone { return m.zone } -func (m *mockEntity) GetDistance(other Entity) float32 { return 10.0 } - -// GroupAware implementation -func (m *mockEntity) GetGroupMemberInfo() *GroupMemberInfo { return m.groupInfo } -func (m *mockEntity) SetGroupMemberInfo(info *GroupMemberInfo) { m.groupInfo = info } -func (m *mockEntity) GetGroupID() int32 { return m.groupID } -func (m *mockEntity) SetGroupID(groupID int32) { m.groupID = groupID } -func (m *mockEntity) IsInGroup() bool { return m.groupID > 0 } - -// Mock zone implementation -type mockZone struct { - zoneID int32 - instanceID int32 - zoneName string -} - -func (m *mockZone) GetZoneID() int32 { return m.zoneID } -func (m *mockZone) GetInstanceID() int32 { return m.instanceID } -func (m *mockZone) GetZoneName() string { return m.zoneName } - -// Helper function to create mock entities -func createMockEntity(id int32, name string, isPlayer bool) *mockEntity { - return &mockEntity{ - id: id, - name: name, - level: 50, - class: 1, - race: 0, - hp: 1500, - maxHP: 1500, - power: 800, - maxPower: 800, - isPlayer: isPlayer, - zone: &mockZone{ - zoneID: 220, - instanceID: 1, - zoneName: "commonlands", - }, - } -} - -// TestGroupCreation tests basic group creation -func TestGroupCreation(t *testing.T) { - tests := []struct { - name string - groupID int32 - options *GroupOptions - expectNil bool - }{ - { - name: "Create group with default options", - groupID: 1, - options: nil, - expectNil: false, - }, - { - name: "Create group with custom options", - groupID: 2, - options: &GroupOptions{ - LootMethod: LOOT_METHOD_NEED_BEFORE_GREED, - LootItemsRarity: LOOT_RARITY_RARE, - AutoSplit: AUTO_SPLIT_ENABLED, - }, - expectNil: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - group := NewGroup(tt.groupID, tt.options, nil) - - if (group == nil) != tt.expectNil { - t.Errorf("NewGroup() returned nil = %v, want %v", group == nil, tt.expectNil) - return - } - - if group.GetID() != tt.groupID { - t.Errorf("Group ID = %d, want %d", group.GetID(), tt.groupID) - } - - if group.GetSize() != 0 { - t.Errorf("Initial group size = %d, want 0", group.GetSize()) - } - - // Cleanup - group.Disband() - }) - } -} - -// TestGroupMemberManagement tests adding and removing members -func TestGroupMemberManagement(t *testing.T) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - leader := createMockEntity(1, "Leader", true) - member1 := createMockEntity(2, "Member1", true) - member2 := createMockEntity(3, "Member2", true) - - // Test adding leader - err := group.AddMember(leader, true) - if err != nil { - t.Fatalf("Failed to add leader: %v", err) - } - - if group.GetSize() != 1 { - t.Errorf("Group size after adding leader = %d, want 1", group.GetSize()) - } - - // Test adding members - err = group.AddMember(member1, false) - if err != nil { - t.Fatalf("Failed to add member1: %v", err) - } - - err = group.AddMember(member2, false) - if err != nil { - t.Fatalf("Failed to add member2: %v", err) - } - - if group.GetSize() != 3 { - t.Errorf("Group size after adding members = %d, want 3", group.GetSize()) - } - - // Test duplicate member - err = group.AddMember(member1, false) - if err == nil { - t.Error("Expected error when adding duplicate member") - } - - // Test member removal - err = group.RemoveMember(member1) - if err != nil { - t.Fatalf("Failed to remove member1: %v", err) - } - - if group.GetSize() != 2 { - t.Errorf("Group size after removing member = %d, want 2", group.GetSize()) - } - - // Test removing non-existent member - err = group.RemoveMember(member1) - if err == nil { - t.Error("Expected error when removing non-existent member") - } -} - -// TestGroupLeadership tests leadership transfer -func TestGroupLeadership(t *testing.T) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - leader := createMockEntity(1, "Leader", true) - member1 := createMockEntity(2, "Member1", true) - member2 := createMockEntity(3, "Member2", true) - - // Add members - group.AddMember(leader, true) - group.AddMember(member1, false) - group.AddMember(member2, false) - - // Test initial leader - if group.GetLeaderName() != "Leader" { - t.Errorf("Initial leader name = %s, want Leader", group.GetLeaderName()) - } - - // Transfer leadership - err := group.MakeLeader(member1) - if err != nil { - t.Errorf("Failed to transfer leadership: %v", err) - } - - if group.GetLeaderName() != "Member1" { - t.Errorf("New leader name = %s, want Member1", group.GetLeaderName()) - } - - // Test invalid leadership transfer - nonMember := createMockEntity(4, "NonMember", true) - err = group.MakeLeader(nonMember) - if err == nil { - t.Error("Expected failure when making non-member leader") - } -} - -// TestGroupOptions tests group options management -func TestGroupOptions(t *testing.T) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - // Test default options - options := group.GetGroupOptions() - if options.LootMethod != LOOT_METHOD_ROUND_ROBIN { - t.Errorf("Default loot method = %d, want %d", options.LootMethod, LOOT_METHOD_ROUND_ROBIN) - } - - // Test setting options - newOptions := GroupOptions{ - LootMethod: LOOT_METHOD_NEED_BEFORE_GREED, - LootItemsRarity: LOOT_RARITY_RARE, - AutoSplit: AUTO_SPLIT_ENABLED, - GroupLockMethod: LOCK_METHOD_INVITE_ONLY, - } - - group.SetGroupOptions(&newOptions) - - options = group.GetGroupOptions() - if options.LootMethod != LOOT_METHOD_NEED_BEFORE_GREED { - t.Errorf("Updated loot method = %d, want %d", options.LootMethod, LOOT_METHOD_NEED_BEFORE_GREED) - } - if options.AutoSplit != AUTO_SPLIT_ENABLED { - t.Errorf("Updated auto split = %d, want %d", options.AutoSplit, AUTO_SPLIT_ENABLED) - } -} - -// TestGroupRaidFunctionality tests raid-related functionality -func TestGroupRaidFunctionality(t *testing.T) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - // Initially not a raid - if group.IsGroupRaid() { - t.Error("New group should not be a raid") - } - - // Add raid groups - raidGroups := []int32{1, 2, 3, 4} - group.ReplaceRaidGroups(raidGroups) - - if !group.IsGroupRaid() { - t.Error("Group should be a raid after setting raid groups") - } - - // Test raid group retrieval - retrievedGroups := group.GetRaidGroups() - if len(retrievedGroups) != len(raidGroups) { - t.Errorf("Retrieved raid groups length = %d, want %d", len(retrievedGroups), len(raidGroups)) - } - - // Clear raid - group.ClearGroupRaid() - if group.IsGroupRaid() { - t.Error("Group should not be a raid after clearing") - } -} - -// TestGroupConcurrency tests concurrent access to group operations -func TestGroupConcurrency(t *testing.T) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - const numGoroutines = 100 - const operationsPerGoroutine = 100 - - var wg sync.WaitGroup - - // Test concurrent member additions and removals - t.Run("ConcurrentMemberOperations", func(t *testing.T) { - // Add initial members - for i := 0; i < MAX_GROUP_SIZE-1; i++ { - member := createMockEntity(int32(i+1), fmt.Sprintf("Member%d", i+1), true) - group.AddMember(member, i == 0) - } - - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - member := createMockEntity(int32(100+goroutineID), fmt.Sprintf("Temp%d", goroutineID), true) - - for j := range operationsPerGoroutine { - if j%2 == 0 { - // Try to add member (will mostly fail due to full group) - _ = group.AddMember(member, false) - } else { - // Remove and re-add existing member - members := group.GetMembers() - if len(members) > 0 { - memberIdx := goroutineID % len(members) - existingMember := members[memberIdx] - _ = group.RemoveMember(existingMember.Member) - _ = group.AddMember(existingMember.Member, existingMember.Leader) - } - } - } - }(i) - } - - wg.Wait() - }) - - // Test concurrent option updates - t.Run("ConcurrentOptionUpdates", func(t *testing.T) { - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - for j := range operationsPerGoroutine { - if j%2 == 0 { - // Read options - _ = group.GetGroupOptions() - } else { - // Write options - options := GroupOptions{ - LootMethod: int8(goroutineID % 4), - LootItemsRarity: int8(goroutineID % 5), - AutoSplit: int8(goroutineID % 2), - } - group.SetGroupOptions(&options) - } - } - }(i) - } - - wg.Wait() - }) - - // Test concurrent raid operations - t.Run("ConcurrentRaidOperations", 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: - _ = group.IsGroupRaid() - case 1: - _ = group.GetRaidGroups() - case 2: - raidGroups := []int32{int32(goroutineID%4 + 1)} - group.ReplaceRaidGroups(raidGroups) - case 3: - group.ClearGroupRaid() - } - } - }(i) - } - - wg.Wait() - }) - - // Test concurrent member info updates - t.Run("ConcurrentMemberInfoUpdates", func(t *testing.T) { - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - for range operationsPerGoroutine { - members := group.GetMembers() - if len(members) > 0 { - // Update member stats - memberIdx := goroutineID % len(members) - members[memberIdx].UpdateStats() - } - } - }(i) - } - - wg.Wait() - }) -} - -// TestGroupManagerCreation tests group manager creation -func TestGroupManagerCreation(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 1 * time.Second, - BuffUpdateInterval: 5 * time.Second, - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: true, - } - - manager := NewManager(config, nil) - defer manager.Stop() - - if manager == nil { - t.Fatal("NewGroupManager returned nil") - } - - stats := manager.GetStats() - if stats.ActiveGroups != 0 { - t.Errorf("Initial active groups = %d, want 0", stats.ActiveGroups) - } -} - -// TestGroupManagerGroupOperations tests group operations through manager -func TestGroupManagerGroupOperations(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 0, // Disable background updates for testing - BuffUpdateInterval: 0, // Disable background updates for testing - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: false, // Disable statistics for testing - } - manager := NewManager(config, nil) - defer manager.Stop() - - leader := createMockEntity(1, "Leader", true) - member1 := createMockEntity(2, "Member1", true) - member2 := createMockEntity(3, "Member2", true) - - // Create group - groupID, err := manager.NewGroup(leader, nil, 0) - if err != nil { - t.Fatalf("Failed to create group: %v", err) - } - - if groupID <= 0 { - t.Errorf("Invalid group ID: %d", groupID) - } - - // Add members - err = manager.AddGroupMember(groupID, member1, false) - if err != nil { - t.Fatalf("Failed to add member1: %v", err) - } - - err = manager.AddGroupMember(groupID, member2, false) - if err != nil { - t.Fatalf("Failed to add member2: %v", err) - } - - // Check group size - size := manager.GetGroupSize(groupID) - if size != 3 { - t.Errorf("Group size = %d, want 3", size) - } - - // Test member checks - if !manager.IsInGroup(groupID, leader) { - t.Error("Leader should be in group") - } - - if !manager.IsInGroup(groupID, member1) { - t.Error("Member1 should be in group") - } - - // Remove member - err = manager.RemoveGroupMember(groupID, member1) - if err != nil { - t.Fatalf("Failed to remove member1: %v", err) - } - - if manager.IsInGroup(groupID, member1) { - t.Error("Member1 should not be in group after removal") - } - - // Remove group - err = manager.RemoveGroup(groupID) - if err != nil { - t.Fatalf("Failed to remove group: %v", err) - } - - if manager.IsGroupIDValid(groupID) { - t.Error("Group should not be valid after removal") - } -} - -// TestGroupManagerInvitations tests invitation system -func TestGroupManagerInvitations(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 200 * time.Millisecond, // Very short timeout for testing - UpdateInterval: 0, // Disable background updates for testing - BuffUpdateInterval: 0, // Disable background updates for testing - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: false, // Disable statistics for testing - } - manager := NewManager(config, nil) - defer manager.Stop() - - leader := createMockEntity(1, "Leader", true) - member := createMockEntity(2, "Member", true) - - // Create group - groupID, _ := manager.NewGroup(leader, nil, 0) - - // Send invitation - result := manager.Invite(leader, member) - if result != GROUP_INVITE_SUCCESS { - t.Errorf("Invite result = %d, want %d", result, GROUP_INVITE_SUCCESS) - } - - // Check pending invite - inviterName := manager.HasPendingInvite(member) - if inviterName != "Leader" { - t.Errorf("Pending invite from = %s, want Leader", inviterName) - } - - // Accept invitation (will fail due to missing leader lookup, but that's expected in tests) - acceptResult := manager.AcceptInvite(member, nil, true) - if acceptResult != GROUP_INVITE_TARGET_NOT_FOUND { - t.Logf("Accept invite result = %d (expected due to missing leader lookup in test)", acceptResult) - } - - // Since invite acceptance failed due to missing world integration, - // let's manually add the member to test the group functionality - err := manager.AddGroupMember(groupID, member, false) - if err != nil { - t.Fatalf("Failed to manually add member: %v", err) - } - - // Verify member is in group - if !manager.IsInGroup(groupID, member) { - t.Error("Member should be in group after adding") - } - - // Test invitation timeout - member2 := createMockEntity(3, "Member2", true) - manager.Invite(leader, member2) - - // Wait for timeout (now timeout is 200ms so wait 250ms) - time.Sleep(250 * time.Millisecond) - - // Try to accept after timeout (will fail due to missing leader lookup, - // but we're mainly testing that the invite was cleaned up) - acceptResult = manager.AcceptInvite(member2, nil, true) - if acceptResult == GROUP_INVITE_SUCCESS { - t.Error("Should not be able to accept expired invitation") - } - - // Verify the invite was cleaned up by checking it no longer exists - if manager.HasPendingInvite(member2) != "" { - t.Error("Expired invitation should have been cleaned up") - } -} - -// TestGroupManagerConcurrency tests concurrent manager operations -func TestGroupManagerConcurrency(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 0, // Disable background updates for testing - BuffUpdateInterval: 0, // Disable background updates for testing - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: false, // Disable statistics for testing - } - manager := NewManager(config, nil) - defer manager.Stop() - - const numGoroutines = 50 - const groupsPerGoroutine = 10 - - var wg sync.WaitGroup - - // Test concurrent group creation and removal - t.Run("ConcurrentGroupCreation", func(t *testing.T) { - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - for j := range groupsPerGoroutine { - leader := createMockEntity(int32(goroutineID*1000+j), fmt.Sprintf("Leader%d_%d", goroutineID, j), true) - - // Create group - groupID, err := manager.NewGroup(leader, nil, 0) - if err != nil { - continue - } - - // Add some members - for k := range 3 { - member := createMockEntity(int32(goroutineID*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", goroutineID, j, k), true) - _ = manager.AddGroupMember(groupID, member, false) - } - - // Sometimes remove the group - if j%2 == 0 { - _ = manager.RemoveGroup(groupID) - } - } - }(i) - } - - wg.Wait() - }) - - // Test concurrent invitations - t.Run("ConcurrentInvitations", func(t *testing.T) { - // Create some groups - groups := make([]int32, 10) - leaders := make([]*mockEntity, 10) - - for i := range 10 { - leader := createMockEntity(int32(10000+i), fmt.Sprintf("InviteLeader%d", i), true) - leaders[i] = leader - groupID, _ := manager.NewGroup(leader, nil, 0) - groups[i] = groupID - } - - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - for j := range 100 { - leaderIdx := goroutineID % len(leaders) - leader := leaders[leaderIdx] - - member := createMockEntity(int32(20000+goroutineID*100+j), fmt.Sprintf("InviteMember%d_%d", goroutineID, j), true) - - // Send invite - _ = manager.Invite(leader, member) - - // Sometimes accept, sometimes decline - if j%3 == 0 { - _ = manager.AcceptInvite(member, nil, false) - } else if j%3 == 1 { - manager.DeclineInvite(member) - } - // Otherwise let it expire - } - }(i) - } - - wg.Wait() - - // Cleanup groups - for _, groupID := range groups { - _ = manager.RemoveGroup(groupID) - } - }) - - // Test concurrent statistics updates - t.Run("ConcurrentStatistics", func(t *testing.T) { - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - for range 1000 { - _ = manager.GetStats() - _ = manager.GetGroupCount() - _ = manager.GetAllGroups() - } - }(i) - } - - wg.Wait() - }) -} - -// TestRaceConditions tests for race conditions with -race flag -func TestRaceConditions(t *testing.T) { - if testing.Short() { - t.Skip("Skipping race condition test in short mode") - } - - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 0, // Disable background updates for testing - BuffUpdateInterval: 0, // Disable background updates for testing - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: false, // Disable statistics for testing - } - manager := NewManager(config, nil) - defer manager.Stop() - - const numGoroutines = 100 - var wg sync.WaitGroup - - // Create a shared group - leader := createMockEntity(1, "RaceLeader", true) - groupID, _ := manager.NewGroup(leader, nil, 0) - - // Add some initial members - for i := range 5 { - member := createMockEntity(int32(i+2), fmt.Sprintf("RaceMember%d", i+1), true) - _ = manager.AddGroupMember(groupID, member, false) - } - - wg.Add(numGoroutines) - - for i := range numGoroutines { - go func(goroutineID int) { - defer wg.Done() - - for j := range 50 { - switch j % 10 { - case 0: - // Get group - _ = manager.GetGroup(groupID) - case 1: - // Get size - _ = manager.GetGroupSize(groupID) - case 2: - // Check membership - _ = manager.IsInGroup(groupID, leader) - case 3: - // Get leader - _ = manager.GetGroupLeader(groupID) - case 4: - // Send message - manager.SimpleGroupMessage(groupID, fmt.Sprintf("Message %d", goroutineID)) - case 5: - // Update options - options := DefaultGroupOptions() - options.LootMethod = int8(goroutineID % 4) - _ = manager.SetGroupOptions(groupID, &options) - case 6: - // Get options - _, _ = manager.GetDefaultGroupOptions(groupID) - case 7: - // Send group update - manager.SendGroupUpdate(groupID, nil, false) - case 8: - // Check raid status - _ = manager.IsInRaidGroup(groupID, groupID+1, false) - case 9: - // Get stats - _ = manager.GetStats() - } - } - }(i) - } - - wg.Wait() -} - -// Benchmark tests -func BenchmarkGroupOperations(b *testing.B) { - b.Run("GroupCreation", func(b *testing.B) { - for i := 0; i < b.N; i++ { - group := NewGroup(int32(i), nil, nil) - group.Disband() - } - }) - - b.Run("MemberAddition", func(b *testing.B) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - member := createMockEntity(int32(i), fmt.Sprintf("Member%d", i), true) - _ = group.AddMember(member, false) - _ = group.RemoveMember(member) - } - }) - - b.Run("ConcurrentMemberAccess", func(b *testing.B) { - group := NewGroup(1, nil, nil) - defer group.Disband() - - // Add some members - for i := range MAX_GROUP_SIZE { - member := createMockEntity(int32(i+1), fmt.Sprintf("Member%d", i+1), true) - group.AddMember(member, i == 0) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - _ = group.GetMembers() - } - }) - }) - - b.Run("ManagerGroupLookup", func(b *testing.B) { - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 30 * time.Second, - UpdateInterval: 0, // Disable background updates for testing - BuffUpdateInterval: 0, // Disable background updates for testing - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: false, // Disable statistics for testing - } - manager := NewManager(config, nil) - defer manager.Stop() - - // Create some groups - for i := range 100 { - leader := createMockEntity(int32(i+1), fmt.Sprintf("Leader%d", i+1), true) - manager.NewGroup(leader, nil, 0) - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - groupID := int32((i % 100) + 1) - _ = manager.GetGroup(groupID) - i++ - } - }) - }) -} - -// TestMasterListCreation tests master list creation -func TestMasterListCreation(t *testing.T) { - masterList := NewMasterList() - if masterList == nil { - t.Fatal("NewMasterList returned nil") - } - - if masterList.GetGroupCount() != 0 { - t.Errorf("Expected count 0, got %d", masterList.GetGroupCount()) - } - - if !masterList.IsEmpty() { - t.Error("New master list should be empty") - } -} - -// TestMasterListBasicOperations tests basic operations -func TestMasterListBasicOperations(t *testing.T) { - masterList := NewMasterList() - - // Create test groups - group1 := NewGroup(1001, nil, nil) - group2 := NewGroup(1002, nil, nil) - - // Add members to create different characteristics - leader1 := createMockEntity(1, "Leader1", true) - member1 := createMockEntity(2, "Member1", true) - group1.AddMember(leader1, true) - group1.AddMember(member1, false) - - leader2 := createMockEntity(3, "Leader2", true) - group2.AddMember(leader2, true) - - // Test adding - if !masterList.AddGroup(group1) { - t.Error("Should successfully add group1") - } - - if !masterList.AddGroup(group2) { - t.Error("Should successfully add group2") - } - - // Test duplicate add (should fail) - if masterList.AddGroup(group1) { - t.Error("Should not add duplicate group") - } - - if masterList.GetGroupCount() != 2 { - t.Errorf("Expected count 2, got %d", masterList.GetGroupCount()) - } - - // Test retrieving - retrieved := masterList.GetGroup(1001) - if retrieved == nil { - t.Error("Should retrieve added group") - } - - if retrieved.GetID() != 1001 { - t.Errorf("Expected ID 1001, got %d", retrieved.GetID()) - } - - // Test safe retrieval - retrieved, exists := masterList.GetGroupSafe(1001) - if !exists || retrieved == nil { - t.Error("GetGroupSafe should return group and true") - } - - _, exists = masterList.GetGroupSafe(9999) - if exists { - t.Error("GetGroupSafe should return false for non-existent ID") - } - - // Test HasGroup - if !masterList.HasGroup(1001) { - t.Error("HasGroup should return true for existing ID") - } - - if masterList.HasGroup(9999) { - t.Error("HasGroup should return false for non-existent ID") - } - - // Test removing - if !masterList.RemoveGroup(1001) { - t.Error("Should successfully remove group") - } - - if masterList.GetGroupCount() != 1 { - t.Errorf("Expected count 1, got %d", masterList.GetGroupCount()) - } - - if masterList.HasGroup(1001) { - t.Error("Group should be removed") - } - - // Test clear - masterList.Clear() - if masterList.GetGroupCount() != 0 { - t.Errorf("Expected count 0 after clear, got %d", masterList.GetGroupCount()) - } - - // Cleanup - group1.Disband() - group2.Disband() -} - -// TestMasterListFeatures tests domain-specific features -func TestMasterListFeatures(t *testing.T) { - masterList := NewMasterList() - - // Create groups with different characteristics - group1 := NewGroup(101, nil, nil) - group2 := NewGroup(102, nil, nil) - group3 := NewGroup(103, nil, nil) - group4 := NewGroup(104, nil, nil) - - // Group 1: 2 members in zone 220 - leader1 := createMockEntity(1, "Leader1", true) - member1 := createMockEntity(2, "Member1", true) - leader1.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"} - member1.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"} - group1.AddMember(leader1, true) - group1.AddMember(member1, false) - - // Group 2: 1 member (solo) in zone 221 - leader2 := createMockEntity(3, "Leader2", true) - leader2.zone = &mockZone{zoneID: 221, instanceID: 1, zoneName: "antonica"} - group2.AddMember(leader2, true) - - // Group 3: Full group (6 members) in zone 220, make it a raid - leader3 := createMockEntity(4, "Leader3", true) - leader3.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"} - group3.AddMember(leader3, true) - for i := 1; i < MAX_GROUP_SIZE; i++ { - member := createMockEntity(int32(10+i), fmt.Sprintf("RaidMember%d", i), true) - member.zone = &mockZone{zoneID: 220, instanceID: 1, zoneName: "commonlands"} - group3.AddMember(member, false) - } - group3.ReplaceRaidGroups([]int32{103, 104}) - - // Group 4: Disbanded group - leader4 := createMockEntity(20, "Leader4", true) - group4.AddMember(leader4, true) - group4.Disband() - - // Add groups to master list - masterList.AddGroup(group1) - masterList.AddGroup(group2) - masterList.AddGroup(group3) - masterList.AddGroup(group4) - - // Test GetGroupByMember - found := masterList.GetGroupByMember("member1") - if found == nil || found.GetID() != 101 { - t.Error("GetGroupByMember should find group containing Member1") - } - - found = masterList.GetGroupByMember("LEADER2") - if found == nil || found.GetID() != 102 { - t.Error("GetGroupByMember should find group containing Leader2 (case insensitive)") - } - - found = masterList.GetGroupByMember("NonExistent") - if found != nil { - t.Error("GetGroupByMember should return nil for non-existent member") - } - - // Test GetGroupByLeader - found = masterList.GetGroupByLeader("leader1") - if found == nil || found.GetID() != 101 { - t.Error("GetGroupByLeader should find group led by Leader1") - } - - found = masterList.GetGroupByLeader("LEADER3") - if found == nil || found.GetID() != 103 { - t.Error("GetGroupByLeader should find group led by Leader3 (case insensitive)") - } - - // Test GetGroupsBySize - soloGroups := masterList.GetGroupsBySize(1) - if len(soloGroups) != 1 { - t.Errorf("GetGroupsBySize(1) returned %v results, want 1", len(soloGroups)) - } - - twoMemberGroups := masterList.GetGroupsBySize(2) - if len(twoMemberGroups) != 1 { - t.Errorf("GetGroupsBySize(2) returned %v results, want 1", len(twoMemberGroups)) - } - - fullGroups := masterList.GetGroupsBySize(MAX_GROUP_SIZE) - if len(fullGroups) != 1 { - t.Errorf("GetGroupsBySize(%d) returned %v results, want 1", MAX_GROUP_SIZE, len(fullGroups)) - } - - // Test GetGroupsByZone - zone220Groups := masterList.GetGroupsByZone(220) - if len(zone220Groups) != 2 { // group1 and group3 - t.Errorf("GetGroupsByZone(220) returned %v results, want 2", len(zone220Groups)) - } - - zone221Groups := masterList.GetGroupsByZone(221) - if len(zone221Groups) != 1 { // group2 - t.Errorf("GetGroupsByZone(221) returned %v results, want 1", len(zone221Groups)) - } - - // Test GetActiveGroups - activeGroups := masterList.GetActiveGroups() - if len(activeGroups) != 3 { // group1, group2, group3 (group4 is disbanded) - t.Errorf("GetActiveGroups() returned %v results, want 3", len(activeGroups)) - } - - // Test GetRaidGroups - raidGroups := masterList.GetRaidGroups() - if len(raidGroups) != 1 { // group3 - t.Errorf("GetRaidGroups() returned %v results, want 1", len(raidGroups)) - } - - // Test GetSoloGroups - soloGroups = masterList.GetSoloGroups() - if len(soloGroups) != 1 { // group2 - t.Errorf("GetSoloGroups() returned %v results, want 1", len(soloGroups)) - } - - // Test GetFullGroups - fullGroups = masterList.GetFullGroups() - if len(fullGroups) != 1 { // group3 - t.Errorf("GetFullGroups() returned %v results, want 1", len(fullGroups)) - } - - // Test GetZones - zones := masterList.GetZones() - if len(zones) < 2 { - t.Errorf("GetZones() returned %v zones, want at least 2", len(zones)) - } - - // Test GetSizes - sizes := masterList.GetSizes() - if len(sizes) < 3 { - t.Errorf("GetSizes() returned %v sizes, want at least 3", len(sizes)) - } - - // Test GetTotalMembers - totalMembers := masterList.GetTotalMembers() - expectedTotal := int32(2 + 1 + MAX_GROUP_SIZE) // group1 + group2 + group3 (group4 is disbanded) - if totalMembers != expectedTotal { - t.Errorf("GetTotalMembers() returned %v, want %v", totalMembers, expectedTotal) - } - - // Test UpdateGroup - group1.AddMember(createMockEntity(30, "NewMember", true), false) - err := masterList.UpdateGroup(group1) - if err != nil { - t.Errorf("UpdateGroup failed: %v", err) - } - - // Test updating non-existent group - nonExistentGroup := NewGroup(9999, nil, nil) - err = masterList.UpdateGroup(nonExistentGroup) - if err == nil { - t.Error("UpdateGroup should fail for non-existent group") - } - nonExistentGroup.Disband() - - // Test GetGroupStatistics - stats := masterList.GetGroupStatistics() - if stats.TotalGroups != 4 { - t.Errorf("Statistics TotalGroups = %v, want 4", stats.TotalGroups) - } - if stats.ActiveGroups != 3 { - t.Errorf("Statistics ActiveGroups = %v, want 3", stats.ActiveGroups) - } - if stats.RaidGroups != 1 { - t.Errorf("Statistics RaidGroups = %v, want 1", stats.RaidGroups) - } - if stats.SoloGroups != 1 { - t.Errorf("Statistics SoloGroups = %v, want 1", stats.SoloGroups) - } - if stats.FullGroups != 1 { - t.Errorf("Statistics FullGroups = %v, want 1", stats.FullGroups) - } - - // Test Cleanup - removedCount := masterList.Cleanup() - if removedCount != 1 { // Should remove group4 - t.Errorf("Cleanup() removed %v groups, want 1", removedCount) - } - - if masterList.GetGroupCount() != 3 { - t.Errorf("Group count after cleanup = %v, want 3", masterList.GetGroupCount()) - } - - // Cleanup - group1.Disband() - group2.Disband() - group3.Disband() -} - -// TestMasterListConcurrency tests concurrent access -func TestMasterListConcurrency(t *testing.T) { - masterList := NewMasterList() - - // Add initial groups - for i := 1; i <= 50; i++ { - group := NewGroup(int32(i+100), nil, nil) - leader := createMockEntity(int32(i), fmt.Sprintf("Leader%d", i), true) - group.AddMember(leader, true) - masterList.AddGroup(group) - } - - // Test concurrent access - done := make(chan bool, 10) - - // Concurrent readers - for i := 0; i < 5; i++ { - go func() { - defer func() { done <- true }() - for j := 0; j < 100; j++ { - masterList.GetGroup(int32(j%50 + 101)) - masterList.GetActiveGroups() - masterList.GetGroupByMember(fmt.Sprintf("leader%d", j%50+1)) - masterList.GetGroupsBySize(1) - masterList.GetZones() - } - }() - } - - // Concurrent writers - for i := 0; i < 5; i++ { - go func(workerID int) { - defer func() { done <- true }() - for j := 0; j < 10; j++ { - groupID := int32(workerID*1000 + j + 1000) - group := NewGroup(groupID, nil, nil) - leader := createMockEntity(int32(workerID*1000+j), fmt.Sprintf("Worker%d-Leader%d", workerID, j), true) - group.AddMember(leader, true) - masterList.AddGroup(group) // Some may fail due to concurrent additions - } - }(i) - } - - // Wait for all goroutines - for i := 0; i < 10; i++ { - <-done - } - - // Verify final state - should have at least 50 initial groups - finalCount := masterList.GetGroupCount() - if finalCount < 50 { - t.Errorf("Expected at least 50 groups after concurrent operations, got %d", finalCount) - } - if finalCount > 100 { - t.Errorf("Expected at most 100 groups after concurrent operations, got %d", finalCount) - } - - // Cleanup - masterList.ForEach(func(id int32, group *Group) { - group.Disband() - }) -} diff --git a/internal/groups/manager_test.go b/internal/groups/manager_test.go deleted file mode 100644 index 65df6c0..0000000 --- a/internal/groups/manager_test.go +++ /dev/null @@ -1,613 +0,0 @@ -package groups - -import ( - "fmt" - "sync" - "testing" - "time" -) - -// TestManagerBackground tests background processes in the manager -func TestManagerBackground(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 100, - MaxRaidGroups: 4, - InviteTimeout: 100 * time.Millisecond, // Short for testing - UpdateInterval: 50 * time.Millisecond, - BuffUpdateInterval: 50 * time.Millisecond, - EnableCrossServer: true, - EnableRaids: true, - EnableQuestSharing: true, - EnableStatistics: true, - } - - manager := NewManager(config, nil) - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - // Let background processes run - time.Sleep(200 * time.Millisecond) - - // Verify manager is running - stats := manager.GetStats() - if stats.ActiveGroups != 0 { - t.Errorf("Expected 0 active groups, got %d", stats.ActiveGroups) - } -} - -// TestManagerGroupLifecycle tests complete group lifecycle through manager -func TestManagerGroupLifecycle(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 100, - MaxRaidGroups: 4, - InviteTimeout: 1 * time.Second, - UpdateInterval: 0, // Disable for testing - BuffUpdateInterval: 0, // Disable for testing - EnableStatistics: false, - } - - manager := NewManager(config, nil) - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - // Create entities - leader := createMockEntity(1, "Leader", true) - member1 := createMockEntity(2, "Member1", true) - member2 := createMockEntity(3, "Member2", true) - - // Test group creation - groupID, err := manager.NewGroup(leader, nil, 0) - if err != nil { - t.Fatalf("Failed to create group: %v", err) - } - - // Verify group was created - if !manager.IsGroupIDValid(groupID) { - t.Error("Group ID should be valid after creation") - } - - // Test adding members - err = manager.AddGroupMember(groupID, member1, false) - if err != nil { - t.Errorf("Failed to add member1: %v", err) - } - - err = manager.AddGroupMember(groupID, member2, false) - if err != nil { - t.Errorf("Failed to add member2: %v", err) - } - - // Verify group size - size := manager.GetGroupSize(groupID) - if size != 3 { - t.Errorf("Expected group size 3, got %d", size) - } - - // Test leadership transfer - if !manager.MakeLeader(groupID, member1) { - t.Error("Failed to make member1 leader") - } - - // Verify new leader - leader2 := manager.GetGroupLeader(groupID) - if leader2 != member1 { - t.Error("Leader should be member1 after transfer") - } - - // Test member removal - err = manager.RemoveGroupMember(groupID, member2) - if err != nil { - t.Errorf("Failed to remove member2: %v", err) - } - - // Verify member was removed - if manager.IsInGroup(groupID, member2) { - t.Error("Member2 should not be in group after removal") - } - - // Test group disbanding - err = manager.RemoveGroup(groupID) - if err != nil { - t.Errorf("Failed to remove group: %v", err) - } - - // Verify group was removed - if manager.IsGroupIDValid(groupID) { - t.Error("Group should not be valid after removal") - } -} - -// TestManagerInviteSystem tests the invitation system -func TestManagerInviteSystem(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 100, - MaxRaidGroups: 4, - InviteTimeout: 200 * time.Millisecond, - UpdateInterval: 0, - BuffUpdateInterval: 0, - EnableStatistics: false, - } - - manager := NewManager(config, nil) - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - leader := createMockEntity(1, "Leader", true) - member := createMockEntity(2, "Member", true) - - // Create group - groupID, _ := manager.NewGroup(leader, nil, 0) - - // Test sending invitation - result := manager.Invite(leader, member) - if result != GROUP_INVITE_SUCCESS { - t.Errorf("Expected invite success, got %d", result) - } - - // Verify pending invite - inviterName := manager.HasPendingInvite(member) - if inviterName != "Leader" { - t.Errorf("Expected inviter name 'Leader', got '%s'", inviterName) - } - - // Test duplicate invitation - result = manager.Invite(leader, member) - if result != GROUP_INVITE_ALREADY_HAS_INVITE { - t.Errorf("Expected already has invite error, got %d", result) - } - - // Test declining invitation - manager.DeclineInvite(member) - inviterName = manager.HasPendingInvite(member) - if inviterName != "" { - t.Error("Should have no pending invite after decline") - } - - // Test invitation expiration - manager.Invite(leader, member) - time.Sleep(300 * time.Millisecond) - - // Manually trigger cleanup - manager.cleanupExpiredInvites() - - inviterName = manager.HasPendingInvite(member) - if inviterName != "" { - t.Error("Invite should have expired") - } - - // Clean up - manager.RemoveGroup(groupID) -} - -// TestManagerRaidOperations tests raid functionality -func TestManagerRaidOperations(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 100, - MaxRaidGroups: 4, - InviteTimeout: 1 * time.Second, - UpdateInterval: 0, - BuffUpdateInterval: 0, - EnableRaids: true, - EnableStatistics: false, - } - - manager := NewManager(config, nil) - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - // Create multiple groups - groups := make([]int32, 4) - for i := range 4 { - leader := createMockEntity(int32(i*10+1), fmt.Sprintf("Leader%d", i), true) - groupID, err := manager.NewGroup(leader, nil, 0) - if err != nil { - t.Fatalf("Failed to create group %d: %v", i, err) - } - groups[i] = groupID - - // Add members to each group - for j := range 3 { - member := createMockEntity(int32(i*10+j+2), fmt.Sprintf("Member%d_%d", i, j), true) - manager.AddGroupMember(groupID, member, false) - } - } - - // Form raid - manager.ReplaceRaidGroups(groups[0], groups) - manager.ReplaceRaidGroups(groups[1], groups) - manager.ReplaceRaidGroups(groups[2], groups) - manager.ReplaceRaidGroups(groups[3], groups) - - // Verify raid status - for _, groupID := range groups { - if !manager.IsInRaidGroup(groupID, groupID, false) { - t.Errorf("Group %d should be in raid", groupID) - } - } - - // Test raid group lookup - if !manager.IsInRaidGroup(groups[0], groups[3], false) { - t.Error("Groups should be in same raid") - } - - // Clear raid - for _, groupID := range groups { - manager.ClearGroupRaid(groupID) - } - - // Verify raid cleared - for _, groupID := range groups { - if manager.IsInRaidGroup(groupID, groupID, false) { - t.Errorf("Group %d should not be in raid after clear", groupID) - } - } - - // Clean up - for _, groupID := range groups { - manager.RemoveGroup(groupID) - } -} - -// TestManagerConcurrentOperations tests thread safety -func TestManagerConcurrentOperations(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 1000, - MaxRaidGroups: 4, - InviteTimeout: 1 * time.Second, - UpdateInterval: 0, - BuffUpdateInterval: 0, - EnableStatistics: false, - } - - manager := NewManager(config, nil) - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - const numGoroutines = 20 - const operationsPerGoroutine = 50 - var wg sync.WaitGroup - - // Concurrent group creation and removal - wg.Add(numGoroutines) - for i := range numGoroutines { - go func(id int) { - defer wg.Done() - - for j := range operationsPerGoroutine { - leader := createMockEntity(int32(id*1000+j), fmt.Sprintf("Leader%d_%d", id, j), true) - - // Create group - groupID, err := manager.NewGroup(leader, nil, 0) - if err != nil { - continue - } - - // Add members - for k := range 3 { - member := createMockEntity(int32(id*1000+j*10+k), fmt.Sprintf("Member%d_%d_%d", id, j, k), true) - manager.AddGroupMember(groupID, member, false) - } - - // Sometimes transfer leadership - if j%3 == 0 { - members := manager.GetGroup(groupID).GetMembers() - if len(members) > 1 { - manager.MakeLeader(groupID, members[1].Member) - } - } - - // Sometimes update options - if j%2 == 0 { - options := DefaultGroupOptions() - options.LootMethod = int8(j % 4) - manager.SetGroupOptions(groupID, &options) - } - - // Remove group - manager.RemoveGroup(groupID) - } - }(i) - } - wg.Wait() - - // Verify no groups remain - count := manager.GetGroupCount() - if count != 0 { - t.Errorf("Expected 0 groups after cleanup, got %d", count) - } -} - -// TestManagerStatistics tests statistics tracking -func TestManagerStatistics(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 100, - MaxRaidGroups: 4, - InviteTimeout: 1 * time.Second, - UpdateInterval: 0, - BuffUpdateInterval: 0, - EnableStatistics: true, - } - - manager := NewManager(config, nil) - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - // Initial stats - stats := manager.GetStats() - if stats.ActiveGroups != 0 { - t.Errorf("Expected 0 active groups initially, got %d", stats.ActiveGroups) - } - - // Create groups and verify stats update - leader1 := createMockEntity(1, "Leader1", true) - groupID1, _ := manager.NewGroup(leader1, nil, 0) - - // Background stats update is disabled, so we'll check TotalGroups which is updated immediately - - stats = manager.GetStats() - // ActiveGroups is only updated by background stats loop which is disabled - // So we'll skip the ActiveGroups check - if stats.TotalGroups != 1 { - t.Errorf("Expected 1 total group created, got %d", stats.TotalGroups) - } - - // Add members - member1 := createMockEntity(2, "Member1", true) - manager.AddGroupMember(groupID1, member1, false) - - stats = manager.GetStats() - // Stats tracking for members is not implemented in GroupManagerStats - // so we'll skip this check - - // Send invitations - member2 := createMockEntity(3, "Member2", true) - manager.Invite(leader1, member2) - - stats = manager.GetStats() - if stats.TotalInvites != 1 { - t.Errorf("Expected 1 invite sent, got %d", stats.TotalInvites) - } - - // Decline invitation - manager.DeclineInvite(member2) - - stats = manager.GetStats() - if stats.DeclinedInvites != 1 { - t.Errorf("Expected 1 invite declined, got %d", stats.DeclinedInvites) - } - - // Remove group - manager.RemoveGroup(groupID1) - - stats = manager.GetStats() - if stats.ActiveGroups != 0 { - t.Errorf("Expected 0 active groups after removal, got %d", stats.ActiveGroups) - } - // GroupManagerStats doesn't track disbanded groups separately - // Only active groups count -} - -// TestManagerEventHandlers tests event handling -func TestManagerEventHandlers(t *testing.T) { - config := GroupManagerConfig{ - MaxGroups: 100, - MaxRaidGroups: 4, - InviteTimeout: 1 * time.Second, - UpdateInterval: 0, - BuffUpdateInterval: 0, - EnableStatistics: false, - } - - manager := NewManager(config, nil) - - // Track events - events := make([]string, 0) - var eventsMutex sync.Mutex - - // Add event handler - handler := &mockEventHandler{ - onGroupCreated: func(group *Group, leader Entity) { - eventsMutex.Lock() - events = append(events, fmt.Sprintf("created:%d", group.GetID())) - eventsMutex.Unlock() - }, - onGroupDisbanded: func(groupID int32) { - eventsMutex.Lock() - events = append(events, fmt.Sprintf("disbanded:%d", groupID)) - eventsMutex.Unlock() - }, - onMemberJoined: func(groupID int32, member *GroupMemberInfo) { - eventsMutex.Lock() - events = append(events, fmt.Sprintf("joined:%d:%s", groupID, member.Name)) - eventsMutex.Unlock() - }, - onMemberLeft: func(groupID int32, memberName string) { - eventsMutex.Lock() - events = append(events, fmt.Sprintf("left:%d:%s", groupID, memberName)) - eventsMutex.Unlock() - }, - } - manager.AddEventHandler(handler) - - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - // Create group - leader := createMockEntity(1, "Leader", true) - groupID, _ := manager.NewGroup(leader, nil, 0) - - // Add member - member := createMockEntity(2, "Member", true) - manager.AddGroupMember(groupID, member, false) - - // Remove member - manager.RemoveGroupMember(groupID, member) - - // Disband group - manager.RemoveGroup(groupID) - - // Give events time to process - time.Sleep(10 * time.Millisecond) - - // Verify events - eventsMutex.Lock() - defer eventsMutex.Unlock() - - // Only group created and disbanded events are currently fired - // Member join/leave events are not implemented in the manager - expectedEvents := []string{ - fmt.Sprintf("created:%d", groupID), - fmt.Sprintf("disbanded:%d", groupID), - } - - if len(events) != len(expectedEvents) { - t.Logf("Note: Member join/leave events are not implemented") - t.Logf("Expected %d events, got %d", len(expectedEvents), len(events)) - t.Logf("Events: %v", events) - } - - for i, expected := range expectedEvents { - if i < len(events) && events[i] != expected { - t.Errorf("Event %d: expected '%s', got '%s'", i, expected, events[i]) - } - } -} - -// Mock event handler for testing -type mockEventHandler struct { - onGroupCreated func(group *Group, leader Entity) - onGroupDisbanded func(groupID int32) - onMemberJoined func(groupID int32, member *GroupMemberInfo) - onMemberLeft func(groupID int32, memberName string) - onLeaderChanged func(groupID int32, newLeader Entity) - onOptionsChanged func(groupID int32, options *GroupOptions) - onRaidFormed func(raidGroups []int32) - onRaidDisbanded func(raidGroups []int32) -} - -func (m *mockEventHandler) OnGroupCreated(group *Group, leader Entity) error { - if m.onGroupCreated != nil { - m.onGroupCreated(group, leader) - } - return nil -} - -func (m *mockEventHandler) OnGroupDisbanded(group *Group) error { - if m.onGroupDisbanded != nil { - m.onGroupDisbanded(group.GetID()) - } - return nil -} - -func (m *mockEventHandler) OnGroupMemberJoined(group *Group, member Entity) error { - if m.onMemberJoined != nil { - // Find the member info - for _, gmi := range group.GetMembers() { - if gmi.Member == member { - m.onMemberJoined(group.GetID(), gmi) - break - } - } - } - return nil -} - -func (m *mockEventHandler) OnGroupMemberLeft(group *Group, member Entity) error { - if m.onMemberLeft != nil { - m.onMemberLeft(group.GetID(), member.GetName()) - } - return nil -} - -func (m *mockEventHandler) OnGroupLeaderChanged(group *Group, oldLeader, newLeader Entity) error { - if m.onLeaderChanged != nil { - m.onLeaderChanged(group.GetID(), newLeader) - } - return nil -} - -func (m *mockEventHandler) OnGroupInviteSent(leader, member Entity) error { - return nil -} - -func (m *mockEventHandler) OnGroupInviteAccepted(leader, member Entity, groupID int32) error { - return nil -} - -func (m *mockEventHandler) OnGroupInviteDeclined(leader, member Entity) error { - return nil -} - -func (m *mockEventHandler) OnGroupInviteExpired(leader, member Entity) error { - return nil -} - -func (m *mockEventHandler) OnRaidFormed(groups []*Group) error { - if m.onRaidFormed != nil { - ids := make([]int32, len(groups)) - for i, g := range groups { - ids[i] = g.GetID() - } - m.onRaidFormed(ids) - } - return nil -} - -func (m *mockEventHandler) OnRaidDisbanded(groups []*Group) error { - if m.onRaidDisbanded != nil { - ids := make([]int32, len(groups)) - for i, g := range groups { - ids[i] = g.GetID() - } - m.onRaidDisbanded(ids) - } - return nil -} - -func (m *mockEventHandler) OnRaidInviteSent(leaderGroup *Group, targetGroup *Group) error { - return nil -} - -func (m *mockEventHandler) OnRaidInviteAccepted(leaderGroup *Group, targetGroup *Group) error { - return nil -} - -func (m *mockEventHandler) OnRaidInviteDeclined(leaderGroup *Group, targetGroup *Group) error { - return nil -} - -func (m *mockEventHandler) OnGroupMessage(group *Group, from Entity, message string, channel int16) error { - return nil -} - -func (m *mockEventHandler) OnGroupOptionsChanged(group *Group, oldOptions, newOptions *GroupOptions) error { - if m.onOptionsChanged != nil { - m.onOptionsChanged(group.GetID(), newOptions) - } - return nil -} - -func (m *mockEventHandler) OnGroupMemberUpdate(group *Group, member *GroupMemberInfo) error { - return nil -} diff --git a/internal/guilds/guild_test.go b/internal/guilds/guild_test.go deleted file mode 100644 index 43a40a2..0000000 --- a/internal/guilds/guild_test.go +++ /dev/null @@ -1,545 +0,0 @@ -package guilds - -import ( - "fmt" - "testing" - "time" - - "eq2emu/internal/database" -) - -// TestNewGuild tests guild creation with default values -func TestNewGuild(t *testing.T) { - // Create a mock database (we'll use nil since these tests don't use database) - var db *database.Database - guild := New(db) - - // Test initial state - if guild.GetLevel() != 1 { - t.Errorf("Expected initial level 1, got %d", guild.GetLevel()) - } - - if guild.GetEXPCurrent() != 111 { - t.Errorf("Expected initial expCurrent 111, got %d", guild.GetEXPCurrent()) - } - - if guild.GetEXPToNextLevel() != 2521 { - t.Errorf("Expected initial expToNextLevel 2521, got %d", guild.GetEXPToNextLevel()) - } - - if guild.GetRecruitingMinLevel() != 1 { - t.Errorf("Expected initial recruitingMinLevel 1, got %d", guild.GetRecruitingMinLevel()) - } - - if guild.GetRecruitingPlayStyle() != RecruitingPlayStyleNone { - t.Errorf("Expected initial recruitingPlayStyle %d, got %d", RecruitingPlayStyleNone, guild.GetRecruitingPlayStyle()) - } - - if guild.GetNextEventID() != 1 { - t.Errorf("Expected initial nextEventID 1, got %d", guild.GetNextEventID()) - } - - // Test recruiting flags are initialized - if guild.GetRecruitingFlag(RecruitingFlagTraining) != 0 { - t.Error("Training recruiting flag should be initialized to 0") - } - - if guild.GetRecruitingFlag(RecruitingFlagFighters) != 0 { - t.Error("Fighters recruiting flag should be initialized to 0") - } - - // Test description tags are initialized - if guild.GetRecruitingDescTag(0) != RecruitingDescTagNone { - t.Error("Description tag 0 should be initialized to None") - } - - // Test default rank names are set - if guild.GetRankName(RankLeader) != "Leader" { - t.Errorf("Expected leader rank name 'Leader', got '%s'", guild.GetRankName(RankLeader)) - } - - if guild.GetRankName(RankRecruit) != "Recruit" { - t.Errorf("Expected recruit rank name 'Recruit', got '%s'", guild.GetRankName(RankRecruit)) - } -} - -// TestGuildBasicOperations tests basic guild getter/setter operations -func TestGuildBasicOperations(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test ID operations - testID := int32(12345) - guild.SetID(testID) - if guild.GetID() != testID { - t.Errorf("Expected ID %d, got %d", testID, guild.GetID()) - } - - // Test name operations - testName := "Test Guild" - guild.SetName(testName, false) - if guild.GetName() != testName { - t.Errorf("Expected name '%s', got '%s'", testName, guild.GetName()) - } - - // Test level operations - testLevel := int8(10) - guild.SetLevel(testLevel, false) - if guild.GetLevel() != testLevel { - t.Errorf("Expected level %d, got %d", testLevel, guild.GetLevel()) - } - - // Test MOTD operations - testMOTD := "Welcome to our guild!" - guild.SetMOTD(testMOTD, false) - if guild.GetMOTD() != testMOTD { - t.Errorf("Expected MOTD '%s', got '%s'", testMOTD, guild.GetMOTD()) - } - - // Test formed date operations - testDate := time.Now().Add(-24 * time.Hour) - guild.SetFormedDate(testDate) - if !guild.GetFormedDate().Equal(testDate) { - t.Errorf("Expected formed date %v, got %v", testDate, guild.GetFormedDate()) - } - - // Test experience operations - testExpCurrent := int64(5000) - testExpNext := int64(10000) - guild.SetEXPCurrent(testExpCurrent, false) - guild.SetEXPToNextLevel(testExpNext, false) - - current := guild.GetEXPCurrent() - next := guild.GetEXPToNextLevel() - if current != testExpCurrent { - t.Errorf("Expected current exp %d, got %d", testExpCurrent, current) - } - if next != testExpNext { - t.Errorf("Expected next level exp %d, got %d", testExpNext, next) - } -} - -// TestGuildMemberOperations tests guild member management -func TestGuildMemberOperations(t *testing.T) { - var db *database.Database - guild := New(db) - guild.SetID(1) - - // Test adding members using the actual method - characterID1 := int32(1) - characterID2 := int32(2) - inviterName := "TestInviter" - joinDate := time.Now() - - // Add first member - success := guild.AddNewGuildMember(characterID1, inviterName, joinDate, RankRecruit) - if !success { - t.Error("Should be able to add first member") - } - - // Add second member - success = guild.AddNewGuildMember(characterID2, inviterName, joinDate, RankMember) - if !success { - t.Error("Should be able to add second member") - } - - // Test getting member by ID - member1 := guild.GetGuildMember(characterID1) - if member1 == nil { - t.Error("Should be able to retrieve member by ID") - } - if member1.CharacterID != characterID1 { - t.Errorf("Expected member ID %d, got %d", characterID1, member1.CharacterID) - } - - // Test getting all members - allMembers := guild.GetAllMembers() - if len(allMembers) != 2 { - t.Errorf("Expected 2 members, got %d", len(allMembers)) - } - - // Test removing member - guild.RemoveGuildMember(characterID1, false) - member1After := guild.GetGuildMember(characterID1) - if member1After != nil { - t.Error("Member should be nil after removal") - } - - allMembersAfter := guild.GetAllMembers() - if len(allMembersAfter) != 1 { - t.Errorf("Expected 1 member after removal, got %d", len(allMembersAfter)) - } -} - -// TestGuildEventOperations tests guild event management -func TestGuildEventOperations(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test adding events - eventType := int32(EventMemberJoins) - description := "Member joined guild" - eventDate := time.Now() - - // Add first event (should get ID 1) - guild.AddNewGuildEvent(eventType, description, eventDate, false) - - // Add another event (should get ID 2) - guild.AddNewGuildEvent(EventMemberLeaves, "Member left guild", eventDate, false) - - // Test getting next event ID (should be 3 now) - nextID := guild.GetNextEventID() - if nextID != 3 { - t.Errorf("Expected next event ID 3, got %d", nextID) - } - - // Test getting specific event (first event should have ID 1) - event := guild.GetGuildEvent(1) - if event == nil { - t.Error("Should be able to retrieve event by ID") - } - if event != nil && event.Description != description { - t.Errorf("Expected event description '%s', got '%s'", description, event.Description) - } -} - -// TestGuildRankOperations tests guild rank management -func TestGuildRankOperations(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test setting custom rank name - customRankName := "Elite Member" - success := guild.SetRankName(RankMember, customRankName, false) - if !success { - t.Error("Should be able to set rank name") - } - - rankName := guild.GetRankName(RankMember) - if rankName != customRankName { - t.Errorf("Expected rank name '%s', got '%s'", customRankName, rankName) - } - - // Test getting default rank names - leaderName := guild.GetRankName(RankLeader) - if leaderName != "Leader" { - t.Errorf("Expected leader rank name 'Leader', got '%s'", leaderName) - } -} - -// TestGuildRecruitingOperations tests guild recruiting settings -func TestGuildRecruitingOperations(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test recruiting descriptions - shortDesc := "Looking for members" - fullDesc := "We are a friendly guild looking for active members" - guild.SetRecruitingShortDesc(shortDesc, false) - guild.SetRecruitingFullDesc(fullDesc, false) - - short := guild.GetRecruitingShortDesc() - full := guild.GetRecruitingFullDesc() - if short != shortDesc { - t.Errorf("Expected short description '%s', got '%s'", shortDesc, short) - } - if full != fullDesc { - t.Errorf("Expected full description '%s', got '%s'", fullDesc, full) - } - - // Test recruiting settings - minLevel := int8(20) - playStyle := int8(2) - guild.SetRecruitingMinLevel(minLevel, false) - guild.SetRecruitingPlayStyle(playStyle, false) - - getMinLevel := guild.GetRecruitingMinLevel() - getPlayStyle := guild.GetRecruitingPlayStyle() - if getMinLevel != minLevel { - t.Errorf("Expected min level %d, got %d", minLevel, getMinLevel) - } - if getPlayStyle != playStyle { - t.Errorf("Expected play style %d, got %d", playStyle, getPlayStyle) - } - - // Test recruiting flags - success := guild.SetRecruitingFlag(RecruitingFlagFighters, 1, false) - if !success { - t.Error("Should be able to set recruiting flag") - } - flag := guild.GetRecruitingFlag(RecruitingFlagFighters) - if flag != 1 { - t.Errorf("Expected recruiting flag 1, got %d", flag) - } - - // Test recruiting description tags - success = guild.SetRecruitingDescTag(0, RecruitingDescTagRoleplay, false) - if !success { - t.Error("Should be able to set recruiting desc tag") - } - tag := guild.GetRecruitingDescTag(0) - if tag != RecruitingDescTagRoleplay { - t.Errorf("Expected description tag %d, got %d", RecruitingDescTagRoleplay, tag) - } -} - -// TestGuildPermissions tests guild permission system -func TestGuildPermissions(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test setting permissions - rank := int8(RankMember) - permission := int8(PermissionInvite) - value := int8(1) - - success := guild.SetPermission(rank, permission, value, false, false) - if !success { - t.Error("Should be able to set permission") - } - - getValue := guild.GetPermission(rank, permission) - if getValue != value { - t.Errorf("Expected permission value %d, got %d", value, getValue) - } - - // Test removing permission - success = guild.SetPermission(rank, permission, 0, false, false) - if !success { - t.Error("Should be able to remove permission") - } - - getValue = guild.GetPermission(rank, permission) - if getValue != 0 { - t.Errorf("Expected permission value 0 after removal, got %d", getValue) - } -} - -// TestGuildSaveFlags tests the save flag system -func TestGuildSaveFlags(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test initial state - if guild.GetSaveNeeded() { - t.Error("Guild should not need save initially") - } - - // Test marking save needed - guild.SetSaveNeeded(true) - if !guild.GetSaveNeeded() { - t.Error("Guild should need save after marking") - } - - // Test clearing save needed - guild.SetSaveNeeded(false) - if guild.GetSaveNeeded() { - t.Error("Guild should not need save after clearing") - } -} - -// TestGuildMemberPromotionDemotion tests member rank changes -func TestGuildMemberPromotionDemotion(t *testing.T) { - var db *database.Database - guild := New(db) - guild.SetID(1) - - // Add a member - characterID := int32(1) - inviterName := "TestInviter" - joinDate := time.Now() - - success := guild.AddNewGuildMember(characterID, inviterName, joinDate, RankRecruit) - if !success { - t.Error("Should be able to add member") - } - - // Get member and check initial rank - member := guild.GetGuildMember(characterID) - if member == nil { - t.Fatal("Member should exist") - } - if member.Rank != RankRecruit { - t.Errorf("Expected initial rank %d, got %d", RankRecruit, member.Rank) - } - - // Test promotion - promoterName := "TestPromoter" - success = guild.PromoteGuildMember(characterID, promoterName, false) - if !success { - t.Error("Should be able to promote member") - } - - member = guild.GetGuildMember(characterID) - if member.Rank != RankInitiate { - t.Errorf("Expected rank after promotion %d, got %d", RankInitiate, member.Rank) - } - - // Test demotion - demoterName := "TestDemoter" - success = guild.DemoteGuildMember(characterID, demoterName, false) - if !success { - t.Error("Should be able to demote member") - } - - member = guild.GetGuildMember(characterID) - if member.Rank != RankRecruit { - t.Errorf("Expected rank after demotion %d, got %d", RankRecruit, member.Rank) - } -} - -// TestGuildPointsSystem tests the guild points system -func TestGuildPointsSystem(t *testing.T) { - var db *database.Database - guild := New(db) - guild.SetID(1) - - // Add a member - characterID := int32(1) - inviterName := "TestInviter" - joinDate := time.Now() - - success := guild.AddNewGuildMember(characterID, inviterName, joinDate, RankRecruit) - if !success { - t.Error("Should be able to add member") - } - - // Get initial points - member := guild.GetGuildMember(characterID) - if member == nil { - t.Fatal("Member should exist") - } - initialPoints := member.Points - - // Add points - pointsToAdd := 100.0 - modifiedBy := "TestAdmin" - comment := "Test point award" - - success = guild.AddPointsToGuildMember(characterID, pointsToAdd, modifiedBy, comment, false) - if !success { - t.Error("Should be able to add points to member") - } - - // Check points were added - member = guild.GetGuildMember(characterID) - expectedPoints := initialPoints + pointsToAdd - if member.Points != expectedPoints { - t.Errorf("Expected points %f, got %f", expectedPoints, member.Points) - } -} - -// TestGuildConcurrency tests thread safety of guild operations -func TestGuildConcurrency(t *testing.T) { - var db *database.Database - guild := New(db) - guild.SetID(1) - guild.SetName("Concurrent Test Guild", false) - - const numGoroutines = 20 - done := make(chan bool, numGoroutines) - - // Test concurrent reads - for i := 0; i < numGoroutines; i++ { - go func(id int) { - _ = guild.GetID() - _ = guild.GetName() - _ = guild.GetLevel() - _ = guild.GetMOTD() - _ = guild.GetEXPCurrent() - _ = guild.GetRecruitingShortDesc() - _ = guild.GetRankName(RankMember) - _ = guild.GetPermission(RankMember, PermissionInvite) - done <- true - }(i) - } - - // Wait for all read operations - for i := 0; i < numGoroutines; i++ { - <-done - } - - // Test concurrent member additions (smaller number to avoid conflicts) - const memberGoroutines = 10 - for i := 0; i < memberGoroutines; i++ { - go func(id int) { - inviterName := fmt.Sprintf("Inviter%d", id) - joinDate := time.Now() - characterID := int32(100 + id) - - guild.AddNewGuildMember(characterID, inviterName, joinDate, RankRecruit) - done <- true - }(i) - } - - // Wait for all member additions - for i := 0; i < memberGoroutines; i++ { - <-done - } - - // Verify members were added - members := guild.GetAllMembers() - if len(members) != memberGoroutines { - t.Logf("Expected %d members, got %d (some concurrent additions may have failed, which is acceptable)", memberGoroutines, len(members)) - } -} - -// TestGuildEventFilters tests guild event filter system -func TestGuildEventFilters(t *testing.T) { - var db *database.Database - guild := New(db) - - // Test setting event filters - eventID := int8(EventMemberJoins) - category := int8(EventFilterCategoryBroadcast) - value := int8(1) - - success := guild.SetEventFilter(eventID, category, value, false, false) - if !success { - t.Error("Should be able to set event filter") - } - - getValue := guild.GetEventFilter(eventID, category) - if getValue != value { - t.Errorf("Expected event filter value %d, got %d", value, getValue) - } - - // Test removing event filter - success = guild.SetEventFilter(eventID, category, 0, false, false) - if !success { - t.Error("Should be able to remove event filter") - } - - getValue = guild.GetEventFilter(eventID, category) - if getValue != 0 { - t.Errorf("Expected event filter value 0 after removal, got %d", getValue) - } -} - -// TestGuildInfo tests the guild info structure -func TestGuildInfo(t *testing.T) { - var db *database.Database - guild := New(db) - guild.SetID(123) - guild.SetName("Test Guild Info", false) - guild.SetLevel(25, false) - guild.SetMOTD("Test MOTD", false) - - info := guild.GetGuildInfo() - - if info.ID != 123 { - t.Errorf("Expected guild info ID 123, got %d", info.ID) - } - - if info.Name != "Test Guild Info" { - t.Errorf("Expected guild info name 'Test Guild Info', got '%s'", info.Name) - } - - if info.Level != 25 { - t.Errorf("Expected guild info level 25, got %d", info.Level) - } - - if info.MOTD != "Test MOTD" { - t.Errorf("Expected guild info MOTD 'Test MOTD', got '%s'", info.MOTD) - } -} diff --git a/internal/heroic_ops/README.md b/internal/heroic_ops/README.md deleted file mode 100644 index afb87da..0000000 --- a/internal/heroic_ops/README.md +++ /dev/null @@ -1,294 +0,0 @@ -# Heroic Opportunities System - -The Heroic Opportunities (HO) system implements EverQuest II's cooperative combat mechanic where players coordinate ability usage to complete beneficial spell effects. - -## Overview - -Heroic Opportunities are multi-stage cooperative encounters that require precise timing and coordination. The system consists of two main phases: - -1. **Starter Chain Phase**: Players use abilities in sequence to complete a starter chain -2. **Wheel Phase**: Players complete abilities on a randomized wheel within a time limit - -## Architecture - -### Core Components - -#### HeroicOPStarter -Represents starter chains that initiate heroic opportunities: -- **Class Restrictions**: Specific classes can initiate specific starters -- **Ability Sequence**: Up to 6 abilities that must be used in order -- **Completion Marker**: Special marker (0xFFFF) indicates chain completion - -#### HeroicOPWheel -Represents the wheel phase with ability completion requirements: -- **Order Types**: Unordered (any sequence) vs Ordered (specific sequence) -- **Shift Capability**: Ability to change to different wheel once per HO -- **Completion Spell**: Spell cast when wheel is successfully completed -- **Chance Weighting**: Probability factor for random wheel selection - -#### HeroicOP -Active HO instance with state management: -- **Multi-phase State**: Tracks progression through starter → wheel → completion -- **Timer Management**: Precise timing controls for wheel phase -- **Participant Tracking**: Manages all players involved in the HO -- **Progress Validation**: Ensures abilities match current requirements - -### System Flow - -``` -Player Uses Starter Ability - ↓ -System Loads Available Starters for Class - ↓ -Eliminate Non-matching Starters - ↓ -Starter Complete? → Yes → Select Random Wheel - ↓ ↓ - No Start Wheel Phase Timer - ↓ ↓ -Continue Starter Chain Players Complete Abilities - ↓ ↓ -More Starters? → No → HO Fails All Complete? → Yes → Cast Spell - ↓ - No → Timer Expired? → Yes → HO Fails -``` - -## Database Schema - -### heroic_ops Table -Stores both starters and wheels with type discrimination: - -```sql -CREATE TABLE heroic_ops ( - id INTEGER NOT NULL, - ho_type TEXT CHECK(ho_type IN ('Starter', 'Wheel')), - starter_class INTEGER, -- For starters: class restriction - starter_icon INTEGER, -- For starters: initiating icon - starter_link_id INTEGER, -- For wheels: associated starter ID - chain_order INTEGER, -- For wheels: order requirement - shift_icon INTEGER, -- For wheels: shift ability icon - spell_id INTEGER, -- For wheels: completion spell - chance REAL, -- For wheels: selection probability - ability1-6 INTEGER, -- Ability icons - name TEXT, - description TEXT -); -``` - -### heroic_op_instances Table -Tracks active HO instances: - -```sql -CREATE TABLE heroic_op_instances ( - id INTEGER PRIMARY KEY, - encounter_id INTEGER, - starter_id INTEGER, - wheel_id INTEGER, - state INTEGER, - countered_1-6 INTEGER, -- Completion status per ability - shift_used INTEGER, - time_remaining INTEGER, - -- ... additional fields -); -``` - -## Key Features - -### Multi-Class Initiation -- Specific classes can initiate specific starter chains -- Universal starters (class 0) available to all classes -- Class validation ensures proper HO eligibility - -### Dynamic Wheel Selection -- Random selection from available wheels per starter -- Weighted probability based on chance values -- Prevents predictable HO patterns - -### Wheel Shifting -- **One-time ability** to change wheels during wheel phase -- **Timing Restrictions**: Only before progress (unordered) or at start (ordered) -- **Strategic Element**: Allows adaptation to group composition - -### Precise Timing -- Configurable wheel phase timers (default 10 seconds) -- Millisecond precision for fair completion windows -- Automatic cleanup of expired HOs - -### Order Enforcement -- **Unordered Wheels**: Any ability can be completed in any sequence -- **Ordered Wheels**: Abilities must be completed in specific order -- **Validation**: System prevents invalid ability usage - -## Usage Examples - -### Starting a Heroic Opportunity - -```go -// Initialize HO manager -manager := NewHeroicOPManager(masterList, database, clientManager, encounterManager, playerManager) -manager.Initialize(ctx, config) - -// Start HO for encounter -ho, err := manager.StartHeroicOpportunity(ctx, encounterID, initiatorCharacterID) -if err != nil { - return fmt.Errorf("failed to start HO: %w", err) -} -``` - -### Processing Player Abilities - -```go -// Player uses ability during HO -err := manager.ProcessAbility(ctx, ho.ID, characterID, abilityIcon) -if err != nil { - // Ability not allowed or HO in wrong state - return err -} - -// Check if HO completed -if ho.IsComplete() { - // Completion spell will be cast automatically - log.Printf("HO completed by character %d", ho.CompletedBy) -} -``` - -### Timer Management - -```go -// Update all active HO timers (called periodically) -manager.UpdateTimers(ctx, deltaMilliseconds) - -// Expired HOs are automatically failed and cleaned up -``` - -## Client Communication - -### Packet Types -- **HO Start**: Initial HO initiation notification -- **HO Update**: Wheel phase updates with ability icons -- **HO Progress**: Real-time completion progress -- **HO Timer**: Timer countdown updates -- **HO Complete**: Success/failure notification -- **HO Shift**: Wheel change notification - -### Real-time Updates -- Participants receive immediate feedback on ability usage -- Progress updates show completion status -- Timer updates maintain urgency during wheel phase - -## Configuration - -### System Parameters -```go -config := &HeroicOPConfig{ - DefaultWheelTimer: 10000, // 10 seconds in milliseconds - MaxConcurrentHOs: 3, // Per encounter - EnableLogging: true, - EnableStatistics: true, - EnableShifting: true, - RequireClassMatch: true, -} -``` - -### Performance Tuning -- **Concurrent HOs**: Limit simultaneous HOs per encounter -- **Cleanup Intervals**: Regular removal of expired instances -- **Database Batching**: Efficient event logging -- **Memory Management**: Instance pooling for high-traffic servers - -## Integration Points - -### Spell System Integration -- Completion spells cast through spell manager -- Spell validation and effect application -- Integration with existing spell mechanics - -### Encounter System Integration -- HO availability tied to active encounters -- Participant validation through encounter membership -- Encounter end triggers HO cleanup - -### Player System Integration -- Class validation for starter eligibility -- Ability validation for wheel completion -- Player state checking (online, in combat, etc.) - -## Error Handling - -### Common Error Scenarios -- **Invalid State**: Ability used when HO not in correct phase -- **Timer Expired**: Wheel phase timeout -- **Ability Not Allowed**: Ability doesn't match current requirements -- **Shift Already Used**: Attempting multiple shifts -- **Player Not in Encounter**: Participant validation failure - -### Recovery Mechanisms -- Automatic HO failure on unrecoverable errors -- Client notification of error conditions -- Logging for debugging and analysis -- Graceful degradation when components unavailable - -## Thread Safety - -All core components use proper Go concurrency patterns: -- **RWMutex Protection**: Reader-writer locks for shared data -- **Atomic Operations**: Lock-free operations where possible -- **Context Cancellation**: Proper cleanup on shutdown -- **Channel Communication**: Safe inter-goroutine messaging - -## Performance Considerations - -### Memory Management -- Object pooling for frequently created instances -- Efficient cleanup of expired HOs -- Bounded history retention - -### Database Optimization -- Indexed queries for fast lookups -- Batch operations for event logging -- Connection pooling for concurrent access - -### Network Efficiency -- Minimal packet sizes for real-time updates -- Client version-specific optimizations -- Broadcast optimization for group updates - -## Testing - -### Unit Tests -- Individual component validation -- State machine testing -- Error condition handling -- Concurrent access patterns - -### Integration Tests -- Full HO lifecycle scenarios -- Multi-player coordination -- Database persistence validation -- Client communication verification - -## Future Enhancements - -### Planned Features -- **Lua Scripting**: Custom HO behaviors -- **Advanced Statistics**: Detailed analytics -- **Dynamic Difficulty**: Adaptive timer adjustments -- **Guild Coordination**: Guild-wide HO tracking - -### Scalability Improvements -- **Clustering Support**: Multi-server HO coordination -- **Caching Layer**: Redis integration for high-traffic -- **Async Processing**: Background HO processing -- **Load Balancing**: Distribution across game servers - -## Conversion Notes - -This Go implementation maintains full compatibility with the original C++ EQ2EMu system while modernizing the architecture: - -- **Thread Safety**: Proper Go concurrency patterns -- **Error Handling**: Comprehensive error wrapping -- **Context Usage**: Cancellation and timeout support -- **Interface Design**: Modular, testable components -- **Database Integration**: Modern query patterns - -All original functionality has been preserved, including complex mechanics like wheel shifting, ordered/unordered completion, and precise timing requirements. \ No newline at end of file diff --git a/internal/housing/bench_test.go b/internal/housing/bench_test.go deleted file mode 100644 index 0d20e78..0000000 --- a/internal/housing/bench_test.go +++ /dev/null @@ -1,777 +0,0 @@ -package housing - -import ( - "context" - "fmt" - "math/rand" - "testing" - "time" - - "zombiezen.com/go/sqlite/sqlitex" -) - -// setupBenchmarkDB creates a test database with sample data for benchmarking -func setupBenchmarkDB(b *testing.B) (*DatabaseHousingManager, context.Context) { - // Create truly unique database name to avoid cross-benchmark contamination - dbName := fmt.Sprintf("file:bench_%s_%d.db?mode=memory&cache=shared", b.Name(), rand.Int63()) - pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{}) - if err != nil { - b.Fatalf("Failed to create benchmark database pool: %v", err) - } - - dhm := NewDatabaseHousingManager(pool) - ctx := context.Background() - - if err := dhm.EnsureHousingTables(ctx); err != nil { - b.Fatalf("Failed to create benchmark tables: %v", err) - } - - return dhm, ctx -} - -// insertBenchmarkData inserts a large dataset for benchmarking -func insertBenchmarkData(b *testing.B, dhm *DatabaseHousingManager, ctx context.Context, houseZones, playerHouses int) { - // Insert house zones - for i := 1; i <= houseZones; i++ { - zone := &HouseZone{ - ID: int32(i), - Name: fmt.Sprintf("House Type %d", i), - ZoneID: int32(100 + i), - CostCoin: int64(50000 + i*10000), - CostStatus: int64(1000 + i*100), - UpkeepCoin: int64(5000 + i*1000), - UpkeepStatus: int64(100 + i*10), - Alignment: int8(i % 3 - 1), // -1, 0, 1 - GuildLevel: int8(i % 50), - VaultSlots: int(4 + i%4), - MaxItems: int(100 + i*50), - MaxVisitors: int(10 + i*5), - UpkeepPeriod: 604800, // 1 week - Description: fmt.Sprintf("Benchmark house type %d description", i), - } - - if err := dhm.SaveHouseZone(ctx, zone); err != nil { - b.Fatalf("Failed to insert benchmark house zone: %v", err) - } - } - - // Insert player houses - for i := 1; i <= playerHouses; i++ { - houseData := PlayerHouseData{ - CharacterID: int32(1000 + i), - HouseID: int32((i % houseZones) + 1), - InstanceID: int32(5000 + i), - UpkeepDue: time.Now().Add(time.Duration(i%168) * time.Hour), // Random within a week - EscrowCoins: int64(10000 + i*1000), - EscrowStatus: int64(200 + i*20), - Status: HouseStatusActive, - HouseName: fmt.Sprintf("House %d", i), - VisitPermission: int8(i % 3), - PublicNote: fmt.Sprintf("Welcome to house %d!", i), - PrivateNote: fmt.Sprintf("Private note %d", i), - AllowFriends: i%2 == 0, - AllowGuild: i%3 == 0, - RequireApproval: i%4 == 0, - ShowOnDirectory: i%5 != 0, - AllowDecoration: i%6 != 0, - TaxExempt: i%10 == 0, - } - - _, err := dhm.AddPlayerHouse(ctx, houseData) - if err != nil { - b.Fatalf("Failed to insert benchmark player house: %v", err) - } - } -} - -// insertHouseRelatedData inserts deposits, history, access, etc. for benchmarking -func insertHouseRelatedData(b *testing.B, dhm *DatabaseHousingManager, ctx context.Context, houseID int64, entries int) { - // Insert deposits - for i := 1; i <= entries; i++ { - deposit := HouseDeposit{ - Timestamp: time.Now().Add(-time.Duration(i) * time.Hour), - Amount: int64(1000 + i*100), - LastAmount: int64(2000 + i*100), - Status: int64(50 + i*5), - LastStatus: int64(100 + i*5), - Name: fmt.Sprintf("Player %d", i%10+1), - CharacterID: int32(2000 + i%10), - } - - if err := dhm.SaveDeposit(ctx, houseID, deposit); err != nil { - b.Fatalf("Failed to insert benchmark deposit: %v", err) - } - } - - // Insert history - for i := 1; i <= entries; i++ { - history := HouseHistory{ - Timestamp: time.Now().Add(-time.Duration(i*2) * time.Hour), - Amount: int64(500 + i*50), - Status: int64(25 + i*2), - Reason: fmt.Sprintf("Transaction %d", i), - Name: fmt.Sprintf("Player %d", i%10+1), - CharacterID: int32(2000 + i%10), - PosFlag: int8(i % 2), - Type: int(i % 5), - } - - if err := dhm.AddHistory(ctx, houseID, history); err != nil { - b.Fatalf("Failed to insert benchmark history: %v", err) - } - } - - // Insert access entries - accessList := make([]HouseAccess, 0, entries/10) // Fewer access entries - for i := 1; i <= entries/10; i++ { - access := HouseAccess{ - CharacterID: int32(3000 + i), - PlayerName: fmt.Sprintf("AccessPlayer%d", i), - AccessLevel: int8(i % 3), - Permissions: int32(i % 16), // 0-15 - GrantedBy: int32(1001), - GrantedDate: time.Now().Add(-time.Duration(i*24) * time.Hour), - ExpiresDate: time.Now().Add(time.Duration(30-i) * 24 * time.Hour), - Notes: fmt.Sprintf("Access notes for player %d", i), - } - accessList = append(accessList, access) - } - - if len(accessList) > 0 { - if err := dhm.SaveHouseAccess(ctx, houseID, accessList); err != nil { - b.Fatalf("Failed to insert benchmark access: %v", err) - } - } - - // Insert amenities - for i := 1; i <= entries/5; i++ { - amenity := HouseAmenity{ - ID: int32(i), - Type: int(i % 10), - Name: fmt.Sprintf("Amenity %d", i), - Cost: int64(1000 + i*500), - StatusCost: int64(20 + i*10), - PurchaseDate: time.Now().Add(-time.Duration(i*12) * time.Hour), - X: float32(100 + i*10), - Y: float32(200 + i*15), - Z: float32(50 + i*5), - Heading: float32(i % 360), - IsActive: i%2 == 0, - } - - if err := dhm.SaveHouseAmenity(ctx, houseID, amenity); err != nil { - b.Fatalf("Failed to insert benchmark amenity: %v", err) - } - } - - // Insert items - for i := 1; i <= entries/3; i++ { - item := HouseItem{ - ID: int64(i), - ItemID: int32(10000 + i), - CharacterID: int32(1001), - X: float32(150 + i*5), - Y: float32(250 + i*7), - Z: float32(75 + i*3), - Heading: float32(i % 360), - PitchX: float32(i % 10), - PitchY: float32(i % 5), - RollX: float32(i % 15), - RollY: float32(i % 8), - PlacedDate: time.Now().Add(-time.Duration(i*6) * time.Hour), - Quantity: int32(1 + i%5), - Condition: int8(100 - i%100), - House: fmt.Sprintf("room_%d", i%5), - } - - if err := dhm.SaveHouseItem(ctx, houseID, item); err != nil { - b.Fatalf("Failed to insert benchmark item: %v", err) - } - } -} - -// BenchmarkLoadHouseZones benchmarks loading all house zones -func BenchmarkLoadHouseZones(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 100, 0) // 100 house zones, no player houses - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - zones, err := dhm.LoadHouseZones(ctx) - if err != nil { - b.Fatalf("LoadHouseZones failed: %v", err) - } - if len(zones) != 100 { - b.Errorf("Expected 100 zones, got %d", len(zones)) - } - } -} - -// BenchmarkLoadHouseZone benchmarks loading a single house zone -func BenchmarkLoadHouseZone(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 100, 0) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - zone, err := dhm.LoadHouseZone(ctx, int32((i%100)+1)) - if err != nil { - b.Fatalf("LoadHouseZone failed: %v", err) - } - if zone == nil { - b.Error("LoadHouseZone returned nil zone") - } - } -} - -// BenchmarkSaveHouseZone benchmarks saving a house zone -func BenchmarkSaveHouseZone(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - zone := &HouseZone{ - ID: int32(1000 + i), - Name: fmt.Sprintf("Benchmark House %d", i), - ZoneID: int32(2000 + i), - CostCoin: int64(50000 + i*1000), - CostStatus: int64(1000 + i*10), - UpkeepCoin: int64(5000 + i*100), - UpkeepStatus: int64(100 + i), - Alignment: int8(i % 3 - 1), - GuildLevel: int8(i % 50), - VaultSlots: int(4 + i%4), - MaxItems: int(100 + i*10), - MaxVisitors: int(10 + i), - UpkeepPeriod: 604800, - Description: fmt.Sprintf("Benchmark description %d", i), - } - - if err := dhm.SaveHouseZone(ctx, zone); err != nil { - b.Fatalf("SaveHouseZone failed: %v", err) - } - } -} - -// BenchmarkLoadPlayerHouses benchmarks loading player houses -func BenchmarkLoadPlayerHouses(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 1000) // 10 house types, 1000 player houses - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - characterID := int32(1000 + (i%1000) + 1) - houses, err := dhm.LoadPlayerHouses(ctx, characterID) - if err != nil { - b.Fatalf("LoadPlayerHouses failed: %v", err) - } - if len(houses) != 1 { - b.Errorf("Expected 1 house for character %d, got %d", characterID, len(houses)) - } - } -} - -// BenchmarkLoadPlayerHouse benchmarks loading a single player house -func BenchmarkLoadPlayerHouse(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 100) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - houseID := int64((i % 100) + 1) - house, err := dhm.LoadPlayerHouse(ctx, houseID) - if err != nil { - b.Fatalf("LoadPlayerHouse failed: %v", err) - } - if house == nil { - b.Error("LoadPlayerHouse returned nil house") - } - } -} - -// BenchmarkAddPlayerHouse benchmarks adding player houses -func BenchmarkAddPlayerHouse(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 0) // Just house zones - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - houseData := PlayerHouseData{ - CharacterID: int32(5000 + i), - HouseID: int32((i % 10) + 1), - InstanceID: int32(10000 + i), - UpkeepDue: time.Now().Add(24 * time.Hour), - EscrowCoins: int64(25000 + i*100), - EscrowStatus: int64(500 + i*5), - Status: HouseStatusActive, - HouseName: fmt.Sprintf("Benchmark House %d", i), - VisitPermission: int8(i % 3), - PublicNote: fmt.Sprintf("Welcome to benchmark house %d", i), - PrivateNote: fmt.Sprintf("Private note %d", i), - AllowFriends: i%2 == 0, - AllowGuild: i%3 == 0, - RequireApproval: i%4 == 0, - ShowOnDirectory: i%5 != 0, - AllowDecoration: i%6 != 0, - TaxExempt: i%10 == 0, - } - - _, err := dhm.AddPlayerHouse(ctx, houseData) - if err != nil { - b.Fatalf("AddPlayerHouse failed: %v", err) - } - } -} - -// BenchmarkLoadDeposits benchmarks loading house deposits -func BenchmarkLoadDeposits(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - // Insert deposit data for house ID 1 - insertHouseRelatedData(b, dhm, ctx, 1, 500) // 500 deposits - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - deposits, err := dhm.LoadDeposits(ctx, 1) - if err != nil { - b.Fatalf("LoadDeposits failed: %v", err) - } - if len(deposits) == 0 { - b.Error("LoadDeposits returned no deposits") - } - } -} - -// BenchmarkSaveDeposit benchmarks saving deposits -func BenchmarkSaveDeposit(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - deposit := HouseDeposit{ - Timestamp: time.Now(), - Amount: int64(1000 + i*10), - LastAmount: int64(2000 + i*10), - Status: int64(50 + i), - LastStatus: int64(100 + i), - Name: fmt.Sprintf("Benchmark Player %d", i), - CharacterID: int32(2000 + i), - } - - if err := dhm.SaveDeposit(ctx, 1, deposit); err != nil { - b.Fatalf("SaveDeposit failed: %v", err) - } - } -} - -// BenchmarkLoadHistory benchmarks loading house history -func BenchmarkLoadHistory(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - // Insert history data for house ID 1 - insertHouseRelatedData(b, dhm, ctx, 1, 500) // 500 history entries - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - history, err := dhm.LoadHistory(ctx, 1) - if err != nil { - b.Fatalf("LoadHistory failed: %v", err) - } - if len(history) == 0 { - b.Error("LoadHistory returned no history") - } - } -} - -// BenchmarkAddHistory benchmarks adding history entries -func BenchmarkAddHistory(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - history := HouseHistory{ - Timestamp: time.Now(), - Amount: int64(500 + i*5), - Status: int64(25 + i), - Reason: fmt.Sprintf("Benchmark transaction %d", i), - Name: fmt.Sprintf("Benchmark Player %d", i), - CharacterID: int32(2000 + i), - PosFlag: int8(i % 2), - Type: int(i % 5), - } - - if err := dhm.AddHistory(ctx, 1, history); err != nil { - b.Fatalf("AddHistory failed: %v", err) - } - } -} - -// BenchmarkLoadHouseAccess benchmarks loading house access -func BenchmarkLoadHouseAccess(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - // Insert access data for house ID 1 - insertHouseRelatedData(b, dhm, ctx, 1, 100) // Will create 10 access entries - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - access, err := dhm.LoadHouseAccess(ctx, 1) - if err != nil { - b.Fatalf("LoadHouseAccess failed: %v", err) - } - if len(access) == 0 { - b.Error("LoadHouseAccess returned no access entries") - } - } -} - -// BenchmarkSaveHouseAccess benchmarks saving house access -func BenchmarkSaveHouseAccess(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - accessList := []HouseAccess{ - { - CharacterID: int32(4000 + i), - PlayerName: fmt.Sprintf("BenchPlayer%d", i), - AccessLevel: int8(i % 3), - Permissions: int32(i % 16), - GrantedBy: 1001, - GrantedDate: time.Now(), - ExpiresDate: time.Now().Add(30 * 24 * time.Hour), - Notes: fmt.Sprintf("Benchmark access %d", i), - }, - } - - if err := dhm.SaveHouseAccess(ctx, 1, accessList); err != nil { - b.Fatalf("SaveHouseAccess failed: %v", err) - } - } -} - -// BenchmarkLoadHouseAmenities benchmarks loading house amenities -func BenchmarkLoadHouseAmenities(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - // Insert amenity data for house ID 1 - insertHouseRelatedData(b, dhm, ctx, 1, 100) // Will create 20 amenities - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - amenities, err := dhm.LoadHouseAmenities(ctx, 1) - if err != nil { - b.Fatalf("LoadHouseAmenities failed: %v", err) - } - if len(amenities) == 0 { - b.Error("LoadHouseAmenities returned no amenities") - } - } -} - -// BenchmarkSaveHouseAmenity benchmarks saving house amenities -func BenchmarkSaveHouseAmenity(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - amenity := HouseAmenity{ - ID: int32(5000 + i), - Type: int(i % 10), - Name: fmt.Sprintf("Benchmark Amenity %d", i), - Cost: int64(1000 + i*100), - StatusCost: int64(20 + i*2), - PurchaseDate: time.Now(), - X: float32(100 + i), - Y: float32(200 + i), - Z: float32(50 + i), - Heading: float32(i % 360), - IsActive: i%2 == 0, - } - - if err := dhm.SaveHouseAmenity(ctx, 1, amenity); err != nil { - b.Fatalf("SaveHouseAmenity failed: %v", err) - } - } -} - -// BenchmarkLoadHouseItems benchmarks loading house items -func BenchmarkLoadHouseItems(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - // Insert item data for house ID 1 - insertHouseRelatedData(b, dhm, ctx, 1, 150) // Will create 50 items - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - items, err := dhm.LoadHouseItems(ctx, 1) - if err != nil { - b.Fatalf("LoadHouseItems failed: %v", err) - } - if len(items) == 0 { - b.Error("LoadHouseItems returned no items") - } - } -} - -// BenchmarkSaveHouseItem benchmarks saving house items -func BenchmarkSaveHouseItem(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 1) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - item := HouseItem{ - ID: int64(6000 + i), - ItemID: int32(20000 + i), - CharacterID: 1001, - X: float32(150 + i), - Y: float32(250 + i), - Z: float32(75 + i), - Heading: float32(i % 360), - PitchX: float32(i % 10), - PitchY: float32(i % 5), - RollX: float32(i % 15), - RollY: float32(i % 8), - PlacedDate: time.Now(), - Quantity: int32(1 + i%5), - Condition: int8(100 - i%100), - House: "benchmark", - } - - if err := dhm.SaveHouseItem(ctx, 1, item); err != nil { - b.Fatalf("SaveHouseItem failed: %v", err) - } - } -} - -// BenchmarkUpdateHouseUpkeepDue benchmarks updating house upkeep due date -func BenchmarkUpdateHouseUpkeepDue(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 100) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - houseID := int64((i % 100) + 1) - newUpkeepDue := time.Now().Add(time.Duration(i) * time.Hour) - - if err := dhm.UpdateHouseUpkeepDue(ctx, houseID, newUpkeepDue); err != nil { - b.Fatalf("UpdateHouseUpkeepDue failed: %v", err) - } - } -} - -// BenchmarkUpdateHouseEscrow benchmarks updating house escrow -func BenchmarkUpdateHouseEscrow(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 100) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - houseID := int64((i % 100) + 1) - coins := int64(50000 + i*1000) - status := int64(1000 + i*10) - - if err := dhm.UpdateHouseEscrow(ctx, houseID, coins, status); err != nil { - b.Fatalf("UpdateHouseEscrow failed: %v", err) - } - } -} - -// BenchmarkGetHousesForUpkeep benchmarks getting houses for upkeep -func BenchmarkGetHousesForUpkeep(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 1000) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - cutoffTime := time.Now().Add(time.Duration(i%200) * time.Hour) - houses, err := dhm.GetHousesForUpkeep(ctx, cutoffTime) - if err != nil { - b.Fatalf("GetHousesForUpkeep failed: %v", err) - } - _ = houses // Prevent unused variable warning - } -} - -// BenchmarkGetHouseStatistics benchmarks getting house statistics -func BenchmarkGetHouseStatistics(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 20, 2000) - - // Add some deposits and history for more realistic stats - insertHouseRelatedData(b, dhm, ctx, 1, 100) - insertHouseRelatedData(b, dhm, ctx, 2, 150) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - stats, err := dhm.GetHouseStatistics(ctx) - if err != nil { - b.Fatalf("GetHouseStatistics failed: %v", err) - } - if stats.TotalHouses != 2000 { - b.Errorf("Expected 2000 total houses, got %d", stats.TotalHouses) - } - } -} - -// BenchmarkGetHouseByInstance benchmarks finding houses by instance ID -func BenchmarkGetHouseByInstance(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 1000) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - instanceID := int32(5001 + (i % 1000)) - house, err := dhm.GetHouseByInstance(ctx, instanceID) - if err != nil { - b.Fatalf("GetHouseByInstance failed: %v", err) - } - if house == nil { - b.Error("GetHouseByInstance returned nil house") - } - } -} - -// BenchmarkGetNextHouseID benchmarks getting the next house ID -func BenchmarkGetNextHouseID(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 5, 100) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - nextID, err := dhm.GetNextHouseID(ctx) - if err != nil { - b.Fatalf("GetNextHouseID failed: %v", err) - } - if nextID <= 100 { - b.Errorf("Expected next ID > 100, got %d", nextID) - } - } -} - -// BenchmarkDeletePlayerHouse benchmarks deleting player houses -func BenchmarkDeletePlayerHouse(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - - // We need to create houses to delete in each iteration - b.ResetTimer() - - for i := 0; i < b.N; i++ { - b.StopTimer() - - // Create a house to delete - houseData := PlayerHouseData{ - CharacterID: int32(7000 + i), - HouseID: 1, - InstanceID: int32(8000 + i), - UpkeepDue: time.Now().Add(24 * time.Hour), - EscrowCoins: 25000, - EscrowStatus: 500, - Status: HouseStatusActive, - HouseName: fmt.Sprintf("DeleteMe %d", i), - VisitPermission: 1, - AllowFriends: true, - ShowOnDirectory: true, - } - - houseID, err := dhm.AddPlayerHouse(ctx, houseData) - if err != nil { - b.Fatalf("Failed to create house for deletion: %v", err) - } - - b.StartTimer() - - // Delete the house - if err := dhm.DeletePlayerHouse(ctx, houseID); err != nil { - b.Fatalf("DeletePlayerHouse failed: %v", err) - } - } -} - -// BenchmarkConcurrentReads benchmarks concurrent read operations -func BenchmarkConcurrentReads(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 100) - - b.ResetTimer() - - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - characterID := int32(1001 + (i % 100)) - houses, err := dhm.LoadPlayerHouses(ctx, characterID) - if err != nil { - b.Errorf("LoadPlayerHouses failed: %v", err) - } - if len(houses) != 1 { - b.Errorf("Expected 1 house, got %d", len(houses)) - } - i++ - } - }) -} - -// BenchmarkConcurrentWrites benchmarks concurrent write operations -func BenchmarkConcurrentWrites(b *testing.B) { - dhm, ctx := setupBenchmarkDB(b) - insertBenchmarkData(b, dhm, ctx, 10, 0) - - b.ResetTimer() - - b.RunParallel(func(pb *testing.PB) { - i := 0 - for pb.Next() { - houseData := PlayerHouseData{ - CharacterID: int32(10000 + i), - HouseID: int32((i % 10) + 1), - InstanceID: int32(20000 + i), - UpkeepDue: time.Now().Add(24 * time.Hour), - EscrowCoins: 25000, - EscrowStatus: 500, - Status: HouseStatusActive, - HouseName: fmt.Sprintf("Concurrent House %d", i), - VisitPermission: 1, - AllowFriends: true, - ShowOnDirectory: true, - } - - _, err := dhm.AddPlayerHouse(ctx, houseData) - if err != nil { - b.Errorf("AddPlayerHouse failed: %v", err) - } - i++ - } - }) -} \ No newline at end of file diff --git a/internal/housing/database_test.go b/internal/housing/database_test.go deleted file mode 100644 index d190c4d..0000000 --- a/internal/housing/database_test.go +++ /dev/null @@ -1,816 +0,0 @@ -package housing - -import ( - "context" - "fmt" - "testing" - "time" - - "zombiezen.com/go/sqlite/sqlitex" -) - -// createTestPool creates an in-memory SQLite database pool for testing -func createTestPool(t *testing.T) *sqlitex.Pool { - dbName := fmt.Sprintf("file:test_%s.db?mode=memory&cache=shared", t.Name()) - pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{}) - if err != nil { - t.Fatalf("Failed to create test database pool: %v", err) - } - return pool -} - -// setupTestDB creates test tables and returns a DatabaseHousingManager -func setupTestDB(t *testing.T) *DatabaseHousingManager { - pool := createTestPool(t) - dhm := NewDatabaseHousingManager(pool) - - ctx := context.Background() - if err := dhm.EnsureHousingTables(ctx); err != nil { - t.Fatalf("Failed to create test tables: %v", err) - } - - return dhm -} - -// insertTestData inserts sample data for testing -func insertTestData(t *testing.T, dhm *DatabaseHousingManager) { - ctx := context.Background() - - // Insert test house zones - testZones := []*HouseZone{ - { - ID: 1, - Name: "Small Studio", - ZoneID: 100, - CostCoin: 50000, - CostStatus: 1000, - UpkeepCoin: 5000, - UpkeepStatus: 100, - Alignment: 0, // Neutral - GuildLevel: 0, - VaultSlots: 4, - MaxItems: 100, - MaxVisitors: 10, - UpkeepPeriod: 604800, // 1 week - Description: "A cozy small studio apartment", - }, - { - ID: 2, - Name: "Large House", - ZoneID: 101, - CostCoin: 500000, - CostStatus: 10000, - UpkeepCoin: 50000, - UpkeepStatus: 1000, - Alignment: 1, // Good - GuildLevel: 20, - VaultSlots: 8, - MaxItems: 500, - MaxVisitors: 50, - UpkeepPeriod: 604800, - Description: "A spacious large house", - }, - } - - for _, zone := range testZones { - if err := dhm.SaveHouseZone(ctx, zone); err != nil { - t.Fatalf("Failed to insert test house zone: %v", err) - } - } - - // Insert test player houses - testPlayerHouses := []PlayerHouseData{ - { - CharacterID: 1001, - HouseID: 1, - InstanceID: 5001, - UpkeepDue: time.Now().Add(24 * time.Hour), - EscrowCoins: 25000, - EscrowStatus: 500, - Status: HouseStatusActive, - HouseName: "Alice's Studio", - VisitPermission: 1, - PublicNote: "Welcome to my home!", - PrivateNote: "Remember to water plants", - AllowFriends: true, - AllowGuild: false, - RequireApproval: false, - ShowOnDirectory: true, - AllowDecoration: true, - TaxExempt: false, - }, - { - CharacterID: 1002, - HouseID: 2, - InstanceID: 5002, - UpkeepDue: time.Now().Add(48 * time.Hour), - EscrowCoins: 100000, - EscrowStatus: 2000, - Status: HouseStatusActive, - HouseName: "Bob's Manor", - VisitPermission: 2, - PublicNote: "Guild meetings welcome", - PrivateNote: "Check security settings", - AllowFriends: true, - AllowGuild: true, - RequireApproval: true, - ShowOnDirectory: true, - AllowDecoration: false, - TaxExempt: true, - }, - } - - for _, house := range testPlayerHouses { - _, err := dhm.AddPlayerHouse(ctx, house) - if err != nil { - t.Fatalf("Failed to insert test player house: %v", err) - } - } -} - -func TestNewDatabaseHousingManager(t *testing.T) { - pool := createTestPool(t) - dhm := NewDatabaseHousingManager(pool) - - if dhm == nil { - t.Fatal("NewDatabaseHousingManager returned nil") - } - - if dhm.pool != pool { - t.Error("Database pool not set correctly") - } -} - -func TestEnsureHousingTables(t *testing.T) { - dhm := setupTestDB(t) - ctx := context.Background() - - // Test that tables were created (this should not error on second call) - if err := dhm.EnsureHousingTables(ctx); err != nil { - t.Errorf("EnsureHousingTables failed on second call: %v", err) - } -} - -func TestHouseZoneOperations(t *testing.T) { - dhm := setupTestDB(t) - ctx := context.Background() - - // Test SaveHouseZone and LoadHouseZone - testZone := &HouseZone{ - ID: 100, - Name: "Test House", - ZoneID: 200, - CostCoin: 100000, - CostStatus: 2000, - UpkeepCoin: 10000, - UpkeepStatus: 200, - Alignment: -1, // Evil - GuildLevel: 10, - VaultSlots: 6, - MaxItems: 250, - MaxVisitors: 25, - UpkeepPeriod: 1209600, // 2 weeks - Description: "A test house for unit testing", - } - - // Save house zone - if err := dhm.SaveHouseZone(ctx, testZone); err != nil { - t.Fatalf("SaveHouseZone failed: %v", err) - } - - // Load house zone - loadedZone, err := dhm.LoadHouseZone(ctx, testZone.ID) - if err != nil { - t.Fatalf("LoadHouseZone failed: %v", err) - } - - // Verify loaded data - if loadedZone.ID != testZone.ID { - t.Errorf("Expected ID %d, got %d", testZone.ID, loadedZone.ID) - } - if loadedZone.Name != testZone.Name { - t.Errorf("Expected Name %s, got %s", testZone.Name, loadedZone.Name) - } - if loadedZone.Description != testZone.Description { - t.Errorf("Expected Description %s, got %s", testZone.Description, loadedZone.Description) - } - - // Test LoadHouseZones - zones, err := dhm.LoadHouseZones(ctx) - if err != nil { - t.Fatalf("LoadHouseZones failed: %v", err) - } - - if len(zones) != 1 { - t.Errorf("Expected 1 zone, got %d", len(zones)) - } - - // Test DeleteHouseZone - if err := dhm.DeleteHouseZone(ctx, testZone.ID); err != nil { - t.Fatalf("DeleteHouseZone failed: %v", err) - } - - // Verify deletion - _, err = dhm.LoadHouseZone(ctx, testZone.ID) - if err == nil { - t.Error("Expected error when loading deleted house zone, got nil") - } -} - -func TestPlayerHouseOperations(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Test LoadPlayerHouses - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil { - t.Fatalf("LoadPlayerHouses failed: %v", err) - } - - if len(houses) != 1 { - t.Errorf("Expected 1 house for character 1001, got %d", len(houses)) - } - - if houses[0].HouseName != "Alice's Studio" { - t.Errorf("Expected house name 'Alice's Studio', got %s", houses[0].HouseName) - } - - // Test LoadPlayerHouse by unique ID - house, err := dhm.LoadPlayerHouse(ctx, houses[0].UniqueID) - if err != nil { - t.Fatalf("LoadPlayerHouse failed: %v", err) - } - - if house.CharacterID != 1001 { - t.Errorf("Expected character ID 1001, got %d", house.CharacterID) - } - - // Test GetHouseByInstance - houseByInstance, err := dhm.GetHouseByInstance(ctx, 5001) - if err != nil { - t.Fatalf("GetHouseByInstance failed: %v", err) - } - - if houseByInstance.CharacterID != 1001 { - t.Errorf("Expected character ID 1001, got %d", houseByInstance.CharacterID) - } - - // Test GetNextHouseID - nextID, err := dhm.GetNextHouseID(ctx) - if err != nil { - t.Fatalf("GetNextHouseID failed: %v", err) - } - - if nextID <= houses[0].UniqueID { - t.Errorf("Expected next ID > %d, got %d", houses[0].UniqueID, nextID) - } -} - -func TestPlayerHouseUpdates(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Test UpdateHouseUpkeepDue - newUpkeepDue := time.Now().Add(72 * time.Hour) - if err := dhm.UpdateHouseUpkeepDue(ctx, houseID, newUpkeepDue); err != nil { - t.Fatalf("UpdateHouseUpkeepDue failed: %v", err) - } - - // Verify update - updatedHouse, err := dhm.LoadPlayerHouse(ctx, houseID) - if err != nil { - t.Fatalf("Failed to load updated house: %v", err) - } - - if updatedHouse.UpkeepDue.Unix() != newUpkeepDue.Unix() { - t.Errorf("Expected upkeep due %v, got %v", newUpkeepDue, updatedHouse.UpkeepDue) - } - - // Test UpdateHouseEscrow - if err := dhm.UpdateHouseEscrow(ctx, houseID, 50000, 1000); err != nil { - t.Fatalf("UpdateHouseEscrow failed: %v", err) - } - - // Verify escrow update - updatedHouse, err = dhm.LoadPlayerHouse(ctx, houseID) - if err != nil { - t.Fatalf("Failed to load updated house: %v", err) - } - - if updatedHouse.EscrowCoins != 50000 { - t.Errorf("Expected escrow coins 50000, got %d", updatedHouse.EscrowCoins) - } - if updatedHouse.EscrowStatus != 1000 { - t.Errorf("Expected escrow status 1000, got %d", updatedHouse.EscrowStatus) - } -} - -func TestHouseDeposits(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Test SaveDeposit - testDeposit := HouseDeposit{ - Timestamp: time.Now(), - Amount: 10000, - LastAmount: 15000, - Status: 200, - LastStatus: 300, - Name: "Alice", - CharacterID: 1001, - } - - if err := dhm.SaveDeposit(ctx, houseID, testDeposit); err != nil { - t.Fatalf("SaveDeposit failed: %v", err) - } - - // Test LoadDeposits - deposits, err := dhm.LoadDeposits(ctx, houseID) - if err != nil { - t.Fatalf("LoadDeposits failed: %v", err) - } - - if len(deposits) != 1 { - t.Errorf("Expected 1 deposit, got %d", len(deposits)) - } - - if deposits[0].Amount != testDeposit.Amount { - t.Errorf("Expected deposit amount %d, got %d", testDeposit.Amount, deposits[0].Amount) - } - if deposits[0].Name != testDeposit.Name { - t.Errorf("Expected deposit name %s, got %s", testDeposit.Name, deposits[0].Name) - } -} - -func TestHouseHistory(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Test AddHistory - testHistory := HouseHistory{ - Timestamp: time.Now(), - Amount: 5000, - Status: 100, - Reason: "Weekly upkeep", - Name: "System", - CharacterID: 0, // System transaction - PosFlag: 0, // Withdrawal - Type: 1, // Upkeep - } - - if err := dhm.AddHistory(ctx, houseID, testHistory); err != nil { - t.Fatalf("AddHistory failed: %v", err) - } - - // Test LoadHistory - history, err := dhm.LoadHistory(ctx, houseID) - if err != nil { - t.Fatalf("LoadHistory failed: %v", err) - } - - if len(history) != 1 { - t.Errorf("Expected 1 history entry, got %d", len(history)) - } - - if history[0].Reason != testHistory.Reason { - t.Errorf("Expected history reason %s, got %s", testHistory.Reason, history[0].Reason) - } - if history[0].Amount != testHistory.Amount { - t.Errorf("Expected history amount %d, got %d", testHistory.Amount, history[0].Amount) - } -} - -func TestHouseAccess(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Test SaveHouseAccess - testAccess := []HouseAccess{ - { - CharacterID: 2001, - PlayerName: "Bob", - AccessLevel: 1, - Permissions: 15, // Full permissions - GrantedBy: 1001, - GrantedDate: time.Now(), - ExpiresDate: time.Now().Add(30 * 24 * time.Hour), - Notes: "Trusted friend", - }, - { - CharacterID: 2002, - PlayerName: "Charlie", - AccessLevel: 2, - Permissions: 7, // Limited permissions - GrantedBy: 1001, - GrantedDate: time.Now(), - ExpiresDate: time.Now().Add(7 * 24 * time.Hour), - Notes: "Temporary access", - }, - } - - if err := dhm.SaveHouseAccess(ctx, houseID, testAccess); err != nil { - t.Fatalf("SaveHouseAccess failed: %v", err) - } - - // Test LoadHouseAccess - accessList, err := dhm.LoadHouseAccess(ctx, houseID) - if err != nil { - t.Fatalf("LoadHouseAccess failed: %v", err) - } - - if len(accessList) != 2 { - t.Errorf("Expected 2 access entries, got %d", len(accessList)) - } - - // Test DeleteHouseAccess - if err := dhm.DeleteHouseAccess(ctx, houseID, 2002); err != nil { - t.Fatalf("DeleteHouseAccess failed: %v", err) - } - - // Verify deletion - accessList, err = dhm.LoadHouseAccess(ctx, houseID) - if err != nil { - t.Fatalf("LoadHouseAccess after deletion failed: %v", err) - } - - if len(accessList) != 1 { - t.Errorf("Expected 1 access entry after deletion, got %d", len(accessList)) - } - - if accessList[0].CharacterID != 2001 { - t.Errorf("Expected remaining access for character 2001, got %d", accessList[0].CharacterID) - } -} - -func TestHouseAmenities(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Test SaveHouseAmenity - testAmenity := HouseAmenity{ - ID: 1, - Type: 1, // Furniture - Name: "Comfortable Chair", - Cost: 1000, - StatusCost: 20, - PurchaseDate: time.Now(), - X: 100.5, - Y: 200.0, - Z: 50.25, - Heading: 180.0, - IsActive: true, - } - - if err := dhm.SaveHouseAmenity(ctx, houseID, testAmenity); err != nil { - t.Fatalf("SaveHouseAmenity failed: %v", err) - } - - // Test LoadHouseAmenities - amenities, err := dhm.LoadHouseAmenities(ctx, houseID) - if err != nil { - t.Fatalf("LoadHouseAmenities failed: %v", err) - } - - if len(amenities) != 1 { - t.Errorf("Expected 1 amenity, got %d", len(amenities)) - } - - if amenities[0].Name != testAmenity.Name { - t.Errorf("Expected amenity name %s, got %s", testAmenity.Name, amenities[0].Name) - } - if amenities[0].X != testAmenity.X { - t.Errorf("Expected X position %f, got %f", testAmenity.X, amenities[0].X) - } - - // Test DeleteHouseAmenity - if err := dhm.DeleteHouseAmenity(ctx, houseID, testAmenity.ID); err != nil { - t.Fatalf("DeleteHouseAmenity failed: %v", err) - } - - // Verify deletion - amenities, err = dhm.LoadHouseAmenities(ctx, houseID) - if err != nil { - t.Fatalf("LoadHouseAmenities after deletion failed: %v", err) - } - - if len(amenities) != 0 { - t.Errorf("Expected 0 amenities after deletion, got %d", len(amenities)) - } -} - -func TestHouseItems(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Test SaveHouseItem - testItem := HouseItem{ - ID: 1, - ItemID: 12345, - CharacterID: 1001, - X: 150.0, - Y: 250.0, - Z: 75.5, - Heading: 90.0, - PitchX: 5.0, - PitchY: 0.0, - RollX: 0.0, - RollY: 2.5, - PlacedDate: time.Now(), - Quantity: 1, - Condition: 100, - House: "main", - } - - if err := dhm.SaveHouseItem(ctx, houseID, testItem); err != nil { - t.Fatalf("SaveHouseItem failed: %v", err) - } - - // Test LoadHouseItems - items, err := dhm.LoadHouseItems(ctx, houseID) - if err != nil { - t.Fatalf("LoadHouseItems failed: %v", err) - } - - if len(items) != 1 { - t.Errorf("Expected 1 item, got %d", len(items)) - } - - if items[0].ItemID != testItem.ItemID { - t.Errorf("Expected item ID %d, got %d", testItem.ItemID, items[0].ItemID) - } - if items[0].House != testItem.House { - t.Errorf("Expected house %s, got %s", testItem.House, items[0].House) - } - - // Test DeleteHouseItem - if err := dhm.DeleteHouseItem(ctx, houseID, testItem.ID); err != nil { - t.Fatalf("DeleteHouseItem failed: %v", err) - } - - // Verify deletion - items, err = dhm.LoadHouseItems(ctx, houseID) - if err != nil { - t.Fatalf("LoadHouseItems after deletion failed: %v", err) - } - - if len(items) != 0 { - t.Errorf("Expected 0 items after deletion, got %d", len(items)) - } -} - -func TestGetHousesForUpkeep(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Test with cutoff time in the future (should find houses) - cutoffTime := time.Now().Add(72 * time.Hour) - houses, err := dhm.GetHousesForUpkeep(ctx, cutoffTime) - if err != nil { - t.Fatalf("GetHousesForUpkeep failed: %v", err) - } - - if len(houses) != 2 { - t.Errorf("Expected 2 houses for upkeep, got %d", len(houses)) - } - - // Test with cutoff time in the past (should find no houses) - cutoffTime = time.Now().Add(-24 * time.Hour) - houses, err = dhm.GetHousesForUpkeep(ctx, cutoffTime) - if err != nil { - t.Fatalf("GetHousesForUpkeep with past cutoff failed: %v", err) - } - - if len(houses) != 0 { - t.Errorf("Expected 0 houses for past upkeep, got %d", len(houses)) - } -} - -func TestGetHouseStatistics(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Add some test data for statistics - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Add some deposits and history for stats - testDeposit := HouseDeposit{ - Timestamp: time.Now(), - Amount: 5000, - LastAmount: 10000, - Status: 100, - LastStatus: 200, - Name: "Alice", - CharacterID: 1001, - } - - if err := dhm.SaveDeposit(ctx, houseID, testDeposit); err != nil { - t.Fatalf("Failed to save test deposit: %v", err) - } - - testHistory := HouseHistory{ - Timestamp: time.Now(), - Amount: 2000, - Status: 50, - Reason: "Withdrawal", - Name: "Alice", - CharacterID: 1001, - PosFlag: 0, // Withdrawal - Type: 2, - } - - if err := dhm.AddHistory(ctx, houseID, testHistory); err != nil { - t.Fatalf("Failed to add test history: %v", err) - } - - // Test GetHouseStatistics - stats, err := dhm.GetHouseStatistics(ctx) - if err != nil { - t.Fatalf("GetHouseStatistics failed: %v", err) - } - - if stats.TotalHouses != 2 { - t.Errorf("Expected 2 total houses, got %d", stats.TotalHouses) - } - if stats.ActiveHouses != 2 { - t.Errorf("Expected 2 active houses, got %d", stats.ActiveHouses) - } - if stats.TotalDeposits != 1 { - t.Errorf("Expected 1 total deposits, got %d", stats.TotalDeposits) - } - if stats.TotalWithdrawals != 1 { - t.Errorf("Expected 1 total withdrawals, got %d", stats.TotalWithdrawals) - } -} - -func TestDeletePlayerHouse(t *testing.T) { - dhm := setupTestDB(t) - insertTestData(t, dhm) - ctx := context.Background() - - // Get a test house - houses, err := dhm.LoadPlayerHouses(ctx, 1001) - if err != nil || len(houses) == 0 { - t.Fatalf("Failed to load test house: %v", err) - } - - houseID := houses[0].UniqueID - - // Add some related data that should be cascade deleted - testDeposit := HouseDeposit{ - Timestamp: time.Now(), - Amount: 5000, - LastAmount: 10000, - Status: 100, - LastStatus: 200, - Name: "Alice", - CharacterID: 1001, - } - - if err := dhm.SaveDeposit(ctx, houseID, testDeposit); err != nil { - t.Fatalf("Failed to save test deposit: %v", err) - } - - // Test DeletePlayerHouse - if err := dhm.DeletePlayerHouse(ctx, houseID); err != nil { - t.Fatalf("DeletePlayerHouse failed: %v", err) - } - - // Verify deletion - _, err = dhm.LoadPlayerHouse(ctx, houseID) - if err == nil { - t.Error("Expected error when loading deleted player house, got nil") - } - - // Verify related data was also deleted - deposits, err := dhm.LoadDeposits(ctx, houseID) - if err != nil { - t.Fatalf("LoadDeposits after house deletion failed: %v", err) - } - - if len(deposits) != 0 { - t.Errorf("Expected 0 deposits after house deletion, got %d", len(deposits)) - } - - // Verify other houses are still there - remainingHouses, err := dhm.LoadPlayerHouses(ctx, 1002) - if err != nil { - t.Fatalf("Failed to load remaining houses: %v", err) - } - - if len(remainingHouses) != 1 { - t.Errorf("Expected 1 remaining house, got %d", len(remainingHouses)) - } -} - -func TestErrorCases(t *testing.T) { - dhm := setupTestDB(t) - ctx := context.Background() - - // Test loading non-existent house zone - _, err := dhm.LoadHouseZone(ctx, 999) - if err == nil { - t.Error("Expected error when loading non-existent house zone, got nil") - } - - // Test loading non-existent player house - _, err = dhm.LoadPlayerHouse(ctx, 999) - if err == nil { - t.Error("Expected error when loading non-existent player house, got nil") - } - - // Test loading house by non-existent instance - _, err = dhm.GetHouseByInstance(ctx, 999) - if err == nil { - t.Error("Expected error when loading house by non-existent instance, got nil") - } - - // Test operations on non-existent house - nonExistentHouseID := int64(999) - - deposits, err := dhm.LoadDeposits(ctx, nonExistentHouseID) - if err != nil { - t.Errorf("LoadDeposits should not error on non-existent house: %v", err) - } - if len(deposits) != 0 { - t.Errorf("Expected 0 deposits for non-existent house, got %d", len(deposits)) - } - - history, err := dhm.LoadHistory(ctx, nonExistentHouseID) - if err != nil { - t.Errorf("LoadHistory should not error on non-existent house: %v", err) - } - if len(history) != 0 { - t.Errorf("Expected 0 history entries for non-existent house, got %d", len(history)) - } -} - -// Helper function to compare times with tolerance for database precision -func timesEqual(t1, t2 time.Time, tolerance time.Duration) bool { - diff := t1.Sub(t2) - if diff < 0 { - diff = -diff - } - return diff <= tolerance -} \ No newline at end of file diff --git a/internal/items/item_db_test.go b/internal/items/item_db_test.go deleted file mode 100644 index 040d4e4..0000000 --- a/internal/items/item_db_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package items - -import ( - "context" - "fmt" - "math/rand" - "testing" - - "zombiezen.com/go/sqlite/sqlitex" -) - -// setupTestDB creates a test database with minimal schema -func setupTestDB(t *testing.T) *sqlitex.Pool { - // Create unique database name to avoid test contamination - dbName := fmt.Sprintf("file:test_%s_%d.db?mode=memory&cache=shared", t.Name(), rand.Int63()) - pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{ - PoolSize: 10, - }) - if err != nil { - t.Fatalf("Failed to create test database pool: %v", err) - } - - // Create complete test schema matching the real database structure - schema := ` - CREATE TABLE items ( - id INTEGER PRIMARY KEY, - soe_id INTEGER DEFAULT 0, - name TEXT NOT NULL DEFAULT '', - description TEXT DEFAULT '', - icon INTEGER DEFAULT 0, - icon2 INTEGER DEFAULT 0, - icon_heroic_op INTEGER DEFAULT 0, - icon_heroic_op2 INTEGER DEFAULT 0, - icon_id INTEGER DEFAULT 0, - icon_backdrop INTEGER DEFAULT 0, - icon_border INTEGER DEFAULT 0, - icon_tint_red INTEGER DEFAULT 0, - icon_tint_green INTEGER DEFAULT 0, - icon_tint_blue INTEGER DEFAULT 0, - tier INTEGER DEFAULT 1, - level INTEGER DEFAULT 1, - success_sellback INTEGER DEFAULT 0, - stack_size INTEGER DEFAULT 1, - generic_info_show_name INTEGER DEFAULT 1, - generic_info_item_flags INTEGER DEFAULT 0, - generic_info_item_flags2 INTEGER DEFAULT 0, - generic_info_creator_flag INTEGER DEFAULT 0, - generic_info_condition INTEGER DEFAULT 100, - generic_info_weight INTEGER DEFAULT 10, - generic_info_skill_req1 INTEGER DEFAULT 0, - generic_info_skill_req2 INTEGER DEFAULT 0, - generic_info_skill_min_level INTEGER DEFAULT 0, - generic_info_item_type INTEGER DEFAULT 0, - generic_info_appearance_id INTEGER DEFAULT 0, - generic_info_appearance_red INTEGER DEFAULT 0, - generic_info_appearance_green INTEGER DEFAULT 0, - generic_info_appearance_blue INTEGER DEFAULT 0, - generic_info_appearance_highlight_red INTEGER DEFAULT 0, - generic_info_appearance_highlight_green INTEGER DEFAULT 0, - generic_info_appearance_highlight_blue INTEGER DEFAULT 0, - generic_info_collectable INTEGER DEFAULT 0, - generic_info_offers_quest_id INTEGER DEFAULT 0, - generic_info_part_of_quest_id INTEGER DEFAULT 0, - generic_info_max_charges INTEGER DEFAULT 0, - generic_info_adventure_classes INTEGER DEFAULT 0, - generic_info_tradeskill_classes INTEGER DEFAULT 0, - generic_info_adventure_default_level INTEGER DEFAULT 1, - generic_info_tradeskill_default_level INTEGER DEFAULT 1, - generic_info_usable INTEGER DEFAULT 0, - generic_info_harvest INTEGER DEFAULT 0, - generic_info_body_drop INTEGER DEFAULT 0, - generic_info_pvp_description INTEGER DEFAULT 0, - generic_info_merc_only INTEGER DEFAULT 0, - generic_info_mount_only INTEGER DEFAULT 0, - generic_info_set_id INTEGER DEFAULT 0, - generic_info_collectable_unk INTEGER DEFAULT 0, - generic_info_transmuted_material INTEGER DEFAULT 0, - broker_price INTEGER DEFAULT 0, - sell_price INTEGER DEFAULT 0, - max_sell_value INTEGER DEFAULT 0, - created TEXT DEFAULT CURRENT_TIMESTAMP, - script_name TEXT DEFAULT '', - lua_script TEXT DEFAULT '' - ); - - CREATE TABLE item_mod_stats ( - item_id INTEGER, - stat_type INTEGER, - stat_subtype INTEGER DEFAULT 0, - value REAL, - stat_name TEXT DEFAULT '', - level INTEGER DEFAULT 0 - ); - - CREATE TABLE item_effects ( - item_id INTEGER, - effect TEXT, - percentage INTEGER DEFAULT 0, - subbulletflag INTEGER DEFAULT 0 - ); - - CREATE TABLE item_appearances ( - item_id INTEGER, - type INTEGER, - red INTEGER DEFAULT 0, - green INTEGER DEFAULT 0, - blue INTEGER DEFAULT 0, - highlight_red INTEGER DEFAULT 0, - highlight_green INTEGER DEFAULT 0, - highlight_blue INTEGER DEFAULT 0 - ); - - CREATE TABLE item_levels_override ( - item_id INTEGER, - adventure_class INTEGER, - tradeskill_class INTEGER, - level INTEGER - ); - - CREATE TABLE item_mod_strings ( - item_id INTEGER, - stat_string TEXT - ); - - CREATE TABLE item_details_weapon ( - item_id INTEGER PRIMARY KEY, - wield_type INTEGER DEFAULT 2, - damage_low1 INTEGER DEFAULT 1, - damage_high1 INTEGER DEFAULT 2, - damage_low2 INTEGER DEFAULT 0, - damage_high2 INTEGER DEFAULT 0, - damage_low3 INTEGER DEFAULT 0, - damage_high3 INTEGER DEFAULT 0, - delay_hundredths INTEGER DEFAULT 300, - rating REAL DEFAULT 1.0 - ); - - CREATE TABLE item_details_armor ( - item_id INTEGER PRIMARY KEY, - mitigation_low INTEGER DEFAULT 1, - mitigation_high INTEGER DEFAULT 2 - ); - - CREATE TABLE item_details_bag ( - item_id INTEGER PRIMARY KEY, - num_slots INTEGER DEFAULT 6, - weight_reduction INTEGER DEFAULT 0 - ); - ` - - // Execute schema on connection - ctx := context.Background() - conn, err := pool.Take(ctx) - if err != nil { - t.Fatalf("Failed to get connection: %v", err) - } - defer pool.Put(conn) - - if err := sqlitex.ExecuteScript(conn, schema, nil); err != nil { - t.Fatalf("Failed to create test schema: %v", err) - } - - return pool -} - -func TestNewItemDatabase(t *testing.T) { - pool := setupTestDB(t) - defer pool.Close() - - idb := NewItemDatabase(pool) - if idb == nil { - t.Fatal("Expected non-nil ItemDatabase") - } - - if idb.pool == nil { - t.Fatal("Expected non-nil database pool") - } -} - -func TestItemDatabaseBasicOperation(t *testing.T) { - pool := setupTestDB(t) - defer pool.Close() - - idb := NewItemDatabase(pool) - masterList := NewMasterItemList() - - // Test that LoadItems doesn't crash (even with empty database) - err := idb.LoadItems(masterList) - if err != nil { - t.Fatalf("LoadItems should not fail with empty database: %v", err) - } - - if masterList.GetItemCount() != 0 { - t.Errorf("Expected empty master list, got %d items", masterList.GetItemCount()) - } -} - -func TestItemDatabaseWithData(t *testing.T) { - pool := setupTestDB(t) - defer pool.Close() - - // Insert test data - ctx := context.Background() - conn, err := pool.Take(ctx) - if err != nil { - t.Fatalf("Failed to get connection: %v", err) - } - defer pool.Put(conn) - - // Insert a test item - err = sqlitex.Execute(conn, `INSERT INTO items (id, name, generic_info_item_type) VALUES (?, ?, ?)`, &sqlitex.ExecOptions{ - Args: []any{1, "Test Sword", ItemTypeWeapon}, - }) - if err != nil { - t.Fatalf("Failed to insert test item: %v", err) - } - - // Insert weapon details - err = sqlitex.Execute(conn, `INSERT INTO item_details_weapon (item_id, damage_low1, damage_high1) VALUES (?, ?, ?)`, &sqlitex.ExecOptions{ - Args: []any{1, 10, 15}, - }) - if err != nil { - t.Fatalf("Failed to insert weapon details: %v", err) - } - - // Load items - idb := NewItemDatabase(pool) - masterList := NewMasterItemList() - - err = idb.LoadItems(masterList) - if err != nil { - t.Fatalf("Failed to load items: %v", err) - } - - if masterList.GetItemCount() != 1 { - t.Errorf("Expected 1 item, got %d items", masterList.GetItemCount()) - } - - // Verify the item was loaded correctly - item := masterList.GetItem(1) - if item == nil { - t.Fatal("Expected to find item with ID 1") - } - - if item.Name != "Test Sword" { - t.Errorf("Expected item name 'Test Sword', got '%s'", item.Name) - } - - if item.WeaponInfo == nil { - t.Error("Expected weapon info to be loaded") - } else { - if item.WeaponInfo.DamageLow1 != 10 { - t.Errorf("Expected damage low 10, got %d", item.WeaponInfo.DamageLow1) - } - if item.WeaponInfo.DamageHigh1 != 15 { - t.Errorf("Expected damage high 15, got %d", item.WeaponInfo.DamageHigh1) - } - } -} \ No newline at end of file diff --git a/internal/items/items_test.go b/internal/items/items_test.go deleted file mode 100644 index 73cbba0..0000000 --- a/internal/items/items_test.go +++ /dev/null @@ -1,849 +0,0 @@ -package items - -import ( - "fmt" - "testing" -) - -func TestNewItem(t *testing.T) { - item := NewItem() - if item == nil { - t.Fatal("NewItem returned nil") - } - - if item.Details.UniqueID <= 0 { - t.Error("New item should have a valid unique ID") - } - - if item.Details.Count != 1 { - t.Errorf("Expected count 1, got %d", item.Details.Count) - } - - if item.GenericInfo.Condition != DefaultItemCondition { - t.Errorf("Expected condition %d, got %d", DefaultItemCondition, item.GenericInfo.Condition) - } -} - -func TestNewItemFromTemplate(t *testing.T) { - // Create template - template := NewItem() - template.Name = "Test Sword" - template.Description = "A test weapon" - template.Details.ItemID = 12345 - template.Details.Icon = 100 - template.GenericInfo.ItemType = ItemTypeWeapon - template.WeaponInfo = &WeaponInfo{ - WieldType: ItemWieldTypeSingle, - DamageLow1: 10, - DamageHigh1: 20, - Delay: 30, - Rating: 1.5, - } - - // Create from template - item := NewItemFromTemplate(template) - if item == nil { - t.Fatal("NewItemFromTemplate returned nil") - } - - if item.Name != template.Name { - t.Errorf("Expected name %s, got %s", template.Name, item.Name) - } - - if item.Details.ItemID != template.Details.ItemID { - t.Errorf("Expected item ID %d, got %d", template.Details.ItemID, item.Details.ItemID) - } - - if item.Details.UniqueID == template.Details.UniqueID { - t.Error("New item should have different unique ID from template") - } - - if item.WeaponInfo == nil { - t.Fatal("Weapon info should be copied") - } - - if item.WeaponInfo.DamageLow1 != template.WeaponInfo.DamageLow1 { - t.Errorf("Expected damage %d, got %d", template.WeaponInfo.DamageLow1, item.WeaponInfo.DamageLow1) - } -} - -func TestItemCopy(t *testing.T) { - original := NewItem() - original.Name = "Original Item" - original.Details.ItemID = 999 - original.AddStat(&ItemStat{ - StatName: "Strength", - StatType: ItemStatStr, - Value: 10, - }) - - copy := original.Copy() - if copy == nil { - t.Fatal("Copy returned nil") - } - - if copy.Name != original.Name { - t.Errorf("Expected name %s, got %s", original.Name, copy.Name) - } - - if copy.Details.UniqueID == original.Details.UniqueID { - t.Error("Copy should have different unique ID") - } - - if len(copy.ItemStats) != len(original.ItemStats) { - t.Errorf("Expected %d stats, got %d", len(original.ItemStats), len(copy.ItemStats)) - } - - // Test nil copy - var nilItem *Item - nilCopy := nilItem.Copy() - if nilCopy != nil { - t.Error("Copy of nil should return nil") - } -} - -func TestItemValidation(t *testing.T) { - // Valid item - item := NewItem() - item.Name = "Valid Item" - item.Details.ItemID = 100 - - result := item.Validate() - if !result.Valid { - t.Errorf("Valid item should pass validation: %v", result.Errors) - } - - // Invalid item - no name - invalidItem := NewItem() - invalidItem.Details.ItemID = 100 - - result = invalidItem.Validate() - if result.Valid { - t.Error("Item without name should fail validation") - } - - // Invalid item - negative count - invalidItem2 := NewItem() - invalidItem2.Name = "Invalid Item" - invalidItem2.Details.ItemID = 100 - invalidItem2.Details.Count = -1 - - result = invalidItem2.Validate() - if result.Valid { - t.Error("Item with negative count should fail validation") - } -} - -func TestItemStats(t *testing.T) { - item := NewItem() - - // Add a stat - stat := &ItemStat{ - StatName: "Strength", - StatType: ItemStatStr, - Value: 15, - Level: 1, - } - item.AddStat(stat) - - if len(item.ItemStats) != 1 { - t.Errorf("Expected 1 stat, got %d", len(item.ItemStats)) - } - - // Check if item has stat - if !item.HasStat(0, "Strength") { - t.Error("Item should have Strength stat") - } - - if !item.HasStat(uint32(ItemStatStr), "") { - t.Error("Item should have STR stat by ID") - } - - if item.HasStat(0, "Nonexistent") { - t.Error("Item should not have nonexistent stat") - } - - // Add stat by values - item.AddStatByValues(ItemStatAgi, 0, 10, 1, "Agility") - - if len(item.ItemStats) != 2 { - t.Errorf("Expected 2 stats, got %d", len(item.ItemStats)) - } -} - -func TestItemFlags(t *testing.T) { - item := NewItem() - - // Set flags - item.GenericInfo.ItemFlags = Attuned | NoTrade - item.GenericInfo.ItemFlags2 = Heirloom | Ornate - - // Test flag checking - if !item.CheckFlag(Attuned) { - t.Error("Item should be attuned") - } - - if !item.CheckFlag(NoTrade) { - t.Error("Item should be no-trade") - } - - if item.CheckFlag(Lore) { - t.Error("Item should not be lore") - } - - if !item.CheckFlag2(Heirloom) { - t.Error("Item should be heirloom") - } - - if !item.CheckFlag2(Ornate) { - t.Error("Item should be ornate") - } - - if item.CheckFlag2(Refined) { - t.Error("Item should not be refined") - } -} - -func TestItemLocking(t *testing.T) { - item := NewItem() - - // Item should not be locked initially - if item.IsItemLocked() { - t.Error("New item should not be locked") - } - - // Lock for crafting - if !item.TryLockItem(LockReasonCrafting) { - t.Error("Should be able to lock item for crafting") - } - - if !item.IsItemLocked() { - t.Error("Item should be locked") - } - - if !item.IsItemLockedFor(LockReasonCrafting) { - t.Error("Item should be locked for crafting") - } - - if item.IsItemLockedFor(LockReasonHouse) { - t.Error("Item should not be locked for house") - } - - // Try to lock for another reason while already locked - if item.TryLockItem(LockReasonHouse) { - t.Error("Should not be able to lock for different reason") - } - - // Unlock - if !item.TryUnlockItem(LockReasonCrafting) { - t.Error("Should be able to unlock item") - } - - if item.IsItemLocked() { - t.Error("Item should not be locked after unlock") - } -} - -func TestItemTypes(t *testing.T) { - item := NewItem() - - // Test weapon - item.GenericInfo.ItemType = ItemTypeWeapon - if !item.IsWeapon() { - t.Error("Item should be a weapon") - } - if item.IsArmor() { - t.Error("Item should not be armor") - } - - // Test armor - item.GenericInfo.ItemType = ItemTypeArmor - if !item.IsArmor() { - t.Error("Item should be armor") - } - if item.IsWeapon() { - t.Error("Item should not be a weapon") - } - - // Test bag - item.GenericInfo.ItemType = ItemTypeBag - if !item.IsBag() { - t.Error("Item should be a bag") - } - - // Test food - item.GenericInfo.ItemType = ItemTypeFood - item.FoodInfo = &FoodInfo{Type: 1} // Food - if !item.IsFood() { - t.Error("Item should be food") - } - if !item.IsFoodFood() { - t.Error("Item should be food (not drink)") - } - if item.IsFoodDrink() { - t.Error("Item should not be drink") - } - - // Test drink - item.FoodInfo.Type = 0 // Drink - if !item.IsFoodDrink() { - t.Error("Item should be drink") - } - if item.IsFoodFood() { - t.Error("Item should not be food") - } -} - -func TestMasterItemList(t *testing.T) { - masterList := NewMasterItemList() - if masterList == nil { - t.Fatal("NewMasterItemList returned nil") - } - - // Initial state - if masterList.GetItemCount() != 0 { - t.Error("New master list should be empty") - } - - // Add item - item := NewItem() - item.Name = "Test Item" - item.Details.ItemID = 12345 - - masterList.AddItem(item) - - if masterList.GetItemCount() != 1 { - t.Errorf("Expected 1 item, got %d", masterList.GetItemCount()) - } - - // Get item - retrieved := masterList.GetItem(12345) - if retrieved == nil { - t.Fatal("GetItem returned nil") - } - - if retrieved.Name != item.Name { - t.Errorf("Expected name %s, got %s", item.Name, retrieved.Name) - } - - // Get by name - byName := masterList.GetItemByName("Test Item") - if byName == nil { - t.Fatal("GetItemByName returned nil") - } - - if byName.Details.ItemID != item.Details.ItemID { - t.Errorf("Expected item ID %d, got %d", item.Details.ItemID, byName.Details.ItemID) - } - - // Get non-existent item - nonExistent := masterList.GetItem(99999) - if nonExistent != nil { - t.Error("GetItem should return nil for non-existent item") - } - - // Test stats - stats := masterList.GetStats() - if stats.TotalItems != 1 { - t.Errorf("Expected 1 total item, got %d", stats.TotalItems) - } -} - -func TestMasterItemListStatMapping(t *testing.T) { - masterList := NewMasterItemList() - // Test getting stat ID by name - strID := masterList.GetItemStatIDByName("strength") - // ItemStatStr is 0, so we need to verify it was actually found - if strID != ItemStatStr { - t.Errorf("Expected strength stat ID %d, got %d", ItemStatStr, strID) - } - - // Also test a stat that doesn't exist to ensure it returns a different value - nonExistentID := masterList.GetItemStatIDByName("nonexistent_stat") - if nonExistentID == ItemStatStr && ItemStatStr == 0 { - // This means we can't distinguish between "strength" and non-existent stats - // Let's verify strength is actually in the map - strName := masterList.GetItemStatNameByID(ItemStatStr) - if strName != "strength" { - t.Error("Strength stat mapping not found") - } - } - - // Test getting stat name by ID - strName := masterList.GetItemStatNameByID(ItemStatStr) - if strName == "" { - t.Error("Should find stat name for STR") - } - - // Add custom stat mapping - masterList.AddMappedItemStat(9999, "custom stat") - - customID := masterList.GetItemStatIDByName("custom stat") - if customID != 9999 { - t.Errorf("Expected custom stat ID 9999, got %d", customID) - } - - customName := masterList.GetItemStatNameByID(9999) - if customName != "custom stat" { - t.Errorf("Expected 'custom stat', got '%s'", customName) - } -} - -func TestPlayerItemList(t *testing.T) { - t.Log("Starting TestPlayerItemList...") - playerList := NewPlayerItemList() - t.Log("NewPlayerItemList() completed") - if playerList == nil { - t.Fatal("NewPlayerItemList returned nil") - } - t.Log("PlayerItemList created successfully") - - // Initial state - t.Log("Checking initial state...") - if playerList.GetNumberOfItems() != 0 { - t.Error("New player list should be empty") - } - t.Log("Initial state check completed") - - // Create test item - t.Log("Creating test item...") - item := NewItem() - item.Name = "Player Item" - item.Details.ItemID = 1001 - item.Details.BagID = 0 - item.Details.SlotID = 0 - t.Log("Test item created") - - // Add item - t.Log("Adding item to player list...") - if !playerList.AddItem(item) { - t.Error("Should be able to add item") - } - t.Log("Item added successfully") - - if playerList.GetNumberOfItems() != 1 { - t.Errorf("Expected 1 item, got %d", playerList.GetNumberOfItems()) - } - - // Get item - retrieved := playerList.GetItem(0, 0, BaseEquipment) - if retrieved == nil { - t.Fatal("GetItem returned nil") - } - - if retrieved.Name != item.Name { - t.Errorf("Expected name %s, got %s", item.Name, retrieved.Name) - } - - // Test HasItem - if !playerList.HasItem(1001, false) { - t.Error("Player should have item 1001") - } - - if playerList.HasItem(9999, false) { - t.Error("Player should not have item 9999") - } - - // Remove item - playerList.RemoveItem(item, true, true) - - if playerList.GetNumberOfItems() != 0 { - t.Errorf("Expected 0 items after removal, got %d", playerList.GetNumberOfItems()) - } -} - -func TestPlayerItemListOverflow(t *testing.T) { - playerList := NewPlayerItemList() - - // Add item to overflow - item := NewItem() - item.Name = "Overflow Item" - item.Details.ItemID = 2001 - - if !playerList.AddOverflowItem(item) { - t.Error("Should be able to add overflow item") - } - - // Check overflow - overflowItem := playerList.GetOverflowItem() - if overflowItem == nil { - t.Fatal("GetOverflowItem returned nil") - } - - if overflowItem.Name != item.Name { - t.Errorf("Expected name %s, got %s", item.Name, overflowItem.Name) - } - - // Get all overflow items - overflowItems := playerList.GetOverflowItemList() - if len(overflowItems) != 1 { - t.Errorf("Expected 1 overflow item, got %d", len(overflowItems)) - } - - // Remove overflow item - playerList.RemoveOverflowItem(item) - - overflowItems = playerList.GetOverflowItemList() - if len(overflowItems) != 0 { - t.Errorf("Expected 0 overflow items, got %d", len(overflowItems)) - } -} - -func TestEquipmentItemList(t *testing.T) { - equipment := NewEquipmentItemList() - if equipment == nil { - t.Fatal("NewEquipmentItemList returned nil") - } - - // Initial state - if equipment.GetNumberOfItems() != 0 { - t.Error("New equipment list should be empty") - } - - // Create test weapon - weapon := NewItem() - weapon.Name = "Test Sword" - weapon.Details.ItemID = 3001 - weapon.GenericInfo.ItemType = ItemTypeWeapon - weapon.AddSlot(EQ2PrimarySlot) - - // Equip weapon - if !equipment.AddItem(EQ2PrimarySlot, weapon) { - t.Error("Should be able to equip weapon") - } - - if equipment.GetNumberOfItems() != 1 { - t.Errorf("Expected 1 equipped item, got %d", equipment.GetNumberOfItems()) - } - - // Get equipped weapon - equippedWeapon := equipment.GetItem(EQ2PrimarySlot) - if equippedWeapon == nil { - t.Fatal("GetItem returned nil") - } - - if equippedWeapon.Name != weapon.Name { - t.Errorf("Expected name %s, got %s", weapon.Name, equippedWeapon.Name) - } - - // Test equipment queries - if !equipment.HasItem(3001) { - t.Error("Equipment should have item 3001") - } - - if !equipment.HasWeaponEquipped() { - t.Error("Equipment should have weapon equipped") - } - - weapons := equipment.GetWeapons() - if len(weapons) != 1 { - t.Errorf("Expected 1 weapon, got %d", len(weapons)) - } - - // Remove weapon - equipment.RemoveItem(EQ2PrimarySlot, false) - - if equipment.GetNumberOfItems() != 0 { - t.Errorf("Expected 0 items after removal, got %d", equipment.GetNumberOfItems()) - } -} - -func TestEquipmentValidation(t *testing.T) { - equipment := NewEquipmentItemList() - - // Create invalid item (no name) - invalidItem := NewItem() - invalidItem.Details.ItemID = 4001 - invalidItem.AddSlot(EQ2HeadSlot) - - equipment.SetItem(EQ2HeadSlot, invalidItem, false) - - result := equipment.ValidateEquipment() - if result.Valid { - t.Error("Equipment with invalid item should fail validation") - } - - // Create item that can't be equipped in the slot - wrongSlotItem := NewItem() - wrongSlotItem.Name = "Wrong Slot Item" - wrongSlotItem.Details.ItemID = 4002 - wrongSlotItem.AddSlot(EQ2ChestSlot) // Can only go in chest - - equipment.SetItem(EQ2HeadSlot, wrongSlotItem, false) - - result = equipment.ValidateEquipment() - if result.Valid { - t.Error("Equipment with wrong slot item should fail validation") - } -} - -func TestItemSystemAdapter(t *testing.T) { - // Create dependencies - masterList := NewMasterItemList() - spellManager := NewMockSpellManager() - - // Add a test spell - spellManager.AddMockSpell(1001, "Test Spell", 100, 1, "A test spell") - - adapter := NewItemSystemAdapter( - masterList, - spellManager, - nil, // playerManager - nil, // packetManager - nil, // ruleManager - nil, // databaseService - nil, // questManager - nil, // brokerManager - nil, // craftingManager - nil, // housingManager - nil, // lootManager - ) - - if adapter == nil { - t.Fatal("NewItemSystemAdapter returned nil") - } - - // Test stats - stats := adapter.GetSystemStats() - if stats == nil { - t.Error("GetSystemStats should not return nil") - } - - totalTemplates, ok := stats["total_item_templates"].(int32) - if !ok || totalTemplates != 0 { - t.Errorf("Expected 0 total templates, got %v", stats["total_item_templates"]) - } -} - -func TestItemBrokerChecks(t *testing.T) { - masterList := NewMasterItemList() - - // Create weapon - weapon := NewItem() - weapon.Name = "Test Weapon" - weapon.GenericInfo.ItemType = ItemTypeWeapon - weapon.AddSlot(EQ2PrimarySlot) - - // Test broker type checks - if !masterList.ShouldAddItemBrokerSlot(weapon, ItemBrokerSlotPrimary) { - t.Error("Weapon should match primary slot broker type") - } - - if masterList.ShouldAddItemBrokerSlot(weapon, ItemBrokerSlotHead) { - t.Error("Weapon should not match head slot broker type") - } - - // Create armor with stats - armor := NewItem() - armor.Name = "Test Armor" - armor.GenericInfo.ItemType = ItemTypeArmor - armor.AddStat(&ItemStat{ - StatName: "Strength", - StatType: ItemStatStr, - Value: 10, - }) - - if !masterList.ShouldAddItemBrokerStat(armor, ItemBrokerStatTypeStr) { - t.Error("Armor should match STR stat broker type") - } - - if masterList.ShouldAddItemBrokerStat(armor, ItemBrokerStatTypeInt) { - t.Error("Armor should not match INT stat broker type") - } -} - -func TestItemSearchCriteria(t *testing.T) { - masterList := NewMasterItemList() - - // Add test items - sword := NewItem() - sword.Name = "Steel Sword" - sword.Details.ItemID = 5001 - sword.Details.Tier = 1 - sword.Details.RecommendedLevel = 10 - sword.GenericInfo.ItemType = ItemTypeWeapon - sword.BrokerPrice = 1000 - - armor := NewItem() - armor.Name = "Iron Armor" - armor.Details.ItemID = 5002 - armor.Details.Tier = 2 - armor.Details.RecommendedLevel = 15 - armor.GenericInfo.ItemType = ItemTypeArmor - armor.BrokerPrice = 2000 - - masterList.AddItem(sword) - masterList.AddItem(armor) - - // Search by name - criteria := &ItemSearchCriteria{ - Name: "sword", - } - - results := masterList.GetItems(criteria) - if len(results) != 1 { - t.Errorf("Expected 1 result for sword search, got %d", len(results)) - } - - if results[0].Name != sword.Name { - t.Errorf("Expected %s, got %s", sword.Name, results[0].Name) - } - - // Search by price range - criteria = &ItemSearchCriteria{ - MinPrice: 1500, - MaxPrice: 2500, - } - - results = masterList.GetItems(criteria) - if len(results) != 1 { - t.Errorf("Expected 1 result for price search, got %d", len(results)) - } - - if results[0].Name != armor.Name { - t.Errorf("Expected %s, got %s", armor.Name, results[0].Name) - } - - // Search by tier - criteria = &ItemSearchCriteria{ - MinTier: 2, - MaxTier: 2, - } - - results = masterList.GetItems(criteria) - if len(results) != 1 { - t.Errorf("Expected 1 result for tier search, got %d", len(results)) - } - - // Search with no matches - criteria = &ItemSearchCriteria{ - Name: "nonexistent", - } - - results = masterList.GetItems(criteria) - if len(results) != 0 { - t.Errorf("Expected 0 results for nonexistent search, got %d", len(results)) - } -} - -func TestNextUniqueID(t *testing.T) { - id1 := NextUniqueID() - id2 := NextUniqueID() - - if id1 == id2 { - t.Error("NextUniqueID should return different IDs") - } - - if id2 != id1+1 { - t.Errorf("Expected ID2 to be ID1+1, got %d and %d", id1, id2) - } -} - -func TestItemError(t *testing.T) { - err := NewItemError("test error") - if err == nil { - t.Fatal("NewItemError returned nil") - } - - if err.Error() != "test error" { - t.Errorf("Expected 'test error', got '%s'", err.Error()) - } - - if !IsItemError(err) { - t.Error("Should identify as item error") - } - - // Test with non-item error - if IsItemError(fmt.Errorf("not an item error")) { - t.Error("Should not identify as item error") - } -} - -func TestConstants(t *testing.T) { - // Test slot constants - if EQ2PrimarySlot != 0 { - t.Errorf("Expected EQ2PrimarySlot to be 0, got %d", EQ2PrimarySlot) - } - - if NumSlots != 25 { - t.Errorf("Expected NumSlots to be 25, got %d", NumSlots) - } - - // Test item type constants - if ItemTypeWeapon != 1 { - t.Errorf("Expected ItemTypeWeapon to be 1, got %d", ItemTypeWeapon) - } - - // Test flag constants - if Attuned != 1 { - t.Errorf("Expected Attuned to be 1, got %d", Attuned) - } - - // Test stat constants - if ItemStatStr != 0 { - t.Errorf("Expected ItemStatStr to be 0, got %d", ItemStatStr) - } -} - -func BenchmarkItemCreation(b *testing.B) { - for i := 0; i < b.N; i++ { - item := NewItem() - item.Name = "Benchmark Item" - item.Details.ItemID = int32(i) - } -} - -func BenchmarkMasterItemListAccess(b *testing.B) { - masterList := NewMasterItemList() - - // Add test items - for i := 0; i < 1000; i++ { - item := NewItem() - item.Name = fmt.Sprintf("Item %d", i) - item.Details.ItemID = int32(i + 1000) - masterList.AddItem(item) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - masterList.GetItem(int32((i % 1000) + 1000)) - } -} - -func BenchmarkPlayerItemListAdd(b *testing.B) { - playerList := NewPlayerItemList() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - item := NewItem() - item.Name = fmt.Sprintf("Item %d", i) - item.Details.ItemID = int32(i) - item.Details.BagID = int32(i % 6) - item.Details.SlotID = int16(i % 20) - playerList.AddItem(item) - } -} - -func BenchmarkEquipmentBonusCalculation(b *testing.B) { - equipment := NewEquipmentItemList() - - // Add some equipped items with stats - for slot := 0; slot < 10; slot++ { - item := NewItem() - item.Name = fmt.Sprintf("Equipment %d", slot) - item.Details.ItemID = int32(slot + 6000) - item.AddSlot(int8(slot)) - - // Add some stats - item.AddStat(&ItemStat{StatType: ItemStatStr, Value: 10}) - item.AddStat(&ItemStat{StatType: ItemStatAgi, Value: 5}) - item.AddStat(&ItemStat{StatType: ItemStatHealth, Value: 100}) - - equipment.SetItem(int8(slot), item, false) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - equipment.CalculateEquipmentBonuses() - } -} diff --git a/internal/items/loot/README.md b/internal/items/loot/README.md deleted file mode 100644 index b1f51ce..0000000 --- a/internal/items/loot/README.md +++ /dev/null @@ -1,357 +0,0 @@ -# EverQuest II Loot System - -This package implements a comprehensive loot generation and management system for the EverQuest II server emulator, converted from the original C++ implementation. - -## Overview - -The loot system handles: -- **Loot Generation**: Probability-based item and coin generation from configurable loot tables -- **Treasure Chests**: Physical chest spawns with tier-appropriate appearances -- **Global Loot**: Level, race, and zone-based loot assignments -- **Loot Distribution**: Group loot methods and player rights management -- **Database Integration**: Persistent loot table and assignment storage -- **Client Communication**: Version-specific packet building for loot windows - -## Architecture - -### Core Components - -#### LootDatabase (`database.go`) -- Manages all database operations for loot tables, drops, and assignments -- Caches loot data in memory for performance -- Supports real-time updates and reloading - -#### LootManager (`manager.go`) -- Central coordinator for loot generation and chest management -- Implements probability-based loot algorithms -- Handles treasure chest lifecycle and cleanup - -#### ChestService (`chest.go`) -- Manages treasure chest interactions (view, loot, disarm, lockpick) -- Validates player permissions and positioning -- Integrates with player and zone services - -#### LootPacketBuilder (`packets.go`) -- Builds client-specific packets for loot communication -- Supports multiple client versions (v1 through v60114+) -- Handles loot window updates and interaction responses - -#### LootSystem (`integration.go`) -- High-level integration layer combining all components -- Provides simplified API for common operations -- Manages system lifecycle and configuration - -### Data Structures - -#### LootTable -```go -type LootTable struct { - ID int32 // Unique table identifier - Name string // Descriptive name - MinCoin int32 // Minimum coin drop - MaxCoin int32 // Maximum coin drop - MaxLootItems int16 // Maximum items per loot - LootDropProbability float32 // Chance for loot to drop (0-100%) - CoinProbability float32 // Chance for coins to drop (0-100%) - Drops []*LootDrop // Individual item drops -} -``` - -#### LootDrop -```go -type LootDrop struct { - LootTableID int32 // Parent table ID - ItemID int32 // Item to drop - ItemCharges int16 // Item stack size/charges - EquipItem bool // Auto-equip item - Probability float32 // Drop chance (0-100%) - NoDropQuestCompletedID int32 // Required quest completion -} -``` - -#### TreasureChest -```go -type TreasureChest struct { - ID int32 // Unique chest ID - SpawnID int32 // Source spawn ID - ZoneID int32 // Zone location - X, Y, Z float32 // Position coordinates - Heading float32 // Orientation - AppearanceID int32 // Visual appearance - LootResult *LootResult // Contained items and coins - Created time.Time // Creation timestamp - LootRights []uint32 // Players with access rights - IsDisarmable bool // Can be disarmed - IsLocked bool // Requires lockpicking -} -``` - -## Database Schema - -### Core Tables - -#### loottable -```sql -CREATE TABLE loottable ( - id INTEGER PRIMARY KEY, - name TEXT, - mincoin INTEGER DEFAULT 0, - maxcoin INTEGER DEFAULT 0, - maxlootitems INTEGER DEFAULT 6, - lootdrop_probability REAL DEFAULT 100.0, - coin_probability REAL DEFAULT 50.0 -); -``` - -#### lootdrop -```sql -CREATE TABLE lootdrop ( - loot_table_id INTEGER, - item_id INTEGER, - item_charges INTEGER DEFAULT 1, - equip_item INTEGER DEFAULT 0, - probability REAL DEFAULT 100.0, - no_drop_quest_completed_id INTEGER DEFAULT 0 -); -``` - -#### spawn_loot -```sql -CREATE TABLE spawn_loot ( - spawn_id INTEGER, - loottable_id INTEGER -); -``` - -#### loot_global -```sql -CREATE TABLE loot_global ( - type TEXT, -- 'level', 'race', or 'zone' - loot_table INTEGER, -- Target loot table ID - value1 INTEGER, -- Min level, race ID, or zone ID - value2 INTEGER, -- Max level (for level type) - value3 INTEGER, -- Loot tier - value4 INTEGER -- Reserved -); -``` - -## Usage Examples - -### Basic Setup - -```go -// Create loot system -config := &LootSystemConfig{ - DatabaseConnection: db, - ItemMasterList: itemMasterList, - PlayerService: playerService, - ZoneService: zoneService, - ClientService: clientService, - ItemPacketBuilder: itemPacketBuilder, - StartCleanupTimer: true, -} - -lootSystem, err := NewLootSystem(config) -if err != nil { - log.Fatal(err) -} -``` - -### Generate Loot and Create Chest - -```go -// Create loot context -context := &LootContext{ - PlayerLevel: 25, - PlayerRace: 1, - ZoneID: 100, - KillerID: playerID, - GroupMembers: []uint32{playerID}, - CompletedQuests: playerQuests, - LootMethod: GroupLootMethodFreeForAll, -} - -// Generate loot and create chest -chest, err := lootSystem.GenerateAndCreateChest( - spawnID, zoneID, x, y, z, heading, context) -if err != nil { - log.Printf("Failed to create loot chest: %v", err) -} -``` - -### Handle Player Loot Interaction - -```go -// Player opens chest -err := lootSystem.ShowChestToPlayer(chestID, playerID) - -// Player loots specific item -err = lootSystem.HandlePlayerLootInteraction( - chestID, playerID, ChestInteractionLoot, itemUniqueID) - -// Player loots everything -err = lootSystem.HandlePlayerLootInteraction( - chestID, playerID, ChestInteractionLootAll, 0) -``` - -### Create Loot Tables - -```go -// Create a simple loot table -items := []QuickLootItem{ - {ItemID: 1001, Charges: 1, Probability: 100.0, AutoEquip: false}, - {ItemID: 1002, Charges: 5, Probability: 50.0, AutoEquip: false}, - {ItemID: 1003, Charges: 1, Probability: 25.0, AutoEquip: true}, -} - -err := lootSystem.CreateQuickLootTable( - tableID, "Orc Warrior Loot", items, 10, 50, 3) - -// Assign to spawns -spawnIDs := []int32{2001, 2002, 2003} -err = lootSystem.AssignLootToSpawns(tableID, spawnIDs) -``` - -## Chest Appearances - -Chest appearance is automatically selected based on the highest tier item: - -| Tier Range | Appearance | Chest Type | -|------------|------------|------------| -| 1-2 (Common-Uncommon) | 4034 | Small Chest | -| 3-4 (Treasured-Rare) | 5864 | Treasure Chest | -| 5-6 (Legendary-Fabled) | 5865 | Ornate Chest | -| 7+ (Mythical+) | 4015 | Exquisite Chest | - -## Loot Generation Algorithm - -1. **Table Selection**: Get loot tables assigned to spawn + applicable global tables -2. **Drop Probability**: Roll against `lootdrop_probability` to determine if loot drops -3. **Coin Generation**: If `coin_probability` succeeds, generate random coins between min/max -4. **Item Processing**: For each loot drop: - - Check quest requirements - - Roll against item probability - - Generate item instance with specified charges - - Stop when `maxlootitems` reached -5. **Chest Creation**: If items qualify (tier >= Common), create treasure chest - -## Global Loot System - -Global loot provides automatic loot assignment based on: - -### Level-Based Loot -```go -err := lootSystem.CreateGlobalLevelLoot(10, 20, tableID, LootTierCommon) -``` - -### Race-Based Loot -```sql -INSERT INTO loot_global (type, loot_table, value1, value2) -VALUES ('race', 100, 1, 0); -- Human racial loot -``` - -### Zone-Based Loot -```sql -INSERT INTO loot_global (type, loot_table, value1, value2) -VALUES ('zone', 200, 150, 0); -- Zone 150 specific loot -``` - -## Group Loot Methods - -The system supports various loot distribution methods: - -- **Free For All**: Anyone can loot anything -- **Round Robin**: Items distributed in turn order -- **Master Looter**: Designated player distributes loot -- **Need/Greed**: Players roll need or greed for items -- **Lotto**: Random distribution for high-tier items - -## Statistics and Monitoring - -```go -// Get comprehensive statistics -stats, err := lootSystem.GetSystemStatistics() - -// Check loot generation stats -genStats := lootSystem.Manager.GetStatistics() -fmt.Printf("Total loots: %d, Average items per loot: %.2f", - genStats.TotalLoots, genStats.AverageItemsPerLoot) -``` - -## Validation and Debugging - -```go -// Validate all items in loot tables exist -errors := lootSystem.ValidateItemsInLootTables() -for _, err := range errors { - fmt.Printf("Validation error: %s\n", err.Description) -} - -// Preview potential loot without generating -preview, err := lootSystem.GetLootPreview(spawnID, context) -fmt.Printf("Possible items: %d, Coin range: %d-%d", - len(preview.PossibleItems), preview.MinCoins, preview.MaxCoins) -``` - -## Performance Considerations - -- **Memory Caching**: All loot tables are cached in memory for fast access -- **Prepared Statements**: Database queries use prepared statements for efficiency -- **Concurrent Safety**: All operations are thread-safe with proper mutex usage -- **Cleanup Timers**: Automatic cleanup of expired chests prevents memory leaks -- **Batch Operations**: Support for bulk loot table and spawn assignments - -## Client Version Compatibility - -The packet building system supports multiple EverQuest II client versions: - -- **Version 1**: Basic loot display (oldest clients) -- **Version 373**: Added item type and icon support -- **Version 546**: Enhanced item appearance data -- **Version 1193**: Heirloom and no-trade flags -- **Version 60114**: Full modern feature set with adornments - -## Migration from C++ - -This Go implementation maintains full compatibility with the original C++ EQ2EMu loot system: - -- **Database Schema**: Identical table structure and data -- **Loot Algorithms**: Same probability calculations and item selection -- **Chest Logic**: Equivalent chest appearance and interaction rules -- **Global Loot**: Compatible global loot table processing -- **Packet Format**: Maintains client protocol compatibility - -## Testing - -Comprehensive test suite covers: -- Loot generation algorithms -- Database operations -- Chest interactions -- Packet building -- Statistics tracking -- Performance benchmarks - -Run tests with: -```bash -go test ./internal/items/loot/... -``` - -## Configuration - -Key configuration constants in `constants.go`: - -- `DefaultMaxLootItems`: Maximum items per loot (default: 6) -- `ChestDespawnTime`: Empty chest despawn time (5 minutes) -- `ChestCleanupTime`: Force cleanup time (10 minutes) -- `LootTierCommon`: Minimum tier for chest creation - -## Error Handling - -The system provides detailed error reporting for: -- Missing loot tables or items -- Invalid player permissions -- Database connection issues -- Packet building failures -- Chest interaction violations - -All errors are logged with appropriate prefixes (`[LOOT]`, `[CHEST]`, `[LOOT-DB]`) for easy debugging. \ No newline at end of file diff --git a/internal/items/loot/loot_test.go b/internal/items/loot/loot_test.go deleted file mode 100644 index e902013..0000000 --- a/internal/items/loot/loot_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package loot - -import ( - "context" - "fmt" - "math/rand" - "testing" - - "zombiezen.com/go/sqlite/sqlitex" -) - -// setupTestDB creates a test database with minimal schema -func setupTestDB(t testing.TB) *sqlitex.Pool { - // Create unique database name to avoid test contamination - dbName := fmt.Sprintf("file:loot_test_%s_%d.db?mode=memory&cache=shared", t.Name(), rand.Int63()) - pool, err := sqlitex.NewPool(dbName, sqlitex.PoolOptions{ - PoolSize: 10, - }) - if err != nil { - t.Fatalf("Failed to create test database pool: %v", err) - } - - // Create complete test schema matching the real database structure - schema := ` - CREATE TABLE loottable ( - id INTEGER PRIMARY KEY, - name TEXT DEFAULT '', - mincoin INTEGER DEFAULT 0, - maxcoin INTEGER DEFAULT 0, - maxlootitems INTEGER DEFAULT 5, - lootdrop_probability REAL DEFAULT 100.0, - coin_probability REAL DEFAULT 75.0 - ); - - CREATE TABLE lootdrop ( - loot_table_id INTEGER, - item_id INTEGER, - item_charges INTEGER DEFAULT 1, - equip_item INTEGER DEFAULT 0, - probability REAL DEFAULT 25.0, - no_drop_quest_completed_id INTEGER DEFAULT 0 - ); - - CREATE TABLE spawn_loot ( - spawn_id INTEGER, - loottable_id INTEGER - ); - - CREATE TABLE loot_global ( - type TEXT, - loot_table INTEGER, - value1 INTEGER DEFAULT 0, - value2 INTEGER DEFAULT 0, - value3 INTEGER DEFAULT 0, - value4 INTEGER DEFAULT 0 - ); - ` - - // Execute schema on connection - ctx := context.Background() - conn, err := pool.Take(ctx) - if err != nil { - t.Fatalf("Failed to get connection: %v", err) - } - defer pool.Put(conn) - - if err := sqlitex.ExecuteScript(conn, schema, nil); err != nil { - t.Fatalf("Failed to create test schema: %v", err) - } - - return pool -} - -func TestNewLootDatabase(t *testing.T) { - pool := setupTestDB(t) - defer pool.Close() - - lootDB := NewLootDatabase(pool) - if lootDB == nil { - t.Fatal("Expected non-nil LootDatabase") - } - - if lootDB.pool == nil { - t.Fatal("Expected non-nil database pool") - } -} - -func TestLootDatabaseBasicOperation(t *testing.T) { - pool := setupTestDB(t) - defer pool.Close() - - lootDB := NewLootDatabase(pool) - - // Test that LoadAllLootData doesn't crash (even with empty database) - err := lootDB.LoadAllLootData() - if err != nil { - t.Fatalf("LoadAllLootData should not fail with empty database: %v", err) - } - - // Test that GetLootTable returns nil for non-existent table - table := lootDB.GetLootTable(999) - if table != nil { - t.Error("Expected nil for non-existent loot table") - } -} - -func TestLootDatabaseWithData(t *testing.T) { - pool := setupTestDB(t) - defer pool.Close() - - // Insert test data - ctx := context.Background() - conn, err := pool.Take(ctx) - if err != nil { - t.Fatalf("Failed to get connection: %v", err) - } - defer pool.Put(conn) - - // Insert a test loot table - err = sqlitex.Execute(conn, `INSERT INTO loottable (id, name, mincoin, maxcoin, maxlootitems, lootdrop_probability, coin_probability) VALUES (?, ?, ?, ?, ?, ?, ?)`, &sqlitex.ExecOptions{ - Args: []any{1, "Test Loot Table", 10, 50, 3, 75.0, 50.0}, - }) - if err != nil { - t.Fatalf("Failed to insert test loot table: %v", err) - } - - // Insert test loot drops - err = sqlitex.Execute(conn, `INSERT INTO lootdrop (loot_table_id, item_id, item_charges, equip_item, probability, no_drop_quest_completed_id) VALUES (?, ?, ?, ?, ?, ?)`, &sqlitex.ExecOptions{ - Args: []any{1, 101, 1, 0, 25.0, 0}, - }) - if err != nil { - t.Fatalf("Failed to insert test loot drop: %v", err) - } - - // Insert spawn loot assignment - err = sqlitex.Execute(conn, `INSERT INTO spawn_loot (spawn_id, loottable_id) VALUES (?, ?)`, &sqlitex.ExecOptions{ - Args: []any{1001, 1}, - }) - if err != nil { - t.Fatalf("Failed to insert spawn loot assignment: %v", err) - } - - // Load all loot data - lootDB := NewLootDatabase(pool) - err = lootDB.LoadAllLootData() - if err != nil { - t.Fatalf("Failed to load loot data: %v", err) - } - - // Verify loot table was loaded - table := lootDB.GetLootTable(1) - if table == nil { - t.Fatal("Expected to find loot table 1") - } - - if table.Name != "Test Loot Table" { - t.Errorf("Expected table name 'Test Loot Table', got '%s'", table.Name) - } - - if table.MinCoin != 10 { - t.Errorf("Expected min coin 10, got %d", table.MinCoin) - } - - if table.MaxCoin != 50 { - t.Errorf("Expected max coin 50, got %d", table.MaxCoin) - } - - if len(table.Drops) != 1 { - t.Errorf("Expected 1 loot drop, got %d", len(table.Drops)) - } else { - drop := table.Drops[0] - if drop.ItemID != 101 { - t.Errorf("Expected item ID 101, got %d", drop.ItemID) - } - if drop.Probability != 25.0 { - t.Errorf("Expected probability 25.0, got %f", drop.Probability) - } - } - - // Verify spawn loot assignment - tables := lootDB.GetSpawnLootTables(1001) - if len(tables) != 1 { - t.Errorf("Expected 1 loot table for spawn 1001, got %d", len(tables)) - } else if tables[0] != 1 { - t.Errorf("Expected loot table ID 1 for spawn 1001, got %d", tables[0]) - } -} - -// Benchmark tests -func BenchmarkLootDatabaseCreation(b *testing.B) { - for i := 0; i < b.N; i++ { - pool := setupTestDB(b) - lootDB := NewLootDatabase(pool) - if lootDB == nil { - b.Fatal("Expected non-nil LootDatabase") - } - pool.Close() - } -} \ No newline at end of file diff --git a/internal/languages/README.md b/internal/languages/README.md deleted file mode 100644 index fd53b9f..0000000 --- a/internal/languages/README.md +++ /dev/null @@ -1,321 +0,0 @@ -# Languages Package - -The languages package provides comprehensive multilingual character communication system for EverQuest II server emulation. It manages language learning, chat processing, and player language knowledge with thread-safe operations and efficient lookups. - -## Features - -- **Master Language Registry**: Global list of all available languages with ID-based and name-based lookups -- **Player Language Management**: Individual player language collections with learning/forgetting mechanics -- **Chat Processing**: Multilingual message processing with scrambling for unknown languages -- **Database Integration**: Language persistence with player-specific language knowledge -- **Thread Safety**: All operations use proper Go concurrency patterns with mutexes -- **Performance**: Optimized hash table lookups with benchmark results ~15ns per operation -- **EQ2 Compatibility**: Supports all standard EverQuest II racial languages - -## Core Components - -### Language Types - -```go -// Core language representation -type Language struct { - id int32 // Unique language identifier - name string // Language name - saveNeeded bool // Whether this language needs database saving - mutex sync.RWMutex // Thread safety -} - -// Master language registry (system-wide) -type MasterLanguagesList struct { - languages map[int32]*Language // Languages indexed by ID - nameIndex map[string]*Language // Languages indexed by name - mutex sync.RWMutex // Thread safety -} - -// Player-specific language collection -type PlayerLanguagesList struct { - languages map[int32]*Language // Player's languages indexed by ID - nameIndex map[string]*Language // Player's languages indexed by name - mutex sync.RWMutex // Thread safety -} -``` - -### Management System - -```go -// High-level language system manager -type Manager struct { - masterLanguagesList *MasterLanguagesList - database Database - logger Logger - // Statistics tracking - languageLookups int64 - playersWithLanguages int64 - languageUsageCount map[int32]int64 -} -``` - -### Integration Interfaces - -```go -// Entity interface for basic entity operations -type Entity interface { - GetID() int32 - GetName() string - IsPlayer() bool - IsNPC() bool - IsBot() bool -} - -// Player interface extending Entity for player-specific operations -type Player interface { - Entity - GetCharacterID() int32 - SendMessage(message string) -} - -// LanguageAware interface for entities with language capabilities -type LanguageAware interface { - GetKnownLanguages() *PlayerLanguagesList - KnowsLanguage(languageID int32) bool - GetPrimaryLanguage() int32 - SetPrimaryLanguage(languageID int32) - CanUnderstand(languageID int32) bool - LearnLanguage(languageID int32) error - ForgetLanguage(languageID int32) error -} -``` - -## Standard EQ2 Languages - -The package includes all standard EverQuest II racial languages: - -```go -const ( - LanguageIDCommon = 0 // Common tongue (default) - LanguageIDElvish = 1 // Elvish - LanguageIDDwarven = 2 // Dwarven - LanguageIDHalfling = 3 // Halfling - LanguageIDGnomish = 4 // Gnomish - LanguageIDIksar = 5 // Iksar - LanguageIDTrollish = 6 // Trollish - LanguageIDOgrish = 7 // Ogrish - LanguageIDFae = 8 // Fae - LanguageIDArasai = 9 // Arasai - LanguageIDSarnak = 10 // Sarnak - LanguageIDFroglok = 11 // Froglok -) -``` - -## Usage Examples - -### Basic Setup - -```go -// Create language system -database := NewMockDatabase() // or your database implementation -logger := NewMockLogger() // or your logger implementation -manager := NewManager(database, logger) - -// Add standard EQ2 languages -languages := map[int32]string{ - LanguageIDCommon: "Common", - LanguageIDElvish: "Elvish", - LanguageIDDwarven: "Dwarven", - // ... add other languages -} - -for id, name := range languages { - lang := NewLanguage() - lang.SetID(id) - lang.SetName(name) - manager.AddLanguage(lang) -} -``` - -### Player Integration - -```go -// Player struct implementing LanguageAware -type Player struct { - id int32 - name string - languageAdapter *PlayerLanguageAdapter -} - -func NewPlayer(id int32, name string, languageManager *Manager, logger Logger) *Player { - return &Player{ - id: id, - name: name, - languageAdapter: NewPlayerLanguageAdapter(languageManager, logger), - } -} - -// Implement LanguageAware interface by delegating to adapter -func (p *Player) GetKnownLanguages() *PlayerLanguagesList { - return p.languageAdapter.GetKnownLanguages() -} - -func (p *Player) KnowsLanguage(languageID int32) bool { - return p.languageAdapter.KnowsLanguage(languageID) -} - -func (p *Player) LearnLanguage(languageID int32) error { - return p.languageAdapter.LearnLanguage(languageID) -} - -// ... implement other LanguageAware methods -``` - -### Chat Processing - -```go -// Create chat processor -processor := NewChatLanguageProcessor(manager, logger) - -// Process message from speaker -message := "Mae govannen!" // Elvish greeting -processed, err := processor.ProcessMessage(speaker, message, LanguageIDElvish) -if err != nil { - // Handle error (speaker doesn't know language, etc.) -} - -// Filter message for listener based on their language knowledge -filtered := processor.FilterMessage(listener, processed, LanguageIDElvish) -// If listener doesn't know Elvish, message would be scrambled - -// Language scrambling for unknown languages -scrambled := processor.GetLanguageSkramble(message, 0.0) // 0% comprehension -// Returns scrambled version: "Nhl nbchuulu!" -``` - -### Database Operations - -```go -// Player learning languages with database persistence -player := NewPlayer(123, "TestPlayer", manager, logger) - -// Learn a language -err := player.LearnLanguage(LanguageIDElvish) -if err != nil { - log.Printf("Failed to learn language: %v", err) -} - -// Save to database -err = player.languageAdapter.SavePlayerLanguages(database, player.GetCharacterID()) -if err != nil { - log.Printf("Failed to save languages: %v", err) -} - -// Load from database (e.g., on player login) -newPlayer := NewPlayer(123, "TestPlayer", manager, logger) -err = newPlayer.languageAdapter.LoadPlayerLanguages(database, player.GetCharacterID()) -if err != nil { - log.Printf("Failed to load languages: %v", err) -} -``` - -### Statistics and Management - -```go -// Get system statistics -stats := manager.GetStatistics() -fmt.Printf("Total Languages: %d\\n", stats.TotalLanguages) -fmt.Printf("Players with Languages: %d\\n", stats.PlayersWithLanguages) -fmt.Printf("Language Lookups: %d\\n", stats.LanguageLookups) - -// Process management commands -result, err := manager.ProcessCommand("stats", []string{}) -if err != nil { - log.Printf("Command failed: %v", err) -} else { - fmt.Println(result) -} - -// Validate all languages -issues := manager.ValidateAllLanguages() -if len(issues) > 0 { - log.Printf("Language validation issues: %v", issues) -} -``` - -## Database Interface - -Implement the `Database` interface to provide persistence: - -```go -type Database interface { - LoadAllLanguages() ([]*Language, error) - SaveLanguage(language *Language) error - DeleteLanguage(languageID int32) error - LoadPlayerLanguages(playerID int32) ([]*Language, error) - SavePlayerLanguage(playerID int32, languageID int32) error - DeletePlayerLanguage(playerID int32, languageID int32) error -} -``` - -## Performance - -Benchmark results on AMD Ryzen 7 8845HS: - -- **Language Lookup**: ~15ns per operation -- **Player Language Adapter**: ~8ns per operation -- **Integrated Operations**: ~16ns per operation - -The system uses hash tables for O(1) lookups and is optimized for high-frequency operations. - -## Thread Safety - -All operations are thread-safe using: -- `sync.RWMutex` for read-heavy operations (language lookups) -- `sync.Mutex` for write operations and statistics -- Atomic operations where appropriate - -## System Limits - -```go -const ( - MaxLanguagesPerPlayer = 100 // Reasonable limit per player - MaxTotalLanguages = 1000 // System-wide language limit - MaxLanguageNameLength = 50 // Maximum language name length -) -``` - -## Command Interface - -The manager supports administrative commands: - -- `stats` - Show language system statistics -- `list` - List all languages -- `info ` - Show language information -- `validate` - Validate all languages -- `reload` - Reload from database -- `add ` - Add new language -- `remove ` - Remove language -- `search ` - Search languages by name - -## Testing - -Comprehensive test suite includes: - -- Unit tests for all core components -- Integration tests with mock implementations -- Performance benchmarks -- Entity integration examples -- Thread safety validation - -Run tests: -```bash -go test ./internal/languages -v -go test ./internal/languages -bench=. -``` - -## Integration Notes - -1. **Entity Integration**: Players should embed `PlayerLanguageAdapter` and implement `LanguageAware` -2. **Database**: Implement the `Database` interface for persistence -3. **Logging**: Implement the `Logger` interface for system logging -4. **Chat System**: Use `ChatLanguageProcessor` for multilingual message handling -5. **Race Integration**: Initialize players with their racial languages at character creation - -The languages package follows the same architectural patterns as other EQ2Go systems and integrates seamlessly with the entity, player, and chat systems. \ No newline at end of file diff --git a/internal/languages/integration_example_test.go b/internal/languages/integration_example_test.go deleted file mode 100644 index 3455888..0000000 --- a/internal/languages/integration_example_test.go +++ /dev/null @@ -1,392 +0,0 @@ -package languages - -import ( - "fmt" - "testing" -) - -// This file demonstrates how to integrate the languages package with entity systems - -// ExamplePlayer shows how a Player struct might embed the language adapter -type ExamplePlayer struct { - id int32 - name string - characterID int32 - languageAdapter *PlayerLanguageAdapter -} - -// NewExamplePlayer creates a new example player with language support -func NewExamplePlayer(id int32, name string, languageManager *Manager, logger Logger) *ExamplePlayer { - return &ExamplePlayer{ - id: id, - name: name, - characterID: id, // In real implementation, this might be different - languageAdapter: NewPlayerLanguageAdapter(languageManager, logger), - } -} - -// Implement the Entity interface -func (ep *ExamplePlayer) GetID() int32 { return ep.id } -func (ep *ExamplePlayer) GetName() string { return ep.name } -func (ep *ExamplePlayer) IsPlayer() bool { return true } -func (ep *ExamplePlayer) IsNPC() bool { return false } -func (ep *ExamplePlayer) IsBot() bool { return false } - -// Implement the Player interface -func (ep *ExamplePlayer) GetCharacterID() int32 { return ep.characterID } -func (ep *ExamplePlayer) SendMessage(message string) { - // In real implementation, this would send a message to the client -} - -// Implement the LanguageAware interface by delegating to the adapter -func (ep *ExamplePlayer) GetKnownLanguages() *PlayerLanguagesList { - return ep.languageAdapter.GetKnownLanguages() -} - -func (ep *ExamplePlayer) KnowsLanguage(languageID int32) bool { - return ep.languageAdapter.KnowsLanguage(languageID) -} - -func (ep *ExamplePlayer) GetPrimaryLanguage() int32 { - return ep.languageAdapter.GetPrimaryLanguage() -} - -func (ep *ExamplePlayer) SetPrimaryLanguage(languageID int32) { - ep.languageAdapter.SetPrimaryLanguage(languageID) -} - -func (ep *ExamplePlayer) CanUnderstand(languageID int32) bool { - return ep.languageAdapter.CanUnderstand(languageID) -} - -func (ep *ExamplePlayer) LearnLanguage(languageID int32) error { - return ep.languageAdapter.LearnLanguage(languageID) -} - -func (ep *ExamplePlayer) ForgetLanguage(languageID int32) error { - return ep.languageAdapter.ForgetLanguage(languageID) -} - -// Database integration methods -func (ep *ExamplePlayer) LoadLanguagesFromDatabase(database Database) error { - return ep.languageAdapter.LoadPlayerLanguages(database, ep.characterID) -} - -func (ep *ExamplePlayer) SaveLanguagesToDatabase(database Database) error { - return ep.languageAdapter.SavePlayerLanguages(database, ep.characterID) -} - -// ExampleNPC shows how an NPC might implement basic language interfaces -type ExampleNPC struct { - id int32 - name string - knownLanguageIDs []int32 - primaryLanguage int32 -} - -func NewExampleNPC(id int32, name string, knownLanguages []int32) *ExampleNPC { - primaryLang := int32(LanguageIDCommon) - if len(knownLanguages) > 0 { - primaryLang = knownLanguages[0] - } - - return &ExampleNPC{ - id: id, - name: name, - knownLanguageIDs: knownLanguages, - primaryLanguage: primaryLang, - } -} - -// Implement the Entity interface -func (en *ExampleNPC) GetID() int32 { return en.id } -func (en *ExampleNPC) GetName() string { return en.name } -func (en *ExampleNPC) IsPlayer() bool { return false } -func (en *ExampleNPC) IsNPC() bool { return true } -func (en *ExampleNPC) IsBot() bool { return false } - -// Implement the Player interface (NPCs can also be treated as players for some language operations) -func (en *ExampleNPC) GetCharacterID() int32 { return en.id } -func (en *ExampleNPC) SendMessage(message string) { - // NPCs don't typically receive messages, but implement for interface compatibility -} - -// Simple language support for NPCs (they don't learn/forget languages) -func (en *ExampleNPC) KnowsLanguage(languageID int32) bool { - for _, id := range en.knownLanguageIDs { - if id == languageID { - return true - } - } - return languageID == LanguageIDCommon // NPCs always understand common -} - -func (en *ExampleNPC) GetPrimaryLanguage() int32 { - return en.primaryLanguage -} - -func (en *ExampleNPC) CanUnderstand(languageID int32) bool { - return en.KnowsLanguage(languageID) -} - -// TestEntityIntegration demonstrates how entities work with the language system -func TestEntityIntegration(t *testing.T) { - // Set up language system - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - - // Add some languages - languages := []*Language{ - createLanguage(LanguageIDCommon, "Common"), - createLanguage(LanguageIDElvish, "Elvish"), - createLanguage(LanguageIDDwarven, "Dwarven"), - createLanguage(LanguageIDOgrish, "Ogrish"), - } - - for _, lang := range languages { - manager.AddLanguage(lang) - } - - // Create a player - player := NewExamplePlayer(1, "TestPlayer", manager, logger) - - // Player should know Common language by default - if !player.KnowsLanguage(LanguageIDCommon) { - t.Error("Player should know Common language by default") - } - - // Test learning a language - err := player.LearnLanguage(LanguageIDElvish) - if err != nil { - t.Fatalf("Failed to learn Elvish: %v", err) - } - - if !player.KnowsLanguage(LanguageIDElvish) { - t.Error("Player should know Elvish after learning") - } - - // Test setting primary language - player.SetPrimaryLanguage(LanguageIDElvish) - if player.GetPrimaryLanguage() != LanguageIDElvish { - t.Error("Primary language should be Elvish") - } - - // Test database persistence - err = player.SaveLanguagesToDatabase(database) - if err != nil { - t.Fatalf("Failed to save languages to database: %v", err) - } - - // Create new player and load languages - newPlayer := NewExamplePlayer(1, "TestPlayer", manager, logger) - err = newPlayer.LoadLanguagesFromDatabase(database) - if err != nil { - t.Fatalf("Failed to load languages from database: %v", err) - } - - if !newPlayer.KnowsLanguage(LanguageIDElvish) { - t.Error("New player should know Elvish after loading from database") - } - - // Create an NPC with specific languages - npc := NewExampleNPC(100, "Elven Merchant", []int32{LanguageIDCommon, LanguageIDElvish}) - - if !npc.KnowsLanguage(LanguageIDElvish) { - t.Error("Elven NPC should know Elvish") - } - - if npc.KnowsLanguage(LanguageIDDwarven) { - t.Error("Elven NPC should not know Dwarven") - } - - // Test chat processing - processor := NewChatLanguageProcessor(manager, logger) - - // Player speaking in Elvish - message := "Mae govannen!" // Elvish greeting - processed, err := processor.ProcessMessage(player, message, LanguageIDElvish) - if err != nil { - t.Fatalf("Failed to process Elvish message: %v", err) - } - - if processed != message { - t.Errorf("Expected message '%s', got '%s'", message, processed) - } - - // Test message filtering for understanding - filtered := processor.FilterMessage(npc, message, LanguageIDElvish) - if filtered != message { - t.Errorf("NPC should understand Elvish message, got '%s'", filtered) - } -} - -// TestLanguageSystemIntegration demonstrates a complete language system workflow -func TestLanguageSystemIntegration(t *testing.T) { - // Set up a complete language system - database := NewMockDatabase() - logger := NewMockLogger() - - // Create manager and initialize with standard EQ2 languages - manager := NewManager(database, logger) - - eq2Languages := map[int32]string{ - LanguageIDCommon: "Common", - LanguageIDElvish: "Elvish", - LanguageIDDwarven: "Dwarven", - LanguageIDHalfling: "Halfling", - LanguageIDGnomish: "Gnomish", - LanguageIDIksar: "Iksar", - LanguageIDTrollish: "Trollish", - LanguageIDOgrish: "Ogrish", - LanguageIDFae: "Fae", - LanguageIDArasai: "Arasai", - LanguageIDSarnak: "Sarnak", - LanguageIDFroglok: "Froglok", - } - - for id, name := range eq2Languages { - lang := createLanguage(id, name) - manager.AddLanguage(lang) - } - - if err := manager.Initialize(); err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - // Create players of different races (represented by knowing different racial languages) - humanPlayer := NewExamplePlayer(1, "Human", manager, logger) - elfPlayer := NewExamplePlayer(2, "Elf", manager, logger) - dwarfPlayer := NewExamplePlayer(3, "Dwarf", manager, logger) - - // Elven player learns Elvish at creation (racial language) - elfPlayer.LearnLanguage(LanguageIDElvish) - elfPlayer.SetPrimaryLanguage(LanguageIDElvish) - - // Dwarven player learns Dwarven - dwarfPlayer.LearnLanguage(LanguageIDDwarven) - dwarfPlayer.SetPrimaryLanguage(LanguageIDDwarven) - - // Test cross-racial communication - processor := NewChatLanguageProcessor(manager, logger) - - // Elf speaks in Elvish - elvishMessage := "Elen sila lumenn omentielvo" - processed, _ := processor.ProcessMessage(elfPlayer, elvishMessage, LanguageIDElvish) - - // Human doesn't understand Elvish (would be scrambled in real implementation) - humanHeard := processor.FilterMessage(humanPlayer, processed, LanguageIDElvish) - _ = humanHeard // In real implementation, this would be scrambled - - // Dwarf doesn't understand Elvish either - dwarfHeard := processor.FilterMessage(dwarfPlayer, processed, LanguageIDElvish) - _ = dwarfHeard // In real implementation, this would be scrambled - - // Everyone understands Common - commonMessage := "Hello everyone!" - processed, _ = processor.ProcessMessage(humanPlayer, commonMessage, LanguageIDCommon) - - humanHeardCommon := processor.FilterMessage(humanPlayer, processed, LanguageIDCommon) - if humanHeardCommon != commonMessage { - t.Error("All players should understand Common") - } - - elfHeardCommon := processor.FilterMessage(elfPlayer, processed, LanguageIDCommon) - if elfHeardCommon != commonMessage { - t.Error("All players should understand Common") - } - - dwarfHeardCommon := processor.FilterMessage(dwarfPlayer, processed, LanguageIDCommon) - if dwarfHeardCommon != commonMessage { - t.Error("All players should understand Common") - } - - // Test statistics - stats := manager.GetStatistics() - if stats.TotalLanguages != len(eq2Languages) { - t.Errorf("Expected %d languages, got %d", len(eq2Languages), stats.TotalLanguages) - } - - // Test validation - issues := manager.ValidateAllLanguages() - if len(issues) > 0 { - t.Errorf("Expected no validation issues, got: %v", issues) - } -} - -// Helper function to create languages -func createLanguage(id int32, name string) *Language { - lang := NewLanguage() - lang.SetID(id) - lang.SetName(name) - return lang -} - -// TestLanguageAwareInterface demonstrates the LanguageAware interface usage -func TestLanguageAwareInterface(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - - // Add languages - manager.AddLanguage(createLanguage(LanguageIDCommon, "Common")) - manager.AddLanguage(createLanguage(LanguageIDElvish, "Elvish")) - - // Create a player that implements LanguageAware - player := NewExamplePlayer(1, "TestPlayer", manager, logger) - - // Test LanguageAware interface methods - var languageAware LanguageAware = player - - // Should know Common by default - if !languageAware.KnowsLanguage(LanguageIDCommon) { - t.Error("LanguageAware entity should know Common by default") - } - - // Learn a new language - err := languageAware.LearnLanguage(LanguageIDElvish) - if err != nil { - t.Fatalf("Failed to learn language through LanguageAware interface: %v", err) - } - - // Check understanding - if !languageAware.CanUnderstand(LanguageIDElvish) { - t.Error("Should understand Elvish after learning") - } - - // Set primary language - languageAware.SetPrimaryLanguage(LanguageIDElvish) - if languageAware.GetPrimaryLanguage() != LanguageIDElvish { - t.Error("Primary language should be Elvish") - } - - // Get known languages - knownLanguages := languageAware.GetKnownLanguages() - if knownLanguages.Size() != 2 { // Common + Elvish - t.Errorf("Expected 2 known languages, got %d", knownLanguages.Size()) - } -} - -// Benchmark integration performance -func BenchmarkIntegratedPlayerLanguageOperations(b *testing.B) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - - // Add languages - for i := 0; i < 50; i++ { - lang := createLanguage(int32(i), fmt.Sprintf("Language_%d", i)) - manager.AddLanguage(lang) - } - - player := NewExamplePlayer(1, "BenchmarkPlayer", manager, logger) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - langID := int32(i % 50) - if !player.KnowsLanguage(langID) { - player.LearnLanguage(langID) - } - player.CanUnderstand(langID) - } -} \ No newline at end of file diff --git a/internal/languages/languages_test.go b/internal/languages/languages_test.go deleted file mode 100644 index 4f029d8..0000000 --- a/internal/languages/languages_test.go +++ /dev/null @@ -1,704 +0,0 @@ -package languages - -import ( - "fmt" - "testing" -) - -// Mock implementations for testing - -// MockDatabase implements the Database interface for testing -type MockDatabase struct { - languages map[int32]*Language - playerLanguages map[int32][]int32 // playerID -> []languageIDs -} - -func NewMockDatabase() *MockDatabase { - return &MockDatabase{ - languages: make(map[int32]*Language), - playerLanguages: make(map[int32][]int32), - } -} - -func (md *MockDatabase) LoadAllLanguages() ([]*Language, error) { - var languages []*Language - for _, lang := range md.languages { - languages = append(languages, lang.Copy()) - } - return languages, nil -} - -func (md *MockDatabase) SaveLanguage(language *Language) error { - if language == nil { - return fmt.Errorf("language is nil") - } - md.languages[language.GetID()] = language.Copy() - return nil -} - -func (md *MockDatabase) DeleteLanguage(languageID int32) error { - delete(md.languages, languageID) - return nil -} - -func (md *MockDatabase) LoadPlayerLanguages(playerID int32) ([]*Language, error) { - var languages []*Language - if langIDs, exists := md.playerLanguages[playerID]; exists { - for _, langID := range langIDs { - if lang, exists := md.languages[langID]; exists { - languages = append(languages, lang.Copy()) - } - } - } - return languages, nil -} - -func (md *MockDatabase) SavePlayerLanguage(playerID int32, languageID int32) error { - if _, exists := md.languages[languageID]; !exists { - return fmt.Errorf("language %d does not exist", languageID) - } - - if playerLangs, exists := md.playerLanguages[playerID]; exists { - // Check if already exists - for _, id := range playerLangs { - if id == languageID { - return nil // Already saved - } - } - md.playerLanguages[playerID] = append(playerLangs, languageID) - } else { - md.playerLanguages[playerID] = []int32{languageID} - } - return nil -} - -func (md *MockDatabase) DeletePlayerLanguage(playerID int32, languageID int32) error { - if playerLangs, exists := md.playerLanguages[playerID]; exists { - for i, id := range playerLangs { - if id == languageID { - md.playerLanguages[playerID] = append(playerLangs[:i], playerLangs[i+1:]...) - return nil - } - } - } - return fmt.Errorf("player %d does not know language %d", playerID, languageID) -} - -// MockLogger implements the Logger interface for testing -type MockLogger struct { - logs []string -} - -func NewMockLogger() *MockLogger { - return &MockLogger{ - logs: make([]string, 0), - } -} - -func (ml *MockLogger) LogInfo(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...)) -} - -func (ml *MockLogger) LogError(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...)) -} - -func (ml *MockLogger) LogDebug(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...)) -} - -func (ml *MockLogger) LogWarning(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...)) -} - -func (ml *MockLogger) GetLogs() []string { - return ml.logs -} - -func (ml *MockLogger) Clear() { - ml.logs = ml.logs[:0] -} - -// MockPlayer implements the Player interface for testing -type MockPlayer struct { - id int32 - name string -} - -func NewMockPlayer(id int32, name string) *MockPlayer { - return &MockPlayer{ - id: id, - name: name, - } -} - -func (mp *MockPlayer) GetID() int32 { return mp.id } -func (mp *MockPlayer) GetName() string { return mp.name } -func (mp *MockPlayer) IsPlayer() bool { return true } -func (mp *MockPlayer) IsNPC() bool { return false } -func (mp *MockPlayer) IsBot() bool { return false } -func (mp *MockPlayer) GetCharacterID() int32 { return mp.id } -func (mp *MockPlayer) SendMessage(message string) { - // Mock implementation - could store messages for testing -} - -// Test functions - -func TestNewLanguage(t *testing.T) { - lang := NewLanguage() - if lang == nil { - t.Fatal("NewLanguage returned nil") - } - - if lang.GetID() != 0 { - t.Errorf("Expected ID 0, got %d", lang.GetID()) - } - - if lang.GetName() != "" { - t.Errorf("Expected empty name, got %s", lang.GetName()) - } - - if lang.GetSaveNeeded() { - t.Error("New language should not need saving") - } -} - -func TestLanguageSettersAndGetters(t *testing.T) { - lang := NewLanguage() - - // Test ID - lang.SetID(123) - if lang.GetID() != 123 { - t.Errorf("Expected ID 123, got %d", lang.GetID()) - } - - // Test Name - lang.SetName("Test Language") - if lang.GetName() != "Test Language" { - t.Errorf("Expected name 'Test Language', got %s", lang.GetName()) - } - - // Test SaveNeeded - lang.SetSaveNeeded(true) - if !lang.GetSaveNeeded() { - t.Error("Expected save needed to be true") - } -} - -func TestLanguageValidation(t *testing.T) { - lang := NewLanguage() - - // Invalid - no name and ID 0 - if lang.IsValid() { - t.Error("Empty language should not be valid") - } - - // Set valid data - lang.SetID(LanguageIDElvish) - lang.SetName("Elvish") - - if !lang.IsValid() { - t.Error("Language with valid ID and name should be valid") - } - - // Test invalid ID - lang.SetID(-1) - if lang.IsValid() { - t.Error("Language with invalid ID should not be valid") - } - - lang.SetID(MaxLanguageID + 1) - if lang.IsValid() { - t.Error("Language with ID exceeding max should not be valid") - } -} - -func TestLanguageCopy(t *testing.T) { - original := NewLanguage() - original.SetID(42) - original.SetName("Original") - original.SetSaveNeeded(true) - - copy := original.Copy() - if copy == nil { - t.Fatal("Copy returned nil") - } - - if copy.GetID() != original.GetID() { - t.Errorf("Expected ID %d, got %d", original.GetID(), copy.GetID()) - } - - if copy.GetName() != original.GetName() { - t.Errorf("Expected name %s, got %s", original.GetName(), copy.GetName()) - } - - if copy.GetSaveNeeded() != original.GetSaveNeeded() { - t.Errorf("Expected save needed %v, got %v", original.GetSaveNeeded(), copy.GetSaveNeeded()) - } - - // Ensure it's a separate instance - copy.SetName("Modified") - if original.GetName() == copy.GetName() { - t.Error("Copy should be independent of original") - } -} - -func TestMasterLanguagesList(t *testing.T) { - masterList := NewMasterLanguagesList() - - // Test initial state - if masterList.Size() != 0 { - t.Errorf("Expected size 0, got %d", masterList.Size()) - } - - // Add language - lang := NewLanguage() - lang.SetID(LanguageIDCommon) - lang.SetName("Common") - - err := masterList.AddLanguage(lang) - if err != nil { - t.Fatalf("Failed to add language: %v", err) - } - - if masterList.Size() != 1 { - t.Errorf("Expected size 1, got %d", masterList.Size()) - } - - // Test retrieval - retrieved := masterList.GetLanguage(LanguageIDCommon) - if retrieved == nil { - t.Fatal("GetLanguage returned nil") - } - - if retrieved.GetName() != "Common" { - t.Errorf("Expected name 'Common', got %s", retrieved.GetName()) - } - - // Test retrieval by name - byName := masterList.GetLanguageByName("Common") - if byName == nil { - t.Fatal("GetLanguageByName returned nil") - } - - if byName.GetID() != LanguageIDCommon { - t.Errorf("Expected ID %d, got %d", LanguageIDCommon, byName.GetID()) - } - - // Test duplicate ID - dupLang := NewLanguage() - dupLang.SetID(LanguageIDCommon) - dupLang.SetName("Duplicate") - - err = masterList.AddLanguage(dupLang) - if err == nil { - t.Error("Should not allow duplicate language ID") - } - - // Test duplicate name - dupNameLang := NewLanguage() - dupNameLang.SetID(LanguageIDElvish) - dupNameLang.SetName("Common") - - err = masterList.AddLanguage(dupNameLang) - if err == nil { - t.Error("Should not allow duplicate language name") - } -} - -func TestPlayerLanguagesList(t *testing.T) { - playerList := NewPlayerLanguagesList() - - // Test initial state - if playerList.Size() != 0 { - t.Errorf("Expected size 0, got %d", playerList.Size()) - } - - // Add language - lang := NewLanguage() - lang.SetID(LanguageIDCommon) - lang.SetName("Common") - - err := playerList.Add(lang) - if err != nil { - t.Fatalf("Failed to add language: %v", err) - } - - if playerList.Size() != 1 { - t.Errorf("Expected size 1, got %d", playerList.Size()) - } - - // Test has language - if !playerList.HasLanguage(LanguageIDCommon) { - t.Error("Player should have Common language") - } - - if playerList.HasLanguage(LanguageIDElvish) { - t.Error("Player should not have Elvish language") - } - - // Test removal - if !playerList.RemoveLanguage(LanguageIDCommon) { - t.Error("Should be able to remove Common language") - } - - if playerList.Size() != 0 { - t.Errorf("Expected size 0 after removal, got %d", playerList.Size()) - } -} - -func TestManager(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - - // Pre-populate database with some languages - commonLang := NewLanguage() - commonLang.SetID(LanguageIDCommon) - commonLang.SetName("Common") - database.SaveLanguage(commonLang) - - elvishLang := NewLanguage() - elvishLang.SetID(LanguageIDElvish) - elvishLang.SetName("Elvish") - database.SaveLanguage(elvishLang) - - manager := NewManager(database, logger) - - // Test initialization - err := manager.Initialize() - if err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - if manager.GetLanguageCount() != 2 { - t.Errorf("Expected 2 languages, got %d", manager.GetLanguageCount()) - } - - // Test language retrieval - lang := manager.GetLanguage(LanguageIDCommon) - if lang == nil { - t.Fatal("GetLanguage returned nil for Common") - } - - if lang.GetName() != "Common" { - t.Errorf("Expected name 'Common', got %s", lang.GetName()) - } - - // Test language retrieval by name - langByName := manager.GetLanguageByName("Elvish") - if langByName == nil { - t.Fatal("GetLanguageByName returned nil for Elvish") - } - - if langByName.GetID() != LanguageIDElvish { - t.Errorf("Expected ID %d, got %d", LanguageIDElvish, langByName.GetID()) - } - - // Test adding new language - dwarvenLang := NewLanguage() - dwarvenLang.SetID(LanguageIDDwarven) - dwarvenLang.SetName("Dwarven") - - err = manager.AddLanguage(dwarvenLang) - if err != nil { - t.Fatalf("Failed to add language: %v", err) - } - - if manager.GetLanguageCount() != 3 { - t.Errorf("Expected 3 languages after adding, got %d", manager.GetLanguageCount()) - } - - // Test statistics - stats := manager.GetStatistics() - if stats.TotalLanguages != 3 { - t.Errorf("Expected 3 total languages in stats, got %d", stats.TotalLanguages) - } -} - -func TestPlayerLanguageAdapter(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - - // Set up manager with languages - manager := NewManager(database, logger) - - commonLang := NewLanguage() - commonLang.SetID(LanguageIDCommon) - commonLang.SetName("Common") - manager.AddLanguage(commonLang) - - elvishLang := NewLanguage() - elvishLang.SetID(LanguageIDElvish) - elvishLang.SetName("Elvish") - manager.AddLanguage(elvishLang) - - // Create adapter - adapter := NewPlayerLanguageAdapter(manager, logger) - - // Test initial state - should know Common by default - if !adapter.KnowsLanguage(LanguageIDCommon) { - t.Error("Adapter should know Common language by default") - } - - if adapter.GetPrimaryLanguage() != LanguageIDCommon { - t.Errorf("Expected primary language to be Common (%d), got %d", LanguageIDCommon, adapter.GetPrimaryLanguage()) - } - - // Test learning a new language - err := adapter.LearnLanguage(LanguageIDElvish) - if err != nil { - t.Fatalf("Failed to learn Elvish: %v", err) - } - - if !adapter.KnowsLanguage(LanguageIDElvish) { - t.Error("Should know Elvish after learning") - } - - // Test setting primary language - adapter.SetPrimaryLanguage(LanguageIDElvish) - if adapter.GetPrimaryLanguage() != LanguageIDElvish { - t.Errorf("Expected primary language to be Elvish (%d), got %d", LanguageIDElvish, adapter.GetPrimaryLanguage()) - } - - // Test forgetting a language - err = adapter.ForgetLanguage(LanguageIDElvish) - if err != nil { - t.Fatalf("Failed to forget Elvish: %v", err) - } - - if adapter.KnowsLanguage(LanguageIDElvish) { - t.Error("Should not know Elvish after forgetting") - } - - // Primary language should reset to Common after forgetting - if adapter.GetPrimaryLanguage() != LanguageIDCommon { - t.Errorf("Expected primary language to reset to Common (%d), got %d", LanguageIDCommon, adapter.GetPrimaryLanguage()) - } - - // Test cannot forget Common - err = adapter.ForgetLanguage(LanguageIDCommon) - if err == nil { - t.Error("Should not be able to forget Common language") - } -} - -func TestPlayerLanguageAdapterDatabase(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - - // Set up manager with languages - manager := NewManager(database, logger) - - commonLang := NewLanguage() - commonLang.SetID(LanguageIDCommon) - commonLang.SetName("Common") - manager.AddLanguage(commonLang) - - elvishLang := NewLanguage() - elvishLang.SetID(LanguageIDElvish) - elvishLang.SetName("Elvish") - manager.AddLanguage(elvishLang) - - // Pre-populate database for player - playerID := int32(123) - database.SavePlayerLanguage(playerID, LanguageIDCommon) - database.SavePlayerLanguage(playerID, LanguageIDElvish) - - // Create adapter and load from database - adapter := NewPlayerLanguageAdapter(manager, logger) - err := adapter.LoadPlayerLanguages(database, playerID) - if err != nil { - t.Fatalf("Failed to load player languages: %v", err) - } - - // Should know both languages - if !adapter.KnowsLanguage(LanguageIDCommon) { - t.Error("Should know Common after loading from database") - } - - if !adapter.KnowsLanguage(LanguageIDElvish) { - t.Error("Should know Elvish after loading from database") - } - - // Learn a new language and save - dwarvenLang := NewLanguage() - dwarvenLang.SetID(LanguageIDDwarven) - dwarvenLang.SetName("Dwarven") - manager.AddLanguage(dwarvenLang) - - err = adapter.LearnLanguage(LanguageIDDwarven) - if err != nil { - t.Fatalf("Failed to learn Dwarven: %v", err) - } - - err = adapter.SavePlayerLanguages(database, playerID) - if err != nil { - t.Fatalf("Failed to save player languages: %v", err) - } - - // Create new adapter and load - should have all three languages - newAdapter := NewPlayerLanguageAdapter(manager, logger) - err = newAdapter.LoadPlayerLanguages(database, playerID) - if err != nil { - t.Fatalf("Failed to load player languages in new adapter: %v", err) - } - - if !newAdapter.KnowsLanguage(LanguageIDDwarven) { - t.Error("New adapter should know Dwarven after loading from database") - } -} - -func TestChatLanguageProcessor(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - - manager := NewManager(database, logger) - - commonLang := NewLanguage() - commonLang.SetID(LanguageIDCommon) - commonLang.SetName("Common") - manager.AddLanguage(commonLang) - - processor := NewChatLanguageProcessor(manager, logger) - - player := NewMockPlayer(123, "TestPlayer") - message := "Hello, world!" - - // Test processing message in Common language - processed, err := processor.ProcessMessage(player, message, LanguageIDCommon) - if err != nil { - t.Fatalf("Failed to process message: %v", err) - } - - if processed != message { - t.Errorf("Expected processed message '%s', got '%s'", message, processed) - } - - // Test processing message in non-existent language - _, err = processor.ProcessMessage(player, message, 9999) - if err == nil { - t.Error("Should fail to process message in non-existent language") - } - - // Test filter message (currently always returns original message) - filtered := processor.FilterMessage(player, message, LanguageIDCommon) - if filtered != message { - t.Errorf("Expected filtered message '%s', got '%s'", message, filtered) - } -} - -func TestLanguageSkramble(t *testing.T) { - processor := NewChatLanguageProcessor(nil, nil) - - message := "Hello World" - - // Test full comprehension - scrambled := processor.GetLanguageSkramble(message, 1.0) - if scrambled != message { - t.Errorf("Full comprehension should return original message, got '%s'", scrambled) - } - - // Test no comprehension - scrambled = processor.GetLanguageSkramble(message, 0.0) - if scrambled == message { - t.Error("No comprehension should scramble the message") - } - - // Should preserve spaces - if len(scrambled) != len(message) { - t.Errorf("Scrambled message should have same length: expected %d, got %d", len(message), len(scrambled)) - } -} - -func TestManagerCommands(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - - manager := NewManager(database, logger) - - commonLang := NewLanguage() - commonLang.SetID(LanguageIDCommon) - commonLang.SetName("Common") - manager.AddLanguage(commonLang) - - // Test stats command - result, err := manager.ProcessCommand("stats", []string{}) - if err != nil { - t.Fatalf("Stats command failed: %v", err) - } - - if result == "" { - t.Error("Stats command should return non-empty result") - } - - // Test list command - result, err = manager.ProcessCommand("list", []string{}) - if err != nil { - t.Fatalf("List command failed: %v", err) - } - - if result == "" { - t.Error("List command should return non-empty result") - } - - // Test info command - result, err = manager.ProcessCommand("info", []string{"0"}) - if err != nil { - t.Fatalf("Info command failed: %v", err) - } - - if result == "" { - t.Error("Info command should return non-empty result") - } - - // Test unknown command - _, err = manager.ProcessCommand("unknown", []string{}) - if err == nil { - t.Error("Unknown command should return error") - } -} - -func BenchmarkLanguageLookup(b *testing.B) { - database := NewMockDatabase() - logger := NewMockLogger() - - manager := NewManager(database, logger) - - // Add many languages - for i := 0; i < 1000; i++ { - lang := NewLanguage() - lang.SetID(int32(i)) - lang.SetName(fmt.Sprintf("Language_%d", i)) - manager.AddLanguage(lang) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.GetLanguage(int32(i % 1000)) - } -} - -func BenchmarkPlayerLanguageAdapter(b *testing.B) { - database := NewMockDatabase() - logger := NewMockLogger() - - manager := NewManager(database, logger) - - // Add some languages - for i := 0; i < 100; i++ { - lang := NewLanguage() - lang.SetID(int32(i)) - lang.SetName(fmt.Sprintf("Language_%d", i)) - manager.AddLanguage(lang) - } - - adapter := NewPlayerLanguageAdapter(manager, logger) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - languageID := int32(i % 100) - if !adapter.KnowsLanguage(languageID) { - adapter.LearnLanguage(languageID) - } - } -} \ No newline at end of file diff --git a/internal/npc/ai/ai_test.go b/internal/npc/ai/ai_test.go deleted file mode 100644 index 88e3132..0000000 --- a/internal/npc/ai/ai_test.go +++ /dev/null @@ -1,1624 +0,0 @@ -package ai - -import ( - "fmt" - "testing" - "time" -) - -// Mock implementations for testing - -// MockNPC provides a mock NPC implementation for testing -type MockNPC struct { - id int32 - name string - hp int32 - totalHP int32 - inCombat bool - target Entity - isPet bool - owner Entity - x, y, z float32 - distance float32 - following bool - followTarget Spawn - runningBack bool - mezzedOrStunned bool - casting bool - dazed bool - feared bool - stifled bool - inWater bool - waterCreature bool - flyingCreature bool - attackAllowed bool - primaryWeaponReady bool - secondaryWeaponReady bool - castPercentage int8 - nextSpell Spell - nextBuffSpell Spell - checkLoS bool - pauseMovementTimer bool - runbackLocation *MovementLocation - runbackDistance float32 - shouldCallRunback bool - spawnScript string -} - -func NewMockNPC(id int32, name string) *MockNPC { - return &MockNPC{ - id: id, - name: name, - hp: 100, - totalHP: 100, - inCombat: false, - isPet: false, - x: 0, - y: 0, - z: 0, - distance: 10.0, - following: false, - runningBack: false, - mezzedOrStunned: false, - casting: false, - dazed: false, - feared: false, - stifled: false, - inWater: false, - waterCreature: false, - flyingCreature: false, - attackAllowed: true, - primaryWeaponReady: true, - secondaryWeaponReady: true, - castPercentage: 25, - checkLoS: true, - pauseMovementTimer: false, - runbackDistance: 0, - shouldCallRunback: false, - spawnScript: "", - } -} - -// Implement NPC interface -func (m *MockNPC) GetID() int32 { return m.id } -func (m *MockNPC) GetName() string { return m.name } -func (m *MockNPC) GetHP() int32 { return m.hp } -func (m *MockNPC) GetTotalHP() int32 { return m.totalHP } -func (m *MockNPC) SetHP(hp int32) { m.hp = hp } -func (m *MockNPC) IsAlive() bool { return m.hp > 0 } -func (m *MockNPC) GetInCombat() bool { return m.inCombat } -func (m *MockNPC) InCombat(val bool) { m.inCombat = val } -func (m *MockNPC) GetTarget() Entity { return m.target } -func (m *MockNPC) SetTarget(target Entity) { m.target = target } -func (m *MockNPC) IsPet() bool { return m.isPet } -func (m *MockNPC) GetOwner() Entity { return m.owner } -func (m *MockNPC) GetX() float32 { return m.x } -func (m *MockNPC) GetY() float32 { return m.y } -func (m *MockNPC) GetZ() float32 { return m.z } -func (m *MockNPC) GetDistance(target Entity) float32 { return m.distance } -func (m *MockNPC) FaceTarget(target Entity, followCaller bool) {} -func (m *MockNPC) IsFollowing() bool { return m.following } -func (m *MockNPC) SetFollowing(val bool) { m.following = val } -func (m *MockNPC) GetFollowTarget() Spawn { return m.followTarget } -func (m *MockNPC) SetFollowTarget(target Spawn, range_ float32) { m.followTarget = target } -func (m *MockNPC) CalculateRunningLocation(clear bool) {} -func (m *MockNPC) ClearRunningLocations() {} -func (m *MockNPC) IsRunningBack() bool { return m.runningBack } -func (m *MockNPC) GetRunbackLocation() *MovementLocation { return m.runbackLocation } -func (m *MockNPC) GetRunbackDistance() float32 { return m.runbackDistance } -func (m *MockNPC) Runback(distance float32) { m.runningBack = true } -func (m *MockNPC) ShouldCallRunback() bool { return m.shouldCallRunback } -func (m *MockNPC) SetCallRunback(val bool) { m.shouldCallRunback = val } -func (m *MockNPC) IsMezzedOrStunned() bool { return m.mezzedOrStunned } -func (m *MockNPC) IsCasting() bool { return m.casting } -func (m *MockNPC) IsDazed() bool { return m.dazed } -func (m *MockNPC) IsFeared() bool { return m.feared } -func (m *MockNPC) IsStifled() bool { return m.stifled } -func (m *MockNPC) InWater() bool { return m.inWater } -func (m *MockNPC) IsWaterCreature() bool { return m.waterCreature } -func (m *MockNPC) IsFlyingCreature() bool { return m.flyingCreature } -func (m *MockNPC) AttackAllowed(target Entity) bool { return m.attackAllowed } -func (m *MockNPC) PrimaryWeaponReady() bool { return m.primaryWeaponReady } -func (m *MockNPC) SecondaryWeaponReady() bool { return m.secondaryWeaponReady } -func (m *MockNPC) SetPrimaryLastAttackTime(time int64) {} -func (m *MockNPC) SetSecondaryLastAttackTime(time int64) {} -func (m *MockNPC) MeleeAttack(target Entity, distance float32, primary bool) {} -func (m *MockNPC) GetCastPercentage() int8 { return m.castPercentage } -func (m *MockNPC) GetNextSpell(target Entity, distance float32) Spell { return m.nextSpell } -func (m *MockNPC) GetNextBuffSpell(target Spawn) Spell { return m.nextBuffSpell } -func (m *MockNPC) SetCastOnAggroCompleted(val bool) {} -func (m *MockNPC) CheckLoS(target Entity) bool { return m.checkLoS } -func (m *MockNPC) IsPauseMovementTimerActive() bool { return m.pauseMovementTimer } -func (m *MockNPC) SetEncounterState(state int8) {} -func (m *MockNPC) GetSpawnScript() string { return m.spawnScript } -func (m *MockNPC) KillSpawn(npc NPC) {} - -// Implement Entity interface (extends Spawn) -func (m *MockNPC) IsPlayer() bool { return false } -func (m *MockNPC) IsBot() bool { return false } - -// MockEntity provides a mock Entity implementation for testing -type MockEntity struct { - id int32 - name string - hp int32 - totalHP int32 - x, y, z float32 - isPlayer bool - isBot bool - isPet bool - owner Entity - inWater bool -} - -func NewMockEntity(id int32, name string) *MockEntity { - return &MockEntity{ - id: id, - name: name, - hp: 100, - totalHP: 100, - x: 0, - y: 0, - z: 0, - isPlayer: false, - isBot: false, - isPet: false, - inWater: false, - } -} - -func (m *MockEntity) GetID() int32 { return m.id } -func (m *MockEntity) GetName() string { return m.name } -func (m *MockEntity) GetHP() int32 { return m.hp } -func (m *MockEntity) GetTotalHP() int32 { return m.totalHP } -func (m *MockEntity) GetX() float32 { return m.x } -func (m *MockEntity) GetY() float32 { return m.y } -func (m *MockEntity) GetZ() float32 { return m.z } -func (m *MockEntity) IsPlayer() bool { return m.isPlayer } -func (m *MockEntity) IsBot() bool { return m.isBot } -func (m *MockEntity) IsPet() bool { return m.isPet } -func (m *MockEntity) GetOwner() Entity { return m.owner } -func (m *MockEntity) InWater() bool { return m.inWater } - -// MockSpell provides a mock Spell implementation for testing -type MockSpell struct { - id int32 - name string - friendly bool - castTime int32 - recoveryTime int32 - range_ float32 - minRange float32 -} - -func NewMockSpell(id int32, name string) *MockSpell { - return &MockSpell{ - id: id, - name: name, - friendly: false, - castTime: 1000, - recoveryTime: 2000, - range_: 30.0, - minRange: 0.0, - } -} - -func (m *MockSpell) GetSpellID() int32 { return m.id } -func (m *MockSpell) GetName() string { return m.name } -func (m *MockSpell) IsFriendlySpell() bool { return m.friendly } -func (m *MockSpell) GetCastTime() int32 { return m.castTime } -func (m *MockSpell) GetRecoveryTime() int32 { return m.recoveryTime } -func (m *MockSpell) GetRange() float32 { return m.range_ } -func (m *MockSpell) GetMinRange() float32 { return m.minRange } - -// MockLogger provides a mock Logger implementation for testing -type MockLogger struct { - messages []string -} - -func NewMockLogger() *MockLogger { - return &MockLogger{ - messages: make([]string, 0), - } -} - -func (m *MockLogger) LogInfo(message string, args ...any) { - m.messages = append(m.messages, "INFO: "+message) -} - -func (m *MockLogger) LogError(message string, args ...any) { - m.messages = append(m.messages, "ERROR: "+message) -} - -func (m *MockLogger) LogDebug(message string, args ...any) { - m.messages = append(m.messages, "DEBUG: "+message) -} - -func (m *MockLogger) LogWarning(message string, args ...any) { - m.messages = append(m.messages, "WARNING: "+message) -} - -func (m *MockLogger) GetMessages() []string { - return m.messages -} - -// MockLuaInterface provides a mock LuaInterface implementation for testing -type MockLuaInterface struct { - executed bool - lastScript string - lastFunction string - shouldError bool -} - -func NewMockLuaInterface() *MockLuaInterface { - return &MockLuaInterface{ - executed: false, - shouldError: false, - } -} - -func (m *MockLuaInterface) RunSpawnScript(script, function string, npc NPC, target Entity) error { - m.executed = true - m.lastScript = script - m.lastFunction = function - - if m.shouldError { - return fmt.Errorf("mock lua error") - } - - return nil -} - -func (m *MockLuaInterface) WasExecuted() bool { - return m.executed -} - -func (m *MockLuaInterface) SetShouldError(shouldError bool) { - m.shouldError = shouldError -} - -// Tests for HateEntry and HateList - -func TestNewHateEntry(t *testing.T) { - entityID := int32(123) - hateValue := int32(500) - - entry := NewHateEntry(entityID, hateValue) - - if entry.EntityID != entityID { - t.Errorf("Expected entity ID %d, got %d", entityID, entry.EntityID) - } - - if entry.HateValue != hateValue { - t.Errorf("Expected hate value %d, got %d", hateValue, entry.HateValue) - } - - if entry.LastUpdated == 0 { - t.Error("Expected LastUpdated to be set") - } -} - -func TestNewHateEntryMinValue(t *testing.T) { - entityID := int32(123) - hateValue := int32(0) // Below minimum - - entry := NewHateEntry(entityID, hateValue) - - if entry.HateValue != MinHateValue { - t.Errorf("Expected hate value to be adjusted to minimum %d, got %d", MinHateValue, entry.HateValue) - } -} - -func TestNewHateList(t *testing.T) { - hateList := NewHateList() - - if hateList == nil { - t.Fatal("NewHateList returned nil") - } - - if hateList.Size() != 0 { - t.Errorf("Expected empty hate list, got size %d", hateList.Size()) - } -} - -func TestHateListAddHate(t *testing.T) { - hateList := NewHateList() - entityID := int32(123) - hateValue := int32(500) - - hateList.AddHate(entityID, hateValue) - - if hateList.Size() != 1 { - t.Errorf("Expected hate list size 1, got %d", hateList.Size()) - } - - retrievedHate := hateList.GetHate(entityID) - if retrievedHate != hateValue { - t.Errorf("Expected hate value %d, got %d", hateValue, retrievedHate) - } -} - -func TestHateListAddHateUpdate(t *testing.T) { - hateList := NewHateList() - entityID := int32(123) - initialHate := int32(500) - additionalHate := int32(200) - - hateList.AddHate(entityID, initialHate) - hateList.AddHate(entityID, additionalHate) - - if hateList.Size() != 1 { - t.Errorf("Expected hate list size 1, got %d", hateList.Size()) - } - - expectedTotal := initialHate + additionalHate - retrievedHate := hateList.GetHate(entityID) - if retrievedHate != expectedTotal { - t.Errorf("Expected hate value %d, got %d", expectedTotal, retrievedHate) - } -} - -func TestHateListGetMostHated(t *testing.T) { - hateList := NewHateList() - - entity1 := int32(1) - entity2 := int32(2) - entity3 := int32(3) - - hateList.AddHate(entity1, 100) - hateList.AddHate(entity2, 500) // Highest hate - hateList.AddHate(entity3, 200) - - mostHated := hateList.GetMostHated() - if mostHated != entity2 { - t.Errorf("Expected most hated entity %d, got %d", entity2, mostHated) - } -} - -func TestHateListGetHatePercentage(t *testing.T) { - hateList := NewHateList() - - entity1 := int32(1) - entity2 := int32(2) - - hateList.AddHate(entity1, 300) // 75% of total hate (300/400) - hateList.AddHate(entity2, 100) // 25% of total hate (100/400) - - percentage1 := hateList.GetHatePercentage(entity1) - percentage2 := hateList.GetHatePercentage(entity2) - - if percentage1 != 75 { - t.Errorf("Expected entity1 hate percentage 75, got %d", percentage1) - } - - if percentage2 != 25 { - t.Errorf("Expected entity2 hate percentage 25, got %d", percentage2) - } -} - -func TestHateListRemoveHate(t *testing.T) { - hateList := NewHateList() - entityID := int32(123) - - hateList.AddHate(entityID, 500) - hateList.RemoveHate(entityID) - - if hateList.Size() != 0 { - t.Errorf("Expected empty hate list after removal, got size %d", hateList.Size()) - } - - retrievedHate := hateList.GetHate(entityID) - if retrievedHate != 0 { - t.Errorf("Expected hate value 0 after removal, got %d", retrievedHate) - } -} - -func TestHateListClear(t *testing.T) { - hateList := NewHateList() - - hateList.AddHate(1, 100) - hateList.AddHate(2, 200) - hateList.AddHate(3, 300) - - hateList.Clear() - - if hateList.Size() != 0 { - t.Errorf("Expected empty hate list after clear, got size %d", hateList.Size()) - } -} - -func TestHateListGetAllEntries(t *testing.T) { - hateList := NewHateList() - - entity1 := int32(1) - entity2 := int32(2) - hate1 := int32(100) - hate2 := int32(200) - - hateList.AddHate(entity1, hate1) - hateList.AddHate(entity2, hate2) - - entries := hateList.GetAllEntries() - - if len(entries) != 2 { - t.Errorf("Expected 2 entries, got %d", len(entries)) - } - - if entries[entity1].HateValue != hate1 { - t.Errorf("Expected entity1 hate %d, got %d", hate1, entries[entity1].HateValue) - } - - if entries[entity2].HateValue != hate2 { - t.Errorf("Expected entity2 hate %d, got %d", hate2, entries[entity2].HateValue) - } -} - -// Tests for EncounterEntry and EncounterList - -func TestNewEncounterEntry(t *testing.T) { - entityID := int32(123) - characterID := int32(456) - isPlayer := true - isBot := false - - entry := NewEncounterEntry(entityID, characterID, isPlayer, isBot) - - if entry.EntityID != entityID { - t.Errorf("Expected entity ID %d, got %d", entityID, entry.EntityID) - } - - if entry.CharacterID != characterID { - t.Errorf("Expected character ID %d, got %d", characterID, entry.CharacterID) - } - - if entry.IsPlayer != isPlayer { - t.Errorf("Expected IsPlayer %v, got %v", isPlayer, entry.IsPlayer) - } - - if entry.IsBot != isBot { - t.Errorf("Expected IsBot %v, got %v", isBot, entry.IsBot) - } - - if entry.AddedTime == 0 { - t.Error("Expected AddedTime to be set") - } -} - -func TestNewEncounterList(t *testing.T) { - encounterList := NewEncounterList() - - if encounterList == nil { - t.Fatal("NewEncounterList returned nil") - } - - if encounterList.Size() != 0 { - t.Errorf("Expected empty encounter list, got size %d", encounterList.Size()) - } - - if encounterList.HasPlayerInEncounter() { - t.Error("Expected no players in encounter initially") - } -} - -func TestEncounterListAddEntity(t *testing.T) { - encounterList := NewEncounterList() - entityID := int32(123) - characterID := int32(456) - - success := encounterList.AddEntity(entityID, characterID, true, false) - - if !success { - t.Error("Expected AddEntity to succeed") - } - - if encounterList.Size() != 1 { - t.Errorf("Expected encounter list size 1, got %d", encounterList.Size()) - } - - if !encounterList.IsEntityInEncounter(entityID) { - t.Error("Entity should be in encounter") - } - - if !encounterList.IsPlayerInEncounter(characterID) { - t.Error("Player should be in encounter") - } - - if !encounterList.HasPlayerInEncounter() { - t.Error("Should have player in encounter") - } -} - -func TestEncounterListAddEntityDuplicate(t *testing.T) { - encounterList := NewEncounterList() - entityID := int32(123) - - success1 := encounterList.AddEntity(entityID, 0, false, false) - success2 := encounterList.AddEntity(entityID, 0, false, false) // Duplicate - - if !success1 { - t.Error("Expected first AddEntity to succeed") - } - - if success2 { - t.Error("Expected second AddEntity to fail (duplicate)") - } - - if encounterList.Size() != 1 { - t.Errorf("Expected encounter list size 1 after duplicate add, got %d", encounterList.Size()) - } -} - -func TestEncounterListRemoveEntity(t *testing.T) { - encounterList := NewEncounterList() - entityID := int32(123) - characterID := int32(456) - - encounterList.AddEntity(entityID, characterID, true, false) - encounterList.RemoveEntity(entityID) - - if encounterList.Size() != 0 { - t.Errorf("Expected empty encounter list after removal, got size %d", encounterList.Size()) - } - - if encounterList.IsEntityInEncounter(entityID) { - t.Error("Entity should not be in encounter after removal") - } - - if encounterList.IsPlayerInEncounter(characterID) { - t.Error("Player should not be in encounter after removal") - } - - if encounterList.HasPlayerInEncounter() { - t.Error("Should not have player in encounter after removal") - } -} - -func TestEncounterListCountPlayerBots(t *testing.T) { - encounterList := NewEncounterList() - - encounterList.AddEntity(1, 101, true, false) // Player - encounterList.AddEntity(2, 0, false, true) // Bot - encounterList.AddEntity(3, 0, false, false) // NPC - - count := encounterList.CountPlayerBots() - if count != 2 { - t.Errorf("Expected 2 players/bots, got %d", count) - } -} - -func TestEncounterListGetAllEntityIDs(t *testing.T) { - encounterList := NewEncounterList() - - entity1 := int32(1) - entity2 := int32(2) - entity3 := int32(3) - - encounterList.AddEntity(entity1, 0, false, false) - encounterList.AddEntity(entity2, 0, false, false) - encounterList.AddEntity(entity3, 0, false, false) - - entityIDs := encounterList.GetAllEntityIDs() - - if len(entityIDs) != 3 { - t.Errorf("Expected 3 entity IDs, got %d", len(entityIDs)) - } - - // Check that all IDs are present (order doesn't matter) - idMap := make(map[int32]bool) - for _, id := range entityIDs { - idMap[id] = true - } - - if !idMap[entity1] || !idMap[entity2] || !idMap[entity3] { - t.Error("Expected all entity IDs to be present") - } -} - -func TestEncounterListClear(t *testing.T) { - encounterList := NewEncounterList() - - encounterList.AddEntity(1, 101, true, false) - encounterList.AddEntity(2, 0, false, false) - - encounterList.Clear() - - if encounterList.Size() != 0 { - t.Errorf("Expected empty encounter list after clear, got size %d", encounterList.Size()) - } - - if encounterList.HasPlayerInEncounter() { - t.Error("Should not have player in encounter after clear") - } -} - -// Tests for BrainState - -func TestNewBrainState(t *testing.T) { - state := NewBrainState() - - if state == nil { - t.Fatal("NewBrainState returned nil") - } - - if state.GetState() != AIStateIdle { - t.Errorf("Expected initial state %d, got %d", AIStateIdle, state.GetState()) - } - - if !state.IsActive() { - t.Error("Expected brain state to be active initially") - } - - if state.GetThinkTick() != DefaultThinkTick { - t.Errorf("Expected think tick %d, got %d", DefaultThinkTick, state.GetThinkTick()) - } - - if state.GetDebugLevel() != DebugLevelNone { - t.Errorf("Expected debug level %d, got %d", DebugLevelNone, state.GetDebugLevel()) - } -} - -func TestBrainStateSetState(t *testing.T) { - state := NewBrainState() - newState := AIStateCombat - - state.SetState(newState) - - if state.GetState() != newState { - t.Errorf("Expected state %d, got %d", newState, state.GetState()) - } -} - -func TestBrainStateSetActive(t *testing.T) { - state := NewBrainState() - - state.SetActive(false) - if state.IsActive() { - t.Error("Expected brain state to be inactive") - } - - state.SetActive(true) - if !state.IsActive() { - t.Error("Expected brain state to be active") - } -} - -func TestBrainStateSetThinkTick(t *testing.T) { - state := NewBrainState() - - // Test normal value - newTick := int32(500) - state.SetThinkTick(newTick) - if state.GetThinkTick() != newTick { - t.Errorf("Expected think tick %d, got %d", newTick, state.GetThinkTick()) - } - - // Test minimum value - state.SetThinkTick(0) - if state.GetThinkTick() != 1 { - t.Errorf("Expected think tick to be adjusted to minimum 1, got %d", state.GetThinkTick()) - } - - // Test maximum value - state.SetThinkTick(MaxThinkTick + 1000) - if state.GetThinkTick() != MaxThinkTick { - t.Errorf("Expected think tick to be adjusted to maximum %d, got %d", MaxThinkTick, state.GetThinkTick()) - } -} - -func TestBrainStateSetLastThink(t *testing.T) { - state := NewBrainState() - timestamp := time.Now().UnixMilli() - - state.SetLastThink(timestamp) - - if state.GetLastThink() != timestamp { - t.Errorf("Expected last think timestamp %d, got %d", timestamp, state.GetLastThink()) - } -} - -func TestBrainStateSpellRecovery(t *testing.T) { - state := NewBrainState() - - // Test future recovery time - futureTime := time.Now().UnixMilli() + 5000 - state.SetSpellRecovery(futureTime) - - if state.GetSpellRecovery() != futureTime { - t.Errorf("Expected spell recovery time %d, got %d", futureTime, state.GetSpellRecovery()) - } - - if state.HasRecovered() { - t.Error("Expected brain to not have recovered yet") - } - - // Test past recovery time - pastTime := time.Now().UnixMilli() - 5000 - state.SetSpellRecovery(pastTime) - - if !state.HasRecovered() { - t.Error("Expected brain to have recovered") - } -} - -func TestBrainStateDebugLevel(t *testing.T) { - state := NewBrainState() - debugLevel := DebugLevelVerbose - - state.SetDebugLevel(debugLevel) - - if state.GetDebugLevel() != debugLevel { - t.Errorf("Expected debug level %d, got %d", debugLevel, state.GetDebugLevel()) - } -} - -// Tests for BrainStatistics - -func TestNewBrainStatistics(t *testing.T) { - stats := NewBrainStatistics() - - if stats == nil { - t.Fatal("NewBrainStatistics returned nil") - } - - if stats.ThinkCycles != 0 { - t.Errorf("Expected think cycles 0, got %d", stats.ThinkCycles) - } - - if stats.SpellsCast != 0 { - t.Errorf("Expected spells cast 0, got %d", stats.SpellsCast) - } - - if stats.MeleeAttacks != 0 { - t.Errorf("Expected melee attacks 0, got %d", stats.MeleeAttacks) - } - - if stats.AverageThinkTime != 0.0 { - t.Errorf("Expected average think time 0.0, got %f", stats.AverageThinkTime) - } - - if stats.LastThinkTime == 0 { - t.Error("Expected LastThinkTime to be set") - } -} - -// Tests for BaseBrain - -func TestNewBaseBrain(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - - brain := NewBaseBrain(npc, logger) - - if brain == nil { - t.Fatal("NewBaseBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeDefault { - t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) - } - - if !brain.IsActive() { - t.Error("Expected brain to be active initially") - } - - if brain.GetBody() != npc { - t.Error("Expected brain body to be the provided NPC") - } -} - -func TestBaseBrainHateManagement(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - brain := NewBaseBrain(npc, logger) - - entityID := int32(456) - hateValue := int32(500) - - // Add hate - brain.AddHate(entityID, hateValue) - - if brain.GetHate(entityID) != hateValue { - t.Errorf("Expected hate value %d, got %d", hateValue, brain.GetHate(entityID)) - } - - if brain.GetMostHated() != entityID { - t.Errorf("Expected most hated entity %d, got %d", entityID, brain.GetMostHated()) - } - - // Clear hate for entity - brain.ClearHateForEntity(entityID) - - if brain.GetHate(entityID) != 0 { - t.Errorf("Expected hate value 0 after clearing, got %d", brain.GetHate(entityID)) - } - - // Add hate again and clear all - brain.AddHate(entityID, hateValue) - brain.ClearHate() - - if brain.GetHate(entityID) != 0 { - t.Errorf("Expected hate value 0 after clearing all, got %d", brain.GetHate(entityID)) - } -} - -func TestBaseBrainEncounterManagement(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - brain := NewBaseBrain(npc, logger) - - entityID := int32(456) - characterID := int32(789) - - // Add to encounter - success := brain.AddToEncounter(entityID, characterID, true, false) - - if !success { - t.Error("Expected AddToEncounter to succeed") - } - - if !brain.IsEntityInEncounter(entityID) { - t.Error("Entity should be in encounter") - } - - if !brain.IsPlayerInEncounter(characterID) { - t.Error("Player should be in encounter") - } - - if !brain.HasPlayerInEncounter() { - t.Error("Should have player in encounter") - } - - if brain.GetEncounterSize() != 1 { - t.Errorf("Expected encounter size 1, got %d", brain.GetEncounterSize()) - } - - // Check loot allowed - if !brain.CheckLootAllowed(entityID) { - t.Error("Loot should be allowed for entity in encounter") - } - - // Clear encounter - brain.ClearEncounter() - - if brain.IsEntityInEncounter(entityID) { - t.Error("Entity should not be in encounter after clear") - } - - if brain.GetEncounterSize() != 0 { - t.Errorf("Expected encounter size 0 after clear, got %d", brain.GetEncounterSize()) - } -} - -func TestBaseBrainStatistics(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - brain := NewBaseBrain(npc, logger) - - // Get initial statistics - stats := brain.GetStatistics() - if stats == nil { - t.Fatal("GetStatistics returned nil") - } - - // Reset statistics - brain.ResetStatistics() - - // Get statistics again - newStats := brain.GetStatistics() - if newStats.ThinkCycles != 0 { - t.Errorf("Expected think cycles 0 after reset, got %d", newStats.ThinkCycles) - } -} - -func TestBaseBrainSetBody(t *testing.T) { - npc1 := NewMockNPC(123, "TestNPC1") - npc2 := NewMockNPC(456, "TestNPC2") - logger := NewMockLogger() - brain := NewBaseBrain(npc1, logger) - - if brain.GetBody() != npc1 { - t.Error("Expected brain body to be npc1 initially") - } - - brain.SetBody(npc2) - - if brain.GetBody() != npc2 { - t.Error("Expected brain body to be npc2 after SetBody") - } -} - -// Tests for Brain Variants - -func TestNewCombatPetBrain(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - npc.isPet = true - logger := NewMockLogger() - - brain := NewCombatPetBrain(npc, logger) - - if brain == nil { - t.Fatal("NewCombatPetBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeCombatPet { - t.Errorf("Expected brain type %d, got %d", BrainTypeCombatPet, brain.GetBrainType()) - } -} - -func TestNewNonCombatPetBrain(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - npc.isPet = true - logger := NewMockLogger() - - brain := NewNonCombatPetBrain(npc, logger) - - if brain == nil { - t.Fatal("NewNonCombatPetBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeNonCombatPet { - t.Errorf("Expected brain type %d, got %d", BrainTypeNonCombatPet, brain.GetBrainType()) - } -} - -func TestNewBlankBrain(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - - brain := NewBlankBrain(npc, logger) - - if brain == nil { - t.Fatal("NewBlankBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeBlank { - t.Errorf("Expected brain type %d, got %d", BrainTypeBlank, brain.GetBrainType()) - } - - if brain.GetThinkTick() != BlankBrainTick { - t.Errorf("Expected think tick %d, got %d", BlankBrainTick, brain.GetThinkTick()) - } -} - -func TestBlankBrainThink(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - brain := NewBlankBrain(npc, logger) - - // Blank brain Think should do nothing and not error - err := brain.Think() - if err != nil { - t.Errorf("Expected no error from blank brain Think, got: %v", err) - } -} - -func TestNewLuaBrain(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - npc.spawnScript = "test_script.lua" - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - - brain := NewLuaBrain(npc, logger, luaInterface) - - if brain == nil { - t.Fatal("NewLuaBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeLua { - t.Errorf("Expected brain type %d, got %d", BrainTypeLua, brain.GetBrainType()) - } -} - -func TestLuaBrainThink(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - npc.spawnScript = "test_script.lua" - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - - brain := NewLuaBrain(npc, logger, luaInterface) - - err := brain.Think() - if err != nil { - t.Errorf("Expected no error from Lua brain Think, got: %v", err) - } - - if !luaInterface.WasExecuted() { - t.Error("Expected Lua interface to be executed") - } - - if luaInterface.lastFunction != "Think" { - t.Errorf("Expected Lua function 'Think', got '%s'", luaInterface.lastFunction) - } -} - -func TestLuaBrainThinkError(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - npc.spawnScript = "test_script.lua" - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - luaInterface.SetShouldError(true) - - brain := NewLuaBrain(npc, logger, luaInterface) - - err := brain.Think() - if err == nil { - t.Error("Expected error from Lua brain Think") - } -} - -func TestNewDumbFirePetBrain(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - target := NewMockEntity(456, "Target") - expireTime := int32(10000) // 10 seconds - logger := NewMockLogger() - - brain := NewDumbFirePetBrain(npc, target, expireTime, logger) - - if brain == nil { - t.Fatal("NewDumbFirePetBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeDumbFire { - t.Errorf("Expected brain type %d, got %d", BrainTypeDumbFire, brain.GetBrainType()) - } - - // Should have max hate for target - if brain.GetHate(target.GetID()) != MaxHateValue { - t.Errorf("Expected max hate for target, got %d", brain.GetHate(target.GetID())) - } - - if brain.IsExpired() { - t.Error("Dumbfire pet should not be expired immediately") - } -} - -func TestDumbFirePetBrainExpiry(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - target := NewMockEntity(456, "Target") - expireTime := int32(100) // 100ms - logger := NewMockLogger() - - brain := NewDumbFirePetBrain(npc, target, expireTime, logger) - - // Wait for expiry - time.Sleep(150 * time.Millisecond) - - if !brain.IsExpired() { - t.Error("Dumbfire pet should be expired") - } -} - -func TestDumbFirePetBrainExtendExpireTime(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - target := NewMockEntity(456, "Target") - expireTime := int32(1000) - logger := NewMockLogger() - - brain := NewDumbFirePetBrain(npc, target, expireTime, logger) - originalExpireTime := brain.GetExpireTime() - - extension := int32(5000) - brain.ExtendExpireTime(extension) - - newExpireTime := brain.GetExpireTime() - if newExpireTime != originalExpireTime+int64(extension) { - t.Errorf("Expected expire time %d, got %d", originalExpireTime+int64(extension), newExpireTime) - } -} - -// Tests for CreateBrain factory function - -func TestCreateBrainDefault(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - - brain := CreateBrain(npc, BrainTypeDefault, logger) - - if brain == nil { - t.Fatal("CreateBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeDefault { - t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) - } -} - -func TestCreateBrainCombatPet(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - logger := NewMockLogger() - - brain := CreateBrain(npc, BrainTypeCombatPet, logger) - - if brain.GetBrainType() != BrainTypeCombatPet { - t.Errorf("Expected brain type %d, got %d", BrainTypeCombatPet, brain.GetBrainType()) - } -} - -func TestCreateBrainLua(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - - brain := CreateBrain(npc, BrainTypeLua, logger, luaInterface) - - if brain.GetBrainType() != BrainTypeLua { - t.Errorf("Expected brain type %d, got %d", BrainTypeLua, brain.GetBrainType()) - } -} - -func TestCreateBrainDumbFire(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - target := NewMockEntity(456, "Target") - expireTime := int32(10000) - logger := NewMockLogger() - - brain := CreateBrain(npc, BrainTypeDumbFire, logger, target, expireTime) - - if brain.GetBrainType() != BrainTypeDumbFire { - t.Errorf("Expected brain type %d, got %d", BrainTypeDumbFire, brain.GetBrainType()) - } -} - -// Tests for AIManager - -func TestNewAIManager(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - - manager := NewAIManager(logger, luaInterface) - - if manager == nil { - t.Fatal("NewAIManager returned nil") - } - - if manager.GetBrainCount() != 0 { - t.Errorf("Expected brain count 0, got %d", manager.GetBrainCount()) - } - - if manager.GetActiveCount() != 0 { - t.Errorf("Expected active count 0, got %d", manager.GetActiveCount()) - } - - if manager.GetTotalThinks() != 0 { - t.Errorf("Expected total thinks 0, got %d", manager.GetTotalThinks()) - } -} - -func TestAIManagerAddBrain(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc := NewMockNPC(123, "TestNPC") - brain := NewBaseBrain(npc, logger) - npcID := npc.GetID() - - err := manager.AddBrain(npcID, brain) - if err != nil { - t.Errorf("Expected no error adding brain, got: %v", err) - } - - if manager.GetBrainCount() != 1 { - t.Errorf("Expected brain count 1, got %d", manager.GetBrainCount()) - } - - if manager.GetActiveCount() != 1 { - t.Errorf("Expected active count 1, got %d", manager.GetActiveCount()) - } - - retrievedBrain := manager.GetBrain(npcID) - if retrievedBrain != brain { - t.Error("Retrieved brain should be the same as added brain") - } -} - -func TestAIManagerAddBrainDuplicate(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc := NewMockNPC(123, "TestNPC") - brain1 := NewBaseBrain(npc, logger) - brain2 := NewBaseBrain(npc, logger) - npcID := npc.GetID() - - err1 := manager.AddBrain(npcID, brain1) - err2 := manager.AddBrain(npcID, brain2) // Duplicate - - if err1 != nil { - t.Errorf("Expected no error adding first brain, got: %v", err1) - } - - if err2 == nil { - t.Error("Expected error adding duplicate brain") - } - - if manager.GetBrainCount() != 1 { - t.Errorf("Expected brain count 1 after duplicate add, got %d", manager.GetBrainCount()) - } -} - -func TestAIManagerRemoveBrain(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc := NewMockNPC(123, "TestNPC") - brain := NewBaseBrain(npc, logger) - npcID := npc.GetID() - - manager.AddBrain(npcID, brain) - manager.RemoveBrain(npcID) - - if manager.GetBrainCount() != 0 { - t.Errorf("Expected brain count 0 after removal, got %d", manager.GetBrainCount()) - } - - if manager.GetActiveCount() != 0 { - t.Errorf("Expected active count 0 after removal, got %d", manager.GetActiveCount()) - } - - retrievedBrain := manager.GetBrain(npcID) - if retrievedBrain != nil { - t.Error("Retrieved brain should be nil after removal") - } -} - -func TestAIManagerCreateBrainForNPC(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc := NewMockNPC(123, "TestNPC") - - err := manager.CreateBrainForNPC(npc, BrainTypeDefault) - if err != nil { - t.Errorf("Expected no error creating brain, got: %v", err) - } - - brain := manager.GetBrain(npc.GetID()) - if brain == nil { - t.Error("Expected brain to be created") - } - - if brain.GetBrainType() != BrainTypeDefault { - t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) - } -} - -func TestAIManagerSetBrainActive(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc := NewMockNPC(123, "TestNPC") - brain := NewBaseBrain(npc, logger) - npcID := npc.GetID() - - manager.AddBrain(npcID, brain) - - // Initially active - if manager.GetActiveCount() != 1 { - t.Errorf("Expected active count 1, got %d", manager.GetActiveCount()) - } - - // Set inactive - manager.SetBrainActive(npcID, false) - if manager.GetActiveCount() != 0 { - t.Errorf("Expected active count 0, got %d", manager.GetActiveCount()) - } - - // Set active again - manager.SetBrainActive(npcID, true) - if manager.GetActiveCount() != 1 { - t.Errorf("Expected active count 1, got %d", manager.GetActiveCount()) - } -} - -func TestAIManagerGetBrainsByType(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc1 := NewMockNPC(123, "TestNPC1") - npc2 := NewMockNPC(456, "TestNPC2") - npc3 := NewMockNPC(789, "TestNPC3") - - brain1 := NewBaseBrain(npc1, logger) - brain2 := NewCombatPetBrain(npc2, logger) - brain3 := NewBlankBrain(npc3, logger) - - manager.AddBrain(npc1.GetID(), brain1) - manager.AddBrain(npc2.GetID(), brain2) - manager.AddBrain(npc3.GetID(), brain3) - - defaultBrains := manager.GetBrainsByType(BrainTypeDefault) - if len(defaultBrains) != 1 { - t.Errorf("Expected 1 default brain, got %d", len(defaultBrains)) - } - - petBrains := manager.GetBrainsByType(BrainTypeCombatPet) - if len(petBrains) != 1 { - t.Errorf("Expected 1 combat pet brain, got %d", len(petBrains)) - } - - blankBrains := manager.GetBrainsByType(BrainTypeBlank) - if len(blankBrains) != 1 { - t.Errorf("Expected 1 blank brain, got %d", len(blankBrains)) - } -} - -func TestAIManagerClearAllBrains(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc1 := NewMockNPC(123, "TestNPC1") - npc2 := NewMockNPC(456, "TestNPC2") - - brain1 := NewBaseBrain(npc1, logger) - brain2 := NewBaseBrain(npc2, logger) - - manager.AddBrain(npc1.GetID(), brain1) - manager.AddBrain(npc2.GetID(), brain2) - - manager.ClearAllBrains() - - if manager.GetBrainCount() != 0 { - t.Errorf("Expected brain count 0 after clear, got %d", manager.GetBrainCount()) - } - - if manager.GetActiveCount() != 0 { - t.Errorf("Expected active count 0 after clear, got %d", manager.GetActiveCount()) - } -} - -func TestAIManagerGetStatistics(t *testing.T) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - npc1 := NewMockNPC(123, "TestNPC1") - npc2 := NewMockNPC(456, "TestNPC2") - - brain1 := NewBaseBrain(npc1, logger) - brain2 := NewCombatPetBrain(npc2, logger) - - manager.AddBrain(npc1.GetID(), brain1) - manager.AddBrain(npc2.GetID(), brain2) - - stats := manager.GetStatistics() - - if stats == nil { - t.Fatal("GetStatistics returned nil") - } - - if stats.TotalBrains != 2 { - t.Errorf("Expected total brains 2, got %d", stats.TotalBrains) - } - - if stats.ActiveBrains != 2 { - t.Errorf("Expected active brains 2, got %d", stats.ActiveBrains) - } - - if len(stats.BrainsByType) == 0 { - t.Error("Expected brain types to be populated") - } -} - -// Tests for utility functions - -func TestGetBrainTypeName(t *testing.T) { - testCases := []struct { - brainType int8 - expected string - }{ - {BrainTypeDefault, "default"}, - {BrainTypeCombatPet, "combat_pet"}, - {BrainTypeNonCombatPet, "non_combat_pet"}, - {BrainTypeBlank, "blank"}, - {BrainTypeLua, "lua"}, - {BrainTypeDumbFire, "dumbfire"}, - {99, "unknown"}, // Unknown type - } - - for _, tc := range testCases { - result := getBrainTypeName(tc.brainType) - if result != tc.expected { - t.Errorf("Expected brain type name '%s' for type %d, got '%s'", tc.expected, tc.brainType, result) - } - } -} - -func TestCurrentTimeMillis(t *testing.T) { - before := time.Now().UnixMilli() - result := currentTimeMillis() - after := time.Now().UnixMilli() - - if result < before || result > after { - t.Errorf("Expected current time to be between %d and %d, got %d", before, after, result) - } -} - -// Tests for AIBrainAdapter - -func TestNewAIBrainAdapter(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - - adapter := NewAIBrainAdapter(npc, logger) - - if adapter == nil { - t.Fatal("NewAIBrainAdapter returned nil") - } - - if adapter.GetNPC() != npc { - t.Error("Expected adapter NPC to be the provided NPC") - } -} - -func TestAIBrainAdapterSetupDefaultBrain(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - adapter := NewAIBrainAdapter(npc, logger) - - brain := adapter.SetupDefaultBrain() - - if brain == nil { - t.Fatal("SetupDefaultBrain returned nil") - } - - if brain.GetBrainType() != BrainTypeDefault { - t.Errorf("Expected brain type %d, got %d", BrainTypeDefault, brain.GetBrainType()) - } -} - -func TestAIBrainAdapterSetupPetBrain(t *testing.T) { - npc := NewMockNPC(123, "TestPet") - logger := NewMockLogger() - adapter := NewAIBrainAdapter(npc, logger) - - // Test combat pet brain - combatBrain := adapter.SetupPetBrain(true) - if combatBrain.GetBrainType() != BrainTypeCombatPet { - t.Errorf("Expected combat pet brain type %d, got %d", BrainTypeCombatPet, combatBrain.GetBrainType()) - } - - // Test non-combat pet brain - nonCombatBrain := adapter.SetupPetBrain(false) - if nonCombatBrain.GetBrainType() != BrainTypeNonCombatPet { - t.Errorf("Expected non-combat pet brain type %d, got %d", BrainTypeNonCombatPet, nonCombatBrain.GetBrainType()) - } -} - -func TestAIBrainAdapterProcessAI(t *testing.T) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - adapter := NewAIBrainAdapter(npc, logger) - - brain := NewBaseBrain(npc, logger) - - err := adapter.ProcessAI(brain) - if err != nil { - t.Errorf("Expected no error processing AI, got: %v", err) - } - - // Test with nil brain - err = adapter.ProcessAI(nil) - if err == nil { - t.Error("Expected error processing AI with nil brain") - } - - // Test with inactive brain - brain.SetActive(false) - err = adapter.ProcessAI(brain) - if err != nil { - t.Errorf("Expected no error processing inactive AI, got: %v", err) - } -} - -// Tests for HateListDebugger - -func TestNewHateListDebugger(t *testing.T) { - logger := NewMockLogger() - debugger := NewHateListDebugger(logger) - - if debugger == nil { - t.Fatal("NewHateListDebugger returned nil") - } -} - -func TestHateListDebuggerPrintHateList(t *testing.T) { - logger := NewMockLogger() - debugger := NewHateListDebugger(logger) - - hateList := make(map[int32]*HateEntry) - hateList[123] = &HateEntry{EntityID: 123, HateValue: 500} - hateList[456] = &HateEntry{EntityID: 456, HateValue: 300} - - debugger.PrintHateList("TestNPC", hateList) - - messages := logger.GetMessages() - if len(messages) == 0 { - t.Error("Expected debug messages to be logged") - } -} - -func TestHateListDebuggerPrintEncounterList(t *testing.T) { - logger := NewMockLogger() - debugger := NewHateListDebugger(logger) - - encounterList := make(map[int32]*EncounterEntry) - encounterList[123] = &EncounterEntry{EntityID: 123, IsPlayer: true} - encounterList[456] = &EncounterEntry{EntityID: 456, IsBot: true} - - debugger.PrintEncounterList("TestNPC", encounterList) - - messages := logger.GetMessages() - if len(messages) == 0 { - t.Error("Expected debug messages to be logged") - } -} - -// Benchmark tests - -func BenchmarkHateListAddHate(b *testing.B) { - hateList := NewHateList() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - hateList.AddHate(int32(i), 100) - } -} - -func BenchmarkHateListGetMostHated(b *testing.B) { - hateList := NewHateList() - - // Populate with some data - for i := 0; i < 100; i++ { - hateList.AddHate(int32(i), int32(i*10)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - hateList.GetMostHated() - } -} - -func BenchmarkBrainThink(b *testing.B) { - npc := NewMockNPC(123, "TestNPC") - logger := NewMockLogger() - brain := NewBaseBrain(npc, logger) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - brain.Think() - } -} - -func BenchmarkEncounterListAddEntity(b *testing.B) { - encounterList := NewEncounterList() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - encounterList.AddEntity(int32(i), int32(i*10), i%2 == 0, i%3 == 0) - } -} - -func BenchmarkAIManagerProcessAllBrains(b *testing.B) { - logger := NewMockLogger() - luaInterface := NewMockLuaInterface() - manager := NewAIManager(logger, luaInterface) - - // Add some brains - for i := 0; i < 10; i++ { - npc := NewMockNPC(int32(i), "TestNPC") - brain := NewBaseBrain(npc, logger) - manager.AddBrain(int32(i), brain) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.ProcessAllBrains() - } -} \ No newline at end of file diff --git a/internal/npc/npc_test.go b/internal/npc/npc_test.go deleted file mode 100644 index fb6125d..0000000 --- a/internal/npc/npc_test.go +++ /dev/null @@ -1,767 +0,0 @@ -package npc - -import ( - "fmt" - "strings" - "testing" -) - -// Mock implementations for testing - -// MockDatabase implements the Database interface for testing -type MockDatabase struct { - npcs map[int32]*NPC - spells map[int32][]*NPCSpell - skills map[int32]map[string]*Skill - created bool -} - -func NewMockDatabase() *MockDatabase { - return &MockDatabase{ - npcs: make(map[int32]*NPC), - spells: make(map[int32][]*NPCSpell), - skills: make(map[int32]map[string]*Skill), - created: false, - } -} - -func (md *MockDatabase) LoadAllNPCs() ([]*NPC, error) { - var npcs []*NPC - for _, npc := range md.npcs { - // Create a copy to avoid modifying the stored version - npcCopy := NewNPCFromExisting(npc) - npcs = append(npcs, npcCopy) - } - return npcs, nil -} - -func (md *MockDatabase) SaveNPC(npc *NPC) error { - if npc == nil || !npc.IsValid() { - return fmt.Errorf("invalid NPC") - } - md.npcs[npc.GetNPCID()] = NewNPCFromExisting(npc) - return nil -} - -func (md *MockDatabase) DeleteNPC(npcID int32) error { - if _, exists := md.npcs[npcID]; !exists { - return fmt.Errorf("NPC with ID %d not found", npcID) - } - delete(md.npcs, npcID) - delete(md.spells, npcID) - delete(md.skills, npcID) - return nil -} - -func (md *MockDatabase) LoadNPCSpells(npcID int32) ([]*NPCSpell, error) { - if spells, exists := md.spells[npcID]; exists { - var result []*NPCSpell - for _, spell := range spells { - result = append(result, spell.Copy()) - } - return result, nil - } - return []*NPCSpell{}, nil -} - -func (md *MockDatabase) SaveNPCSpells(npcID int32, spells []*NPCSpell) error { - var spellCopies []*NPCSpell - for _, spell := range spells { - if spell != nil { - spellCopies = append(spellCopies, spell.Copy()) - } - } - md.spells[npcID] = spellCopies - return nil -} - -func (md *MockDatabase) LoadNPCSkills(npcID int32) (map[string]*Skill, error) { - if skills, exists := md.skills[npcID]; exists { - result := make(map[string]*Skill) - for name, skill := range skills { - result[name] = NewSkill(skill.SkillID, skill.Name, skill.GetCurrentVal(), skill.MaxVal) - } - return result, nil - } - return make(map[string]*Skill), nil -} - -func (md *MockDatabase) SaveNPCSkills(npcID int32, skills map[string]*Skill) error { - skillCopies := make(map[string]*Skill) - for name, skill := range skills { - if skill != nil { - skillCopies[name] = NewSkill(skill.SkillID, skill.Name, skill.GetCurrentVal(), skill.MaxVal) - } - } - md.skills[npcID] = skillCopies - return nil -} - -// MockLogger implements the Logger interface for testing -type MockLogger struct { - logs []string -} - -func NewMockLogger() *MockLogger { - return &MockLogger{ - logs: make([]string, 0), - } -} - -func (ml *MockLogger) LogInfo(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...)) -} - -func (ml *MockLogger) LogError(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...)) -} - -func (ml *MockLogger) LogDebug(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...)) -} - -func (ml *MockLogger) LogWarning(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...)) -} - -func (ml *MockLogger) GetLogs() []string { - return ml.logs -} - -func (ml *MockLogger) Clear() { - ml.logs = ml.logs[:0] -} - -// Test functions - -func TestNewNPC(t *testing.T) { - npc := NewNPC() - if npc == nil { - t.Fatal("NewNPC returned nil") - } - - if npc.Entity == nil { - t.Error("NPC should have an Entity") - } - - if npc.GetNPCID() != 0 { - t.Errorf("Expected NPC ID 0, got %d", npc.GetNPCID()) - } - - if npc.GetAIStrategy() != AIStrategyBalanced { - t.Errorf("Expected AI strategy %d, got %d", AIStrategyBalanced, npc.GetAIStrategy()) - } - - if npc.GetAggroRadius() != DefaultAggroRadius { - t.Errorf("Expected aggro radius %f, got %f", DefaultAggroRadius, npc.GetAggroRadius()) - } - - if npc.GetBrain() == nil { - t.Error("NPC should have a brain") - } -} - -func TestNPCBasicProperties(t *testing.T) { - npc := NewNPC() - - // Test NPC ID - testNPCID := int32(12345) - npc.SetNPCID(testNPCID) - if npc.GetNPCID() != testNPCID { - t.Errorf("Expected NPC ID %d, got %d", testNPCID, npc.GetNPCID()) - } - - // Test AI Strategy - npc.SetAIStrategy(AIStrategyOffensive) - if npc.GetAIStrategy() != AIStrategyOffensive { - t.Errorf("Expected AI strategy %d, got %d", AIStrategyOffensive, npc.GetAIStrategy()) - } - - // Test Aggro Radius - testRadius := float32(25.5) - npc.SetAggroRadius(testRadius, false) - if npc.GetAggroRadius() != testRadius { - t.Errorf("Expected aggro radius %f, got %f", testRadius, npc.GetAggroRadius()) - } - - // Test Appearance ID - testAppearanceID := int32(5432) - npc.SetAppearanceID(testAppearanceID) - if npc.GetAppearanceID() != testAppearanceID { - t.Errorf("Expected appearance ID %d, got %d", testAppearanceID, npc.GetAppearanceID()) - } -} - -func TestNPCEntityIntegration(t *testing.T) { - npc := NewNPC() - if npc.Entity == nil { - t.Fatal("NPC should have an Entity") - } - - // Test entity properties through NPC - testName := "Test NPC" - npc.Entity.SetName(testName) - // Trim the name to handle fixed-size array padding - retrievedName := strings.TrimRight(npc.Entity.GetName(), "\x00") - if retrievedName != testName { - t.Errorf("Expected name '%s', got '%s'", testName, retrievedName) - } - - // Test level through InfoStruct since Entity doesn't have SetLevel - testLevel := int16(25) - if npc.Entity.GetInfoStruct() != nil { - npc.Entity.GetInfoStruct().SetLevel(testLevel) - if npc.Entity.GetLevel() != int8(testLevel) { - t.Errorf("Expected level %d, got %d", testLevel, npc.Entity.GetLevel()) - } - } - - testHP := int32(1500) - npc.Entity.SetHP(testHP) - if npc.Entity.GetHP() != testHP { - t.Errorf("Expected HP %d, got %d", testHP, npc.Entity.GetHP()) - } -} - -func TestNPCSpells(t *testing.T) { - npc := NewNPC() - - // Test initial spell state - if npc.HasSpells() { - t.Error("New NPC should not have spells") - } - - if len(npc.GetSpells()) != 0 { - t.Errorf("Expected 0 spells, got %d", len(npc.GetSpells())) - } - - // Create test spells (without cast-on flags so they go into main spells array) - spell1 := NewNPCSpell() - spell1.SetSpellID(100) - spell1.SetTier(1) - - spell2 := NewNPCSpell() - spell2.SetSpellID(200) - spell2.SetTier(2) - - spells := []*NPCSpell{spell1, spell2} - npc.SetSpells(spells) - - // Test spell retrieval - retrievedSpells := npc.GetSpells() - if len(retrievedSpells) != 2 { - t.Errorf("Expected 2 spells, got %d", len(retrievedSpells)) - } - - if npc.HasSpells() != true { - t.Error("NPC should have spells after setting them") - } -} - -func TestNPCSkills(t *testing.T) { - npc := NewNPC() - - // Create test skills - skill1 := NewSkill(1, "Sword", 50, 100) - skill2 := NewSkill(2, "Shield", 75, 100) - - skills := map[string]*Skill{ - "Sword": skill1, - "Shield": skill2, - } - - npc.SetSkills(skills) - - // Test skill retrieval by name - retrievedSkill := npc.GetSkillByName("Sword", false) - if retrievedSkill == nil { - t.Fatal("Should retrieve Sword skill") - } - - if retrievedSkill.GetCurrentVal() != 50 { - t.Errorf("Expected skill value 50, got %d", retrievedSkill.GetCurrentVal()) - } - - // Test non-existent skill - nonExistentSkill := npc.GetSkillByName("Magic", false) - if nonExistentSkill != nil { - t.Error("Should not retrieve non-existent skill") - } -} - -func TestNPCRunback(t *testing.T) { - npc := NewNPC() - - // Test initial runback state - if npc.GetRunbackLocation() != nil { - t.Error("New NPC should not have runback location") - } - - if npc.IsRunningBack() { - t.Error("New NPC should not be running back") - } - - // Set runback location - testX, testY, testZ := float32(10.5), float32(20.3), float32(30.7) - testGridID := int32(12) - npc.SetRunbackLocation(testX, testY, testZ, testGridID, true) - - runbackLoc := npc.GetRunbackLocation() - if runbackLoc == nil { - t.Fatal("Should have runback location after setting") - } - - if runbackLoc.X != testX || runbackLoc.Y != testY || runbackLoc.Z != testZ { - t.Errorf("Runback location mismatch: expected (%f,%f,%f), got (%f,%f,%f)", - testX, testY, testZ, runbackLoc.X, runbackLoc.Y, runbackLoc.Z) - } - - if runbackLoc.GridID != testGridID { - t.Errorf("Expected grid ID %d, got %d", testGridID, runbackLoc.GridID) - } - - // Test clearing runback - npc.ClearRunback() - if npc.GetRunbackLocation() != nil { - t.Error("Runback location should be cleared") - } -} - -func TestNPCMovementTimer(t *testing.T) { - npc := NewNPC() - - // Test initial timer state - if npc.IsPauseMovementTimerActive() { - t.Error("Movement timer should not be active initially") - } - - // Test pausing movement - if !npc.PauseMovement(100) { - t.Error("Should be able to pause movement") - } - - // Note: Timer might not be immediately active due to implementation details - // The test focuses on the API being callable without errors -} - -func TestNPCBrain(t *testing.T) { - npc := NewNPC() - - // Test default brain - brain := npc.GetBrain() - if brain == nil { - t.Fatal("NPC should have a default brain") - } - - if !brain.IsActive() { - t.Error("Default brain should be active") - } - - if brain.GetBody() != npc { - t.Error("Brain should reference the NPC") - } - - // Test brain thinking (should not error) - err := brain.Think() - if err != nil { - t.Errorf("Brain thinking should not error: %v", err) - } - - // Test setting brain inactive - brain.SetActive(false) - if brain.IsActive() { - t.Error("Brain should be inactive after setting to false") - } -} - -func TestNPCValidation(t *testing.T) { - npc := NewNPC() - - // Set a valid level for the NPC to pass validation - if npc.Entity != nil && npc.Entity.GetInfoStruct() != nil { - npc.Entity.GetInfoStruct().SetLevel(10) // Valid level between 1-100 - } - - // NPC should be valid if it has an entity with valid level - if !npc.IsValid() { - t.Error("NPC with valid level should be valid") - } - - // Test NPC without entity - npc.Entity = nil - if npc.IsValid() { - t.Error("NPC without entity should not be valid") - } -} - -func TestNPCString(t *testing.T) { - npc := NewNPC() - npc.SetNPCID(123) - if npc.Entity != nil { - npc.Entity.SetName("Test NPC") - } - - str := npc.String() - if str == "" { - t.Error("NPC string representation should not be empty") - } -} - -func TestNPCCopyFromExisting(t *testing.T) { - // Create original NPC - originalNPC := NewNPC() - originalNPC.SetNPCID(100) - originalNPC.SetAIStrategy(AIStrategyDefensive) - originalNPC.SetAggroRadius(30.0, false) - - if originalNPC.Entity != nil { - originalNPC.Entity.SetName("Original NPC") - if originalNPC.Entity.GetInfoStruct() != nil { - originalNPC.Entity.GetInfoStruct().SetLevel(10) - } - } - - // Create copy - copiedNPC := NewNPCFromExisting(originalNPC) - if copiedNPC == nil { - t.Fatal("NewNPCFromExisting returned nil") - } - - // Verify copy has same properties - if copiedNPC.GetNPCID() != originalNPC.GetNPCID() { - t.Errorf("NPC ID mismatch: expected %d, got %d", originalNPC.GetNPCID(), copiedNPC.GetNPCID()) - } - - if copiedNPC.GetAIStrategy() != originalNPC.GetAIStrategy() { - t.Errorf("AI strategy mismatch: expected %d, got %d", originalNPC.GetAIStrategy(), copiedNPC.GetAIStrategy()) - } - - // Test copying from nil - nilCopy := NewNPCFromExisting(nil) - if nilCopy == nil { - t.Error("NewNPCFromExisting(nil) should return a new NPC, not nil") - } -} - -func TestNPCCombat(t *testing.T) { - npc := NewNPC() - - // Test combat state - npc.InCombat(true) - // Note: The actual combat state checking would depend on Entity implementation - - // Test combat processing (should not error) - npc.ProcessCombat() -} - -func TestNPCShardSystem(t *testing.T) { - npc := NewNPC() - - // Test shard properties - testShardID := int32(5) - npc.SetShardID(testShardID) - if npc.GetShardID() != testShardID { - t.Errorf("Expected shard ID %d, got %d", testShardID, npc.GetShardID()) - } - - testCharID := int32(12345) - npc.SetShardCharID(testCharID) - if npc.GetShardCharID() != testCharID { - t.Errorf("Expected shard char ID %d, got %d", testCharID, npc.GetShardCharID()) - } - - testTimestamp := int64(1609459200) // 2021-01-01 00:00:00 UTC - npc.SetShardCreatedTimestamp(testTimestamp) - if npc.GetShardCreatedTimestamp() != testTimestamp { - t.Errorf("Expected timestamp %d, got %d", testTimestamp, npc.GetShardCreatedTimestamp()) - } -} - -func TestNPCSkillBonuses(t *testing.T) { - npc := NewNPC() - - // Test adding skill bonus - spellID := int32(500) - skillID := int32(10) - bonusValue := float32(15.5) - - npc.AddSkillBonus(spellID, skillID, bonusValue) - - // Test removing skill bonus - npc.RemoveSkillBonus(spellID) -} - -func TestNPCSpellTypes(t *testing.T) { - // Test NPCSpell creation and methods - spell := NewNPCSpell() - if spell == nil { - t.Fatal("NewNPCSpell returned nil") - } - - // Test default values - if spell.GetListID() != 0 { - t.Errorf("Expected list ID 0, got %d", spell.GetListID()) - } - - if spell.GetTier() != 1 { - t.Errorf("Expected tier 1, got %d", spell.GetTier()) - } - - // Test setters and getters - testSpellID := int32(12345) - spell.SetSpellID(testSpellID) - if spell.GetSpellID() != testSpellID { - t.Errorf("Expected spell ID %d, got %d", testSpellID, spell.GetSpellID()) - } - - testTier := int8(5) - spell.SetTier(testTier) - if spell.GetTier() != testTier { - t.Errorf("Expected tier %d, got %d", testTier, spell.GetTier()) - } - - // Test boolean properties - spell.SetCastOnSpawn(true) - if !spell.GetCastOnSpawn() { - t.Error("Expected cast on spawn to be true") - } - - spell.SetCastOnInitialAggro(true) - if !spell.GetCastOnInitialAggro() { - t.Error("Expected cast on initial aggro to be true") - } - - // Test HP ratio - testRatio := int8(75) - spell.SetRequiredHPRatio(testRatio) - if spell.GetRequiredHPRatio() != testRatio { - t.Errorf("Expected HP ratio %d, got %d", testRatio, spell.GetRequiredHPRatio()) - } - - // Test spell copy - spellCopy := spell.Copy() - if spellCopy == nil { - t.Fatal("Spell copy returned nil") - } - - if spellCopy.GetSpellID() != spell.GetSpellID() { - t.Error("Spell copy should have same spell ID") - } - - if spellCopy.GetTier() != spell.GetTier() { - t.Error("Spell copy should have same tier") - } -} - -func TestSkillTypes(t *testing.T) { - // Test Skill creation and methods - testID := int32(10) - testName := "TestSkill" - testCurrent := int16(50) - testMax := int16(100) - - skill := NewSkill(testID, testName, testCurrent, testMax) - if skill == nil { - t.Fatal("NewSkill returned nil") - } - - if skill.SkillID != testID { - t.Errorf("Expected skill ID %d, got %d", testID, skill.SkillID) - } - - if skill.Name != testName { - t.Errorf("Expected skill name '%s', got '%s'", testName, skill.Name) - } - - if skill.GetCurrentVal() != testCurrent { - t.Errorf("Expected current value %d, got %d", testCurrent, skill.GetCurrentVal()) - } - - if skill.MaxVal != testMax { - t.Errorf("Expected max value %d, got %d", testMax, skill.MaxVal) - } - - // Test skill value modification - newValue := int16(75) - skill.SetCurrentVal(newValue) - if skill.GetCurrentVal() != newValue { - t.Errorf("Expected current value %d after setting, got %d", newValue, skill.GetCurrentVal()) - } - - // Test skill increase - originalValue := skill.GetCurrentVal() - increased := skill.IncreaseSkill() - if increased && skill.GetCurrentVal() <= originalValue { - t.Error("Skill value should increase when IncreaseSkill returns true") - } - - // Test skill at max - skill.SetCurrentVal(testMax) - increased = skill.IncreaseSkill() - if increased { - t.Error("Skill at max should not increase") - } -} - -func TestMovementLocation(t *testing.T) { - testX, testY, testZ := float32(1.5), float32(2.5), float32(3.5) - testGridID := int32(99) - - loc := NewMovementLocation(testX, testY, testZ, testGridID) - if loc == nil { - t.Fatal("NewMovementLocation returned nil") - } - - if loc.X != testX || loc.Y != testY || loc.Z != testZ { - t.Errorf("Location coordinates mismatch: expected (%f,%f,%f), got (%f,%f,%f)", - testX, testY, testZ, loc.X, loc.Y, loc.Z) - } - - if loc.GridID != testGridID { - t.Errorf("Expected grid ID %d, got %d", testGridID, loc.GridID) - } - - // Test copy - locCopy := loc.Copy() - if locCopy == nil { - t.Fatal("Movement location copy returned nil") - } - - if locCopy.X != loc.X || locCopy.Y != loc.Y || locCopy.Z != loc.Z { - t.Error("Movement location copy should have same coordinates") - } - - if locCopy.GridID != loc.GridID { - t.Error("Movement location copy should have same grid ID") - } -} - -func TestTimer(t *testing.T) { - timer := NewTimer() - if timer == nil { - t.Fatal("NewTimer returned nil") - } - - // Test initial state - if timer.Enabled() { - t.Error("New timer should not be enabled") - } - - if timer.Check() { - t.Error("Disabled timer should not be checked as expired") - } - - // Test starting timer - timer.Start(100, false) // 100ms - if !timer.Enabled() { - t.Error("Timer should be enabled after starting") - } - - // Test disabling timer - timer.Disable() - if timer.Enabled() { - t.Error("Timer should be disabled after calling Disable") - } -} - -func TestSkillBonus(t *testing.T) { - spellID := int32(123) - bonus := NewSkillBonus(spellID) - if bonus == nil { - t.Fatal("NewSkillBonus returned nil") - } - - if bonus.SpellID != spellID { - t.Errorf("Expected spell ID %d, got %d", spellID, bonus.SpellID) - } - - // Test adding skills - skillID1 := int32(10) - value1 := float32(15.5) - bonus.AddSkill(skillID1, value1) - - skillID2 := int32(20) - value2 := float32(25.0) - bonus.AddSkill(skillID2, value2) - - // Test getting skills - skills := bonus.GetSkills() - if len(skills) != 2 { - t.Errorf("Expected 2 skills, got %d", len(skills)) - } - - if skills[skillID1].Value != value1 { - t.Errorf("Expected skill 1 value %f, got %f", value1, skills[skillID1].Value) - } - - if skills[skillID2].Value != value2 { - t.Errorf("Expected skill 2 value %f, got %f", value2, skills[skillID2].Value) - } - - // Test removing skill - if !bonus.RemoveSkill(skillID1) { - t.Error("Should be able to remove existing skill") - } - - updatedSkills := bonus.GetSkills() - if len(updatedSkills) != 1 { - t.Errorf("Expected 1 skill after removal, got %d", len(updatedSkills)) - } - - // Test removing non-existent skill - if bonus.RemoveSkill(999) { - t.Error("Should not be able to remove non-existent skill") - } -} - -// Benchmark tests - -func BenchmarkNewNPC(b *testing.B) { - for i := 0; i < b.N; i++ { - NewNPC() - } -} - -func BenchmarkNPCPropertyAccess(b *testing.B) { - npc := NewNPC() - npc.SetNPCID(12345) - npc.SetAIStrategy(AIStrategyOffensive) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - npc.GetNPCID() - npc.GetAIStrategy() - npc.GetAggroRadius() - } -} - -func BenchmarkNPCSpellOperations(b *testing.B) { - npc := NewNPC() - - // Create test spells - spells := make([]*NPCSpell, 10) - for i := 0; i < 10; i++ { - spell := NewNPCSpell() - spell.SetSpellID(int32(i + 100)) - spell.SetTier(int8(i%5 + 1)) - spells[i] = spell - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - npc.SetSpells(spells) - npc.GetSpells() - npc.HasSpells() - } -} - -func BenchmarkSkillOperations(b *testing.B) { - skill := NewSkill(1, "TestSkill", 50, 100) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - skill.GetCurrentVal() - skill.SetCurrentVal(int16(i % 100)) - skill.IncreaseSkill() - } -} \ No newline at end of file diff --git a/internal/npc/race_types/database_test.go b/internal/npc/race_types/database_test.go deleted file mode 100644 index 0968081..0000000 --- a/internal/npc/race_types/database_test.go +++ /dev/null @@ -1,502 +0,0 @@ -package race_types - -import ( - "context" - "fmt" - "path/filepath" - "testing" - - "zombiezen.com/go/sqlite" - "zombiezen.com/go/sqlite/sqlitex" -) - -func TestSQLiteDatabase(t *testing.T) { - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_race_types.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - t.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - - // Test table creation - err = db.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - // Verify table exists - conn, err := pool.Take(context.Background()) - if err != nil { - t.Fatalf("Failed to get connection: %v", err) - } - defer pool.Put(conn) - - var tableExists bool - err = sqlitex.ExecuteTransient(conn, "SELECT name FROM sqlite_master WHERE type='table' AND name='race_types'", &sqlitex.ExecOptions{ - ResultFunc: func(stmt *sqlite.Stmt) error { - tableExists = true - return nil - }, - }) - if err != nil { - t.Fatalf("Failed to check table existence: %v", err) - } - - if !tableExists { - t.Error("race_types table should exist") - } -} - -func TestSQLiteDatabaseOperations(t *testing.T) { - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_race_types_ops.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - t.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - - // Create table - err = db.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - // Test saving race type - raceType := &RaceType{ - RaceTypeID: Sentient, - Category: CategorySentient, - Subcategory: "Human", - ModelName: "Human Male", - } - - err = db.SaveRaceType(100, raceType) - if err != nil { - t.Fatalf("Failed to save race type: %v", err) - } - - // Test loading race types - masterList := NewMasterRaceTypeList() - err = db.LoadRaceTypes(masterList) - if err != nil { - t.Fatalf("Failed to load race types: %v", err) - } - - if masterList.Count() != 1 { - t.Errorf("Expected 1 race type, got %d", masterList.Count()) - } - - retrievedRaceType := masterList.GetRaceType(100) - if retrievedRaceType != Sentient { - t.Errorf("Expected race type %d, got %d", Sentient, retrievedRaceType) - } - - retrievedInfo := masterList.GetRaceTypeByModelID(100) - if retrievedInfo == nil { - t.Fatal("Should retrieve race type info") - } - - if retrievedInfo.Category != CategorySentient { - t.Errorf("Expected category %s, got %s", CategorySentient, retrievedInfo.Category) - } - - if retrievedInfo.Subcategory != "Human" { - t.Errorf("Expected subcategory 'Human', got %s", retrievedInfo.Subcategory) - } - - if retrievedInfo.ModelName != "Human Male" { - t.Errorf("Expected model name 'Human Male', got %s", retrievedInfo.ModelName) - } - - // Test updating (replace) - updatedRaceType := &RaceType{ - RaceTypeID: Sentient, - Category: CategorySentient, - Subcategory: "Human", - ModelName: "Human Female", - } - - err = db.SaveRaceType(100, updatedRaceType) - if err != nil { - t.Fatalf("Failed to update race type: %v", err) - } - - // Reload and verify update - masterList = NewMasterRaceTypeList() - err = db.LoadRaceTypes(masterList) - if err != nil { - t.Fatalf("Failed to load race types after update: %v", err) - } - - updatedInfo := masterList.GetRaceTypeByModelID(100) - if updatedInfo.ModelName != "Human Female" { - t.Errorf("Expected updated model name 'Human Female', got %s", updatedInfo.ModelName) - } - - // Test deletion - err = db.DeleteRaceType(100) - if err != nil { - t.Fatalf("Failed to delete race type: %v", err) - } - - // Verify deletion - masterList = NewMasterRaceTypeList() - err = db.LoadRaceTypes(masterList) - if err != nil { - t.Fatalf("Failed to load race types after deletion: %v", err) - } - - if masterList.Count() != 0 { - t.Errorf("Expected 0 race types after deletion, got %d", masterList.Count()) - } - - // Test deletion of non-existent - err = db.DeleteRaceType(999) - if err == nil { - t.Error("Should fail to delete non-existent race type") - } -} - -func TestSQLiteDatabaseMultipleRaceTypes(t *testing.T) { - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_race_types_multi.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - t.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - - // Create table - err = db.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - // Test data - testData := []struct { - modelID int16 - raceTypeID int16 - category string - subcategory string - modelName string - }{ - {100, Sentient, CategorySentient, "Human", "Human Male"}, - {101, Sentient, CategorySentient, "Human", "Human Female"}, - {200, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior"}, - {201, Undead, CategoryUndead, "Zombie", "Zombie Shambler"}, - {300, Natural, CategoryNatural, "Wolf", "Dire Wolf"}, - {301, Natural, CategoryNatural, "Bear", "Grizzly Bear"}, - } - - // Save all test data - for _, data := range testData { - raceType := &RaceType{ - RaceTypeID: data.raceTypeID, - Category: data.category, - Subcategory: data.subcategory, - ModelName: data.modelName, - } - - err = db.SaveRaceType(data.modelID, raceType) - if err != nil { - t.Fatalf("Failed to save race type %d: %v", data.modelID, err) - } - } - - // Load and verify all data - masterList := NewMasterRaceTypeList() - err = db.LoadRaceTypes(masterList) - if err != nil { - t.Fatalf("Failed to load race types: %v", err) - } - - if masterList.Count() != len(testData) { - t.Errorf("Expected %d race types, got %d", len(testData), masterList.Count()) - } - - // Verify each race type - for _, data := range testData { - retrievedRaceType := masterList.GetRaceType(data.modelID) - if retrievedRaceType != data.raceTypeID { - t.Errorf("Model %d: expected race type %d, got %d", data.modelID, data.raceTypeID, retrievedRaceType) - } - - retrievedInfo := masterList.GetRaceTypeByModelID(data.modelID) - if retrievedInfo == nil { - t.Errorf("Model %d: should have race type info", data.modelID) - continue - } - - if retrievedInfo.Category != data.category { - t.Errorf("Model %d: expected category %s, got %s", data.modelID, data.category, retrievedInfo.Category) - } - - if retrievedInfo.Subcategory != data.subcategory { - t.Errorf("Model %d: expected subcategory %s, got %s", data.modelID, data.subcategory, retrievedInfo.Subcategory) - } - - if retrievedInfo.ModelName != data.modelName { - t.Errorf("Model %d: expected model name %s, got %s", data.modelID, data.modelName, retrievedInfo.ModelName) - } - } - - // Test category-based queries by verifying the loaded data - sentientTypes := masterList.GetRaceTypesByCategory(CategorySentient) - if len(sentientTypes) != 2 { - t.Errorf("Expected 2 sentient types, got %d", len(sentientTypes)) - } - - undeadTypes := masterList.GetRaceTypesByCategory(CategoryUndead) - if len(undeadTypes) != 2 { - t.Errorf("Expected 2 undead types, got %d", len(undeadTypes)) - } - - naturalTypes := masterList.GetRaceTypesByCategory(CategoryNatural) - if len(naturalTypes) != 2 { - t.Errorf("Expected 2 natural types, got %d", len(naturalTypes)) - } -} - -func TestSQLiteDatabaseInvalidRaceType(t *testing.T) { - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_race_types_invalid.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - t.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - - // Create table - err = db.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - // Test saving nil race type - err = db.SaveRaceType(100, nil) - if err == nil { - t.Error("Should fail to save nil race type") - } - - // Test saving invalid race type - invalidRaceType := &RaceType{ - RaceTypeID: 0, // Invalid - Category: "", - Subcategory: "", - ModelName: "", - } - - err = db.SaveRaceType(100, invalidRaceType) - if err == nil { - t.Error("Should fail to save invalid race type") - } -} - -func TestSQLiteDatabaseIndexes(t *testing.T) { - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_race_types_indexes.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - t.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - - // Create table - err = db.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - // Verify indexes exist - conn, err := pool.Take(context.Background()) - if err != nil { - t.Fatalf("Failed to get connection: %v", err) - } - defer pool.Put(conn) - - indexes := []string{ - "idx_race_types_race_id", - "idx_race_types_category", - } - - for _, indexName := range indexes { - var indexExists bool - query := "SELECT name FROM sqlite_master WHERE type='index' AND name=?" - err = sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ - Args: []any{indexName}, - ResultFunc: func(stmt *sqlite.Stmt) error { - indexExists = true - return nil - }, - }) - if err != nil { - t.Fatalf("Failed to check index %s: %v", indexName, err) - } - - if !indexExists { - t.Errorf("Index %s should exist", indexName) - } - } -} - -func TestSQLiteDatabaseConcurrency(t *testing.T) { - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_race_types_concurrent.db") - - // Create database pool with multiple connections - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 3, - }) - if err != nil { - t.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - - // Create table - err = db.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - // Test concurrent operations - const numOperations = 10 - results := make(chan error, numOperations) - - // Concurrent saves - for i := 0; i < numOperations; i++ { - go func(id int) { - raceType := &RaceType{ - RaceTypeID: int16(id%5 + 1), - Category: CategorySentient, - Subcategory: "Test", - ModelName: fmt.Sprintf("Test Model %d", id), - } - results <- db.SaveRaceType(int16(100+id), raceType) - }(i) - } - - // Wait for all operations to complete - for i := 0; i < numOperations; i++ { - if err := <-results; err != nil { - t.Errorf("Concurrent save operation failed: %v", err) - } - } - - // Verify all data was saved - masterList := NewMasterRaceTypeList() - err = db.LoadRaceTypes(masterList) - if err != nil { - t.Fatalf("Failed to load race types after concurrent operations: %v", err) - } - - if masterList.Count() != numOperations { - t.Errorf("Expected %d race types after concurrent operations, got %d", numOperations, masterList.Count()) - } -} - -// Benchmark tests for SQLite database - -func BenchmarkSQLiteDatabaseSave(b *testing.B) { - // Create temporary database - tempDir := b.TempDir() - dbPath := filepath.Join(tempDir, "bench_race_types_save.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - b.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - db.CreateRaceTypesTable() - - raceType := &RaceType{ - RaceTypeID: Sentient, - Category: CategorySentient, - Subcategory: "Human", - ModelName: "Human Male", - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - db.SaveRaceType(int16(i), raceType) - } -} - -func BenchmarkSQLiteDatabaseLoad(b *testing.B) { - // Create temporary database with test data - tempDir := b.TempDir() - dbPath := filepath.Join(tempDir, "bench_race_types_load.db") - - // Create database pool - pool, err := sqlitex.NewPool(dbPath, sqlitex.PoolOptions{ - PoolSize: 1, - }) - if err != nil { - b.Fatalf("Failed to create database pool: %v", err) - } - defer pool.Close() - - db := NewSQLiteDatabase(pool) - db.CreateRaceTypesTable() - - // Add test data - raceType := &RaceType{ - RaceTypeID: Sentient, - Category: CategorySentient, - Subcategory: "Human", - ModelName: "Human Male", - } - - for i := 0; i < 1000; i++ { - db.SaveRaceType(int16(i), raceType) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - masterList := NewMasterRaceTypeList() - db.LoadRaceTypes(masterList) - } -} diff --git a/internal/npc/race_types/race_types_test.go b/internal/npc/race_types/race_types_test.go deleted file mode 100644 index 9fb3cb0..0000000 --- a/internal/npc/race_types/race_types_test.go +++ /dev/null @@ -1,550 +0,0 @@ -package race_types - -import ( - "fmt" - "testing" -) - -// Mock implementations for testing - -// MockDatabase implements the Database interface for testing -type MockDatabase struct { - raceTypes map[int16]*RaceType - created bool -} - -func NewMockDatabase() *MockDatabase { - return &MockDatabase{ - raceTypes: make(map[int16]*RaceType), - created: false, - } -} - -func (md *MockDatabase) LoadRaceTypes(masterList *MasterRaceTypeList) error { - for modelType, raceType := range md.raceTypes { - masterList.AddRaceType(modelType, raceType.RaceTypeID, raceType.Category, raceType.Subcategory, raceType.ModelName, false) - } - return nil -} - -func (md *MockDatabase) SaveRaceType(modelType int16, raceType *RaceType) error { - if raceType == nil || !raceType.IsValid() { - return fmt.Errorf("invalid race type") - } - md.raceTypes[modelType] = &RaceType{ - RaceTypeID: raceType.RaceTypeID, - Category: raceType.Category, - Subcategory: raceType.Subcategory, - ModelName: raceType.ModelName, - } - return nil -} - -func (md *MockDatabase) DeleteRaceType(modelType int16) error { - if _, exists := md.raceTypes[modelType]; !exists { - return fmt.Errorf("race type with model_type %d not found", modelType) - } - delete(md.raceTypes, modelType) - return nil -} - -func (md *MockDatabase) CreateRaceTypesTable() error { - md.created = true - return nil -} - -// MockLogger implements the Logger interface for testing -type MockLogger struct { - logs []string -} - -func NewMockLogger() *MockLogger { - return &MockLogger{ - logs: make([]string, 0), - } -} - -func (ml *MockLogger) LogInfo(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...)) -} - -func (ml *MockLogger) LogError(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...)) -} - -func (ml *MockLogger) LogDebug(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...)) -} - -func (ml *MockLogger) LogWarning(message string, args ...any) { - ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...)) -} - -func (ml *MockLogger) GetLogs() []string { - return ml.logs -} - -func (ml *MockLogger) Clear() { - ml.logs = ml.logs[:0] -} - -// Mock entity for testing race type aware interface -type MockEntity struct { - modelType int16 -} - -func NewMockEntity(modelType int16) *MockEntity { - return &MockEntity{modelType: modelType} -} - -func (me *MockEntity) GetModelType() int16 { - return me.modelType -} - -func (me *MockEntity) SetModelType(modelType int16) { - me.modelType = modelType -} - -// Test functions - -func TestRaceTypeBasics(t *testing.T) { - rt := &RaceType{ - RaceTypeID: Sentient, - Category: CategorySentient, - Subcategory: "Human", - ModelName: "Human Male", - } - - if !rt.IsValid() { - t.Error("Race type should be valid") - } - - if rt.RaceTypeID != Sentient { - t.Errorf("Expected race type ID %d, got %d", Sentient, rt.RaceTypeID) - } - - if rt.Category != CategorySentient { - t.Errorf("Expected category %s, got %s", CategorySentient, rt.Category) - } -} - -func TestRaceTypeInvalid(t *testing.T) { - rt := &RaceType{ - RaceTypeID: 0, // Invalid - Category: "", - Subcategory: "", - ModelName: "", - } - - if rt.IsValid() { - t.Error("Race type with zero ID should not be valid") - } -} - -func TestMasterRaceTypeList(t *testing.T) { - masterList := NewMasterRaceTypeList() - - // Test initial state - if masterList.Count() != 0 { - t.Errorf("Expected count 0, got %d", masterList.Count()) - } - - // Add a race type - modelID := int16(100) - raceTypeID := int16(Sentient) - category := CategorySentient - subcategory := "Human" - modelName := "Human Male" - - if !masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, false) { - t.Error("Failed to add race type") - } - - if masterList.Count() != 1 { - t.Errorf("Expected count 1, got %d", masterList.Count()) - } - - // Test retrieval - retrievedRaceType := masterList.GetRaceType(modelID) - if retrievedRaceType != raceTypeID { - t.Errorf("Expected race type %d, got %d", raceTypeID, retrievedRaceType) - } - - // Test category retrieval - retrievedCategory := masterList.GetRaceTypeCategory(modelID) - if retrievedCategory != category { - t.Errorf("Expected category %s, got %s", category, retrievedCategory) - } - - // Test duplicate addition - if masterList.AddRaceType(modelID, raceTypeID, category, subcategory, modelName, false) { - t.Error("Should not allow duplicate race type without override") - } - - // Test override - newModelName := "Human Female" - if !masterList.AddRaceType(modelID, raceTypeID, category, subcategory, newModelName, true) { - t.Error("Should allow override of existing race type") - } - - retrievedInfo := masterList.GetRaceTypeByModelID(modelID) - if retrievedInfo == nil { - t.Fatal("Should retrieve race type info") - } - - if retrievedInfo.ModelName != newModelName { - t.Errorf("Expected model name %s, got %s", newModelName, retrievedInfo.ModelName) - } -} - -func TestMasterRaceTypeListBaseFunctions(t *testing.T) { - masterList := NewMasterRaceTypeList() - - // Add some test race types - testData := []struct { - modelID int16 - raceTypeID int16 - category string - subcategory string - modelName string - }{ - {100, Sentient, CategorySentient, "Human", "Human Male"}, - {101, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior"}, - {102, Natural, CategoryNatural, "Wolf", "Dire Wolf"}, - {103, Dragonkind, CategoryDragonkind, "Dragon", "Red Dragon"}, - } - - for _, data := range testData { - masterList.AddRaceType(data.modelID, data.raceTypeID, data.category, data.subcategory, data.modelName, false) - } - - // Test base type functions - if masterList.GetRaceBaseType(100) != Sentient { - t.Error("Human should be sentient") - } - - if masterList.GetRaceBaseType(101) != Undead { - t.Error("Skeleton should be undead") - } - - if masterList.GetRaceBaseType(102) != Natural { - t.Error("Wolf should be natural") - } - - if masterList.GetRaceBaseType(103) != Dragonkind { - t.Error("Dragon should be dragonkind") - } - - // Test category functions - sentientTypes := masterList.GetRaceTypesByCategory(CategorySentient) - if len(sentientTypes) != 1 { - t.Errorf("Expected 1 sentient type, got %d", len(sentientTypes)) - } - - undeadTypes := masterList.GetRaceTypesByCategory(CategoryUndead) - if len(undeadTypes) != 1 { - t.Errorf("Expected 1 undead type, got %d", len(undeadTypes)) - } - - // Test subcategory functions - humanTypes := masterList.GetRaceTypesBySubcategory("Human") - if len(humanTypes) != 1 { - t.Errorf("Expected 1 human type, got %d", len(humanTypes)) - } - - // Test statistics - stats := masterList.GetStatistics() - if stats.TotalRaceTypes != 4 { - t.Errorf("Expected 4 total race types, got %d", stats.TotalRaceTypes) - } -} - -func TestMockDatabase(t *testing.T) { - database := NewMockDatabase() - masterList := NewMasterRaceTypeList() - - // Test table creation - err := database.CreateRaceTypesTable() - if err != nil { - t.Fatalf("Failed to create table: %v", err) - } - - if !database.created { - t.Error("Database should be marked as created") - } - - // Test saving - raceType := &RaceType{ - RaceTypeID: Sentient, - Category: CategorySentient, - Subcategory: "Human", - ModelName: "Human Male", - } - - err = database.SaveRaceType(100, raceType) - if err != nil { - t.Fatalf("Failed to save race type: %v", err) - } - - // Test loading - err = database.LoadRaceTypes(masterList) - if err != nil { - t.Fatalf("Failed to load race types: %v", err) - } - - if masterList.Count() != 1 { - t.Errorf("Expected 1 race type loaded, got %d", masterList.Count()) - } - - // Test deletion - err = database.DeleteRaceType(100) - if err != nil { - t.Fatalf("Failed to delete race type: %v", err) - } - - // Test deletion of non-existent - err = database.DeleteRaceType(999) - if err == nil { - t.Error("Should fail to delete non-existent race type") - } -} - -func TestManager(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - - // Test initialization - err := manager.Initialize() - if err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - if !database.created { - t.Error("Database table should be created during initialization") - } - - // Test adding race type - err = manager.AddRaceType(100, Sentient, CategorySentient, "Human", "Human Male") - if err != nil { - t.Fatalf("Failed to add race type: %v", err) - } - - // Test retrieval - raceTypeID := manager.GetRaceType(100) - if raceTypeID != Sentient { - t.Errorf("Expected race type %d, got %d", Sentient, raceTypeID) - } - - info := manager.GetRaceTypeInfo(100) - if info == nil { - t.Fatal("Should retrieve race type info") - } - - if info.ModelName != "Human Male" { - t.Errorf("Expected model name 'Human Male', got %s", info.ModelName) - } - - // Test updating - err = manager.UpdateRaceType(100, Sentient, CategorySentient, "Human", "Human Female") - if err != nil { - t.Fatalf("Failed to update race type: %v", err) - } - - updatedInfo := manager.GetRaceTypeInfo(100) - if updatedInfo.ModelName != "Human Female" { - t.Errorf("Expected updated model name 'Human Female', got %s", updatedInfo.ModelName) - } - - // Test type checking functions - if !manager.IsSentient(100) { - t.Error("Model 100 should be sentient") - } - - if manager.IsUndead(100) { - t.Error("Model 100 should not be undead") - } - - // Test removal - err = manager.RemoveRaceType(100) - if err != nil { - t.Fatalf("Failed to remove race type: %v", err) - } - - // Test removal of non-existent - err = manager.RemoveRaceType(999) - if err == nil { - t.Error("Should fail to remove non-existent race type") - } -} - -func TestNPCRaceTypeAdapter(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - - // Initialize and add test data - manager.Initialize() - manager.AddRaceType(100, Sentient, CategorySentient, "Human", "Human Male") - manager.AddRaceType(101, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior") - - // Create mock entity - entity := NewMockEntity(100) - adapter := NewNPCRaceTypeAdapter(entity, manager) - - // Test race type functions - if adapter.GetRaceType() != Sentient { - t.Errorf("Expected race type %d, got %d", Sentient, adapter.GetRaceType()) - } - - if adapter.GetRaceBaseType() != Sentient { - t.Errorf("Expected base type %d, got %d", Sentient, adapter.GetRaceBaseType()) - } - - if adapter.GetRaceTypeCategory() != CategorySentient { - t.Errorf("Expected category %s, got %s", CategorySentient, adapter.GetRaceTypeCategory()) - } - - // Test type checking - if !adapter.IsSentient() { - t.Error("Human should be sentient") - } - - if adapter.IsUndead() { - t.Error("Human should not be undead") - } - - // Test with undead entity - entity.SetModelType(101) - if !adapter.IsUndead() { - t.Error("Skeleton should be undead") - } - - if adapter.IsSentient() { - t.Error("Skeleton should not be sentient") - } -} - -func TestRaceTypeConstants(t *testing.T) { - // Test that constants are defined correctly - if Sentient == 0 { - t.Error("Sentient should not be 0") - } - - if Natural == 0 { - t.Error("Natural should not be 0") - } - - if Undead == 0 { - t.Error("Undead should not be 0") - } - - // Test category constants - if CategorySentient == "" { - t.Error("CategorySentient should not be empty") - } - - if CategoryNatural == "" { - t.Error("CategoryNatural should not be empty") - } - - if CategoryUndead == "" { - t.Error("CategoryUndead should not be empty") - } -} - -func TestManagerCommands(t *testing.T) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - - // Initialize and add test data - manager.Initialize() - manager.AddRaceType(100, Sentient, CategorySentient, "Human", "Human Male") - manager.AddRaceType(101, Undead, CategoryUndead, "Skeleton", "Skeleton Warrior") - - // Test stats command - result := manager.ProcessCommand([]string{"stats"}) - if result == "" { - t.Error("Stats command should return non-empty result") - } - - // Test list command - result = manager.ProcessCommand([]string{"list", CategorySentient}) - if result == "" { - t.Error("List command should return non-empty result") - } - - // Test info command - result = manager.ProcessCommand([]string{"info", "100"}) - if result == "" { - t.Error("Info command should return non-empty result") - } - - // Test category command - result = manager.ProcessCommand([]string{"category"}) - if result == "" { - t.Error("Category command should return non-empty result") - } - - // Test invalid command - result = manager.ProcessCommand([]string{"invalid"}) - if result == "" { - t.Error("Invalid command should return error message") - } -} - -// Benchmark tests - -func BenchmarkMasterRaceTypeListLookup(b *testing.B) { - masterList := NewMasterRaceTypeList() - - // Add many race types - for i := 0; i < 1000; i++ { - masterList.AddRaceType(int16(i), int16(i%10+1), CategorySentient, "Test", fmt.Sprintf("Model_%d", i), false) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - masterList.GetRaceType(int16(i % 1000)) - } -} - -func BenchmarkManagerOperations(b *testing.B) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - manager.Initialize() - - // Add some test data - for i := 0; i < 100; i++ { - manager.AddRaceType(int16(i), int16(i%10+1), CategorySentient, "Test", fmt.Sprintf("Model_%d", i)) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.GetRaceType(int16(i % 100)) - } -} - -func BenchmarkNPCRaceTypeAdapter(b *testing.B) { - database := NewMockDatabase() - logger := NewMockLogger() - manager := NewManager(database, logger) - manager.Initialize() - - // Add test data - for i := 0; i < 50; i++ { - manager.AddRaceType(int16(i), int16(i%10+1), CategorySentient, "Test", fmt.Sprintf("Model_%d", i)) - } - - entity := NewMockEntity(25) - adapter := NewNPCRaceTypeAdapter(entity, manager) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - entity.SetModelType(int16(i % 50)) - adapter.GetRaceType() - adapter.IsSentient() - } -} \ No newline at end of file diff --git a/internal/object/object_test.go b/internal/object/object_test.go deleted file mode 100644 index 8345b0d..0000000 --- a/internal/object/object_test.go +++ /dev/null @@ -1,1230 +0,0 @@ -package object - -import ( - "strings" - "testing" -) - -// Test Object creation and basic properties -func TestNewObject(t *testing.T) { - obj := NewObject() - if obj == nil { - t.Fatal("NewObject returned nil") - } - - // Test default values - if obj.IsClickable() { - t.Error("New object should not be clickable by default") - } - - if obj.GetDeviceID() != DeviceIDNone { - t.Errorf("Expected device ID %d, got %d", DeviceIDNone, obj.GetDeviceID()) - } - - if obj.GetZoneName() != "" { - t.Errorf("Expected empty zone name, got '%s'", obj.GetZoneName()) - } - - if !obj.IsObject() { - t.Error("Object should return true for IsObject()") - } -} - -func TestObjectBasicProperties(t *testing.T) { - obj := NewObject() - - // Test clickable - obj.SetClickable(true) - if !obj.IsClickable() { - t.Error("Object should be clickable after setting to true") - } - - obj.SetClickable(false) - if obj.IsClickable() { - t.Error("Object should not be clickable after setting to false") - } - - // Test zone name - testZone := "commonlands" - obj.SetZone(testZone) - if obj.GetZoneName() != testZone { - t.Errorf("Expected zone name '%s', got '%s'", testZone, obj.GetZoneName()) - } - - // Test device ID - testDeviceID := int8(5) - obj.SetDeviceID(testDeviceID) - if obj.GetDeviceID() != testDeviceID { - t.Errorf("Expected device ID %d, got %d", testDeviceID, obj.GetDeviceID()) - } -} - -func TestObjectMerchantProperties(t *testing.T) { - obj := NewObject() - - // Test merchant ID - testMerchantID := int32(12345) - obj.SetMerchantID(testMerchantID) - if obj.GetMerchantID() != testMerchantID { - t.Errorf("Expected merchant ID %d, got %d", testMerchantID, obj.GetMerchantID()) - } - - // Test merchant type - testMerchantType := int8(2) - obj.SetMerchantType(testMerchantType) - if obj.GetMerchantType() != testMerchantType { - t.Errorf("Expected merchant type %d, got %d", testMerchantType, obj.GetMerchantType()) - } - - // Test merchant level range - testMinLevel, testMaxLevel := int8(10), int8(50) - obj.SetMerchantLevelRange(testMinLevel, testMaxLevel) - if obj.GetMerchantMinLevel() != testMinLevel { - t.Errorf("Expected min level %d, got %d", testMinLevel, obj.GetMerchantMinLevel()) - } - if obj.GetMerchantMaxLevel() != testMaxLevel { - t.Errorf("Expected max level %d, got %d", testMaxLevel, obj.GetMerchantMaxLevel()) - } - - // Test collector flag - obj.SetCollector(true) - if !obj.IsCollector() { - t.Error("Object should be a collector after setting to true") - } - - obj.SetCollector(false) - if obj.IsCollector() { - t.Error("Object should not be a collector after setting to false") - } -} - -func TestObjectSize(t *testing.T) { - obj := NewObject() - - // Test size - testSize := int16(150) - obj.SetSize(testSize) - if obj.GetSize() != testSize { - t.Errorf("Expected size %d, got %d", testSize, obj.GetSize()) - } - - // Test size offset - testOffset := int8(10) - obj.SetSizeOffset(testOffset) - if obj.GetSizeOffset() != testOffset { - t.Errorf("Expected size offset %d, got %d", testOffset, obj.GetSizeOffset()) - } -} - -func TestObjectCommands(t *testing.T) { - obj := NewObject() - - // Test primary commands - primaryCommands := []string{"use", "examine", "talk"} - obj.SetPrimaryCommands(primaryCommands) - retrievedPrimary := obj.GetPrimaryCommands() - if len(retrievedPrimary) != len(primaryCommands) { - t.Errorf("Expected %d primary commands, got %d", len(primaryCommands), len(retrievedPrimary)) - } - for i, cmd := range primaryCommands { - if retrievedPrimary[i] != cmd { - t.Errorf("Primary command %d: expected '%s', got '%s'", i, cmd, retrievedPrimary[i]) - } - } - - // Test secondary commands - secondaryCommands := []string{"attack", "inspect"} - obj.SetSecondaryCommands(secondaryCommands) - retrievedSecondary := obj.GetSecondaryCommands() - if len(retrievedSecondary) != len(secondaryCommands) { - t.Errorf("Expected %d secondary commands, got %d", len(secondaryCommands), len(retrievedSecondary)) - } - for i, cmd := range secondaryCommands { - if retrievedSecondary[i] != cmd { - t.Errorf("Secondary command %d: expected '%s', got '%s'", i, cmd, retrievedSecondary[i]) - } - } -} - -func TestObjectHP(t *testing.T) { - obj := NewObject() - - // Test total HP - testTotalHP := int32(1000) - obj.SetTotalHP(testTotalHP) - if obj.GetTotalHP() != testTotalHP { - t.Errorf("Expected total HP %d, got %d", testTotalHP, obj.GetTotalHP()) - } - - // Test current HP - testCurrentHP := int32(750) - obj.SetHP(testCurrentHP) - if obj.GetHP() != testCurrentHP { - t.Errorf("Expected current HP %d, got %d", testCurrentHP, obj.GetHP()) - } -} - -func TestObjectPower(t *testing.T) { - obj := NewObject() - - // Test total power - testTotalPower := int32(500) - obj.SetTotalPower(testTotalPower) - if obj.GetTotalPower() != testTotalPower { - t.Errorf("Expected total power %d, got %d", testTotalPower, obj.GetTotalPower()) - } - - // Test current power - testCurrentPower := int32(300) - obj.SetPower(testCurrentPower) - if obj.GetPower() != testCurrentPower { - t.Errorf("Expected current power %d, got %d", testCurrentPower, obj.GetPower()) - } -} - -func TestObjectTransporter(t *testing.T) { - obj := NewObject() - - testTransporterID := int32(9876) - obj.SetTransporterID(testTransporterID) - if obj.GetTransporterID() != testTransporterID { - t.Errorf("Expected transporter ID %d, got %d", testTransporterID, obj.GetTransporterID()) - } -} - -func TestObjectFlags(t *testing.T) { - obj := NewObject() - - // Test sounds disabled flag - obj.SetSoundsDisabled(true) - if !obj.IsSoundsDisabled() { - t.Error("Sounds should be disabled after setting to true") - } - - obj.SetSoundsDisabled(false) - if obj.IsSoundsDisabled() { - t.Error("Sounds should not be disabled after setting to false") - } - - // Test omitted by DB flag - obj.SetOmittedByDBFlag(true) - if !obj.IsOmittedByDBFlag() { - t.Error("Object should be omitted by DB after setting to true") - } - - obj.SetOmittedByDBFlag(false) - if obj.IsOmittedByDBFlag() { - t.Error("Object should not be omitted by DB after setting to false") - } -} - -func TestObjectLoot(t *testing.T) { - obj := NewObject() - - // Test loot tier - testLootTier := int8(3) - obj.SetLootTier(testLootTier) - if obj.GetLootTier() != testLootTier { - t.Errorf("Expected loot tier %d, got %d", testLootTier, obj.GetLootTier()) - } - - // Test loot drop type - testDropType := int8(2) - obj.SetLootDropType(testDropType) - if obj.GetLootDropType() != testDropType { - t.Errorf("Expected loot drop type %d, got %d", testDropType, obj.GetLootDropType()) - } -} - -func TestObjectSpawnScript(t *testing.T) { - obj := NewObject() - - testScript := "test_script.lua" - obj.SetSpawnScript(testScript) - if obj.GetSpawnScript() != testScript { - t.Errorf("Expected spawn script '%s', got '%s'", testScript, obj.GetSpawnScript()) - } - - if !obj.GetSpawnScriptSetDB() { - t.Error("Spawn script set DB flag should be true after setting script") - } - - // Test empty script - obj.SetSpawnScript("") - if obj.GetSpawnScriptSetDB() { - t.Error("Spawn script set DB flag should be false after setting empty script") - } -} - -func TestObjectCommandIcon(t *testing.T) { - obj := NewObject() - - // Test show command icon - obj.SetShowCommandIcon(true) - if !obj.ShowsCommandIcon() { - t.Error("Object should show command icon after setting to true") - } - - obj.SetShowCommandIcon(false) - if obj.ShowsCommandIcon() { - t.Error("Object should not show command icon after setting to false") - } -} - -func TestObjectCopy(t *testing.T) { - // Create original object with various properties set - original := NewObject() - original.SetClickable(true) - original.SetZone("testzone") - original.SetDeviceID(5) - original.SetMerchantID(12345) - original.SetMerchantType(2) - original.SetCollector(true) - original.SetSize(100) - original.SetSizeOffset(10) - original.SetTransporterID(9876) - original.SetTotalHP(1000) - original.SetHP(750) - original.SetShowCommandIcon(true) - - // Create copy - copied := original.Copy() - if copied == nil { - t.Fatal("Copy returned nil") - } - - // Verify copy has same properties - if copied.IsClickable() != original.IsClickable() { - t.Error("Copied object clickable state doesn't match original") - } - - if copied.GetZoneName() != original.GetZoneName() { - t.Error("Copied object zone name doesn't match original") - } - - if copied.GetDeviceID() != original.GetDeviceID() { - t.Error("Copied object device ID doesn't match original") - } - - if copied.GetMerchantID() != original.GetMerchantID() { - t.Error("Copied object merchant ID doesn't match original") - } - - if copied.IsCollector() != original.IsCollector() { - t.Error("Copied object collector flag doesn't match original") - } - - if copied.GetTransporterID() != original.GetTransporterID() { - t.Error("Copied object transporter ID doesn't match original") - } - - // Test size randomization with offset - if original.GetSizeOffset() > 0 { - // Size should be different but within reasonable range - originalSize := original.GetSize() - copiedSize := copied.GetSize() - sizeDiff := copiedSize - originalSize - offsetInt16 := int16(original.GetSizeOffset()) - if sizeDiff < -offsetInt16 || sizeDiff > offsetInt16 { - t.Errorf("Copied object size randomization out of range: original=%d, copied=%d, offset=%d", - originalSize, copiedSize, original.GetSizeOffset()) - } - } - - // Verify they are separate objects - copied.SetClickable(false) - if copied.IsClickable() == original.IsClickable() { - t.Error("Copied object should be independent from original") - } -} - -func TestObjectHandleUse(t *testing.T) { - obj := NewObject() - clientID := int32(12345) - - // Test non-interactive object - err := obj.HandleUse(clientID, "use") - if err == nil { - t.Error("Expected error for non-interactive object") - } - - // Test with transporter ID - obj.SetTransporterID(100) - err = obj.HandleUse(clientID, "use") - if err == nil { - t.Error("Expected error for transport system not implemented") - } - if !strings.Contains(err.Error(), "transport system not yet implemented") { - t.Errorf("Expected transport error message, got: %v", err) - } - - // Test with command icon - obj.SetTransporterID(0) // Remove transporter - obj.SetShowCommandIcon(true) - obj.SetPrimaryCommands([]string{"use", "examine"}) - - err = obj.HandleUse(clientID, "use") - if err != nil { - t.Errorf("Expected no error for valid command, got: %v", err) - } - - err = obj.HandleUse(clientID, "invalid") - if err == nil { - t.Error("Expected error for invalid command") - } - if !strings.Contains(err.Error(), "command 'invalid' not found") { - t.Errorf("Expected command not found error, got: %v", err) - } -} - -func TestObjectInfo(t *testing.T) { - obj := NewObject() - obj.SetClickable(true) - obj.SetZone("testzone") - obj.SetDeviceID(5) - obj.SetMerchantID(12345) - obj.SetTransporterID(9876) - obj.SetCollector(true) - obj.SetSize(150) - obj.SetPrimaryCommands([]string{"use", "examine"}) - obj.SetSecondaryCommands([]string{"attack"}) - obj.SetShowCommandIcon(true) - - info := obj.GetObjectInfo() - if info == nil { - t.Fatal("GetObjectInfo returned nil") - } - - // Check various properties - if info["clickable"] != true { - t.Error("Info should show object as clickable") - } - - if info["zone_name"] != "testzone" { - t.Error("Info should show correct zone name") - } - - if info["device_id"] != int8(5) { - t.Error("Info should show correct device ID") - } - - if info["merchant_id"] != int32(12345) { - t.Error("Info should show correct merchant ID") - } - - if info["transporter_id"] != int32(9876) { - t.Error("Info should show correct transporter ID") - } - - if info["is_collector"] != true { - t.Error("Info should show object as collector") - } - - if info["primary_commands"] != 2 { - t.Error("Info should show correct primary command count") - } - - if info["secondary_commands"] != 1 { - t.Error("Info should show correct secondary command count") - } - - if info["shows_command_icon"] != true { - t.Error("Info should show command icon as visible") - } -} - -// Test ObjectSpawn creation and integration -func TestNewObjectSpawn(t *testing.T) { - objectSpawn := NewObjectSpawn() - if objectSpawn == nil { - t.Fatal("NewObjectSpawn returned nil") - } - - if objectSpawn.Spawn == nil { - t.Fatal("ObjectSpawn should have embedded Spawn") - } - - // Test default spawn type - if objectSpawn.GetSpawnType() != ObjectSpawnType { - t.Errorf("Expected spawn type %d, got %d", ObjectSpawnType, objectSpawn.GetSpawnType()) - } - - // Test default appearance values - appearance := objectSpawn.GetAppearanceData() - if appearance.ActivityStatus != ObjectActivityStatus { - t.Errorf("Expected activity status %d, got %d", ObjectActivityStatus, appearance.ActivityStatus) - } - if appearance.Pos.State != ObjectPosState { - t.Errorf("Expected pos state %d, got %d", ObjectPosState, appearance.Pos.State) - } - if appearance.Difficulty != ObjectDifficulty { - t.Errorf("Expected difficulty %d, got %d", ObjectDifficulty, appearance.Difficulty) - } - - if !objectSpawn.IsObject() { - t.Error("ObjectSpawn should return true for IsObject()") - } -} - -func TestObjectSpawnMerchantProperties(t *testing.T) { - objectSpawn := NewObjectSpawn() - - // Test merchant ID - testMerchantID := int32(54321) - objectSpawn.SetMerchantID(testMerchantID) - if objectSpawn.GetMerchantID() != testMerchantID { - t.Errorf("Expected merchant ID %d, got %d", testMerchantID, objectSpawn.GetMerchantID()) - } - - // Test merchant type - testMerchantType := int8(3) - objectSpawn.SetMerchantType(testMerchantType) - if objectSpawn.GetMerchantType() != testMerchantType { - t.Errorf("Expected merchant type %d, got %d", testMerchantType, objectSpawn.GetMerchantType()) - } - - // Test merchant level range - minLevel, maxLevel := int8(5), int8(25) - objectSpawn.SetMerchantLevelRange(minLevel, maxLevel) - if objectSpawn.GetMerchantMinLevel() != minLevel { - t.Errorf("Expected min level %d, got %d", minLevel, objectSpawn.GetMerchantMinLevel()) - } - if objectSpawn.GetMerchantMaxLevel() != maxLevel { - t.Errorf("Expected max level %d, got %d", maxLevel, objectSpawn.GetMerchantMaxLevel()) - } - - // Test collector - objectSpawn.SetCollector(true) - if !objectSpawn.IsCollector() { - t.Error("ObjectSpawn should be a collector after setting to true") - } -} - -func TestObjectSpawnTransporter(t *testing.T) { - objectSpawn := NewObjectSpawn() - - testTransporterID := int32(11111) - objectSpawn.SetTransporterID(testTransporterID) - if objectSpawn.GetTransporterID() != testTransporterID { - t.Errorf("Expected transporter ID %d, got %d", testTransporterID, objectSpawn.GetTransporterID()) - } -} - -func TestObjectSpawnCommandIcon(t *testing.T) { - objectSpawn := NewObjectSpawn() - - // Test setting command icon - objectSpawn.SetShowCommandIcon(true) - if !objectSpawn.ShowsCommandIcon() { - t.Error("ObjectSpawn should show command icon after setting to true") - } - - objectSpawn.SetShowCommandIcon(false) - if objectSpawn.ShowsCommandIcon() { - t.Error("ObjectSpawn should not show command icon after setting to false") - } -} - -func TestObjectSpawnCopy(t *testing.T) { - // Create original with properties - original := NewObjectSpawn() - original.SetClickable(true) - original.SetDeviceID(7) - original.SetMerchantID(98765) - original.SetMerchantType(4) - original.SetCollector(true) - original.SetTransporterID(55555) - original.SetDatabaseID(12345) - original.SetName("Test Object") - original.SetLevel(10) - - // Create copy - copied := original.Copy() - if copied == nil { - t.Fatal("Copy returned nil") - } - - // Verify properties were copied - if copied.IsClickable() != original.IsClickable() { - t.Error("Copied clickable state doesn't match") - } - - if copied.GetDeviceID() != original.GetDeviceID() { - t.Error("Copied device ID doesn't match") - } - - if copied.GetMerchantID() != original.GetMerchantID() { - t.Error("Copied merchant ID doesn't match") - } - - if copied.IsCollector() != original.IsCollector() { - t.Error("Copied collector flag doesn't match") - } - - if copied.GetTransporterID() != original.GetTransporterID() { - t.Error("Copied transporter ID doesn't match") - } - - if copied.GetDatabaseID() != original.GetDatabaseID() { - t.Error("Copied database ID doesn't match") - } - - if strings.TrimRight(copied.GetName(), "\x00") != strings.TrimRight(original.GetName(), "\x00") { - t.Error("Copied name doesn't match") - } - - if copied.GetLevel() != original.GetLevel() { - t.Error("Copied level doesn't match") - } - - // Verify independence - copied.SetClickable(false) - if copied.IsClickable() == original.IsClickable() { - t.Error("Copy should be independent from original") - } -} - -func TestObjectSpawnInfo(t *testing.T) { - objectSpawn := NewObjectSpawn() - objectSpawn.SetClickable(true) - objectSpawn.SetDeviceID(8) - objectSpawn.SetMerchantID(77777) - objectSpawn.SetTransporterID(88888) - objectSpawn.SetCollector(true) - objectSpawn.SetShowCommandIcon(true) - objectSpawn.SetDatabaseID(99999) - - info := objectSpawn.GetObjectInfo() - if info == nil { - t.Fatal("GetObjectInfo returned nil") - } - - // Check spawn info - if info["database_id"] != int32(99999) { - t.Error("Info should show correct database ID") - } - - // Check object info - if info["clickable"] != true { - t.Error("Info should show object as clickable") - } - - if info["device_id"] != int8(8) { - t.Error("Info should show correct device ID") - } - - if info["merchant_id"] != int32(77777) { - t.Error("Info should show correct merchant ID") - } - - if info["transporter_id"] != int32(88888) { - t.Error("Info should show correct transporter ID") - } - - if info["is_collector"] != true { - t.Error("Info should show object as collector") - } - - if info["shows_command_icon"] != true { - t.Error("Info should show command icon as visible") - } -} - -// Test ObjectManager functionality -func TestNewObjectManager(t *testing.T) { - manager := NewObjectManager() - if manager == nil { - t.Fatal("NewObjectManager returned nil") - } - - if manager.GetObjectCount() != 0 { - t.Error("New manager should have 0 objects") - } - - if manager.GetZoneCount() != 0 { - t.Error("New manager should have 0 zones") - } -} - -func TestObjectManagerAddRemove(t *testing.T) { - manager := NewObjectManager() - - // Test adding nil object - err := manager.AddObject(nil) - if err == nil { - t.Error("Expected error when adding nil object") - } - - // Create test object - obj := NewObject() - obj.SetDatabaseID(12345) - obj.SetZone("testzone") - - // Test adding object without database ID - objNoDB := NewObject() - err = manager.AddObject(objNoDB) - if err == nil { - t.Error("Expected error when adding object without database ID") - } - - // Test adding valid object - err = manager.AddObject(obj) - if err != nil { - t.Errorf("Unexpected error adding object: %v", err) - } - - if manager.GetObjectCount() != 1 { - t.Error("Manager should have 1 object after adding") - } - - // Test adding duplicate - err = manager.AddObject(obj) - if err == nil { - t.Error("Expected error when adding duplicate object") - } - - // Test retrieving object - retrieved := manager.GetObject(12345) - if retrieved == nil { - t.Error("Should be able to retrieve added object") - } - - if retrieved.GetDatabaseID() != obj.GetDatabaseID() { - t.Error("Retrieved object should match added object") - } - - // Test removing object - err = manager.RemoveObject(12345) - if err != nil { - t.Errorf("Unexpected error removing object: %v", err) - } - - if manager.GetObjectCount() != 0 { - t.Error("Manager should have 0 objects after removing") - } - - // Test removing non-existent object - err = manager.RemoveObject(99999) - if err == nil { - t.Error("Expected error when removing non-existent object") - } -} - -func TestObjectManagerZoneOperations(t *testing.T) { - manager := NewObjectManager() - - // Create objects in different zones - obj1 := NewObject() - obj1.SetDatabaseID(1) - obj1.SetZone("zone1") - - obj2 := NewObject() - obj2.SetDatabaseID(2) - obj2.SetZone("zone1") - - obj3 := NewObject() - obj3.SetDatabaseID(3) - obj3.SetZone("zone2") - - // Add objects - manager.AddObject(obj1) - manager.AddObject(obj2) - manager.AddObject(obj3) - - // Test zone count - if manager.GetZoneCount() != 2 { - t.Errorf("Expected 2 zones, got %d", manager.GetZoneCount()) - } - - // Test getting objects by zone - zone1Objects := manager.GetObjectsByZone("zone1") - if len(zone1Objects) != 2 { - t.Errorf("Expected 2 objects in zone1, got %d", len(zone1Objects)) - } - - zone2Objects := manager.GetObjectsByZone("zone2") - if len(zone2Objects) != 1 { - t.Errorf("Expected 1 object in zone2, got %d", len(zone2Objects)) - } - - emptyZoneObjects := manager.GetObjectsByZone("nonexistent") - if len(emptyZoneObjects) != 0 { - t.Errorf("Expected 0 objects in nonexistent zone, got %d", len(emptyZoneObjects)) - } - - // Test clearing zone - clearedCount := manager.ClearZone("zone1") - if clearedCount != 2 { - t.Errorf("Expected to clear 2 objects, cleared %d", clearedCount) - } - - if manager.GetObjectCount() != 1 { - t.Errorf("Expected 1 object remaining, got %d", manager.GetObjectCount()) - } - - if manager.GetZoneCount() != 1 { - t.Errorf("Expected 1 zone remaining, got %d", manager.GetZoneCount()) - } -} - -func TestObjectManagerTypeFiltering(t *testing.T) { - manager := NewObjectManager() - - // Create objects with different properties - interactive := NewObject() - interactive.SetDatabaseID(1) - interactive.SetZone("testzone") - interactive.SetClickable(true) - - transport := NewObject() - transport.SetDatabaseID(2) - transport.SetZone("testzone") - transport.SetTransporterID(100) - - merchant := NewObject() - merchant.SetDatabaseID(3) - merchant.SetZone("testzone") - merchant.SetMerchantID(200) - - collector := NewObject() - collector.SetDatabaseID(4) - collector.SetZone("testzone") - collector.SetCollector(true) - - // Add objects - manager.AddObject(interactive) - manager.AddObject(transport) - manager.AddObject(merchant) - manager.AddObject(collector) - - // Test type filtering - interactiveObjects := manager.GetInteractiveObjects() - if len(interactiveObjects) != 1 { - t.Errorf("Expected 1 interactive object, got %d", len(interactiveObjects)) - } - - transportObjects := manager.GetTransportObjects() - if len(transportObjects) != 1 { - t.Errorf("Expected 1 transport object, got %d", len(transportObjects)) - } - - merchantObjects := manager.GetMerchantObjects() - if len(merchantObjects) != 1 { - t.Errorf("Expected 1 merchant object, got %d", len(merchantObjects)) - } - - collectorObjects := manager.GetCollectorObjects() - if len(collectorObjects) != 1 { - t.Errorf("Expected 1 collector object, got %d", len(collectorObjects)) - } - - // Test GetObjectsByType - if len(manager.GetObjectsByType("interactive")) != 1 { - t.Error("GetObjectsByType should return 1 interactive object") - } - - if len(manager.GetObjectsByType("transport")) != 1 { - t.Error("GetObjectsByType should return 1 transport object") - } - - if len(manager.GetObjectsByType("merchant")) != 1 { - t.Error("GetObjectsByType should return 1 merchant object") - } - - if len(manager.GetObjectsByType("collector")) != 1 { - t.Error("GetObjectsByType should return 1 collector object") - } - - if len(manager.GetObjectsByType("invalid")) != 0 { - t.Error("GetObjectsByType should return 0 objects for invalid type") - } -} - -func TestObjectManagerUpdate(t *testing.T) { - manager := NewObjectManager() - - obj := NewObject() - obj.SetDatabaseID(12345) - obj.SetZone("zone1") - obj.SetClickable(false) - - manager.AddObject(obj) - - // Test update - err := manager.UpdateObject(12345, func(o *Object) { - o.SetClickable(true) - o.SetZone("zone2") - }) - - if err != nil { - t.Errorf("Unexpected error updating object: %v", err) - } - - // Verify update took effect - updated := manager.GetObject(12345) - if !updated.IsClickable() { - t.Error("Object should be clickable after update") - } - - if updated.GetZoneName() != "zone2" { - t.Error("Object should be in zone2 after update") - } - - // Verify zone indexing was updated - zone1Objects := manager.GetObjectsByZone("zone1") - zone2Objects := manager.GetObjectsByZone("zone2") - - if len(zone1Objects) != 0 { - t.Error("zone1 should have 0 objects after update") - } - - if len(zone2Objects) != 1 { - t.Error("zone2 should have 1 object after update") - } - - // Test updating non-existent object - err = manager.UpdateObject(99999, func(o *Object) {}) - if err == nil { - t.Error("Expected error when updating non-existent object") - } -} - -func TestObjectManagerStatistics(t *testing.T) { - manager := NewObjectManager() - - // Create test objects - obj1 := NewObject() - obj1.SetDatabaseID(1) - obj1.SetZone("zone1") - obj1.SetClickable(true) - - obj2 := NewObject() - obj2.SetDatabaseID(2) - obj2.SetZone("zone2") - obj2.SetMerchantID(100) - - manager.AddObject(obj1) - manager.AddObject(obj2) - - stats := manager.GetStatistics() - if stats == nil { - t.Fatal("GetStatistics returned nil") - } - - if stats["total_objects"] != 2 { - t.Error("Statistics should show 2 total objects") - } - - if stats["zones_with_objects"] != 2 { - t.Error("Statistics should show 2 zones with objects") - } - - if stats["interactive_objects"] != 1 { - t.Error("Statistics should show 1 interactive object") - } - - if stats["merchant_objects"] != 1 { - t.Error("Statistics should show 1 merchant object") - } - - zoneStats, ok := stats["objects_by_zone"].(map[string]int) - if !ok { - t.Fatal("Zone stats should be a map[string]int") - } - - if zoneStats["zone1"] != 1 { - t.Error("zone1 should have 1 object in statistics") - } - - if zoneStats["zone2"] != 1 { - t.Error("zone2 should have 1 object in statistics") - } -} - -func TestObjectManagerShutdown(t *testing.T) { - manager := NewObjectManager() - - // Add some objects - obj1 := NewObject() - obj1.SetDatabaseID(1) - obj1.SetZone("zone1") - - manager.AddObject(obj1) - - if manager.GetObjectCount() != 1 { - t.Error("Manager should have 1 object before shutdown") - } - - // Test shutdown - manager.Shutdown() - - if manager.GetObjectCount() != 0 { - t.Error("Manager should have 0 objects after shutdown") - } - - if manager.GetZoneCount() != 0 { - t.Error("Manager should have 0 zones after shutdown") - } -} - -// Test utility functions -func TestCreateSpecialObjectSpawns(t *testing.T) { - // Test creating merchant object spawn - merchant := CreateMerchantObjectSpawn(12345, 2) - if merchant == nil { - t.Fatal("CreateMerchantObjectSpawn returned nil") - } - - if merchant.GetMerchantID() != 12345 { - t.Error("Merchant object should have correct merchant ID") - } - - if merchant.GetMerchantType() != 2 { - t.Error("Merchant object should have correct merchant type") - } - - if !merchant.IsClickable() { - t.Error("Merchant object should be clickable") - } - - if !merchant.ShowsCommandIcon() { - t.Error("Merchant object should show command icon") - } - - // Test creating transport object spawn - transport := CreateTransportObjectSpawn(54321) - if transport == nil { - t.Fatal("CreateTransportObjectSpawn returned nil") - } - - if transport.GetTransporterID() != 54321 { - t.Error("Transport object should have correct transporter ID") - } - - // Test creating device object spawn - device := CreateDeviceObjectSpawn(7) - if device == nil { - t.Fatal("CreateDeviceObjectSpawn returned nil") - } - - if device.GetDeviceID() != 7 { - t.Error("Device object should have correct device ID") - } - - // Test creating collector object spawn - collector := CreateCollectorObjectSpawn() - if collector == nil { - t.Fatal("CreateCollectorObjectSpawn returned nil") - } - - if !collector.IsCollector() { - t.Error("Collector object should be a collector") - } -} - -// Test ObjectSpawnManager -func TestObjectSpawnManager(t *testing.T) { - manager := NewObjectSpawnManager() - if manager == nil { - t.Fatal("NewObjectSpawnManager returned nil") - } - - // Test adding object spawn - objectSpawn := NewObjectSpawn() - objectSpawn.SetDatabaseID(12345) - - err := manager.AddObjectSpawn(objectSpawn) - if err != nil { - t.Errorf("Unexpected error adding object spawn: %v", err) - } - - // Test retrieving object spawn - retrieved := manager.GetObjectSpawn(objectSpawn.GetID()) - if retrieved == nil { - t.Error("Should be able to retrieve added object spawn") - } - - // Test removing object spawn - err = manager.RemoveObjectSpawn(objectSpawn.GetID()) - if err != nil { - t.Errorf("Unexpected error removing object spawn: %v", err) - } - - // Test adding nil object spawn - err = manager.AddObjectSpawn(nil) - if err == nil { - t.Error("Expected error when adding nil object spawn") - } -} - -// Test interface implementations -func TestObjectItem(t *testing.T) { - item := NewObjectItem(12345, "Test Item", 5) - if item == nil { - t.Fatal("NewObjectItem returned nil") - } - - if item.GetID() != 12345 { - t.Error("Item should have correct ID") - } - - if item.GetName() != "Test Item" { - t.Error("Item should have correct name") - } - - if item.GetQuantity() != 5 { - t.Error("Item should have correct quantity") - } - - // Test setting properties - item.SetQuantity(10) - if item.GetQuantity() != 10 { - t.Error("Item quantity should be updated") - } - - item.SetNoTrade(true) - if !item.IsNoTrade() { - t.Error("Item should be no-trade after setting") - } - - item.SetHeirloom(true) - if !item.IsHeirloom() { - t.Error("Item should be heirloom after setting") - } - - item.SetAttuned(true) - if !item.IsAttuned() { - t.Error("Item should be attuned after setting") - } - - // Test creation time - if item.GetCreationTime().IsZero() { - t.Error("Item should have a creation time") - } - - // Test group character IDs - groupIDs := []int32{1, 2, 3} - item.SetGroupCharacterIDs(groupIDs) - retrievedIDs := item.GetGroupCharacterIDs() - if len(retrievedIDs) != len(groupIDs) { - t.Error("Group character IDs length should match") - } - for i, id := range groupIDs { - if retrievedIDs[i] != id { - t.Errorf("Group character ID %d should be %d, got %d", i, id, retrievedIDs[i]) - } - } -} - -func TestObjectSpawnAsEntity(t *testing.T) { - objectSpawn := NewObjectSpawn() - entity := NewObjectSpawnAsEntity(objectSpawn) - if entity == nil { - t.Fatal("NewObjectSpawnAsEntity returned nil") - } - - // Test entity interface methods - if entity.GetID() != objectSpawn.GetID() { - t.Error("Entity should have same ID as object spawn") - } - - if entity.IsPlayer() { - t.Error("Object entity should not be a player") - } - - if entity.IsBot() { - t.Error("Object entity should not be a bot by default") - } - - entity.SetBot(true) - if !entity.IsBot() { - t.Error("Object entity should be a bot after setting") - } - - // Test coins - if entity.HasCoins(100) { - t.Error("Object entity should not have coins by default") - } - - entity.SetCoins(500) - if !entity.HasCoins(100) { - t.Error("Object entity should have sufficient coins after setting") - } - - if entity.HasCoins(1000) { - t.Error("Object entity should not have more coins than set") - } - - // Test client version - if entity.GetClientVersion() != 1000 { - t.Error("Object entity should have default client version") - } - - entity.SetClientVersion(2000) - if entity.GetClientVersion() != 2000 { - t.Error("Object entity should have updated client version") - } - - // Test name - if entity.GetName() != "Object" { - t.Error("Object entity should have default name") - } - - entity.SetName("Test Object") - if entity.GetName() != "Test Object" { - t.Error("Object entity should have updated name") - } -} - -// Benchmark tests -func BenchmarkNewObject(b *testing.B) { - for i := 0; i < b.N; i++ { - NewObject() - } -} - -func BenchmarkObjectCopy(b *testing.B) { - obj := NewObject() - obj.SetClickable(true) - obj.SetZone("testzone") - obj.SetMerchantID(12345) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - obj.Copy() - } -} - -func BenchmarkObjectManagerAdd(b *testing.B) { - manager := NewObjectManager() - objects := make([]*Object, b.N) - - for i := 0; i < b.N; i++ { - obj := NewObject() - obj.SetDatabaseID(int32(i + 1)) - obj.SetZone("testzone") - objects[i] = obj - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.AddObject(objects[i]) - } -} - -func BenchmarkObjectManagerGet(b *testing.B) { - manager := NewObjectManager() - - // Pre-populate with objects - for i := 0; i < 1000; i++ { - obj := NewObject() - obj.SetDatabaseID(int32(i + 1)) - obj.SetZone("testzone") - manager.AddObject(obj) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.GetObject(int32((i % 1000) + 1)) - } -} \ No newline at end of file diff --git a/internal/player/README.md b/internal/player/README.md deleted file mode 100644 index 26646f6..0000000 --- a/internal/player/README.md +++ /dev/null @@ -1,370 +0,0 @@ -# Player System - -The player system (`internal/player`) provides comprehensive player character management for the EQ2Go server emulator. This system is converted from the original C++ EQ2EMu Player implementation with modern Go concurrency patterns and clean architecture principles. - -## Overview - -The player system manages all aspects of player characters including: - -- **Character Data**: Basic character information, stats, appearance -- **Experience**: Adventure and tradeskill XP, vitality, debt recovery -- **Skills**: Skill progression, bonuses, and management -- **Spells**: Spell book, casting, maintained effects, passive spells -- **Combat**: Auto-attack, combat state, weapon handling -- **Quests**: Quest tracking, progress, completion, rewards -- **Social**: Friends, ignore lists, guild membership -- **Economy**: Currency management, trading, banking -- **Movement**: Position tracking, spawn visibility, zone transfers -- **Character Flags**: Various character state flags and preferences -- **History**: Character history tracking and events -- **Items**: Inventory, equipment, appearance items -- **Housing**: House ownership, vault access - -## Architecture - -### Core Components - -**Player** - Main player character struct extending Entity -**PlayerInfo** - Detailed character information for serialization -**PlayerManager** - Multi-player management and coordination -**PlayerControlFlags** - Character flag state management -**CharacterInstances** - Instance lockout tracking - -### Key Files - -- `player.go` - Core Player struct and basic functionality -- `player_info.go` - PlayerInfo struct for character sheet data -- `character_flags.go` - Character flag management -- `currency.go` - Coin and currency handling -- `experience.go` - XP, leveling, and vitality systems -- `combat.go` - Combat mechanics and processing -- `quest_management.go` - Quest system integration -- `spell_management.go` - Spell book and casting management -- `skill_management.go` - Skill system integration -- `spawn_management.go` - Spawn visibility and tracking -- `manager.go` - Multi-player management system -- `interfaces.go` - System integration interfaces -- `constants.go` - All character flag and system constants -- `types.go` - Data structures and type definitions - -## Player Creation - -```go -// Create a new player -player := player.NewPlayer() - -// Set basic information -player.SetCharacterID(12345) -player.SetName("TestPlayer") -player.SetLevel(10) -player.SetClass(1) // Fighter -player.SetRace(0) // Human - -// Initialize player info -info := player.GetPlayerInfo() -info.SetBindZone(1) -info.SetHouseZone(0) -``` - -## Character Flags - -The system supports all EverQuest II character flags: - -```go -// Set character flags -player.SetCharacterFlag(player.CF_ANONYMOUS) -player.SetCharacterFlag(player.CF_LFG) -player.ResetCharacterFlag(player.CF_AFK) - -// Check flags -if player.GetCharacterFlag(player.CF_ROLEPLAYING) { - // Handle roleplaying state -} -``` - -### Available Flags - -- **CF_COMBAT_EXPERIENCE_ENABLED** - Adventure XP enabled -- **CF_QUEST_EXPERIENCE_ENABLED** - Tradeskill XP enabled -- **CF_ANONYMOUS** - Anonymous mode -- **CF_ROLEPLAYING** - Roleplaying flag -- **CF_AFK** - Away from keyboard -- **CF_LFG** - Looking for group -- **CF_LFW** - Looking for work -- **CF_HIDE_HOOD/CF_HIDE_HELM** - Hide equipment -- **CF_ALLOW_DUEL_INVITES** - Allow duel invites -- **CF_ALLOW_TRADE_INVITES** - Allow trade invites -- **CF_ALLOW_GROUP_INVITES** - Allow group invites -- And many more... - -## Experience System - -```go -// Add adventure XP -if player.AddXP(1000) { - // Player may have leveled up - if player.GetLevel() > oldLevel { - // Handle level up - } -} - -// Add tradeskill XP -player.AddTSXP(500) - -// Check XP status -if player.AdventureXPEnabled() { - currentXP := player.GetXP() - neededXP := player.GetNeededXP() -} - -// Calculate XP from kills -victim := &entity.Spawn{...} -xpReward := player.CalculateXP(victim) -``` - -## Currency Management - -```go -// Add coins -player.AddCoins(10000) // Add 1 gold - -// Remove coins (with validation) -if player.RemoveCoins(5000) { - // Transaction successful -} - -// Check coin amounts -copper := player.GetCoinsCopper() -silver := player.GetCoinsSilver() -gold := player.GetCoinsGold() -plat := player.GetCoinsPlat() - -// Validate sufficient funds -if player.HasCoins(1000) { - // Player can afford the cost -} -``` - -## Combat System - -```go -// Enter combat -player.InCombat(true, false) // Melee combat - -// Enter ranged combat -player.InCombat(true, true) // Ranged combat - -// Exit combat -player.StopCombat(0) // Stop all combat - -// Process combat (called periodically) -player.ProcessCombat() - -// Check combat state -if player.GetInfoStruct().GetEngageCommands() != 0 { - // Player is in combat -} -``` - -## Spell Management - -```go -// Add spell to spell book -player.AddSpellBookEntry(spellID, tier, slot, spellType, timer, true) - -// Check if player has spell -if player.HasSpell(spellID, tier, false, false) { - // Player has the spell -} - -// Get spell tier -tier := player.GetSpellTier(spellID) - -// Lock/unlock spells -player.LockAllSpells() -player.UnlockAllSpells(true, nil) - -// Manage passive spells -player.AddPassiveSpell(spellID, tier) -player.ApplyPassiveSpells() -``` - -## Quest Integration - -```go -// Get active quest -quest := player.GetQuest(questID) - -// Check quest completion -if player.HasQuestBeenCompleted(questID) { - count := player.GetQuestCompletedCount(questID) -} - -// Update quest progress -player.SetStepComplete(questID, stepID) -player.AddStepProgress(questID, stepID, progress) - -// Check quest requirements -if player.CanReceiveQuest(questID, nil) { - // Player can accept quest -} -``` - -## Social Features - -```go -// Manage friends -player.AddFriend("FriendName", true) -if player.IsFriend("SomeName") { - // Handle friend interaction -} - -// Manage ignore list -player.AddIgnore("PlayerName", true) -if player.IsIgnored("SomeName") { - // Player is ignored -} - -// Get social lists -friends := player.GetFriends() -ignored := player.GetIgnoredPlayers() -``` - -## Spawn Management - -```go -// Check spawn visibility -if player.ShouldSendSpawn(spawn) { - // Send spawn to player - player.SetSpawnSentState(spawn, player.SPAWN_STATE_SENDING) -} - -// Get spawn by player index -spawn := player.GetSpawnByIndex(index) - -// Remove spawn from player view -player.RemoveSpawn(spawn, false) - -// Process spawn updates -player.CheckSpawnStateQueue() -``` - -## Player Manager - -```go -// Create manager -config := player.ManagerConfig{ - MaxPlayers: 1000, - SaveInterval: 5 * time.Minute, - StatsInterval: 1 * time.Minute, - EnableValidation: true, - EnableEvents: true, - EnableStatistics: true, -} -manager := player.NewManager(config) - -// Start manager -manager.Start() - -// Add player -err := manager.AddPlayer(player) - -// Get players -allPlayers := manager.GetAllPlayers() -zonePlayers := manager.GetPlayersInZone(zoneID) -player := manager.GetPlayerByName("PlayerName") - -// Send messages -manager.SendToAll(message) -manager.SendToZone(zoneID, message) - -// Get statistics -stats := manager.GetPlayerStats() -``` - -## Database Integration - -The system provides interfaces for database operations: - -```go -type PlayerDatabase interface { - LoadPlayer(characterID int32) (*Player, error) - SavePlayer(player *Player) error - DeletePlayer(characterID int32) error - LoadPlayerQuests(characterID int32) ([]*quests.Quest, error) - SavePlayerQuests(characterID int32, quests []*quests.Quest) error - // ... more methods -} -``` - -## Event System - -```go -type PlayerEventHandler interface { - OnPlayerLogin(player *Player) error - OnPlayerLogout(player *Player) error - OnPlayerDeath(player *Player, killer *entity.Entity) error - OnPlayerLevelUp(player *Player, newLevel int8) error - // ... more events -} - -// Register event handler -manager.AddEventHandler(myHandler) -``` - -## Thread Safety - -All player operations are thread-safe using appropriate synchronization: - -- **RWMutex** for read-heavy operations (spawn maps, quest lists) -- **Atomic operations** for simple flags and counters -- **Channel-based communication** for background processing -- **Proper lock ordering** to prevent deadlocks - -## Integration with Other Systems - -The player system integrates with: - -- **Entity System** - Players extend entities for combat capabilities -- **Spell System** - Complete spell casting and effect management -- **Quest System** - Quest tracking and progression -- **Skill System** - Skill advancement and bonuses -- **Faction System** - Reputation and standings -- **Title System** - Character titles and achievements -- **Trade System** - Player-to-player trading -- **Housing System** - House ownership and access - -## Performance Considerations - -- **Efficient spawn tracking** with hash maps for O(1) lookups -- **Periodic processing** batched for better performance -- **Memory-efficient data structures** with proper cleanup -- **Background save operations** to avoid blocking gameplay -- **Statistics collection** with minimal overhead - -## Migration from C++ - -This Go implementation maintains compatibility with the original C++ EQ2EMu player system while providing: - -- **Modern concurrency** with goroutines and channels -- **Better error handling** with Go's error interface -- **Cleaner architecture** with interface-based design -- **Improved maintainability** with package organization -- **Enhanced testing** capabilities - -## TODO Items - -The conversion includes TODO comments marking areas for future implementation: - -- **LUA integration** for scripting support -- **Advanced packet handling** for client communication -- **Complete database schema** implementation -- **Full item system** integration -- **Group and raid** management -- **PvP mechanics** and flagging -- **Mail system** implementation -- **Appearance system** completion - -## Usage Examples - -See the individual file documentation and method comments for detailed usage examples. The system is designed to be used alongside the existing EQ2Go server infrastructure with proper initialization and configuration. \ No newline at end of file diff --git a/internal/player/player_test.go b/internal/player/player_test.go deleted file mode 100644 index 69d4d5d..0000000 --- a/internal/player/player_test.go +++ /dev/null @@ -1,584 +0,0 @@ -package player - -import ( - "fmt" - "testing" - "time" - - "eq2emu/internal/quests" -) - -// TestNewPlayer tests player creation -func TestNewPlayer(t *testing.T) { - p := NewPlayer() - if p == nil { - t.Fatal("NewPlayer returned nil") - } - - // Verify default values - if p.GetCharacterID() != 0 { - t.Errorf("Expected character ID 0, got %d", p.GetCharacterID()) - } - - if p.GetTutorialStep() != 0 { - t.Errorf("Expected tutorial step 0, got %d", p.GetTutorialStep()) - } - - if !p.IsPlayer() { - t.Error("Expected IsPlayer to return true") - } - - // Check that maps are initialized - if p.playerQuests == nil { - t.Error("playerQuests map not initialized") - } - if p.completedQuests == nil { - t.Error("completedQuests map not initialized") - } - if p.friendList == nil { - t.Error("friendList map not initialized") - } -} - -// TestPlayerManager tests the player manager functionality -func TestPlayerManager(t *testing.T) { - config := ManagerConfig{ - MaxPlayers: 100, - SaveInterval: time.Minute * 5, - StatsInterval: time.Second * 30, - } - - manager := NewManager(config) - if manager == nil { - t.Fatal("NewManager returned nil") - } - - // Test adding a player - player := NewPlayer() - player.SetSpawnID(1001) // Set unique spawn ID - player.SetCharacterID(123) - player.SetName("TestPlayer") - player.SetLevel(10) - - err := manager.AddPlayer(player) - if err != nil { - t.Fatalf("Failed to add player: %v", err) - } - - // Test retrieving player by ID - retrieved := manager.GetPlayer(player.GetSpawnID()) - if retrieved == nil { - t.Error("Failed to retrieve player by ID") - } - - // Test retrieving player by name - byName := manager.GetPlayerByName("TestPlayer") - if byName == nil { - // Debug: Check what name was actually stored - allPlayers := manager.GetAllPlayers() - if len(allPlayers) > 0 { - t.Errorf("Failed to retrieve player by name. Player has name: %s", allPlayers[0].GetName()) - } else { - t.Error("Failed to retrieve player by name. No players in manager") - } - } - - // Test retrieving player by character ID - byCharID := manager.GetPlayerByCharacterID(123) - if byCharID == nil { - t.Error("Failed to retrieve player by character ID") - } - - // Test player count - count := manager.GetPlayerCount() - if count != 1 { - t.Errorf("Expected player count 1, got %d", count) - } - - // Test removing player - err = manager.RemovePlayer(player.GetSpawnID()) - if err != nil { - t.Fatalf("Failed to remove player: %v", err) - } - - count = manager.GetPlayerCount() - if count != 0 { - t.Errorf("Expected player count 0 after removal, got %d", count) - } -} - -// TestPlayerDatabase tests database operations -func TestPlayerDatabase(t *testing.T) { - t.Skip("Skipping test - requires MySQL database connection and proper PlayerDatabase implementation") - // TODO: Implement TestPlayerDatabase with MySQL connection - // This test needs to be rewritten to use the new database wrapper - // and MySQL instead of SQLite - - // TODO: Re-implement with MySQL database - // Test player save/load/delete operations -} - -// TestPlayerCombat tests combat-related functionality -func TestPlayerCombat(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - player.SetLevel(10) - player.SetHP(100) - player.SetTotalHP(100) - - // Test combat state - player.InCombat(true, false) - if player.GetPlayerEngageCommands() == 0 { - t.Error("Expected player to be in combat") - } - - player.InCombat(false, false) - if player.GetPlayerEngageCommands() != 0 { - t.Error("Expected player to not be in combat") - } - - // Test death state - player.SetHP(0) - if !player.IsDead() { - t.Error("Expected player to be dead with 0 HP") - } - - player.SetHP(50) - if player.IsDead() { - t.Error("Expected player to be alive with 50 HP") - } - - // Test mentorship - player.SetMentorStats(5, 0, true) - player.EnableResetMentorship() - if !player.ResetMentorship() { - t.Error("Expected mentorship reset to succeed") - } -} - -// TestPlayerExperience tests experience system -func TestPlayerExperience(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - player.SetLevel(1) - - // Set initial XP - player.SetXP(0) - player.SetNeededXP(1000) - - // Test XP gain - leveledUp := player.AddXP(500) - if leveledUp { - t.Error("Should not level up with 500/1000 XP") - } - - currentXP := player.GetXP() - if currentXP != 500 { - t.Errorf("Expected XP 500, got %d", currentXP) - } - - // Test level up - leveledUp = player.AddXP(600) // Total: 1100, should level up - if !leveledUp { - t.Error("Should level up with 1100/1000 XP") - } - - newLevel := player.GetLevel() - if newLevel != 2 { - t.Errorf("Expected level 2 after level up, got %d", newLevel) - } - - // Test tradeskill XP - player.SetTSXP(0) - player.SetNeededTSXP(500) - - leveledUp = player.AddTSXP(600) - if !leveledUp { - t.Error("Should level up tradeskill with 600/500 XP") - } -} - -// TestPlayerQuests tests quest management -func TestPlayerQuests(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Create test quest - quest := &quests.Quest{ - ID: 100, - Name: "Test Quest", - } - - // Test adding quest - // Note: AddQuest method doesn't exist yet - would need implementation - // player.AddQuest(quest) - player.playerQuests[100] = quest - - retrieved := player.GetQuest(100) - if retrieved == nil { - t.Error("Failed to retrieve added quest") - } - - allQuests := player.GetPlayerQuests() - if len(allQuests) != 1 { - t.Errorf("Expected 1 quest, got %d", len(allQuests)) - } - - // Test completing quest - player.RemoveQuest(100, true) - - retrieved = player.GetQuest(100) - if retrieved != nil { - t.Error("Quest should be removed after completion") - } - - completed := player.GetCompletedQuest(100) - if completed == nil { - t.Error("Quest should be in completed list") - } -} - -// TestPlayerSkills tests skill management -func TestPlayerSkills(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test adding skill - player.AddSkill(1, 10, 100, true) - - // Test skill retrieval by name - skill := player.GetSkillByName("TestSkill", false) - // Note: This will return nil since we're using stubs - _ = skill - - // Test removing skill - player.RemovePlayerSkill(1, false) -} - -// TestPlayerFlags tests character flag management -func TestPlayerFlags(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test setting flags - player.SetCharacterFlag(CF_ANONYMOUS) - if !player.GetCharacterFlag(CF_ANONYMOUS) { - t.Error("Expected anonymous flag to be set") - } - - // Test resetting flags - player.ResetCharacterFlag(CF_ANONYMOUS) - if player.GetCharacterFlag(CF_ANONYMOUS) { - t.Error("Expected anonymous flag to be reset") - } - - // Test toggle - player.ToggleCharacterFlag(CF_ANONYMOUS) - if !player.GetCharacterFlag(CF_ANONYMOUS) { - t.Error("Expected anonymous flag to be toggled on") - } - - player.ToggleCharacterFlag(CF_ANONYMOUS) - if player.GetCharacterFlag(CF_ANONYMOUS) { - t.Error("Expected anonymous flag to be toggled off") - } -} - -// TestPlayerFriends tests friend list management -func TestPlayerFriends(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test adding friend - player.AddFriend("BestFriend", false) - - if !player.IsFriend("BestFriend") { - t.Error("Expected BestFriend to be in friend list") - } - - friends := player.GetFriends() - if len(friends) != 1 { - t.Errorf("Expected 1 friend, got %d", len(friends)) - } - - // Test removing friend - player.RemoveFriend("BestFriend") - - if player.IsFriend("BestFriend") { - t.Error("Expected BestFriend to be removed from friend list") - } -} - -// TestPlayerIgnore tests ignore list management -func TestPlayerIgnore(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test adding to ignore list - player.AddIgnore("Annoying", false) - - if !player.IsIgnored("Annoying") { - t.Error("Expected Annoying to be in ignore list") - } - - ignored := player.GetIgnoredPlayers() - if len(ignored) != 1 { - t.Errorf("Expected 1 ignored player, got %d", len(ignored)) - } - - // Test removing from ignore list - player.RemoveIgnore("Annoying") - - if player.IsIgnored("Annoying") { - t.Error("Expected Annoying to be removed from ignore list") - } -} - -// TestPlayerMovement tests movement-related methods -func TestPlayerMovement(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test position - player.SetX(100.5) - player.SetY(200.5, false) - player.SetZ(300.5) - - if player.GetX() != 100.5 { - t.Errorf("Expected X 100.5, got %f", player.GetX()) - } - - // Test heading - player.SetHeadingFromFloat(180.0) - heading := player.GetHeading() - _ = heading // Heading conversion is complex, just ensure it doesn't panic - - // Test distance calculation - distance := player.GetDistance(150.5, 250.5, 350.5, true) - if distance <= 0 { - t.Error("Expected positive distance") - } - - // Test movement speeds - player.SetSideSpeed(5.0, false) - if player.GetSideSpeed() != 5.0 { - t.Errorf("Expected side speed 5.0, got %f", player.GetSideSpeed()) - } - - player.SetVertSpeed(3.0, false) - if player.GetVertSpeed() != 3.0 { - t.Errorf("Expected vert speed 3.0, got %f", player.GetVertSpeed()) - } -} - -// TestPlayerCurrency tests currency management -func TestPlayerCurrency(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test adding coins - player.AddCoins(1000) - - if !player.HasCoins(1000) { - t.Error("Expected player to have 1000 coins") - } - - // Test removing coins - success := player.RemoveCoins(500) - if !success { - t.Error("Expected to successfully remove 500 coins") - } - - if !player.HasCoins(500) { - t.Error("Expected player to have 500 coins remaining") - } - - // Test insufficient coins - success = player.RemoveCoins(1000) - if success { - t.Error("Should not be able to remove 1000 coins when only 500 available") - } -} - -// TestPlayerSpells tests spell management -func TestPlayerSpells(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Test spell book - player.AddSpellBookEntry(100, 1, 1, 0, 0, true) - - hasSpell := player.HasSpell(100, 1, false, false) - if !hasSpell { - t.Error("Expected player to have spell 100") - } - - // Test removing spell - player.RemoveSpellBookEntry(100, false) - - hasSpell = player.HasSpell(100, 1, false, false) - if hasSpell { - t.Error("Expected spell to be removed") - } - - // Test passive spells - player.ApplyPassiveSpells() - player.RemoveAllPassives() -} - -// TestPlayerInfo tests PlayerInfo functionality -func TestPlayerInfo(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - info := NewPlayerInfo(player) - if info == nil { - t.Fatal("NewPlayerInfo returned nil") - } - - // Test bind point - info.SetBindZone(100) - info.SetBindX(50.0) - info.SetBindY(60.0) - info.SetBindZ(70.0) - info.SetBindHeading(90.0) - - // Test house zone - info.SetHouseZone(200) - - // Test account age - info.SetAccountAge(365) - - // Test XP calculations - player.SetXP(500) - player.SetNeededXP(1000) - info.CalculateXPPercentages() - - // Test TS XP calculations - player.SetTSXP(250) - player.SetNeededTSXP(500) - info.CalculateTSXPPercentages() -} - -// TestPlayerEquipment tests equipment and appearance -func TestPlayerEquipment(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - player.SetHP(100) // Set HP so player is not dead - player.SetTotalHP(100) - - // Test equipment allowance check - canEquip := player.IsAllowedCombatEquip(0, false) - if !canEquip { - t.Error("Expected to be able to change equipment out of combat") - } - - // Test in combat equipment change - player.InCombat(true, false) - canEquip = player.IsAllowedCombatEquip(0, false) - if canEquip { - t.Error("Should not be able to change primary weapon in combat") - } -} - -// TestPlayerCleanup tests cleanup methods -func TestPlayerCleanup(t *testing.T) { - player := NewPlayer() - player.SetCharacterID(1) - - // Add some data - player.AddFriend("Friend1", false) - player.AddIgnore("Ignored1", false) - quest := &quests.Quest{ID: 1, Name: "Quest1"} - // player.AddQuest(quest) - method doesn't exist yet - player.playerQuests[1] = quest - - // Test cleanup - player.ClearEverything() - - // Verify data is cleared - friends := player.GetFriends() - if len(friends) != 0 { - t.Error("Expected friends list to be cleared") - } - - ignored := player.GetIgnoredPlayers() - if len(ignored) != 0 { - t.Error("Expected ignore list to be cleared") - } -} - -// BenchmarkPlayerCreation benchmarks player creation -func BenchmarkPlayerCreation(b *testing.B) { - for i := 0; i < b.N; i++ { - p := NewPlayer() - p.SetCharacterID(int32(i)) - p.SetName(fmt.Sprintf("Player%d", i)) - } -} - -// BenchmarkManagerOperations benchmarks manager operations -func BenchmarkManagerOperations(b *testing.B) { - config := ManagerConfig{ - MaxPlayers: 1000, - } - manager := NewManager(config) - - // Create players - players := make([]*Player, 100) - for i := 0; i < 100; i++ { - players[i] = NewPlayer() - players[i].SetCharacterID(int32(i)) - players[i].SetName(fmt.Sprintf("Player%d", i)) - players[i].SetSpawnID(int32(2000 + i)) // Unique spawn IDs - manager.AddPlayer(players[i]) - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - // Benchmark lookups - _ = manager.GetPlayer(int32(i % 100)) - _ = manager.GetPlayerByCharacterID(int32(i % 100)) - } -} - -// TestConcurrentAccess tests thread safety -func TestConcurrentAccess(t *testing.T) { - config := ManagerConfig{ - MaxPlayers: 100, - } - manager := NewManager(config) - - // Start manager - err := manager.Start() - if err != nil { - t.Fatalf("Failed to start manager: %v", err) - } - defer manager.Stop() - - // Concurrent player additions - done := make(chan bool, 10) - - for i := 0; i < 10; i++ { - go func(id int) { - player := NewPlayer() - player.SetCharacterID(int32(id)) - player.SetName(fmt.Sprintf("Player%d", id)) - player.SetSpawnID(int32(1000 + id)) // Unique spawn IDs - manager.AddPlayer(player) - done <- true - }(i) - } - - // Wait for all goroutines - for i := 0; i < 10; i++ { - <-done - } - - // Verify all players added - count := manager.GetPlayerCount() - if count != 10 { - t.Errorf("Expected 10 players, got %d", count) - } -} \ No newline at end of file diff --git a/internal/quests/quests_test.go b/internal/quests/quests_test.go deleted file mode 100644 index 014720b..0000000 --- a/internal/quests/quests_test.go +++ /dev/null @@ -1,1315 +0,0 @@ -package quests - -import ( - "fmt" - "strings" - "testing" - "time" -) - -// Test Location functionality -func TestNewLocation(t *testing.T) { - id := int32(1) - x, y, z := float32(10.5), float32(20.3), float32(30.7) - zoneID := int32(100) - - loc := NewLocation(id, x, y, z, zoneID) - if loc == nil { - t.Fatal("NewLocation returned nil") - } - - if loc.ID != id { - t.Errorf("Expected ID %d, got %d", id, loc.ID) - } - if loc.X != x { - t.Errorf("Expected X %f, got %f", x, loc.X) - } - if loc.Y != y { - t.Errorf("Expected Y %f, got %f", y, loc.Y) - } - if loc.Z != z { - t.Errorf("Expected Z %f, got %f", z, loc.Z) - } - if loc.ZoneID != zoneID { - t.Errorf("Expected ZoneID %d, got %d", zoneID, loc.ZoneID) - } -} - -// Test QuestFactionPrereq functionality -func TestNewQuestFactionPrereq(t *testing.T) { - factionID := int32(123) - min, max := int32(-1000), int32(1000) - - prereq := NewQuestFactionPrereq(factionID, min, max) - if prereq == nil { - t.Fatal("NewQuestFactionPrereq returned nil") - } - - if prereq.FactionID != factionID { - t.Errorf("Expected FactionID %d, got %d", factionID, prereq.FactionID) - } - if prereq.Min != min { - t.Errorf("Expected Min %d, got %d", min, prereq.Min) - } - if prereq.Max != max { - t.Errorf("Expected Max %d, got %d", max, prereq.Max) - } -} - -// Test QuestStep functionality -func TestNewQuestStep(t *testing.T) { - id := int32(1) - stepType := StepTypeKill - description := "Kill 5 goblins" - ids := []int32{100, 101, 102} - quantity := int32(5) - taskGroup := "Combat Tasks" - maxVariation := float32(10.0) - percentage := float32(90.0) - usableItemID := int32(0) - - step := NewQuestStep(id, stepType, description, ids, quantity, taskGroup, nil, maxVariation, percentage, usableItemID) - if step == nil { - t.Fatal("NewQuestStep returned nil") - } - - // Test basic properties - if step.ID != id { - t.Errorf("Expected ID %d, got %d", id, step.ID) - } - if step.Type != stepType { - t.Errorf("Expected Type %d, got %d", stepType, step.Type) - } - if step.Description != description { - t.Errorf("Expected Description '%s', got '%s'", description, step.Description) - } - if step.TaskGroup != taskGroup { - t.Errorf("Expected TaskGroup '%s', got '%s'", taskGroup, step.TaskGroup) - } - if step.Quantity != quantity { - t.Errorf("Expected Quantity %d, got %d", quantity, step.Quantity) - } - if step.MaxVariation != maxVariation { - t.Errorf("Expected MaxVariation %f, got %f", maxVariation, step.MaxVariation) - } - if step.Percentage != percentage { - t.Errorf("Expected Percentage %f, got %f", percentage, step.Percentage) - } - if step.UsableItemID != usableItemID { - t.Errorf("Expected UsableItemID %d, got %d", usableItemID, step.UsableItemID) - } - - // Test IDs map - if len(step.IDs) != len(ids) { - t.Errorf("Expected %d IDs, got %d", len(ids), len(step.IDs)) - } - for _, expectedID := range ids { - if !step.IDs[expectedID] { - t.Errorf("Expected ID %d to be in IDs map", expectedID) - } - } - - // Test defaults - if step.StepProgress != 0 { - t.Errorf("Expected StepProgress 0, got %d", step.StepProgress) - } - if step.Icon != DefaultIcon { - t.Errorf("Expected Icon %d, got %d", DefaultIcon, step.Icon) - } - if step.Updated != false { - t.Errorf("Expected Updated false, got %t", step.Updated) - } -} - -func TestQuestStepLocation(t *testing.T) { - id := int32(2) - stepType := StepTypeLocation - description := "Visit the ancient ruins" - locations := []*Location{ - NewLocation(1, 100.0, 200.0, 300.0, 50), - NewLocation(2, 150.0, 250.0, 350.0, 50), - } - quantity := int32(1) - maxVariation := float32(5.0) - - step := NewQuestStep(id, stepType, description, nil, quantity, "", locations, maxVariation, 100.0, 0) - if step == nil { - t.Fatal("NewQuestStep returned nil") - } - - // Test locations were copied - if len(step.Locations) != len(locations) { - t.Errorf("Expected %d locations, got %d", len(locations), len(step.Locations)) - } - - // Test IDs map should be nil for location steps - if step.IDs != nil { - t.Error("Expected IDs to be nil for location step") - } -} - -func TestQuestStepProgress(t *testing.T) { - step := NewQuestStep(1, StepTypeKill, "Kill goblins", []int32{100}, 5, "", nil, 0, 100.0, 0) - - // Test initial state - if step.Complete() { - t.Error("New step should not be complete") - } - if step.GetStepProgress() != 0 { - t.Error("New step should have 0 progress") - } - - // Test adding progress - added := step.AddStepProgress(2) - if added != 2 { - t.Errorf("Expected 2 progress added, got %d", added) - } - if step.GetStepProgress() != 2 { - t.Errorf("Expected progress 2, got %d", step.GetStepProgress()) - } - if !step.WasUpdated() { - t.Error("Step should be marked as updated") - } - - // Test completing step - added = step.AddStepProgress(5) // Should cap at quantity (5) - if added != 3 { - t.Errorf("Expected 3 progress added (to reach cap), got %d", added) - } - if step.GetStepProgress() != 5 { - t.Errorf("Expected progress 5, got %d", step.GetStepProgress()) - } - if !step.Complete() { - t.Error("Step should be complete") - } - - // Test setting complete directly - step2 := NewQuestStep(2, StepTypeChat, "Talk to NPC", []int32{200}, 1, "", nil, 0, 100.0, 0) - step2.SetComplete() - if !step2.Complete() { - t.Error("Step should be complete after SetComplete") - } - if step2.GetStepProgress() != step2.Quantity { - t.Error("Step progress should equal quantity after SetComplete") - } -} - -func TestQuestStepReferencedID(t *testing.T) { - ids := []int32{100, 101, 102} - step := NewQuestStep(1, StepTypeKill, "Kill goblins", ids, 5, "", nil, 0, 100.0, 0) - - // Test referenced IDs - for _, id := range ids { - if !step.CheckStepReferencedID(id) { - t.Errorf("Expected ID %d to be referenced", id) - } - } - - // Test non-referenced ID - if step.CheckStepReferencedID(999) { - t.Error("Expected ID 999 to not be referenced") - } -} - -func TestQuestStepLocationUpdate(t *testing.T) { - locations := []*Location{ - NewLocation(1, 100.0, 200.0, 300.0, 50), - } - maxVariation := float32(5.0) - step := NewQuestStep(1, StepTypeLocation, "Visit location", nil, 1, "", locations, maxVariation, 100.0, 0) - - // Test exact location match - if !step.CheckStepLocationUpdate(100.0, 200.0, 300.0, 50) { - t.Error("Should match exact location") - } - - // Test location within variation (total diff = 2+2+2 = 6, which is > 5.0 maxVariation) - // Need smaller differences: 1+1+1 = 3, which is < 5.0 - if !step.CheckStepLocationUpdate(101.0, 201.0, 301.0, 50) { - t.Error("Should match location within variation") - } - - // Test location outside variation - if step.CheckStepLocationUpdate(110.0, 210.0, 310.0, 50) { - t.Error("Should not match location outside variation") - } - - // Test wrong zone - if step.CheckStepLocationUpdate(100.0, 200.0, 300.0, 99) { - t.Error("Should not match location in wrong zone") - } -} - -func TestQuestStepCopy(t *testing.T) { - ids := []int32{100, 101} - locations := []*Location{ - NewLocation(1, 50.0, 60.0, 70.0, 25), - } - original := NewQuestStep(1, StepTypeKill, "Original step", ids, 10, "Task Group", locations, 5.0, 75.0, 123) - original.SetStepProgress(3) // Add some progress - original.SetWasUpdated(true) - original.SetUpdateName("Test Update") - - copied := original.Copy() - if copied == nil { - t.Fatal("Copy returned nil") - } - - // Test basic properties were copied - if copied.ID != original.ID { - t.Error("Copied step should have same ID") - } - if copied.Type != original.Type { - t.Error("Copied step should have same Type") - } - if copied.Description != original.Description { - t.Error("Copied step should have same Description") - } - if copied.TaskGroup != original.TaskGroup { - t.Error("Copied step should have same TaskGroup") - } - if copied.Quantity != original.Quantity { - t.Error("Copied step should have same Quantity") - } - - // Test progress was reset - if copied.StepProgress != 0 { - t.Error("Copied step progress should be reset to 0") - } - if copied.Updated != false { - t.Error("Copied step Updated flag should be reset to false") - } - - // Test IDs map was copied - if len(copied.IDs) != len(original.IDs) { - t.Error("Copied step should have same number of IDs") - } - for id := range original.IDs { - if !copied.IDs[id] { - t.Errorf("Copied step should have ID %d", id) - } - } - - // Test independence - copied.AddStepProgress(5) - if original.GetStepProgress() != 3 { - t.Error("Original step should not be affected by changes to copy") - } -} - -func TestQuestStepGettersSetters(t *testing.T) { - step := NewQuestStep(1, StepTypeKill, "Test step", []int32{100}, 5, "", nil, 0, 100.0, 0) - - // Test quantity getters - if step.GetCurrentQuantity() != 0 { - t.Error("Initial current quantity should be 0") - } - if step.GetNeededQuantity() != 5 { - t.Error("Needed quantity should match step quantity") - } - - // Test setters - step.SetDescription("New description") - if step.Description != "New description" { - t.Error("Description should be updated") - } - - step.SetTaskGroup("New Task Group") - if step.TaskGroup != "New Task Group" { - t.Error("Task group should be updated") - } - - step.SetUpdateName("Update Name") - if step.UpdateName != "Update Name" { - t.Error("Update name should be updated") - } - - step.SetUpdateTargetName("Target Name") - if step.UpdateTargetName != "Target Name" { - t.Error("Update target name should be updated") - } - - step.SetIcon(42) - if step.Icon != 42 { - t.Error("Icon should be updated") - } - - // Test reset task group - step.ResetTaskGroup() - if step.TaskGroup != "" { - t.Error("Task group should be empty after reset") - } -} - -// Test Quest functionality -func TestNewQuest(t *testing.T) { - id := int32(1001) - quest := NewQuest(id) - if quest == nil { - t.Fatal("NewQuest returned nil") - } - - if quest.ID != id { - t.Errorf("Expected ID %d, got %d", id, quest.ID) - } - - // Test defaults - if quest.PrereqLevel != DefaultPrereqLevel { - t.Errorf("Expected PrereqLevel %d, got %d", DefaultPrereqLevel, quest.PrereqLevel) - } - if quest.TaskGroupNum != DefaultTaskGroupNum { - t.Errorf("Expected TaskGroupNum %d, got %d", DefaultTaskGroupNum, quest.TaskGroupNum) - } - if quest.Visible != DefaultVisible { - t.Errorf("Expected Visible %d, got %d", DefaultVisible, quest.Visible) - } - - // Test maps are initialized - if quest.QuestStepMap == nil { - t.Error("QuestStepMap should be initialized") - } - if quest.QuestStepReverseMap == nil { - t.Error("QuestStepReverseMap should be initialized") - } - if quest.TaskGroupOrder == nil { - t.Error("TaskGroupOrder should be initialized") - } - if quest.TaskGroup == nil { - t.Error("TaskGroup should be initialized") - } - if quest.CompleteActions == nil { - t.Error("CompleteActions should be initialized") - } - if quest.ProgressActions == nil { - t.Error("ProgressActions should be initialized") - } - if quest.FailedActions == nil { - t.Error("FailedActions should be initialized") - } - if quest.RewardFactions == nil { - t.Error("RewardFactions should be initialized") - } - - // Test date fields - now := time.Now() - if quest.Day != int8(now.Day()) { - t.Error("Day should be set to current day") - } - if quest.Month != int8(now.Month()) { - t.Error("Month should be set to current month") - } - if quest.Year != int8(now.Year()-2000) { - t.Error("Year should be set to current year - 2000") - } -} - -func TestQuestRegisterQuest(t *testing.T) { - quest := NewQuest(1001) - name := "The Great Adventure" - questType := "Signature" - zone := "commonlands" - level := int8(25) - description := "A quest of epic proportions" - - quest.RegisterQuest(name, questType, zone, level, description) - - if quest.Name != name { - t.Errorf("Expected Name '%s', got '%s'", name, quest.Name) - } - if quest.Type != questType { - t.Errorf("Expected Type '%s', got '%s'", questType, quest.Type) - } - if quest.Zone != zone { - t.Errorf("Expected Zone '%s', got '%s'", zone, quest.Zone) - } - if quest.Level != level { - t.Errorf("Expected Level %d, got %d", level, quest.Level) - } - if quest.Description != description { - t.Errorf("Expected Description '%s', got '%s'", description, quest.Description) - } - if !quest.NeedsSave { - t.Error("NeedsSave should be true after RegisterQuest") - } -} - -func TestQuestAddRemoveSteps(t *testing.T) { - quest := NewQuest(1001) - - // Test adding steps - step1 := NewQuestStep(1, StepTypeKill, "Kill goblins", []int32{100}, 5, "Combat", nil, 0, 100.0, 0) - step2 := NewQuestStep(2, StepTypeChat, "Talk to NPC", []int32{200}, 1, "Social", nil, 0, 100.0, 0) - - if !quest.AddQuestStep(step1) { - t.Error("Should be able to add first step") - } - if !quest.AddQuestStep(step2) { - t.Error("Should be able to add second step") - } - - // Test duplicate step ID - duplicate := NewQuestStep(1, StepTypeObtainItem, "Get item", []int32{300}, 1, "", nil, 0, 100.0, 0) - if quest.AddQuestStep(duplicate) { - t.Error("Should not be able to add step with duplicate ID") - } - - // Test quest step tracking - if len(quest.QuestSteps) != 2 { - t.Errorf("Expected 2 steps, got %d", len(quest.QuestSteps)) - } - if len(quest.QuestStepMap) != 2 { - t.Errorf("Expected 2 steps in map, got %d", len(quest.QuestStepMap)) - } - if len(quest.QuestStepReverseMap) != 2 { - t.Errorf("Expected 2 steps in reverse map, got %d", len(quest.QuestStepReverseMap)) - } - - // Test task groups - if len(quest.TaskGroup) != 2 { - t.Errorf("Expected 2 task groups, got %d", len(quest.TaskGroup)) - } - - // Test getting step - retrieved := quest.GetQuestStep(1) - if retrieved != step1 { - t.Error("GetQuestStep should return the correct step") - } - - // Test removing step - if !quest.RemoveQuestStep(1) { - t.Error("Should be able to remove existing step") - } - if quest.RemoveQuestStep(999) { - t.Error("Should not be able to remove non-existent step") - } - - // Test step was removed from all tracking - if len(quest.QuestSteps) != 1 { - t.Error("Step should be removed from slice") - } - if quest.GetQuestStep(1) != nil { - t.Error("Step should not be found after removal") - } - if quest.QuestStepMap[1] != nil { - t.Error("Step should be removed from map") - } -} - -func TestQuestCreateStep(t *testing.T) { - quest := NewQuest(1001) - - step := quest.CreateQuestStep(1, StepTypeKill, "Kill monsters", []int32{100, 101}, 3, "Combat", nil, 0, 100.0, 0) - if step == nil { - t.Fatal("CreateQuestStep should return created step") - } - - if step.ID != 1 { - t.Error("Created step should have correct ID") - } - if quest.GetQuestStep(1) != step { - t.Error("Created step should be added to quest") - } - - // Test creating step with duplicate ID - duplicate := quest.CreateQuestStep(1, StepTypeChat, "Talk", []int32{200}, 1, "", nil, 0, 100.0, 0) - if duplicate != nil { - t.Error("Should not be able to create step with duplicate ID") - } -} - -func TestQuestStepCompletion(t *testing.T) { - quest := NewQuest(1001) - step := NewQuestStep(1, StepTypeKill, "Kill goblins", []int32{100}, 3, "", nil, 0, 100.0, 0) - quest.AddQuestStep(step) - - // Test setting step complete - if !quest.SetStepComplete(1) { - t.Error("Should be able to complete step") - } - if !quest.GetQuestStepCompleted(1) { - t.Error("Step should be marked as completed") - } - if quest.SetStepComplete(1) { - t.Error("Should not be able to complete already completed step") - } - - // Test completing non-existent step - if quest.SetStepComplete(999) { - t.Error("Should not be able to complete non-existent step") - } - - // Test quest is complete - if !quest.GetCompleted() { - t.Error("Quest should be complete when all steps are complete") - } -} - -func TestQuestCurrentStep(t *testing.T) { - quest := NewQuest(1001) - step1 := NewQuestStep(1, StepTypeKill, "Kill", []int32{100}, 1, "", nil, 0, 100.0, 0) - step2 := NewQuestStep(2, StepTypeChat, "Chat", []int32{200}, 1, "", nil, 0, 100.0, 0) - quest.AddQuestStep(step1) - quest.AddQuestStep(step2) - - // Test first incomplete step - current := quest.GetCurrentQuestStep() - if current != 1 { - t.Errorf("Expected current step 1, got %d", current) - } - - // Test step is active - if !quest.QuestStepIsActive(1) { - t.Error("Step 1 should be active") - } - if !quest.QuestStepIsActive(2) { - t.Error("Step 2 should be active") - } - - // Complete first step - quest.SetStepComplete(1) - - // Test next step becomes current - current = quest.GetCurrentQuestStep() - if current != 2 { - t.Errorf("Expected current step 2, got %d", current) - } - - // Test completed step is not active - if quest.QuestStepIsActive(1) { - t.Error("Step 1 should not be active after completion") - } - - // Complete all steps - quest.SetStepComplete(2) - - // Test no current step when all complete - current = quest.GetCurrentQuestStep() - if current != 0 { - t.Errorf("Expected current step 0 when all complete, got %d", current) - } -} - -func TestQuestKillUpdate(t *testing.T) { - quest := NewQuest(1001) - killStep := NewQuestStep(1, StepTypeKill, "Kill goblins", []int32{100, 101}, 2, "", nil, 0, 100.0, 0) - otherStep := NewQuestStep(2, StepTypeChat, "Chat with NPC", []int32{200}, 1, "", nil, 0, 100.0, 0) - quest.AddQuestStep(killStep) - quest.AddQuestStep(otherStep) - - // Test checking for kill updates - if !quest.CheckQuestReferencedSpawns(100) { - t.Error("Should reference spawn 100") - } - if !quest.CheckQuestReferencedSpawns(101) { - t.Error("Should reference spawn 101") - } - if quest.CheckQuestReferencedSpawns(999) { - t.Error("Should not reference spawn 999") - } - - // Test kill update without applying - if !quest.CheckQuestKillUpdate(100, false) { - t.Error("Should detect kill update for spawn 100") - } - if quest.GetStepProgress(1) != 0 { - t.Error("Progress should not change when update=false") - } - - // Test kill update with applying - if !quest.CheckQuestKillUpdate(100, true) { - t.Error("Should process kill update for spawn 100") - } - if quest.GetStepProgress(1) != 1 { - t.Error("Progress should increase after kill update") - } - - // Test kill update for non-referenced spawn - if quest.CheckQuestKillUpdate(999, true) { - t.Error("Should not process kill update for non-referenced spawn") - } -} - -func TestQuestChatUpdate(t *testing.T) { - quest := NewQuest(1001) - chatStep := NewQuestStep(1, StepTypeChat, "Talk to NPC", []int32{200}, 1, "", nil, 0, 100.0, 0) - quest.AddQuestStep(chatStep) - - // Test chat update - if !quest.CheckQuestChatUpdate(200, true) { - t.Error("Should process chat update for NPC 200") - } - if quest.GetStepProgress(1) != 1 { - t.Error("Progress should increase after chat update") - } - - // Test chat update for non-referenced NPC - if quest.CheckQuestChatUpdate(999, true) { - t.Error("Should not process chat update for non-referenced NPC") - } -} - -func TestQuestItemUpdate(t *testing.T) { - quest := NewQuest(1001) - itemStep := NewQuestStep(1, StepTypeObtainItem, "Get items", []int32{300}, 5, "", nil, 0, 100.0, 0) - quest.AddQuestStep(itemStep) - - // Test item update - if !quest.CheckQuestItemUpdate(300, 3) { - t.Error("Should process item update for item 300") - } - if quest.GetStepProgress(1) != 3 { - t.Error("Progress should increase by item quantity") - } - - // Test item update for non-referenced item - if quest.CheckQuestItemUpdate(999, 1) { - t.Error("Should not process item update for non-referenced item") - } -} - -func TestQuestLocationUpdate(t *testing.T) { - quest := NewQuest(1001) - locations := []*Location{ - NewLocation(1, 100.0, 200.0, 300.0, 50), - } - locationStep := NewQuestStep(1, StepTypeLocation, "Visit location", nil, 1, "", locations, 5.0, 100.0, 0) - quest.AddQuestStep(locationStep) - - // Test location update (use smaller differences to stay within 5.0 total variation) - if !quest.CheckQuestLocationUpdate(101.0, 201.0, 301.0, 50) { - t.Error("Should process location update for nearby coordinates") - } - if quest.GetStepProgress(1) != 1 { - t.Error("Progress should increase after location update") - } - - // Test location update for far coordinates - if quest.CheckQuestLocationUpdate(200.0, 300.0, 400.0, 50) { - t.Error("Should not process location update for far coordinates") - } -} - -func TestQuestSpellUpdate(t *testing.T) { - quest := NewQuest(1001) - spellStep := NewQuestStep(1, StepTypeSpell, "Cast spell", []int32{400}, 3, "", nil, 0, 100.0, 0) - quest.AddQuestStep(spellStep) - - // Test spell update - if !quest.CheckQuestSpellUpdate(400) { - t.Error("Should process spell update for spell 400") - } - if quest.GetStepProgress(1) != 1 { - t.Error("Progress should increase after spell update") - } - - // Test spell update for non-referenced spell - if quest.CheckQuestSpellUpdate(999) { - t.Error("Should not process spell update for non-referenced spell") - } -} - -func TestQuestRefIDUpdate(t *testing.T) { - quest := NewQuest(1001) - harvestStep := NewQuestStep(1, StepTypeHarvest, "Harvest resources", []int32{500}, 10, "", nil, 0, 100.0, 0) - craftStep := NewQuestStep(2, StepTypeCraft, "Craft items", []int32{600}, 5, "", nil, 0, 100.0, 0) - quest.AddQuestStep(harvestStep) - quest.AddQuestStep(craftStep) - - // Test harvest update - if !quest.CheckQuestRefIDUpdate(500, 3) { - t.Error("Should process harvest update for ref 500") - } - if quest.GetStepProgress(1) != 3 { - t.Error("Harvest progress should increase") - } - - // Test craft update - if !quest.CheckQuestRefIDUpdate(600, 2) { - t.Error("Should process craft update for ref 600") - } - if quest.GetStepProgress(2) != 2 { - t.Error("Craft progress should increase") - } - - // Test update for non-referenced ref - if quest.CheckQuestRefIDUpdate(999, 1) { - t.Error("Should not process update for non-referenced ref") - } -} - -func TestQuestTaskGroups(t *testing.T) { - quest := NewQuest(1001) - step1 := NewQuestStep(1, StepTypeKill, "Kill 1", []int32{100}, 1, "Group A", nil, 0, 100.0, 0) - step2 := NewQuestStep(2, StepTypeKill, "Kill 2", []int32{101}, 1, "Group A", nil, 0, 100.0, 0) - step3 := NewQuestStep(3, StepTypeChat, "Chat", []int32{200}, 1, "Group B", nil, 0, 100.0, 0) - - quest.AddQuestStep(step1) - quest.AddQuestStep(step2) - quest.AddQuestStep(step3) - - // Test task groups were created - if len(quest.TaskGroup) != 2 { - t.Errorf("Expected 2 task groups, got %d", len(quest.TaskGroup)) - } - if len(quest.TaskGroup["Group A"]) != 2 { - t.Errorf("Expected 2 steps in Group A, got %d", len(quest.TaskGroup["Group A"])) - } - if len(quest.TaskGroup["Group B"]) != 1 { - t.Errorf("Expected 1 step in Group B, got %d", len(quest.TaskGroup["Group B"])) - } - - // Test task group order - if len(quest.TaskGroupOrder) != 2 { - t.Errorf("Expected 2 task group orders, got %d", len(quest.TaskGroupOrder)) - } - - // Test getting current task group step - current := quest.GetTaskGroupStep() - if current != 1 { // Should be first group - t.Errorf("Expected task group step 1, got %d", current) - } - - // Complete first group - quest.SetStepComplete(1) - quest.SetStepComplete(2) - - // Should move to next group - current = quest.GetTaskGroupStep() - if current != 2 { - t.Errorf("Expected task group step 2 after completing first group, got %d", current) - } -} - -func TestQuestCategoryYellow(t *testing.T) { - quest := NewQuest(1001) - - // Test yellow categories - yellowTypes := []string{"Signature", "Heritage", "Hallmark", "Deity", "Miscellaneous", "Language", "Lore and Legend", "World Event", "Tradeskill"} - for _, questType := range yellowTypes { - quest.Type = questType - if !quest.CheckCategoryYellow() { - t.Errorf("Quest type '%s' should be yellow", questType) - } - - // Test case insensitive - quest.Type = strings.ToLower(questType) - if !quest.CheckCategoryYellow() { - t.Errorf("Quest type '%s' (lowercase) should be yellow", questType) - } - } - - // Test non-yellow category - quest.Type = "Random" - if quest.CheckCategoryYellow() { - t.Error("Quest type 'Random' should not be yellow") - } -} - -func TestQuestTimer(t *testing.T) { - quest := NewQuest(1001) - - // Test setting timer - duration := int32(3600) // 1 hour - quest.SetStepTimer(duration) - - expectedTime := int32(time.Now().Unix()) + duration - if quest.Timestamp < expectedTime-1 || quest.Timestamp > expectedTime+1 { - t.Error("Timer should be set to approximately current time + duration") - } - - // Test clearing timer - quest.SetStepTimer(0) - if quest.Timestamp != 0 { - t.Error("Timer should be cleared when duration is 0") - } -} - -func TestQuestTemporaryState(t *testing.T) { - quest := NewQuest(1001) - quest.TmpRewardCoins = 1000 - quest.TmpRewardStatus = 500 - - // Test setting temporary state - quest.SetQuestTemporaryState(true, "Temporary description") - if !quest.QuestStateTemporary { - t.Error("Quest should be in temporary state") - } - if quest.QuestTempDescription != "Temporary description" { - t.Error("Temporary description should be set") - } - - // Test clearing temporary state - quest.SetQuestTemporaryState(false, "") - if quest.QuestStateTemporary { - t.Error("Quest should not be in temporary state") - } - if quest.TmpRewardCoins != 0 { - t.Error("Temporary coins should be cleared") - } - if quest.TmpRewardStatus != 0 { - t.Error("Temporary status should be cleared") - } -} - -func TestQuestShareCriteria(t *testing.T) { - quest := NewQuest(1001) - - // Test no sharing allowed - quest.QuestShareableFlag = ShareableNone - if quest.CanShareQuestCriteria(false, false, 1) { - t.Error("Should not be able to share when flag is ShareableNone") - } - - // Test sharing active quests - quest.QuestShareableFlag = ShareableActive - if !quest.CanShareQuestCriteria(true, false, 1) { - t.Error("Should be able to share active quest") - } - if quest.CanShareQuestCriteria(false, false, 1) { - t.Error("Should not be able to share when player doesn't have quest") - } - - // Test sharing during quest - ShareableDuring allows sharing when step > 1 - // but also needs ShareableActive to allow sharing when hasQuest=true - quest.QuestShareableFlag = ShareableActive | ShareableDuring - if !quest.CanShareQuestCriteria(true, false, 2) { - t.Error("Should be able to share during quest") - } - // With ShareableDuring, step 1 is NOT > 1, so the ShareableDuring condition doesn't apply - // The ShareableActive flag allows sharing of active quests regardless of step - // So this test expectation might be wrong - let's check if it actually CAN be shared - if !quest.CanShareQuestCriteria(true, false, 1) { - t.Error("Should be able to share active quest at any step with ShareableActive") - } - - // Test sharing completed quests - quest.QuestShareableFlag = ShareableCompleted - if !quest.CanShareQuestCriteria(false, true, 1) { - t.Error("Should be able to share completed quest") - } - if quest.CanShareQuestCriteria(false, false, 1) { - t.Error("Should not be able to share uncompleted quest when only completed sharing allowed") - } -} - -func TestQuestCopy(t *testing.T) { - // Create original quest with comprehensive data - original := NewQuest(1001) - original.RegisterQuest("Test Quest", "Signature", "testzone", 25, "A test quest") - original.QuestGiver = 100 - original.ReturnID = 101 - original.PrereqLevel = 20 - original.PrereqRaces = []int8{1, 2, 3} - original.PrereqClasses = []int8{4, 5, 6} - original.PrereqFactions = []*QuestFactionPrereq{ - NewQuestFactionPrereq(1, 100, 1000), - } - original.RewardCoins = 5000 - original.RewardExp = 10000 - original.RewardFactions[1] = 500 - original.Repeatable = true - original.CompleteAction = "test_complete.lua" - - // Add steps - step1 := NewQuestStep(1, StepTypeKill, "Kill goblins", []int32{100}, 5, "Combat", nil, 0, 90.0, 0) - step2 := NewQuestStep(2, StepTypeChat, "Talk to NPC", []int32{200}, 1, "Social", nil, 0, 100.0, 0) - original.AddQuestStep(step1) - original.AddQuestStep(step2) - - // Add actions - original.CompleteActions[1] = "kill_complete.lua" - original.ProgressActions[1] = "kill_progress.lua" - original.FailedActions[1] = "kill_failed.lua" - - // Set some progress and state - original.AddStepProgress(1, 2) - original.TurnedIn = true - original.Deleted = true - - // Create copy - copied := original.Copy() - if copied == nil { - t.Fatal("Copy returned nil") - } - - // Test basic properties were copied - if copied.ID != original.ID { - t.Error("Copied quest should have same ID") - } - if copied.Name != original.Name { - t.Error("Copied quest should have same Name") - } - if copied.Type != original.Type { - t.Error("Copied quest should have same Type") - } - if copied.Zone != original.Zone { - t.Error("Copied quest should have same Zone") - } - if copied.Level != original.Level { - t.Error("Copied quest should have same Level") - } - - // Test prerequisites were copied - if copied.PrereqLevel != original.PrereqLevel { - t.Error("Copied quest should have same PrereqLevel") - } - if len(copied.PrereqRaces) != len(original.PrereqRaces) { - t.Error("Copied quest should have same PrereqRaces length") - } - if len(copied.PrereqFactions) != len(original.PrereqFactions) { - t.Error("Copied quest should have same PrereqFactions length") - } - - // Test rewards were copied - if copied.RewardCoins != original.RewardCoins { - t.Error("Copied quest should have same RewardCoins") - } - if copied.RewardExp != original.RewardExp { - t.Error("Copied quest should have same RewardExp") - } - if copied.RewardFactions[1] != original.RewardFactions[1] { - t.Error("Copied quest should have same faction rewards") - } - - // Test steps were copied - if len(copied.QuestSteps) != len(original.QuestSteps) { - t.Error("Copied quest should have same number of steps") - } - if len(copied.QuestStepMap) != len(original.QuestStepMap) { - t.Error("Copied quest should have same step map size") - } - - // Test actions were copied - if copied.CompleteActions[1] != original.CompleteActions[1] { - t.Error("Copied quest should have same complete actions") - } - - // Test state was reset - if copied.TurnedIn { - t.Error("Copied quest TurnedIn should be reset") - } - if copied.Deleted { - t.Error("Copied quest Deleted should be reset") - } - if !copied.UpdateNeeded { - t.Error("Copied quest UpdateNeeded should be true") - } - - // Test step progress was reset - copiedStep := copied.GetQuestStep(1) - if copiedStep.GetStepProgress() != 0 { - t.Error("Copied quest step progress should be reset") - } - - // Test independence - copied.Name = "Modified Name" - if original.Name == "Modified Name" { - t.Error("Original quest should not be affected by changes to copy") - } -} - -func TestQuestValidation(t *testing.T) { - // Test valid quest - quest := NewQuest(1001) - quest.RegisterQuest("Valid Quest", "Normal", "testzone", 25, "A valid quest") - step := NewQuestStep(1, StepTypeKill, "Kill monsters", []int32{100}, 5, "", nil, 0, 100.0, 0) - quest.AddQuestStep(step) - - if err := quest.ValidateQuest(); err != nil { - t.Errorf("Valid quest should pass validation: %v", err) - } - - // Test invalid quest ID - invalidQuest := NewQuest(-1) - if err := invalidQuest.ValidateQuest(); err == nil { - t.Error("Quest with negative ID should fail validation") - } - - // Test missing name - quest.Name = "" - if err := quest.ValidateQuest(); err == nil { - t.Error("Quest with empty name should fail validation") - } - - // Test name too long - quest.Name = strings.Repeat("a", MaxQuestNameLength+1) - if err := quest.ValidateQuest(); err == nil { - t.Error("Quest with too long name should fail validation") - } - - // Test invalid level - quest.Name = "Valid Name" - quest.Level = 0 - if err := quest.ValidateQuest(); err == nil { - t.Error("Quest with invalid level should fail validation") - } - - quest.Level = 101 - if err := quest.ValidateQuest(); err == nil { - t.Error("Quest with too high level should fail validation") - } - - // Test quest without steps - quest.Level = 25 - quest.QuestSteps = nil - if err := quest.ValidateQuest(); err == nil { - t.Error("Quest without steps should fail validation") - } - - // Test invalid step - quest.AddQuestStep(step) - step.Quantity = -1 - if err := quest.ValidateQuest(); err == nil { - t.Error("Quest with invalid step should fail validation") - } -} - -// Test MasterQuestList functionality -func TestNewMasterQuestList(t *testing.T) { - mql := NewMasterQuestList() - if mql == nil { - t.Fatal("NewMasterQuestList returned nil") - } - - if mql.quests == nil { - t.Error("Quests map should be initialized") - } -} - -func TestMasterQuestListAddQuest(t *testing.T) { - mql := NewMasterQuestList() - quest := NewQuest(1001) - - // Test adding valid quest - err := mql.AddQuest(1001, quest) - if err != nil { - t.Errorf("Should be able to add valid quest: %v", err) - } - - // Test adding nil quest - err = mql.AddQuest(1002, nil) - if err == nil { - t.Error("Should not be able to add nil quest") - } - - // Test ID mismatch - quest2 := NewQuest(1003) - err = mql.AddQuest(1004, quest2) - if err == nil { - t.Error("Should not be able to add quest with mismatched ID") - } - - // Test duplicate quest - err = mql.AddQuest(1001, quest) - if err == nil { - t.Error("Should not be able to add duplicate quest") - } -} - -func TestMasterQuestListGetQuest(t *testing.T) { - mql := NewMasterQuestList() - quest := NewQuest(1001) - quest.Name = "Original Quest" - mql.AddQuest(1001, quest) - - // Test getting quest without copy - retrieved := mql.GetQuest(1001, false) - if retrieved != quest { - t.Error("Should return same quest instance when copyQuest=false") - } - - // Test getting quest with copy - copied := mql.GetQuest(1001, true) - if copied == quest { - t.Error("Should return different instance when copyQuest=true") - } - if copied.ID != quest.ID { - t.Error("Copied quest should have same ID") - } - - // Test getting non-existent quest - nonExistent := mql.GetQuest(9999, false) - if nonExistent != nil { - t.Error("Should return nil for non-existent quest") - } -} - -func TestMasterQuestListHasQuest(t *testing.T) { - mql := NewMasterQuestList() - quest := NewQuest(1001) - mql.AddQuest(1001, quest) - - if !mql.HasQuest(1001) { - t.Error("Should have quest 1001") - } - if mql.HasQuest(9999) { - t.Error("Should not have quest 9999") - } -} - -func TestMasterQuestListRemoveQuest(t *testing.T) { - mql := NewMasterQuestList() - quest := NewQuest(1001) - mql.AddQuest(1001, quest) - - // Test removing existing quest - if !mql.RemoveQuest(1001) { - t.Error("Should be able to remove existing quest") - } - if mql.HasQuest(1001) { - t.Error("Quest should be removed") - } - - // Test removing non-existent quest - if mql.RemoveQuest(9999) { - t.Error("Should not be able to remove non-existent quest") - } -} - -func TestMasterQuestListGetAllQuests(t *testing.T) { - mql := NewMasterQuestList() - quest1 := NewQuest(1001) - quest2 := NewQuest(1002) - mql.AddQuest(1001, quest1) - mql.AddQuest(1002, quest2) - - allQuests := mql.GetAllQuests() - if len(allQuests) != 2 { - t.Errorf("Expected 2 quests, got %d", len(allQuests)) - } - if allQuests[1001] != quest1 { - t.Error("Should return quest1") - } - if allQuests[1002] != quest2 { - t.Error("Should return quest2") - } - - // Test independence of returned map - delete(allQuests, 1001) - if !mql.HasQuest(1001) { - t.Error("Original quest list should not be affected") - } -} - -// Test utility functions and edge cases -func TestQuestStepEdgeCases(t *testing.T) { - // Test step with empty IDs for location type - locationStep := NewQuestStep(1, StepTypeLocation, "Visit", nil, 1, "", []*Location{}, 5.0, 100.0, 0) - if locationStep.IDs != nil { - t.Error("Location step should not have IDs map") - } - - // Test step with empty locations for non-location type - killStep := NewQuestStep(2, StepTypeKill, "Kill", []int32{}, 1, "", nil, 0, 100.0, 0) - if killStep.Locations != nil { - t.Error("Non-location step should not have locations") - } - - // Test step with percentage-based failure - chanceStep := NewQuestStep(3, StepTypeKill, "Maybe kill", []int32{100}, 5, "", nil, 0, 0.1, 0) // Very low success rate - initialProgress := chanceStep.GetStepProgress() - - // Try multiple times, should eventually get some failures - attempts := 0 - for attempts < 100 { - chanceStep.AddStepProgress(1) - attempts++ - if chanceStep.GetStepProgress() == initialProgress { - break // Found a failure case - } - initialProgress = chanceStep.GetStepProgress() - } - // Note: This test is probabilistic, so we don't assert failure but just test the mechanism works -} - -func TestQuestEdgeCases(t *testing.T) { - quest := NewQuest(1001) - - // Test task group with empty name defaults to description - step := NewQuestStep(1, StepTypeKill, "Default Task Group", []int32{100}, 1, "", nil, 0, 100.0, 0) - quest.AddQuestStep(step) - - if _, exists := quest.TaskGroup["Default Task Group"]; !exists { - t.Error("Empty task group should default to description") - } - - // Test removing step - NOTE: Due to implementation bug, empty task groups are NOT removed - // because RemoveQuestStep only checks step.TaskGroup (which is empty) not the resolved name - quest.RemoveQuestStep(1) - if _, exists := quest.TaskGroup["Default Task Group"]; !exists { - t.Error("Task group still exists due to implementation limitation") - } - // The step was not actually removed from the task group due to the bug - if len(quest.TaskGroup["Default Task Group"]) != 1 { - t.Errorf("Expected task group to still contain 1 step due to bug, got %d", len(quest.TaskGroup["Default Task Group"])) - } - - // Test completed step doesn't get updated - completedStep := NewQuestStep(2, StepTypeKill, "Completed", []int32{200}, 1, "", nil, 0, 100.0, 0) - quest.AddQuestStep(completedStep) - quest.SetStepComplete(2) - - // Try to update completed step - if quest.CheckQuestKillUpdate(200, true) { - t.Error("Completed step should not be updated") - } -} - -// Benchmark tests -func BenchmarkNewQuest(b *testing.B) { - for i := 0; i < b.N; i++ { - NewQuest(int32(i)) - } -} - -func BenchmarkQuestAddStep(b *testing.B) { - quest := NewQuest(1001) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - step := NewQuestStep(int32(i), StepTypeKill, "Benchmark step", []int32{int32(i)}, 1, "", nil, 0, 100.0, 0) - quest.AddQuestStep(step) - } -} - -func BenchmarkQuestStepProgress(b *testing.B) { - quest := NewQuest(1001) - step := NewQuestStep(1, StepTypeKill, "Benchmark", []int32{100}, int32(b.N), "", nil, 0, 100.0, 0) - quest.AddQuestStep(step) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - quest.AddStepProgress(1, 1) - } -} - -func BenchmarkQuestCopy(b *testing.B) { - quest := NewQuest(1001) - quest.RegisterQuest("Benchmark Quest", "Normal", "testzone", 25, "A quest for benchmarking") - - // Add several steps - for i := 0; i < 10; i++ { - step := NewQuestStep(int32(i+1), StepTypeKill, fmt.Sprintf("Step %d", i+1), []int32{int32(100 + i)}, 5, "", nil, 0, 100.0, 0) - quest.AddQuestStep(step) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - quest.Copy() - } -} - -func BenchmarkMasterQuestListOperations(b *testing.B) { - mql := NewMasterQuestList() - - // Pre-populate with quests - for i := 0; i < 1000; i++ { - quest := NewQuest(int32(i)) - mql.AddQuest(int32(i), quest) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - mql.GetQuest(int32(i%1000), false) - } -} diff --git a/internal/races/README.md b/internal/races/README.md deleted file mode 100644 index 41150dc..0000000 --- a/internal/races/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Character Races System - -The character races system manages the 21 playable races in EverQuest II that can be used by both player characters and NPCs. - -## Overview - -This package handles character race definitions, including: -- Race IDs and names for all 21 playable races -- Alignment classifications (good, evil, neutral) -- Race-based stat modifiers and bonuses -- Starting location information -- Race compatibility and restrictions - -## Why Not Under /internal/player? - -While these are "player races" in the sense that they're the races players can choose at character creation, this package is used by the Entity system which is inherited by both: -- **Players**: For character creation and race-based mechanics -- **NPCs**: For humanoid NPCs that use player race models (e.g., Human_NPC, Dwarf_NPC, etc.) - -The Entity system (`internal/entity`) references races for stat calculations, spell bonuses, and other race-based mechanics that apply to both players and NPCs. - -## Relationship to Race Types - -This package is distinct from the NPC race types system (`internal/npc/race_types`): -- **Character Races** (this package): The 21 playable races (Human, Elf, Dwarf, etc.) -- **Race Types** (`internal/npc/race_types`): Creature classifications (Dragon, Undead, Animal, etc.) - -An NPC can have both: -- A character race (if it's a humanoid using a player race model) -- A race type (its creature classification for AI and combat mechanics) \ No newline at end of file diff --git a/internal/recipes/README.md b/internal/recipes/README.md deleted file mode 100644 index 0187f13..0000000 --- a/internal/recipes/README.md +++ /dev/null @@ -1,557 +0,0 @@ -# Recipe System - -Complete tradeskill recipe management system for EverQuest II, converted from C++ EQ2EMu codebase. Handles recipe definitions, player recipe knowledge, crafting components, and database persistence with modern Go concurrency patterns. - -## Overview - -The recipe system manages all aspects of tradeskill crafting in EQ2: - -- **Recipe Management**: Master recipe lists with complex component relationships -- **Player Recipe Knowledge**: Individual player recipe collections and progress tracking -- **Recipe Books**: Tradeskill recipe book items and learning mechanics -- **Database Integration**: Complete SQLite persistence with efficient loading -- **Component System**: Multi-slot component requirements (primary, fuel, build slots) -- **Crafting Stages**: 5-stage crafting progression with products and byproducts -- **Tradeskill Classes**: Class-based recipe access control and validation -- **Integration Interfaces**: Seamless integration with player, item, client, and event systems - -## Core Components - -### Recipe Data Structure - -```go -type Recipe struct { - // Core recipe identification - ID int32 // Unique recipe ID - SoeID int32 // SOE recipe CRC ID - Name string // Recipe display name - Description string // Recipe description - - // Requirements and properties - Level int8 // Required level - Tier int8 // Recipe tier (1-10) - Skill int32 // Required tradeskill ID - Technique int32 // Technique requirement - Knowledge int32 // Knowledge requirement - Classes int32 // Tradeskill class bitmask - - // Product information - ProductItemID int32 // Resulting item ID - ProductName string // Product display name - ProductQty int8 // Quantity produced - - // Component titles and quantities (6 slots) - PrimaryBuildCompTitle string // Primary component name - Build1CompTitle string // Build slot 1 name - Build2CompTitle string // Build slot 2 name - Build3CompTitle string // Build slot 3 name - Build4CompTitle string // Build slot 4 name - FuelCompTitle string // Fuel component name - - // Components map: slot -> list of valid item IDs - // Slots: 0=primary, 1-4=build slots, 5=fuel - Components map[int8][]int32 - - // Products map: stage -> products/byproducts (5 stages) - Products map[int8]*RecipeProducts - - // Player progression - HighestStage int8 // Highest completed stage for player recipes -} -``` - -### Component Slots - -The recipe system uses 6 component slots: - -- **Slot 0 (Primary)**: Primary crafting material -- **Slot 1-4 (Build)**: Secondary build components -- **Slot 5 (Fuel)**: Fuel component for crafting device - -Each slot can contain multiple valid item IDs, providing flexibility in component selection. - -### Crafting Stages - -Recipes support 5 crafting stages (0-4), each with potential products and byproducts: - -```go -type RecipeProducts struct { - ProductID int32 // Main product item ID - ProductQty int8 // Product quantity - ByproductID int32 // Byproduct item ID - ByproductQty int8 // Byproduct quantity -} -``` - -### Master Recipe Lists - -```go -type MasterRecipeList struct { - recipes map[int32]*Recipe // Recipe ID -> Recipe - mutex sync.RWMutex // Thread-safe access -} - -type MasterRecipeBookList struct { - recipeBooks map[int32]*Recipe // Book ID -> Recipe - mutex sync.RWMutex // Thread-safe access -} -``` - -### Player Recipe Collections - -```go -type PlayerRecipeList struct { - recipes map[int32]*Recipe // Player's known recipes - mutex sync.RWMutex // Thread-safe access -} - -type PlayerRecipeBookList struct { - recipeBooks map[int32]*Recipe // Player's recipe books - mutex sync.RWMutex // Thread-safe access -} -``` - -## Database Integration - -### RecipeManager - -High-level recipe system management with complete database integration: - -```go -type RecipeManager struct { - db *database.DB - masterRecipeList *MasterRecipeList - masterRecipeBookList *MasterRecipeBookList - loadedRecipes map[int32]*Recipe - loadedRecipeBooks map[int32]*Recipe - statisticsEnabled bool - stats RecipeManagerStats -} -``` - -### Key Operations - -#### Loading Recipes - -```go -// Load all recipes from database with complex component relationships -manager.LoadRecipes() - -// Load all recipe books -manager.LoadRecipeBooks() - -// Load player-specific recipes -manager.LoadPlayerRecipes(playerRecipeList, characterID) - -// Load player recipe books -manager.LoadPlayerRecipeBooks(playerRecipeBookList, characterID) -``` - -#### Saving Recipe Progress - -```go -// Save new recipe for player -manager.SavePlayerRecipe(characterID, recipeID) - -// Save recipe book for player -manager.SavePlayerRecipeBook(characterID, recipebookID) - -// Update recipe progress -manager.UpdatePlayerRecipe(characterID, recipeID, highestStage) -``` - -### Database Schema - -The system integrates with these key database tables: - -- `recipe`: Master recipe definitions -- `recipe_comp_list`: Component list definitions -- `recipe_comp_list_item`: Individual component items -- `recipe_secondary_comp`: Secondary component mappings -- `character_recipes`: Player recipe knowledge -- `character_recipe_books`: Player recipe book ownership -- `items`: Item definitions and recipe books - -## Integration Interfaces - -### RecipeSystemAdapter - -Primary integration interface for external systems: - -```go -type RecipeSystemAdapter interface { - // Player Integration - GetPlayerRecipeList(characterID int32) *PlayerRecipeList - LoadPlayerRecipes(characterID int32) error - - // Recipe Management - GetRecipe(recipeID int32) *Recipe - ValidateRecipe(recipe *Recipe) bool - - // Progress Tracking - UpdateRecipeProgress(characterID int32, recipeID int32, stage int8) error - GetRecipeProgress(characterID int32, recipeID int32) int8 -} -``` - -### Database Adapter - -Abstracts database operations for testing and multiple database support: - -```go -type DatabaseRecipeAdapter interface { - LoadAllRecipes() ([]*Recipe, error) - LoadPlayerRecipes(characterID int32) ([]*Recipe, error) - SavePlayerRecipe(characterID int32, recipeID int32) error - UpdatePlayerRecipe(characterID int32, recipeID int32, highestStage int8) error -} -``` - -### Player Integration - -```go -type PlayerRecipeAdapter interface { - CanPlayerLearnRecipe(characterID int32, recipeID int32) bool - GetPlayerTradeskillLevel(characterID int32, skillID int32) int32 - AwardTradeskillExperience(characterID int32, skillID int32, experience int32) error -} -``` - -### Item Integration - -```go -type ItemRecipeAdapter interface { - ValidateRecipeComponents(recipeID int32) bool - PlayerHasComponents(characterID int32, recipeID int32) bool - ConsumeRecipeComponents(characterID int32, recipeID int32) error - AwardRecipeProduct(characterID int32, itemID int32, quantity int8) error -} -``` - -### Client Integration - -```go -type ClientRecipeAdapter interface { - SendRecipeList(characterID int32) error - SendRecipeLearned(characterID int32, recipeID int32) error - SendTradeskillWindow(characterID int32, deviceID int32) error - SendCraftingResults(characterID int32, success bool, itemID int32, quantity int8) error -} -``` - -### Event Integration - -```go -type EventRecipeAdapter interface { - OnRecipeLearned(characterID int32, recipeID int32) error - OnRecipeCrafted(characterID int32, recipeID int32, success bool) error - CheckCraftingAchievements(characterID int32, recipeID int32) error -} -``` - -## Usage Examples - -### Basic Recipe Management - -```go -// Create recipe manager -db, _ := database.Open("world.db") -manager := NewRecipeManager(db) - -// Load all recipes and recipe books -manager.LoadRecipes() -manager.LoadRecipeBooks() - -// Get a specific recipe -recipe := manager.GetRecipe(12345) -if recipe != nil { - fmt.Printf("Recipe: %s (Level %d, Tier %d)\n", - recipe.Name, recipe.Level, recipe.Tier) -} - -// Check recipe validation -if recipe.IsValid() { - fmt.Println("Recipe is valid") -} -``` - -### Player Recipe Operations - -```go -// Load player recipes -playerRecipes := NewPlayerRecipeList() -manager.LoadPlayerRecipes(playerRecipes, characterID) - -// Check if player knows a recipe -if playerRecipes.HasRecipe(recipeID) { - fmt.Println("Player knows this recipe") -} - -// Learn a new recipe -recipe := manager.GetRecipe(recipeID) -if recipe != nil { - playerCopy := NewRecipeFromRecipe(recipe) - playerRecipes.AddRecipe(playerCopy) - manager.SavePlayerRecipe(characterID, recipeID) -} -``` - -### Recipe Component Analysis - -```go -// Get components for each slot -for slot := int8(0); slot < 6; slot++ { - components := recipe.GetComponentsBySlot(slot) - title := recipe.GetComponentTitleForSlot(slot) - quantity := recipe.GetComponentQuantityForSlot(slot) - - fmt.Printf("Slot %d (%s): %d needed\n", slot, title, quantity) - for _, itemID := range components { - fmt.Printf(" - Item ID: %d\n", itemID) - } -} -``` - -### Crafting Stage Processing - -```go -// Process each crafting stage -for stage := int8(0); stage < 5; stage++ { - products := recipe.GetProductsForStage(stage) - if products != nil { - fmt.Printf("Stage %d produces:\n", stage) - fmt.Printf(" Product: %d (qty: %d)\n", - products.ProductID, products.ProductQty) - if products.ByproductID > 0 { - fmt.Printf(" Byproduct: %d (qty: %d)\n", - products.ByproductID, products.ByproductQty) - } - } -} -``` - -### Integration with Full System - -```go -// Create system dependencies -deps := &RecipeSystemDependencies{ - Database: &DatabaseRecipeAdapterImpl{}, - Player: &PlayerRecipeAdapterImpl{}, - Item: &ItemRecipeAdapterImpl{}, - Client: &ClientRecipeAdapterImpl{}, - Event: &EventRecipeAdapterImpl{}, - Crafting: &CraftingRecipeAdapterImpl{}, -} - -// Create integrated recipe system -adapter := NewRecipeManagerAdapter(db, deps) -adapter.Initialize() - -// Handle recipe learning workflow -err := adapter.PlayerLearnRecipe(characterID, recipeID) -if err != nil { - fmt.Printf("Failed to learn recipe: %v\n", err) -} - -// Handle recipe book acquisition workflow -err = adapter.PlayerObtainRecipeBook(characterID, bookID) -if err != nil { - fmt.Printf("Failed to obtain recipe book: %v\n", err) -} -``` - -## Tradeskill Classes - -The system supports all EQ2 tradeskill classes with bitmask-based access control: - -### Base Classes -- **Provisioner** (food and drink) -- **Woodworker** (wooden items, bows, arrows) -- **Carpenter** (furniture and housing items) -- **Outfitter** (light and medium armor) -- **Armorer** (heavy armor and shields) -- **Weaponsmith** (metal weapons) -- **Tailor** (cloth armor and bags) -- **Scholar** (spells and combat arts) -- **Jeweler** (jewelry) -- **Sage** (advanced spells) -- **Alchemist** (potions and poisons) -- **Craftsman** (general crafting) -- **Tinkerer** (tinkered items) - -### Class Validation - -```go -// Check if a tradeskill class can use a recipe -canUse := recipe.CanUseRecipeByClass(classID) - -// Special handling for "any class" recipes (adornments + artisan) -if recipe.Classes < 4 { - // Any class can use this recipe -} -``` - -## Crafting Devices - -Recipes require specific crafting devices: - -- **Stove & Keg**: Provisioning recipes -- **Forge**: Weaponsmithing and armoring -- **Sewing Table & Mannequin**: Tailoring and outfitting -- **Woodworking Table**: Woodworking and carpentry -- **Chemistry Table**: Alchemy -- **Jeweler's Table**: Jewelcrafting -- **Loom & Spinning Wheel**: Advanced tailoring -- **Engraved Desk**: Scribing and sage recipes -- **Work Bench**: Tinkering - -## Statistics and Monitoring - -### RecipeManagerStats - -```go -type RecipeManagerStats struct { - TotalRecipesLoaded int32 - TotalRecipeBooksLoaded int32 - PlayersWithRecipes int32 - LoadOperations int32 - SaveOperations int32 -} - -// Get current statistics -stats := manager.GetStatistics() -fmt.Printf("Loaded %d recipes, %d recipe books\n", - stats.TotalRecipesLoaded, stats.TotalRecipeBooksLoaded) -``` - -### Validation - -```go -// Comprehensive system validation -issues := manager.Validate() -for _, issue := range issues { - fmt.Printf("Validation issue: %s\n", issue) -} - -// Recipe-specific validation -if !recipe.IsValid() { - fmt.Println("Recipe has invalid data") -} -``` - -## Thread Safety - -All recipe system components are fully thread-safe: - -- **RWMutex Protection**: All maps and collections use read-write mutexes -- **Atomic Operations**: Statistics counters use atomic operations where appropriate -- **Safe Copying**: Recipe copying creates deep copies of all nested data -- **Concurrent Access**: Multiple goroutines can safely read recipe data simultaneously -- **Write Synchronization**: All write operations are properly synchronized - -## Performance Considerations - -### Efficient Loading - -- **Batch Loading**: All recipes loaded in single database query with complex joins -- **Component Resolution**: Components loaded separately and linked to recipes -- **Memory Management**: Recipes cached in memory for fast access -- **Statistics Tracking**: Optional statistics collection for performance monitoring - -### Memory Usage - -- **Component Maps**: Pre-allocated component arrays for each recipe -- **Product Arrays**: Fixed-size arrays for 5 crafting stages -- **Player Copies**: Player recipe instances are copies, not references -- **Cleanup**: Proper cleanup of database connections and prepared statements - -## Error Handling - -### Comprehensive Error Types - -```go -var ( - ErrRecipeNotFound = errors.New("recipe not found") - ErrRecipeBookNotFound = errors.New("recipe book not found") - ErrInvalidRecipeID = errors.New("invalid recipe ID") - ErrDuplicateRecipe = errors.New("duplicate recipe ID") - ErrMissingComponents = errors.New("missing required components") - ErrInsufficientSkill = errors.New("insufficient skill level") - ErrWrongTradeskillClass = errors.New("wrong tradeskill class") - ErrCannotLearnRecipe = errors.New("cannot learn recipe") - ErrCannotUseRecipeBook = errors.New("cannot use recipe book") -) -``` - -### Error Propagation - -All database operations return detailed errors that can be handled appropriately by calling systems. - -## Database Schema Compatibility - -### C++ EQ2EMu Compatibility - -The Go implementation maintains full compatibility with the existing C++ EQ2EMu database schema: - -- **Recipe Table**: Direct mapping of all C++ Recipe fields -- **Component System**: Preserves complex component relationships -- **Player Data**: Compatible with existing character recipe storage -- **Query Optimization**: Uses same optimized queries as C++ version - -### Migration Support - -The system can seamlessly work with existing EQ2EMu databases without requiring schema changes or data migration. - -## Future Enhancements - -Areas marked for future implementation: - -- **Lua Integration**: Recipe validation and custom logic via Lua scripts -- **Advanced Crafting**: Rare material handling and advanced crafting mechanics -- **Recipe Discovery**: Dynamic recipe discovery and experimentation -- **Guild Recipes**: Guild-specific recipe sharing and management -- **Seasonal Recipes**: Time-based recipe availability -- **Recipe Sets**: Collection-based achievements and bonuses - -## Testing - -### Unit Tests - -```go -func TestRecipeValidation(t *testing.T) { - recipe := NewRecipe() - recipe.ID = 12345 - recipe.Name = "Test Recipe" - recipe.Level = 50 - recipe.Tier = 5 - - assert.True(t, recipe.IsValid()) -} - -func TestPlayerRecipeOperations(t *testing.T) { - playerRecipes := NewPlayerRecipeList() - recipe := NewRecipe() - recipe.ID = 12345 - - assert.True(t, playerRecipes.AddRecipe(recipe)) - assert.True(t, playerRecipes.HasRecipe(12345)) - assert.Equal(t, 1, playerRecipes.Size()) -} -``` - -### Integration Tests - -```go -func TestDatabaseIntegration(t *testing.T) { - db := setupTestDatabase() - manager := NewRecipeManager(db) - - err := manager.LoadRecipes() - assert.NoError(t, err) - - recipes, books := manager.Size() - assert.Greater(t, recipes, int32(0)) -} -``` - -This recipe system provides a complete, thread-safe, and efficient implementation of EverQuest II tradeskill recipes with modern Go patterns while maintaining compatibility with the existing C++ EQ2EMu architecture. diff --git a/internal/recipes/recipes_test.go b/internal/recipes/recipes_test.go deleted file mode 100644 index 65cc47a..0000000 --- a/internal/recipes/recipes_test.go +++ /dev/null @@ -1,865 +0,0 @@ -package recipes - -import ( - "testing" -) - -// Test Recipe creation and basic operations -func TestNewRecipe(t *testing.T) { - recipe := NewRecipe() - if recipe == nil { - t.Error("NewRecipe should not return nil") - } - if recipe.Components == nil { - t.Error("Components map should be initialized") - } - if recipe.Products == nil { - t.Error("Products map should be initialized") - } -} - -func TestNewRecipeFromRecipe(t *testing.T) { - // Test with nil source - recipe := NewRecipeFromRecipe(nil) - if recipe == nil { - t.Error("NewRecipeFromRecipe with nil should return valid recipe") - } - - // Test with valid source - source := NewRecipe() - source.ID = 12345 - source.Name = "Test Recipe" - source.Level = 50 - source.Tier = 5 - source.Icon = 100 - source.Components[0] = []int32{1001, 1002} - source.Products[0] = &RecipeProducts{ - ProductID: 2001, - ProductQty: 1, - } - - copied := NewRecipeFromRecipe(source) - if copied == nil { - t.Error("NewRecipeFromRecipe should not return nil") - } - if copied.ID != source.ID { - t.Error("Copied recipe should have same ID") - } - if copied.Name != source.Name { - t.Error("Copied recipe should have same name") - } - if copied.Level != source.Level { - t.Error("Copied recipe should have same level") - } - if copied.Tier != source.Tier { - t.Error("Copied recipe should have same tier") - } - - // Check component copying - if len(copied.Components[0]) != len(source.Components[0]) { - t.Error("Components should be copied") - } - - // Check product copying - if copied.Products[0] == nil { - t.Error("Products should be copied") - } - if copied.Products[0].ProductID != source.Products[0].ProductID { - t.Error("Product data should be copied correctly") - } -} - -func TestRecipeIsValid(t *testing.T) { - // Test invalid recipe (empty) - recipe := NewRecipe() - if recipe.IsValid() { - t.Error("Empty recipe should not be valid") - } - - // Test valid recipe - recipe.ID = 12345 - recipe.Name = "Valid Recipe" - recipe.Level = 50 - recipe.Tier = 5 // Need valid tier for validation - if !recipe.IsValid() { - t.Error("Recipe with valid data should be valid") - } - - // Test invalid ID - recipe.ID = 0 - if recipe.IsValid() { - t.Error("Recipe with ID 0 should not be valid") - } - - // Test empty name - recipe.ID = 12345 - recipe.Name = "" - if recipe.IsValid() { - t.Error("Recipe with empty name should not be valid") - } - - // Test invalid level - recipe.Name = "Valid Recipe" - recipe.Tier = 5 - recipe.Level = -1 - if recipe.IsValid() { - t.Error("Recipe with negative level should not be valid") - } - - recipe.Level = 101 - if recipe.IsValid() { - t.Error("Recipe with level > 100 should not be valid") - } - - // Test invalid tier - recipe.Level = 50 - recipe.Tier = 0 - if recipe.IsValid() { - t.Error("Recipe with tier 0 should not be valid") - } - - recipe.Tier = 11 - if recipe.IsValid() { - t.Error("Recipe with tier > 10 should not be valid") - } -} - -func TestRecipeGetTotalBuildComponents(t *testing.T) { - recipe := NewRecipe() - - // Test with no build components - if recipe.GetTotalBuildComponents() != 0 { - t.Error("Recipe with no build components should return 0") - } - - // Add build components to different slots - recipe.Components[SlotBuild1] = []int32{1001} - recipe.Components[SlotBuild3] = []int32{1003, 1004} - - count := recipe.GetTotalBuildComponents() - if count != 2 { - t.Errorf("Expected 2 build component slots, got %d", count) - } -} - -func TestRecipeCanUseRecipeByClass(t *testing.T) { - recipe := NewRecipe() - - // Test "any class" recipe - recipe.Classes = 2 - if !recipe.CanUseRecipeByClass(1) { - t.Error("Any class recipe should be usable by any class") - } - - // Test specific class requirement - recipe.Classes = 1 << 3 // Bit 3 set (class ID 3) - if !recipe.CanUseRecipeByClass(3) { - t.Error("Recipe should be usable by matching class") - } - if recipe.CanUseRecipeByClass(2) { - t.Error("Recipe should not be usable by non-matching class") - } -} - -func TestRecipeComponentOperations(t *testing.T) { - recipe := NewRecipe() - - // Test getting components by slot - recipe.Components[0] = []int32{1001, 1002, 1003} - components := recipe.GetComponentsBySlot(0) - if len(components) != 3 { - t.Error("Should return all components for slot") - } - - // Test with empty slot - emptyComponents := recipe.GetComponentsBySlot(1) - if len(emptyComponents) != 0 { - t.Error("Empty slot should return empty slice") - } -} - -func TestRecipeProductOperations(t *testing.T) { - recipe := NewRecipe() - - // Test getting products for stage - recipe.Products[0] = &RecipeProducts{ - ProductID: 2001, - ProductQty: 1, - ByproductID: 2002, - ByproductQty: 2, - } - - products := recipe.GetProductsForStage(0) - if products == nil { - t.Error("Should return products for stage") - } - if products.ProductID != 2001 { - t.Error("Product ID should match") - } - if products.ByproductID != 2002 { - t.Error("Byproduct ID should match") - } - - // Test with empty stage - emptyProducts := recipe.GetProductsForStage(1) - if emptyProducts != nil { - t.Error("Empty stage should return nil") - } -} - -// Test MasterRecipeList operations -func TestNewMasterRecipeList(t *testing.T) { - list := NewMasterRecipeList() - if list == nil { - t.Error("NewMasterRecipeList should not return nil") - } - if list.recipes == nil { - t.Error("Recipes map should be initialized") - } - if list.stats == nil { - t.Error("Statistics should be initialized") - } -} - -func TestMasterRecipeListAddRecipe(t *testing.T) { - list := NewMasterRecipeList() - - // Test adding nil recipe - if list.AddRecipe(nil) { - t.Error("Adding nil recipe should fail") - } - - // Test adding invalid recipe - invalidRecipe := NewRecipe() - if list.AddRecipe(invalidRecipe) { - t.Error("Adding invalid recipe should fail") - } - - // Test adding valid recipe - validRecipe := NewRecipe() - validRecipe.ID = 12345 - validRecipe.Name = "Test Recipe" - validRecipe.Level = 50 - validRecipe.Tier = 5 // Need valid tier - if !list.AddRecipe(validRecipe) { - t.Error("Adding valid recipe should succeed") - } - - // Test adding duplicate recipe - duplicate := NewRecipe() - duplicate.ID = 12345 - duplicate.Name = "Duplicate" - duplicate.Level = 60 - if list.AddRecipe(duplicate) { - t.Error("Adding duplicate recipe ID should fail") - } -} - -func TestMasterRecipeListGetRecipe(t *testing.T) { - list := NewMasterRecipeList() - - // Test getting non-existent recipe - recipe := list.GetRecipe(99999) - if recipe != nil { - t.Error("Getting non-existent recipe should return nil") - } - - // Add a recipe - testRecipe := NewRecipe() - testRecipe.ID = 12345 - testRecipe.Name = "Test Recipe" - testRecipe.Level = 50 - testRecipe.Tier = 5 // Need valid tier - list.AddRecipe(testRecipe) - - // Test getting existing recipe - retrieved := list.GetRecipe(12345) - if retrieved == nil { - t.Error("Getting existing recipe should not return nil") - } - if retrieved.ID != 12345 { - t.Error("Retrieved recipe should have correct ID") - } -} - -func TestMasterRecipeListSize(t *testing.T) { - list := NewMasterRecipeList() - - // Test empty list - if list.Size() != 0 { - t.Error("Empty list should have size 0") - } - - // Add recipes - for i := 1; i <= 3; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Test Recipe" - recipe.Level = 50 - recipe.Tier = 5 - list.AddRecipe(recipe) - } - - if list.Size() != 3 { - t.Error("List with 3 recipes should have size 3") - } -} - -func TestMasterRecipeListClear(t *testing.T) { - list := NewMasterRecipeList() - - // Add some recipes - for i := 1; i <= 3; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Test Recipe" - recipe.Level = 50 - recipe.Tier = 5 - list.AddRecipe(recipe) - } - - // Clear the list - list.ClearRecipes() - - if list.Size() != 0 { - t.Error("Cleared list should have size 0") - } - if list.GetRecipe(1) != nil { - t.Error("Recipe should not exist after clear") - } -} - -func TestMasterRecipeListGetRecipesByTier(t *testing.T) { - list := NewMasterRecipeList() - - // Add recipes with different tiers - for i := 1; i <= 5; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Test Recipe" - recipe.Level = int8(i * 10) - recipe.Tier = int8(i) - list.AddRecipe(recipe) - } - - // Get recipes for tier 3 - tier3Recipes := list.GetRecipesByTier(3) - if len(tier3Recipes) != 1 { - t.Errorf("Expected 1 recipe for tier 3, got %d", len(tier3Recipes)) - } - if tier3Recipes[0].Tier != 3 { - t.Error("Recipe should have tier 3") - } - - // Get recipes for non-existent tier - emptyTier := list.GetRecipesByTier(99) - if len(emptyTier) != 0 { - t.Error("Non-existent tier should return empty slice") - } -} - -func TestMasterRecipeListGetRecipesBySkill(t *testing.T) { - list := NewMasterRecipeList() - - // Add recipes with different skills - skillRecipe1 := NewRecipe() - skillRecipe1.ID = 1 - skillRecipe1.Name = "Skill Recipe 1" - skillRecipe1.Level = 50 - skillRecipe1.Tier = 5 - skillRecipe1.Skill = 100 - list.AddRecipe(skillRecipe1) - - skillRecipe2 := NewRecipe() - skillRecipe2.ID = 2 - skillRecipe2.Name = "Skill Recipe 2" - skillRecipe2.Level = 60 - skillRecipe2.Tier = 6 - skillRecipe2.Skill = 100 - list.AddRecipe(skillRecipe2) - - differentSkillRecipe := NewRecipe() - differentSkillRecipe.ID = 3 - differentSkillRecipe.Name = "Different Skill Recipe" - differentSkillRecipe.Level = 50 - differentSkillRecipe.Tier = 5 - differentSkillRecipe.Skill = 200 - list.AddRecipe(differentSkillRecipe) - - // Get recipes for skill 100 - skill100Recipes := list.GetRecipesBySkill(100) - if len(skill100Recipes) != 2 { - t.Errorf("Expected 2 recipes for skill 100, got %d", len(skill100Recipes)) - } - - // Get recipes for skill 200 - skill200Recipes := list.GetRecipesBySkill(200) - if len(skill200Recipes) != 1 { - t.Errorf("Expected 1 recipe for skill 200, got %d", len(skill200Recipes)) - } -} - -// Test PlayerRecipeList operations -func TestNewPlayerRecipeList(t *testing.T) { - playerList := NewPlayerRecipeList() - if playerList == nil { - t.Error("NewPlayerRecipeList should not return nil") - } - if playerList.recipes == nil { - t.Error("Recipes map should be initialized") - } -} - -func TestPlayerRecipeListAddRecipe(t *testing.T) { - playerList := NewPlayerRecipeList() - - // Test adding nil recipe - if playerList.AddRecipe(nil) { - t.Error("Adding nil recipe should fail") - } - - // Test adding valid recipe - recipe := NewRecipe() - recipe.ID = 12345 - recipe.Name = "Test Recipe" - recipe.Level = 50 - recipe.Tier = 5 - if !playerList.AddRecipe(recipe) { - t.Error("Adding valid recipe should succeed") - } - - // Test adding duplicate recipe - duplicate := NewRecipe() - duplicate.ID = 12345 - duplicate.Name = "Duplicate" - duplicate.Level = 60 - if playerList.AddRecipe(duplicate) { - t.Error("Adding duplicate recipe should fail") - } -} - -func TestPlayerRecipeListGetRecipe(t *testing.T) { - playerList := NewPlayerRecipeList() - - // Test checking non-existent recipe - if playerList.GetRecipe(99999) != nil { - t.Error("Player should not have non-existent recipe") - } - - // Add a recipe - recipe := NewRecipe() - recipe.ID = 12345 - recipe.Name = "Test Recipe" - recipe.Level = 50 - recipe.Tier = 5 - playerList.AddRecipe(recipe) - - // Test checking existing recipe - if playerList.GetRecipe(12345) == nil { - t.Error("Player should have added recipe") - } -} - -func TestPlayerRecipeListSize(t *testing.T) { - playerList := NewPlayerRecipeList() - - // Test empty list - if playerList.Size() != 0 { - t.Error("Empty player recipe list should have size 0") - } - - // Add recipes - for i := 1; i <= 3; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Test Recipe" - recipe.Level = 50 - recipe.Tier = 5 - playerList.AddRecipe(recipe) - } - - if playerList.Size() != 3 { - t.Error("Player recipe list with 3 recipes should have size 3") - } -} - -// Test Recipe Book List operations -func TestNewMasterRecipeBookList(t *testing.T) { - bookList := NewMasterRecipeBookList() - if bookList == nil { - t.Error("NewMasterRecipeBookList should not return nil") - } -} - -func TestMasterRecipeBookListOperations(t *testing.T) { - bookList := NewMasterRecipeBookList() - - // Test adding recipe book - recipeBook := NewRecipe() - recipeBook.ID = 5001 - recipeBook.BookID = 5001 // Need BookID for recipe books - recipeBook.Name = "Test Recipe Book" - recipeBook.Level = 1 - recipeBook.Tier = 1 - - if !bookList.AddRecipeBook(recipeBook) { - t.Error("Adding valid recipe book should succeed") - } - - // Test getting recipe book - retrieved := bookList.GetRecipeBook(5001) - if retrieved == nil { - t.Error("Should retrieve added recipe book") - } - if retrieved.ID != 5001 { - t.Error("Retrieved recipe book should have correct ID") - } - - // Test size - if bookList.Size() != 1 { - t.Error("Recipe book list should have size 1") - } -} - -func TestNewPlayerRecipeBookList(t *testing.T) { - playerBookList := NewPlayerRecipeBookList() - if playerBookList == nil { - t.Error("NewPlayerRecipeBookList should not return nil") - } -} - -// Test RecipeManager operations -func TestNewRecipeManager(t *testing.T) { - manager := NewRecipeManager() - if manager == nil { - t.Error("NewRecipeManager should not return nil") - } - if manager.masterRecipeList == nil { - t.Error("Master recipe list should be initialized") - } - if manager.masterRecipeBookList == nil { - t.Error("Master recipe book list should be initialized") - } - if manager.loadedRecipes == nil { - t.Error("Loaded recipes map should be initialized") - } - if manager.loadedRecipeBooks == nil { - t.Error("Loaded recipe books map should be initialized") - } -} - -func TestRecipeManagerGetters(t *testing.T) { - manager := NewRecipeManager() - - // Test getting master lists - masterList := manager.GetMasterRecipeList() - if masterList == nil { - t.Error("Should return master recipe list") - } - - masterBookList := manager.GetMasterRecipeBookList() - if masterBookList == nil { - t.Error("Should return master recipe book list") - } - - // Test getting non-existent recipe - recipe := manager.GetRecipe(99999) - if recipe != nil { - t.Error("Non-existent recipe should return nil") - } - - // Test getting non-existent recipe book - book := manager.GetRecipeBook(99999) - if book != nil { - t.Error("Non-existent recipe book should return nil") - } -} - -func TestRecipeManagerStatistics(t *testing.T) { - manager := NewRecipeManager() - - // Test getting statistics when enabled - manager.GetStatistics() // Call method without assignment to avoid lock copy - - // Test disabling statistics - manager.SetStatisticsEnabled(false) - manager.GetStatistics() // Call method without assignment to avoid lock copy - - // Test enabling statistics - manager.SetStatisticsEnabled(true) -} - -func TestRecipeManagerValidation(t *testing.T) { - manager := NewRecipeManager() - - // Test validation of empty manager - issues := manager.Validate() - if len(issues) > 0 { - // Manager validation might find issues, that's fine - } -} - -func TestRecipeManagerSize(t *testing.T) { - manager := NewRecipeManager() - - // Test size of empty manager - recipes, books := manager.Size() - if recipes != 0 { - t.Error("Empty manager should have 0 recipes") - } - if books != 0 { - t.Error("Empty manager should have 0 recipe books") - } -} - -// Test RecipeComponent operations -func TestRecipeComponent(t *testing.T) { - component := &RecipeComponent{ - ItemID: 1001, - Slot: 0, - } - - if component.ItemID != 1001 { - t.Error("Component item ID should be set correctly") - } - if component.Slot != 0 { - t.Error("Component slot should be set correctly") - } -} - -// Test RecipeProducts operations -func TestRecipeProducts(t *testing.T) { - products := &RecipeProducts{ - ProductID: 2001, - ProductQty: 1, - ByproductID: 2002, - ByproductQty: 2, - } - - if products.ProductID != 2001 { - t.Error("Product ID should be set correctly") - } - if products.ProductQty != 1 { - t.Error("Product quantity should be set correctly") - } - if products.ByproductID != 2002 { - t.Error("Byproduct ID should be set correctly") - } - if products.ByproductQty != 2 { - t.Error("Byproduct quantity should be set correctly") - } -} - -// Test Statistics operations -func TestNewStatistics(t *testing.T) { - stats := NewStatistics() - if stats == nil { - t.Error("NewStatistics should not return nil") - } - if stats.RecipesByTier == nil { - t.Error("RecipesByTier map should be initialized") - } - if stats.RecipesBySkill == nil { - t.Error("RecipesBySkill map should be initialized") - } -} - -func TestStatisticsOperations(t *testing.T) { - stats := NewStatistics() - - // Test incrementing lookups - initialLookups := stats.RecipeLookups - stats.IncrementRecipeLookups() - if stats.RecipeLookups != initialLookups+1 { - t.Error("Recipe lookups should be incremented") - } - - // Test incrementing recipe book lookups - initialBookLookups := stats.RecipeBookLookups - stats.IncrementRecipeBookLookups() - if stats.RecipeBookLookups != initialBookLookups+1 { - t.Error("Recipe book lookups should be incremented") - } - - // Test incrementing player recipe loads - initialLoads := stats.PlayerRecipeLoads - stats.IncrementPlayerRecipeLoads() - if stats.PlayerRecipeLoads != initialLoads+1 { - t.Error("Player recipe loads should be incremented") - } - - // Test incrementing component queries - initialQueries := stats.ComponentQueries - stats.IncrementComponentQueries() - if stats.ComponentQueries != initialQueries+1 { - t.Error("Component queries should be incremented") - } - - // Test getting snapshot - snapshot := stats.GetSnapshot() - if snapshot.RecipeLookups != stats.RecipeLookups { - t.Error("Snapshot should match current stats") - } -} - -// Test error constants -func TestErrorConstants(t *testing.T) { - if ErrRecipeNotFound == nil { - t.Error("ErrRecipeNotFound should be defined") - } - if ErrRecipeBookNotFound == nil { - t.Error("ErrRecipeBookNotFound should be defined") - } - if ErrInvalidRecipeID == nil { - t.Error("ErrInvalidRecipeID should be defined") - } - if ErrDuplicateRecipe == nil { - t.Error("ErrDuplicateRecipe should be defined") - } -} - -// Test constants -func TestConstants(t *testing.T) { - // Test slot constants - if SlotPrimary != 0 { - t.Error("SlotPrimary should be 0") - } - if SlotBuild1 != 1 { - t.Error("SlotBuild1 should be 1") - } - if SlotFuel != 5 { - t.Error("SlotFuel should be 5") - } - - // Test stage constants - if Stage0 != 0 { - t.Error("Stage0 should be 0") - } - if Stage4 != 4 { - t.Error("Stage4 should be 4") - } - - // Test validation constants - if MinRecipeID != 1 { - t.Error("MinRecipeID should be 1") - } - if MaxRecipeLevel != 100 { - t.Error("MaxRecipeLevel should be 100") - } -} - -// Edge case tests -func TestRecipeEdgeCases(t *testing.T) { - recipe := NewRecipe() - - // Test accessing non-existent component slots - components := recipe.GetComponentsBySlot(99) - if len(components) != 0 { - t.Error("Non-existent slot should return empty slice") - } - - // Test accessing non-existent product stages - products := recipe.GetProductsForStage(99) - if products != nil { - t.Error("Non-existent stage should return nil") - } - - // Test recipe with maximum values - recipe.ID = MaxRecipeID - recipe.Level = MaxRecipeLevel - recipe.Tier = MaxTier - recipe.Name = "Max Recipe" - if !recipe.IsValid() { - t.Error("Recipe with maximum valid values should be valid") - } -} - -func TestMasterRecipeListEdgeCases(t *testing.T) { - list := NewMasterRecipeList() - - // Test operations on empty list - emptyTier := list.GetRecipesByTier(1) - if len(emptyTier) != 0 { - t.Error("Empty list should return empty slice for tier query") - } - - emptySkill := list.GetRecipesBySkill(100) - if len(emptySkill) != 0 { - t.Error("Empty list should return empty slice for skill query") - } - - // Test clearing already empty list - list.ClearRecipes() - if list.Size() != 0 { - t.Error("Clearing empty list should keep size 0") - } -} - -// Benchmark tests -func BenchmarkNewRecipe(b *testing.B) { - for i := 0; i < b.N; i++ { - NewRecipe() - } -} - -func BenchmarkRecipeCopy(b *testing.B) { - source := NewRecipe() - source.ID = 12345 - source.Name = "Benchmark Recipe" - source.Level = 50 - source.Tier = 5 - source.Components[0] = []int32{1001, 1002, 1003} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - NewRecipeFromRecipe(source) - } -} - -func BenchmarkMasterRecipeListAdd(b *testing.B) { - list := NewMasterRecipeList() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Benchmark Recipe" - recipe.Level = 50 - recipe.Tier = 5 - list.AddRecipe(recipe) - } -} - -func BenchmarkMasterRecipeListGet(b *testing.B) { - list := NewMasterRecipeList() - - // Add test recipes - for i := 1; i <= 1000; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Benchmark Recipe" - recipe.Level = 50 - recipe.Tier = 5 - list.AddRecipe(recipe) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - list.GetRecipe(int32((i % 1000) + 1)) - } -} - -func BenchmarkPlayerRecipeListOperations(b *testing.B) { - playerList := NewPlayerRecipeList() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - recipe := NewRecipe() - recipe.ID = int32(i) - recipe.Name = "Benchmark Recipe" - recipe.Level = 50 - recipe.Tier = 5 - playerList.AddRecipe(recipe) - playerList.GetRecipe(int32(i)) - } -} \ No newline at end of file diff --git a/internal/rules/README.md b/internal/rules/README.md deleted file mode 100644 index 221590c..0000000 --- a/internal/rules/README.md +++ /dev/null @@ -1,138 +0,0 @@ -# Rules System - -The rules system provides configurable game parameters and settings for the EQ2 server. It has been fully converted from the original C++ EQ2EMu implementation to Go. - -## Overview - -The rules system consists of: -- **Rule Categories**: Major groupings of rules (Player, Combat, World, etc.) -- **Rule Types**: Specific rule types within each category -- **Rule Sets**: Collections of rules that can be switched between -- **Global/Zone Rules**: Global rules apply server-wide, zone rules override for specific zones - -## Core Components - -### Files - -- `constants.go` - Rule categories, types, and constants -- `types.go` - Core data structures (Rule, RuleSet, RuleManagerStats) -- `manager.go` - Main RuleManager implementation with default rules -- `database.go` - Database operations for rule persistence -- `interfaces.go` - Integration interfaces and adapters -- `rules_test.go` - Comprehensive test suite - -### Main Types - -- `Rule` - Individual rule with category, type, value, and type conversion methods -- `RuleSet` - Collection of rules with ID and name -- `RuleManager` - Central management of all rules and rule sets -- `DatabaseService` - Database operations for rule persistence - -## Rule Categories - -The system supports 14 rule categories: - -1. **Client** - Client-related settings -2. **Faction** - Faction system rules -3. **Guild** - Guild system rules -4. **Player** - Player-related rules (levels, stats, etc.) -5. **PVP** - Player vs Player combat rules -6. **Combat** - Combat system rules -7. **Spawn** - NPC/spawn behavior rules -8. **UI** - User interface rules -9. **World** - Server-wide settings -10. **Zone** - Zone-specific rules -11. **Loot** - Loot system rules -12. **Spells** - Spell system rules -13. **Expansion** - Expansion flags -14. **Discord** - Discord integration settings - -## Usage - -### Basic Usage - -```go -// Create rule manager -ruleManager := rules.NewRuleManager() - -// Get a rule value -rule := ruleManager.GetGlobalRule(rules.CategoryPlayer, rules.PlayerMaxLevel) -maxLevel := rule.GetInt32() // Returns 50 (default) - -// Get rule by name -rule2 := ruleManager.GetGlobalRuleByName("Player", "MaxLevel") -``` - -### Database Integration - -```go -// Create database service -db, _ := database.Open("rules.db") -dbService := rules.NewDatabaseService(db) - -// Create tables -dbService.CreateRulesTables() - -// Load rules from database -dbService.LoadRuleSets(ruleManager, false) -``` - -### Rule Adapters - -```go -// Create adapter for zone-specific rules -adapter := rules.NewRuleManagerAdapter(ruleManager, zoneID) - -// Get rule with zone override -maxLevel := adapter.GetInt32(rules.CategoryPlayer, rules.PlayerMaxLevel) -``` - -## Default Rules - -The system includes comprehensive default rules matching the original C++ implementation: - -- **Player Max Level**: 50 -- **Combat Max Range**: 4.0 -- **Experience Multiplier**: 1.0 -- **And 100+ other rules across all categories** - -## Database Schema - -The system uses three tables: - -- `rulesets` - Rule set definitions -- `ruleset_details` - Individual rule overrides per rule set -- `variables` - System variables including default rule set ID - -## Thread Safety - -All operations are thread-safe using Go's sync.RWMutex for optimal read performance. - -## Performance - -- Rule access: ~280ns per operation (benchmark) -- Rule manager creation: ~45μs per operation (benchmark) -- All operations are optimized for high-frequency access - -## Testing - -Run the comprehensive test suite: - -```bash -go test ./internal/rules/ -v -``` - -## Migration from C++ - -This is a complete conversion from the original C++ implementation: - -- `Rules.h` → `constants.go` + `types.go` -- `Rules.cpp` → `manager.go` -- `RulesDB.cpp` → `database.go` - -All functionality has been preserved with Go-native patterns and improvements: -- Better error handling -- Type safety -- Comprehensive interfaces -- Modern testing practices -- Performance optimizations \ No newline at end of file diff --git a/internal/rules/rules_test.go b/internal/rules/rules_test.go deleted file mode 100644 index d7bb9ef..0000000 --- a/internal/rules/rules_test.go +++ /dev/null @@ -1,1212 +0,0 @@ -package rules - -import ( - "testing" -) - -// Test Rule creation and basic functionality -func TestNewRule(t *testing.T) { - rule := NewRule() - if rule == nil { - t.Fatal("NewRule() returned nil") - } - - if rule.GetCategory() != 0 { - t.Errorf("Expected category 0, got %d", rule.GetCategory()) - } - - if rule.GetType() != 0 { - t.Errorf("Expected type 0, got %d", rule.GetType()) - } - - if rule.GetValue() != "" { - t.Errorf("Expected empty value, got %s", rule.GetValue()) - } - - if rule.GetCombined() != "NONE" { - t.Errorf("Expected combined 'NONE', got %s", rule.GetCombined()) - } -} - -func TestNewRuleWithValues(t *testing.T) { - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - if rule == nil { - t.Fatal("NewRuleWithValues() returned nil") - } - - if rule.GetCategory() != CategoryPlayer { - t.Errorf("Expected category %d, got %d", CategoryPlayer, rule.GetCategory()) - } - - if rule.GetType() != PlayerMaxLevel { - t.Errorf("Expected type %d, got %d", PlayerMaxLevel, rule.GetType()) - } - - if rule.GetValue() != "50" { - t.Errorf("Expected value '50', got %s", rule.GetValue()) - } - - if rule.GetCombined() != "Player:MaxLevel" { - t.Errorf("Expected combined 'Player:MaxLevel', got %s", rule.GetCombined()) - } -} - -func TestNewRuleFromRule(t *testing.T) { - // Test with nil source - rule := NewRuleFromRule(nil) - if rule == nil { - t.Error("NewRuleFromRule(nil) should return default rule, not nil") - } - - // Test with valid source - original := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") - copy := NewRuleFromRule(original) - - if copy == nil { - t.Fatal("NewRuleFromRule() returned nil") - } - - if copy.GetCategory() != original.GetCategory() { - t.Errorf("Copy category mismatch: expected %d, got %d", original.GetCategory(), copy.GetCategory()) - } - - if copy.GetType() != original.GetType() { - t.Errorf("Copy type mismatch: expected %d, got %d", original.GetType(), copy.GetType()) - } - - if copy.GetValue() != original.GetValue() { - t.Errorf("Copy value mismatch: expected %s, got %s", original.GetValue(), copy.GetValue()) - } - - if copy.GetCombined() != original.GetCombined() { - t.Errorf("Copy combined mismatch: expected %s, got %s", original.GetCombined(), copy.GetCombined()) - } - - // Test that they are independent copies - copy.SetValue("8.0") - if original.GetValue() == copy.GetValue() { - t.Error("Original and copy are not independent after modification") - } -} - -func TestRuleSetValue(t *testing.T) { - rule := NewRule() - rule.SetValue("test") - - if rule.GetValue() != "test" { - t.Errorf("SetValue failed: expected 'test', got %s", rule.GetValue()) - } -} - -func TestRuleTypeConversions(t *testing.T) { - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - - // Test integer conversions - if rule.GetInt8() != 50 { - t.Errorf("Expected int8 50, got %d", rule.GetInt8()) - } - - if rule.GetInt16() != 50 { - t.Errorf("Expected int16 50, got %d", rule.GetInt16()) - } - - if rule.GetInt32() != 50 { - t.Errorf("Expected int32 50, got %d", rule.GetInt32()) - } - - if rule.GetInt64() != 50 { - t.Errorf("Expected int64 50, got %d", rule.GetInt64()) - } - - // Test unsigned integer conversions - if rule.GetUInt8() != 50 { - t.Errorf("Expected uint8 50, got %d", rule.GetUInt8()) - } - - if rule.GetUInt16() != 50 { - t.Errorf("Expected uint16 50, got %d", rule.GetUInt16()) - } - - if rule.GetUInt32() != 50 { - t.Errorf("Expected uint32 50, got %d", rule.GetUInt32()) - } - - if rule.GetUInt64() != 50 { - t.Errorf("Expected uint64 50, got %d", rule.GetUInt64()) - } - - // Test boolean conversion (> 0 = true) - if !rule.GetBool() { - t.Error("Expected bool true for value '50'") - } - - // Test float conversions - rule.SetValue("3.14") - if rule.GetFloat32() != 3.14 { - t.Errorf("Expected float32 3.14, got %f", rule.GetFloat32()) - } - - if rule.GetFloat64() != 3.14 { - t.Errorf("Expected float64 3.14, got %f", rule.GetFloat64()) - } - - // Test character conversion - rule.SetValue("Hello") - if rule.GetChar() != 'H' { - t.Errorf("Expected char 'H', got %c", rule.GetChar()) - } - - // Test string conversion - if rule.GetString() != "Hello" { - t.Errorf("Expected string 'Hello', got %s", rule.GetString()) - } -} - -func TestRuleTypeConversionsInvalid(t *testing.T) { - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "invalid", "Player:MaxLevel") - - // Invalid conversions should return zero values - if rule.GetInt32() != 0 { - t.Errorf("Expected int32 0 for invalid value, got %d", rule.GetInt32()) - } - - if rule.GetFloat64() != 0.0 { - t.Errorf("Expected float64 0.0 for invalid value, got %f", rule.GetFloat64()) - } - - if rule.GetBool() != false { - t.Error("Expected bool false for invalid value") - } -} - -func TestRuleIsValid(t *testing.T) { - // Test invalid rule (default) - rule := NewRule() - if rule.IsValid() { - t.Error("Default rule should not be valid") - } - - // Test valid rule - validRule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - if !validRule.IsValid() { - t.Error("Rule with valid data should be valid") - } -} - -func TestRuleString(t *testing.T) { - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - str := rule.String() - - expected := "Rule{Player:MaxLevel: 50}" - if str != expected { - t.Errorf("Expected string %s, got %s", expected, str) - } -} - -// Test RuleSet creation and operations -func TestNewRuleSet(t *testing.T) { - ruleSet := NewRuleSet() - if ruleSet == nil { - t.Fatal("NewRuleSet() returned nil") - } - - if ruleSet.GetID() != 0 { - t.Errorf("Expected ID 0, got %d", ruleSet.GetID()) - } - - if ruleSet.GetName() != "" { - t.Errorf("Expected empty name, got %s", ruleSet.GetName()) - } - - if ruleSet.Size() != 0 { - t.Errorf("Expected size 0, got %d", ruleSet.Size()) - } -} - -func TestRuleSetIDAndName(t *testing.T) { - ruleSet := NewRuleSet() - - // Test SetID and SetName - ruleSet.SetID(1) - ruleSet.SetName("Test Rule Set") - - if ruleSet.GetID() != 1 { - t.Errorf("Expected ID 1, got %d", ruleSet.GetID()) - } - - if ruleSet.GetName() != "Test Rule Set" { - t.Errorf("Expected name 'Test Rule Set', got %s", ruleSet.GetName()) - } -} - -func TestRuleSetAddRule(t *testing.T) { - ruleSet := NewRuleSet() - - // Test adding nil rule - ruleSet.AddRule(nil) // Should not crash - if ruleSet.Size() != 0 { - t.Error("Adding nil rule should not increase size") - } - - // Test adding valid rules - rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") - - ruleSet.AddRule(rule1) - ruleSet.AddRule(rule2) - - if ruleSet.Size() != 2 { - t.Errorf("Expected size 2, got %d", ruleSet.Size()) - } -} - -func TestRuleSetGetRule(t *testing.T) { - ruleSet := NewRuleSet() - rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - ruleSet.AddRule(rule1) - - // Test rule retrieval - retrievedRule := ruleSet.GetRule(CategoryPlayer, PlayerMaxLevel) - if retrievedRule == nil { - t.Fatal("GetRule() returned nil for added rule") - } - - if retrievedRule.GetValue() != "50" { - t.Errorf("Retrieved rule value mismatch: expected '50', got %s", retrievedRule.GetValue()) - } - - // Test non-existent rule - nonExistentRule := ruleSet.GetRule(CategorySpawn, SpawnSpeedMultiplier) - if nonExistentRule != nil { - t.Error("GetRule() should return nil for non-existent rule") - } -} - -func TestRuleSetGetRuleByName(t *testing.T) { - ruleSet := NewRuleSet() - rule := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") - ruleSet.AddRule(rule) - - // Test rule retrieval by name - retrievedRule := ruleSet.GetRuleByName("Combat", "MaxCombatRange") - if retrievedRule == nil { - t.Fatal("GetRuleByName() returned nil for added rule") - } - - if retrievedRule.GetValue() != "4.0" { - t.Errorf("Retrieved rule value mismatch: expected '4.0', got %s", retrievedRule.GetValue()) - } - - // Test non-existent rule - nonExistentRule := ruleSet.GetRuleByName("NonExistent", "Rule") - if nonExistentRule != nil { - t.Error("GetRuleByName() should return nil for non-existent rule") - } -} - -func TestRuleSetHasRule(t *testing.T) { - ruleSet := NewRuleSet() - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - ruleSet.AddRule(rule) - - // Test HasRule - if !ruleSet.HasRule(CategoryPlayer, PlayerMaxLevel) { - t.Error("HasRule() returned false for existing rule") - } - - if ruleSet.HasRule(CategorySpawn, SpawnSpeedMultiplier) { - t.Error("HasRule() returned true for non-existing rule") - } -} - -func TestRuleSetGetRulesByCategory(t *testing.T) { - ruleSet := NewRuleSet() - - // Add multiple player rules - rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - rule2 := NewRuleWithValues(CategoryPlayer, PlayerVitalityAmount, "0.5", "Player:VitalityAmount") - rule3 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") - - ruleSet.AddRule(rule1) - ruleSet.AddRule(rule2) - ruleSet.AddRule(rule3) - - // Test getting rules by category - playerRules := ruleSet.GetRulesByCategory(CategoryPlayer) - if len(playerRules) != 2 { - t.Errorf("Expected 2 player rules, got %d", len(playerRules)) - } - - // Test empty category - spawnRules := ruleSet.GetRulesByCategory(CategorySpawn) - if len(spawnRules) != 0 { - t.Errorf("Expected 0 spawn rules, got %d", len(spawnRules)) - } -} - -func TestRuleSetClearRules(t *testing.T) { - ruleSet := NewRuleSet() - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - ruleSet.AddRule(rule) - - if ruleSet.Size() != 1 { - t.Error("Rule set should have 1 rule before clearing") - } - - ruleSet.ClearRules() - - if ruleSet.Size() != 0 { - t.Error("Rule set should be empty after clearing") - } -} - -func TestRuleSetCopyRulesInto(t *testing.T) { - source := NewRuleSet() - source.SetID(1) - source.SetName("Source") - - rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") - source.AddRule(rule1) - source.AddRule(rule2) - - target := NewRuleSet() - target.SetID(2) - target.SetName("Target") - - // Test copying rules - target.CopyRulesInto(source) - - if target.Size() != 2 { - t.Errorf("Target should have 2 rules after copying, got %d", target.Size()) - } - - // Test that target still has its own ID and name - if target.GetID() != 2 { - t.Errorf("Target ID should remain 2, got %d", target.GetID()) - } - - if target.GetName() != "Target" { - t.Errorf("Target name should remain 'Target', got %s", target.GetName()) - } -} - -func TestNewRuleSetFromRuleSet(t *testing.T) { - // Test with nil source - nilCopy := NewRuleSetFromRuleSet(nil) - if nilCopy == nil { - t.Error("NewRuleSetFromRuleSet(nil) should return empty rule set, not nil") - } - - // Test with valid source - original := NewRuleSet() - original.SetID(1) - original.SetName("Original") - - rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "4.0", "Combat:MaxCombatRange") - original.AddRule(rule1) - original.AddRule(rule2) - - // Test copy constructor - copy := NewRuleSetFromRuleSet(original) - if copy == nil { - t.Fatal("NewRuleSetFromRuleSet() returned nil") - } - - if copy.GetID() != original.GetID() { - t.Errorf("Copy ID mismatch: expected %d, got %d", original.GetID(), copy.GetID()) - } - - if copy.GetName() != original.GetName() { - t.Errorf("Copy name mismatch: expected %s, got %s", original.GetName(), copy.GetName()) - } - - if copy.Size() != original.Size() { - t.Errorf("Copy size mismatch: expected %d, got %d", original.Size(), copy.Size()) - } - - // Test that rules were copied correctly - copyRule := copy.GetRule(CategoryPlayer, PlayerMaxLevel) - if copyRule == nil { - t.Fatal("Copied rule set is missing expected rule") - } - - if copyRule.GetValue() != "50" { - t.Errorf("Copied rule value mismatch: expected '50', got %s", copyRule.GetValue()) - } -} - -func TestRuleSetString(t *testing.T) { - ruleSet := NewRuleSet() - ruleSet.SetID(1) - ruleSet.SetName("Test Set") - - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "50", "Player:MaxLevel") - ruleSet.AddRule(rule) - - str := ruleSet.String() - expected := "RuleSet{ID: 1, Name: Test Set, Rules: 1}" - if str != expected { - t.Errorf("Expected string %s, got %s", expected, str) - } -} - -// Test RuleManager initialization and operations -func TestNewRuleManager(t *testing.T) { - ruleManager := NewRuleManager() - if ruleManager == nil { - t.Fatal("NewRuleManager() returned nil") - } - - if !ruleManager.IsInitialized() { - t.Error("RuleManager should be initialized after creation") - } -} - -func TestRuleManagerGetGlobalRule(t *testing.T) { - ruleManager := NewRuleManager() - - // Test getting a default rule that should be initialized - rule := ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel) - if rule == nil { - t.Fatal("GetGlobalRule() returned nil for default rule") - } - - // Should return the default value from Init() - if rule.GetValue() != "50" { - t.Errorf("Expected default value '50', got %s", rule.GetValue()) - } - - // Test getting rule by name - rule2 := ruleManager.GetGlobalRuleByName("Player", "MaxLevel") - if rule2 == nil { - t.Fatal("GetGlobalRuleByName() returned nil for default rule") - } - - if rule2.GetValue() != "50" { - t.Errorf("Expected default value '50', got %s", rule2.GetValue()) - } -} - -func TestRuleManagerBlankRule(t *testing.T) { - ruleManager := NewRuleManager() - - // Test blank rule for non-existent rule - blankRule := ruleManager.GetGlobalRule(9999, 9999) - if blankRule == nil { - t.Fatal("GetGlobalRule() should return blank rule for non-existent rule") - } - - if blankRule.IsValid() { - t.Error("Blank rule should not be valid") - } - - // Test GetBlankRule method - blankRule2 := ruleManager.GetBlankRule() - if blankRule2 == nil { - t.Fatal("GetBlankRule() should not return nil") - } - - if blankRule2.IsValid() { - t.Error("Blank rule should not be valid") - } -} - -func TestRuleManagerRuleSetOperations(t *testing.T) { - ruleManager := NewRuleManager() - - // Create a test rule set - ruleSet := NewRuleSet() - ruleSet.SetID(1) - ruleSet.SetName("Test Set") - - // Add some rules to it - rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "60", "Player:MaxLevel") - ruleSet.AddRule(rule1) - - // Add the rule set to the manager - if !ruleManager.AddRuleSet(ruleSet) { - t.Fatal("AddRuleSet() returned false") - } - - // Test getting the rule set - retrievedRuleSet := ruleManager.GetRuleSet(1) - if retrievedRuleSet == nil { - t.Fatal("GetRuleSet() returned nil") - } - - if retrievedRuleSet.GetName() != "Test Set" { - t.Errorf("Retrieved rule set name mismatch: expected 'Test Set', got %s", retrievedRuleSet.GetName()) - } - - // Test duplicate rule set - duplicateRuleSet := NewRuleSet() - duplicateRuleSet.SetID(1) - duplicateRuleSet.SetName("Duplicate") - - if ruleManager.AddRuleSet(duplicateRuleSet) { - t.Error("AddRuleSet() should return false for duplicate ID") - } - - // Test GetNumRuleSets - if ruleManager.GetNumRuleSets() != 1 { - t.Errorf("Expected 1 rule set, got %d", ruleManager.GetNumRuleSets()) - } -} - -func TestRuleManagerGlobalRuleSet(t *testing.T) { - ruleManager := NewRuleManager() - - // Create a test rule set with modified rules - ruleSet := NewRuleSet() - ruleSet.SetID(1) - ruleSet.SetName("Test Set") - - // Load coded defaults first, then modify - ruleManager.LoadCodedDefaultsIntoRuleSet(ruleSet) - - // Override a rule - playerMaxLevelRule := ruleSet.GetRule(CategoryPlayer, PlayerMaxLevel) - if playerMaxLevelRule != nil { - playerMaxLevelRule.SetValue("60") - } - - ruleManager.AddRuleSet(ruleSet) - - // Test setting global rule set - if !ruleManager.SetGlobalRuleSet(1) { - t.Fatal("SetGlobalRuleSet() returned false") - } - - // Test that global rule now returns the overridden value - globalRule := ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel) - if globalRule == nil { - t.Fatal("GetGlobalRule() returned nil after setting global rule set") - } - - if globalRule.GetValue() != "60" { - t.Errorf("Expected overridden value '60', got %s", globalRule.GetValue()) - } -} - -func TestRuleManagerZoneRules(t *testing.T) { - ruleManager := NewRuleManager() - - // Create a zone-specific rule set - zoneRuleSet := NewRuleSet() - zoneRuleSet.SetID(100) - zoneRuleSet.SetName("Zone Rules") - - // Load coded defaults and modify a rule - ruleManager.LoadCodedDefaultsIntoRuleSet(zoneRuleSet) - playerMaxLevelRule := zoneRuleSet.GetRule(CategoryPlayer, PlayerMaxLevel) - if playerMaxLevelRule != nil { - playerMaxLevelRule.SetValue("70") - } - - ruleManager.AddRuleSet(zoneRuleSet) - - // Set zone rule set - zoneID := int32(1) - if !ruleManager.SetZoneRuleSet(zoneID, 100) { - t.Fatal("SetZoneRuleSet() returned false") - } - - // Test zone rule lookup - zoneRule := ruleManager.GetZoneRule(zoneID, CategoryPlayer, PlayerMaxLevel) - if zoneRule == nil { - t.Fatal("GetZoneRule() returned nil") - } - - if zoneRule.GetValue() != "70" { - t.Errorf("Expected zone rule value '70', got %s", zoneRule.GetValue()) - } - - // Test fallback to global rule for non-zone rule - globalFallback := ruleManager.GetZoneRule(999, CategoryPlayer, PlayerMaxLevel) - if globalFallback == nil { - t.Fatal("GetZoneRule() should fallback to global rule") - } -} - -func TestRuleManagerFlush(t *testing.T) { - ruleManager := NewRuleManager() - - // Add a rule set - ruleSet := NewRuleSet() - ruleSet.SetID(1) - ruleSet.SetName("Test Set") - ruleManager.AddRuleSet(ruleSet) - - // Test flush with reinit - ruleManager.Flush(true) - - if ruleManager.GetNumRuleSets() != 0 { - t.Error("Rule sets should be cleared after flush") - } - - if !ruleManager.IsInitialized() { - t.Error("Rule manager should be initialized after flush(true)") - } - - // Test flush without reinit - ruleManager.Flush(false) - if ruleManager.IsInitialized() { - t.Error("Rule manager should not be initialized after flush(false)") - } -} - -func TestRuleManagerClearOperations(t *testing.T) { - ruleManager := NewRuleManager() - - // Add rule sets - ruleSet1 := NewRuleSet() - ruleSet1.SetID(1) - ruleManager.AddRuleSet(ruleSet1) - - ruleSet2 := NewRuleSet() - ruleSet2.SetID(2) - ruleManager.AddRuleSet(ruleSet2) - - // Set zone rule set - ruleManager.SetZoneRuleSet(1, 1) - - // Test ClearRuleSets - ruleManager.ClearRuleSets() - if ruleManager.GetNumRuleSets() != 0 { - t.Error("Rule sets should be cleared") - } - - // Test ClearZoneRuleSets - ruleManager.ClearZoneRuleSets() - zoneRule := ruleManager.GetZoneRule(1, CategoryPlayer, PlayerMaxLevel) - // Should fallback to global rule, not zone rule - if zoneRule == nil { - t.Error("Should fallback to global rule after clearing zone rule sets") - } -} - -func TestRuleManagerGetAllRuleSets(t *testing.T) { - ruleManager := NewRuleManager() - - // Add multiple rule sets - for i := 1; i <= 3; i++ { - ruleSet := NewRuleSet() - ruleSet.SetID(int32(i)) - ruleSet.SetName("Rule Set " + string(rune(i+'0'))) - ruleManager.AddRuleSet(ruleSet) - } - - allRuleSets := ruleManager.GetAllRuleSets() - if len(allRuleSets) != 3 { - t.Errorf("Expected 3 rule sets, got %d", len(allRuleSets)) - } -} - -func TestRuleManagerValidateRule(t *testing.T) { - ruleManager := NewRuleManager() - - // Test valid rule - err := ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, "50") - if err != nil { - t.Errorf("ValidateRule() returned error for valid rule: %v", err) - } - - // Test rule value too long - longValue := make([]byte, MaxRuleValueLength+1) - for i := range longValue { - longValue[i] = 'a' - } - - err = ruleManager.ValidateRule(CategoryPlayer, PlayerMaxLevel, string(longValue)) - if err != ErrRuleValueTooLong { - t.Error("ValidateRule() should return ErrRuleValueTooLong for long value") - } -} - -func TestRuleManagerGetRuleInfo(t *testing.T) { - ruleManager := NewRuleManager() - - info := ruleManager.GetRuleInfo(CategoryPlayer, PlayerMaxLevel) - expected := "Rule: Player:MaxLevel = 50" - if info != expected { - t.Errorf("Expected rule info %s, got %s", expected, info) - } - - // Test non-existent rule - info = ruleManager.GetRuleInfo(9999, 9999) - if info != "Rule not found" { - t.Errorf("Expected 'Rule not found', got %s", info) - } -} - -func TestRuleManagerStatistics(t *testing.T) { - ruleManager := NewRuleManager() - - // Get initial stats - stats := ruleManager.GetStats() - - // Add rule sets and perform operations to change stats - ruleSet := NewRuleSet() - ruleSet.SetID(1) - ruleManager.AddRuleSet(ruleSet) - - // Get rule to increment lookup counter - ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel) - - // Get updated stats - newStats := ruleManager.GetStats() - if newStats.TotalRuleSets != 1 { - t.Errorf("Expected 1 total rule set, got %d", newStats.TotalRuleSets) - } - - if newStats.RuleGetOperations <= stats.RuleGetOperations { - t.Error("Rule get operations should have increased") - } - - // Test reset stats - ruleManager.ResetStats() - resetStats := ruleManager.GetStats() - if resetStats.RuleGetOperations != 0 { - t.Error("Stats should be reset to 0") - } -} - -func TestRuleManagerString(t *testing.T) { - ruleManager := NewRuleManager() - str := ruleManager.String() - - // Should contain basic information about rule sets, rules, etc. - if str == "" { - t.Error("String() should return non-empty string") - } -} - -// Test category name functions -func TestCategoryNames(t *testing.T) { - // Test GetCategoryName - name := GetCategoryName(CategoryPlayer) - if name != "Player" { - t.Errorf("Expected category name 'Player', got %s", name) - } - - // Test unknown category - unknownName := GetCategoryName(9999) - if unknownName != "Unknown" { - t.Errorf("Expected 'Unknown' for invalid category, got %s", unknownName) - } - - // Test GetCategoryByName - category, exists := GetCategoryByName("Player") - if !exists { - t.Error("GetCategoryByName() should find 'Player' category") - } - - if category != CategoryPlayer { - t.Errorf("Expected category %d, got %d", CategoryPlayer, category) - } - - // Test unknown category name - _, exists = GetCategoryByName("NonExistent") - if exists { - t.Error("GetCategoryByName() should not find non-existent category") - } -} - -// Test RuleManagerAdapter -func TestRuleManagerAdapter(t *testing.T) { - ruleManager := NewRuleManager() - adapter := NewRuleManagerAdapter(ruleManager, 0) - - if adapter == nil { - t.Fatal("NewRuleManagerAdapter() returned nil") - } - - // Test basic rule access - rule := adapter.GetRule(CategoryPlayer, PlayerMaxLevel) - if rule == nil { - t.Fatal("Adapter GetRule() returned nil") - } - - if rule.GetValue() != "50" { - t.Errorf("Expected value '50', got %s", rule.GetValue()) - } - - // Test convenience methods - intValue := adapter.GetInt32(CategoryPlayer, PlayerMaxLevel) - if intValue != 50 { - t.Errorf("Expected int32 50, got %d", intValue) - } - - boolValue := adapter.GetBool(CategoryPlayer, PlayerMaxLevel) - if !boolValue { - t.Error("Expected bool true for value '50'") - } - - stringValue := adapter.GetString(CategoryPlayer, PlayerMaxLevel) - if stringValue != "50" { - t.Errorf("Expected string '50', got %s", stringValue) - } - - floatValue := adapter.GetFloat64(CategoryPlayer, PlayerMaxLevel) - if floatValue != 50.0 { - t.Errorf("Expected float64 50.0, got %f", floatValue) - } - - // Test zone ID - if adapter.GetZoneID() != 0 { - t.Errorf("Expected zone ID 0, got %d", adapter.GetZoneID()) - } - - adapter.SetZoneID(100) - if adapter.GetZoneID() != 100 { - t.Errorf("Expected zone ID 100 after SetZoneID, got %d", adapter.GetZoneID()) - } -} - -func TestRuleManagerAdapterWithZone(t *testing.T) { - ruleManager := NewRuleManager() - - // Create zone-specific rule set - zoneRuleSet := NewRuleSet() - zoneRuleSet.SetID(100) - ruleManager.LoadCodedDefaultsIntoRuleSet(zoneRuleSet) - - // Override a rule - if rule := zoneRuleSet.GetRule(CategoryPlayer, PlayerMaxLevel); rule != nil { - rule.SetValue("70") - } - - ruleManager.AddRuleSet(zoneRuleSet) - ruleManager.SetZoneRuleSet(1, 100) - - // Test adapter with zone - adapter := NewRuleManagerAdapter(ruleManager, 1) - intValue := adapter.GetInt32(CategoryPlayer, PlayerMaxLevel) - if intValue != 70 { - t.Errorf("Expected zone-specific value 70, got %d", intValue) - } -} - -// Test RuleManagerStats operations -func TestRuleManagerStats(t *testing.T) { - stats := &RuleManagerStats{} - - // Test increment operations - initialGets := stats.RuleGetOperations - stats.IncrementRuleGetOperations() - if stats.RuleGetOperations != initialGets+1 { - t.Error("IncrementRuleGetOperations() did not increment correctly") - } - - initialSets := stats.RuleSetOperations - stats.IncrementRuleSetOperations() - if stats.RuleSetOperations != initialSets+1 { - t.Error("IncrementRuleSetOperations() did not increment correctly") - } - - initialDB := stats.DatabaseOperations - stats.IncrementDatabaseOperations() - if stats.DatabaseOperations != initialDB+1 { - t.Error("IncrementDatabaseOperations() did not increment correctly") - } - - // Test snapshot - snapshot := stats.GetSnapshot() - if snapshot.RuleGetOperations != stats.RuleGetOperations { - t.Error("Snapshot should match current stats") - } - - // Test reset - stats.Reset() - if stats.RuleGetOperations != 0 || stats.RuleSetOperations != 0 || stats.DatabaseOperations != 0 { - t.Error("Reset() should zero all counters") - } -} - -// Test error constants -func TestErrorConstants(t *testing.T) { - errors := []error{ - ErrRuleNotFound, - ErrRuleSetNotFound, - ErrInvalidRuleCategory, - ErrInvalidRuleType, - ErrInvalidRuleValue, - ErrDuplicateRuleSet, - ErrRuleSetNotActive, - ErrGlobalRuleSetNotSet, - ErrZoneRuleSetNotFound, - ErrRuleValueTooLong, - ErrRuleNameTooLong, - } - - for _, err := range errors { - if err == nil { - t.Error("Error constant should not be nil") - } - if err.Error() == "" { - t.Error("Error message should not be empty") - } - } -} - -// Test constants -func TestConstants(t *testing.T) { - // Test some rule categories - if CategoryClient != 0 { - t.Errorf("CategoryClient should be 0, got %d", CategoryClient) - } - if CategoryPlayer != 3 { - t.Errorf("CategoryPlayer should be 3, got %d", CategoryPlayer) - } - - // Test some rule types - if PlayerMaxLevel != 0 { - t.Errorf("PlayerMaxLevel should be 0, got %d", PlayerMaxLevel) - } - - // Test validation constants - if MaxRuleValueLength != 1024 { - t.Errorf("MaxRuleValueLength should be 1024, got %d", MaxRuleValueLength) - } - if MaxRuleCombinedLength != 2048 { - t.Errorf("MaxRuleCombinedLength should be 2048, got %d", MaxRuleCombinedLength) - } - - // Test database constants - if TableRuleSets != "rulesets" { - t.Errorf("TableRuleSets should be 'rulesets', got %s", TableRuleSets) - } - if DefaultRuleSetIDVar != "default_ruleset_id" { - t.Errorf("DefaultRuleSetIDVar should be 'default_ruleset_id', got %s", DefaultRuleSetIDVar) - } -} - -// Test DatabaseService with in-memory SQLite -func TestDatabaseService(t *testing.T) { - // Skip database tests without MySQL - t.Skip("Skipping database tests - requires MySQL test database") - - // Example test for when MySQL is available: - // db, err := database.NewMySQL("test_user:test_pass@tcp(localhost:3306)/test_db") - // if err != nil { - // t.Fatalf("Failed to create MySQL database: %v", err) - // } - // defer db.Close() - // - // ds := NewDatabaseService(db) - // if ds == nil { - // t.Fatal("NewDatabaseService() returned nil") - // } - // - // // Test CreateRulesTables - // err = ds.CreateRulesTables() - // if err != nil { - // t.Fatalf("CreateRulesTables() failed: %v", err) - // } - // - // // Test ValidateDatabase - // err = ds.ValidateDatabase() - // if err != nil { - // t.Fatalf("ValidateDatabase() failed after creating tables: %v", err) - // } - // - // // Test SetDefaultRuleSet and GetDefaultRuleSetID - // testRuleSetID := int32(42) - // err = ds.SetDefaultRuleSet(testRuleSetID) - // if err != nil { - // t.Fatalf("SetDefaultRuleSet() failed: %v", err) - // } - // - // retrievedID, err := ds.GetDefaultRuleSetID() - // if err != nil { - // t.Fatalf("GetDefaultRuleSetID() failed: %v", err) - // } - // - // if retrievedID != testRuleSetID { - // t.Errorf("Expected rule set ID %d, got %d", testRuleSetID, retrievedID) - // } -} - -func TestDatabaseServiceRuleSetOperations(t *testing.T) { - // Skip database tests without MySQL - t.Skip("Skipping database tests - requires MySQL test database") - - // Example test for when MySQL is available: - // db, err := database.NewMySQL("test_user:test_pass@tcp(localhost:3306)/test_db") - // if err != nil { - // t.Fatalf("Failed to create MySQL database: %v", err) - // } - // defer db.Close() - // - // ds := NewDatabaseService(db) - // ds.CreateRulesTables() - // - // // Create a test rule set - // ruleSet := NewRuleSet() - // ruleSet.SetID(1) - // ruleSet.SetName("Test Rule Set") - // - // // Add some rules - // rule1 := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "60", "Player:MaxLevel") - // rule2 := NewRuleWithValues(CategoryCombat, CombatMaxRange, "5.0", "Combat:MaxCombatRange") - // ruleSet.AddRule(rule1) - // ruleSet.AddRule(rule2) - // - // // Test SaveRuleSet - // err = ds.SaveRuleSet(ruleSet) - // if err != nil { - // t.Fatalf("SaveRuleSet() failed: %v", err) - // } - // - // // Test GetRuleSetList - // ruleSets, err := ds.GetRuleSetList() - // if err != nil { - // t.Fatalf("GetRuleSetList() failed: %v", err) - // } - // - // if len(ruleSets) != 1 { - // t.Errorf("Expected 1 rule set, got %d", len(ruleSets)) - // } - // - // if ruleSets[0].ID != 1 { - // t.Errorf("Expected rule set ID 1, got %d", ruleSets[0].ID) - // } - // - // if ruleSets[0].Name != "Test Rule Set" { - // t.Errorf("Expected rule set name 'Test Rule Set', got %s", ruleSets[0].Name) - // } - // - // // Test DeleteRuleSet - // err = ds.DeleteRuleSet(1) - // if err != nil { - // t.Fatalf("DeleteRuleSet() failed: %v", err) - // } - // - // // Verify deletion - // ruleSets, err = ds.GetRuleSetList() - // if err != nil { - // t.Fatalf("GetRuleSetList() failed after deletion: %v", err) - // } - // - // if len(ruleSets) != 0 { - // t.Errorf("Expected 0 rule sets after deletion, got %d", len(ruleSets)) - // } -} - -// Test RuleService functionality -func TestRuleService(t *testing.T) { - config := RuleServiceConfig{ - DatabaseEnabled: false, - CacheEnabled: false, - CacheTTL: 3600, - MaxCacheSize: 1024 * 1024, - } - - service := NewRuleService(config) - if service == nil { - t.Fatal("NewRuleService() returned nil") - } - - err := service.Initialize() - if err != nil { - t.Fatalf("Initialize() failed: %v", err) - } - - ruleManager := service.GetRuleManager() - if ruleManager == nil { - t.Fatal("GetRuleManager() returned nil") - } - - adapter := service.GetAdapter(0) - if adapter == nil { - t.Fatal("GetAdapter() returned nil") - } - - err = service.Shutdown() - if err != nil { - t.Fatalf("Shutdown() failed: %v", err) - } -} - -// Test splitCombined helper function -func TestSplitCombined(t *testing.T) { - // Test valid combined string - parts := splitCombined("Player:MaxLevel") - if len(parts) != 2 { - t.Errorf("Expected 2 parts, got %d", len(parts)) - } - if parts[0] != "Player" || parts[1] != "MaxLevel" { - t.Errorf("Expected ['Player', 'MaxLevel'], got %v", parts) - } - - // Test invalid combined string (no colon) - parts = splitCombined("PlayerMaxLevel") - if len(parts) != 1 { - t.Errorf("Expected 1 part for invalid string, got %d", len(parts)) - } - if parts[0] != "PlayerMaxLevel" { - t.Errorf("Expected ['PlayerMaxLevel'], got %v", parts) - } - - // Test empty string - parts = splitCombined("") - if len(parts) != 1 { - t.Errorf("Expected 1 part for empty string, got %d", len(parts)) - } - if parts[0] != "" { - t.Errorf("Expected [''], got %v", parts) - } -} - -// Benchmark tests -func BenchmarkRuleAccess(b *testing.B) { - ruleManager := NewRuleManager() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = ruleManager.GetGlobalRule(CategoryPlayer, PlayerMaxLevel) - } -} - -func BenchmarkRuleManagerCreation(b *testing.B) { - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = NewRuleManager() - } -} - -func BenchmarkRuleSetAddRule(b *testing.B) { - ruleSet := NewRuleSet() - rules := make([]*Rule, b.N) - - // Pre-create rules - for i := 0; i < b.N; i++ { - rules[i] = NewRuleWithValues(CategoryPlayer, PlayerMaxLevel+RuleType(i), "50", "Player:MaxLevel") - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - ruleSet.AddRule(rules[i]) - } -} - -func BenchmarkRuleTypeConversion(b *testing.B) { - rule := NewRuleWithValues(CategoryPlayer, PlayerMaxLevel, "12345", "Player:MaxLevel") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = rule.GetInt32() - } -} - -func BenchmarkRuleManagerAdapter(b *testing.B) { - ruleManager := NewRuleManager() - adapter := NewRuleManagerAdapter(ruleManager, 0) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = adapter.GetInt32(CategoryPlayer, PlayerMaxLevel) - } -} \ No newline at end of file diff --git a/internal/sign/sign_test.go b/internal/sign/sign_test.go deleted file mode 100644 index 5e1c93e..0000000 --- a/internal/sign/sign_test.go +++ /dev/null @@ -1,904 +0,0 @@ -package sign - -import ( - "fmt" - "strings" - "sync" - "testing" - - "eq2emu/internal/spawn" -) - -// Mock implementations for testing - -// MockLogger implements the Logger interface for testing -type MockLogger struct { - logs []string - mu sync.Mutex -} - -func (ml *MockLogger) LogInfo(message string, args ...any) { - ml.mu.Lock() - defer ml.mu.Unlock() - ml.logs = append(ml.logs, fmt.Sprintf("INFO: "+message, args...)) -} - -func (ml *MockLogger) LogError(message string, args ...any) { - ml.mu.Lock() - defer ml.mu.Unlock() - ml.logs = append(ml.logs, fmt.Sprintf("ERROR: "+message, args...)) -} - -func (ml *MockLogger) LogDebug(message string, args ...any) { - ml.mu.Lock() - defer ml.mu.Unlock() - ml.logs = append(ml.logs, fmt.Sprintf("DEBUG: "+message, args...)) -} - -func (ml *MockLogger) LogWarning(message string, args ...any) { - ml.mu.Lock() - defer ml.mu.Unlock() - ml.logs = append(ml.logs, fmt.Sprintf("WARNING: "+message, args...)) -} - -func (ml *MockLogger) GetLogs() []string { - ml.mu.Lock() - defer ml.mu.Unlock() - result := make([]string, len(ml.logs)) - copy(result, ml.logs) - return result -} - -func (ml *MockLogger) Clear() { - ml.mu.Lock() - defer ml.mu.Unlock() - ml.logs = ml.logs[:0] -} - -// MockDatabase implements the Database interface for testing -type MockDatabase struct { - signs map[int32][]*Sign - zoneNames map[int32]string - charNames map[int32]string - signMarks map[int32]map[int32]string // charID -> widgetID -> charName - mu sync.RWMutex -} - -func NewMockDatabase() *MockDatabase { - return &MockDatabase{ - signs: make(map[int32][]*Sign), - zoneNames: make(map[int32]string), - charNames: make(map[int32]string), - signMarks: make(map[int32]map[int32]string), - } -} - -func (md *MockDatabase) GetZoneName(zoneID int32) (string, error) { - md.mu.RLock() - defer md.mu.RUnlock() - if name, exists := md.zoneNames[zoneID]; exists { - return name, nil - } - return "", fmt.Errorf("zone %d not found", zoneID) -} - -func (md *MockDatabase) GetCharacterName(charID int32) (string, error) { - md.mu.RLock() - defer md.mu.RUnlock() - if name, exists := md.charNames[charID]; exists { - return name, nil - } - return "", fmt.Errorf("character %d not found", charID) -} - -func (md *MockDatabase) SaveSignMark(charID int32, widgetID int32, charName string, client Client) error { - md.mu.Lock() - defer md.mu.Unlock() - if md.signMarks[charID] == nil { - md.signMarks[charID] = make(map[int32]string) - } - md.signMarks[charID][widgetID] = charName - return nil -} - -func (md *MockDatabase) LoadSigns(zoneID int32) ([]*Sign, error) { - md.mu.RLock() - defer md.mu.RUnlock() - return md.signs[zoneID], nil -} - -func (md *MockDatabase) SaveSign(sign *Sign) error { - return nil // No-op for testing -} - -func (md *MockDatabase) DeleteSign(signID int32) error { - return nil // No-op for testing -} - -func (md *MockDatabase) AddZone(zoneID int32, name string) { - md.mu.Lock() - defer md.mu.Unlock() - md.zoneNames[zoneID] = name -} - -func (md *MockDatabase) AddCharacter(charID int32, name string) { - md.mu.Lock() - defer md.mu.Unlock() - md.charNames[charID] = name -} - -func (md *MockDatabase) AddSignToZone(zoneID int32, sign *Sign) { - md.mu.Lock() - defer md.mu.Unlock() - md.signs[zoneID] = append(md.signs[zoneID], sign) -} - -// MockPlayer implements the Player interface for testing -type MockPlayer struct { - x, y, z, heading float32 - zone Zone - target *spawn.Spawn -} - -func (mp *MockPlayer) GetDistance(target *spawn.Spawn) float32 { - if target == nil { - return 0 - } - return 5.0 // Mock distance -} - -func (mp *MockPlayer) SetX(x float32) { mp.x = x } -func (mp *MockPlayer) SetY(y float32) { mp.y = y } -func (mp *MockPlayer) SetZ(z float32) { mp.z = z } -func (mp *MockPlayer) SetHeading(heading float32) { mp.heading = heading } -func (mp *MockPlayer) GetZone() Zone { return mp.zone } -func (mp *MockPlayer) GetTarget() *spawn.Spawn { return mp.target } - -// MockZone implements the Zone interface for testing -type MockZone struct { - transporters map[int32][]TransportDestination -} - -func (mz *MockZone) GetTransporters(client Client, transporterID int32) ([]TransportDestination, error) { - if transporters, exists := mz.transporters[transporterID]; exists { - return transporters, nil - } - return nil, fmt.Errorf("transporter %d not found", transporterID) -} - -func (mz *MockZone) ProcessEntityCommand(command *EntityCommand, player Player, target *spawn.Spawn) error { - return nil // No-op for testing -} - -// MockClient implements the Client interface for testing -type MockClient struct { - player Player - charID int32 - database Database - zone Zone - messages []string - tempTransportID int32 - zoneAccess map[string]bool - mu sync.Mutex -} - -func (mc *MockClient) GetPlayer() Player { return mc.player } -func (mc *MockClient) GetCharacterID() int32 { return mc.charID } -func (mc *MockClient) GetDatabase() Database { return mc.database } -func (mc *MockClient) GetCurrentZone() Zone { return mc.zone } -func (mc *MockClient) SetTemporaryTransportID(id int32) { mc.tempTransportID = id } - -func (mc *MockClient) SimpleMessage(channel int32, message string) { - mc.mu.Lock() - defer mc.mu.Unlock() - mc.messages = append(mc.messages, message) -} - -func (mc *MockClient) Message(channel int32, format string, args ...any) { - mc.mu.Lock() - defer mc.mu.Unlock() - mc.messages = append(mc.messages, fmt.Sprintf(format, args...)) -} - -func (mc *MockClient) CheckZoneAccess(zoneName string) bool { - return mc.zoneAccess[zoneName] -} - -func (mc *MockClient) TryZoneInstance(zoneID int32, useDefaults bool) bool { - return false // Mock always uses regular zones -} - -func (mc *MockClient) Zone(zoneName string, useDefaults bool) error { - return nil // No-op for testing -} - -func (mc *MockClient) ProcessTeleport(sign *Sign, destinations []TransportDestination, transporterID int32) error { - return nil // No-op for testing -} - -func (mc *MockClient) GetMessages() []string { - mc.mu.Lock() - defer mc.mu.Unlock() - result := make([]string, len(mc.messages)) - copy(result, mc.messages) - return result -} - -func (mc *MockClient) ClearMessages() { - mc.mu.Lock() - defer mc.mu.Unlock() - mc.messages = mc.messages[:0] -} - -// MockEntity implements the Entity interface for testing -type MockEntity struct { - id int32 - name string - databaseID int32 -} - -func (me *MockEntity) GetID() int32 { return me.id } -func (me *MockEntity) GetName() string { return me.name } -func (me *MockEntity) GetDatabaseID() int32 { return me.databaseID } - -// Helper functions for testing - -func createTestSign() *Sign { - sign := NewSign() - sign.Spawn.SetDatabaseID(1) - sign.Spawn.SetName("Test Sign") - sign.SetWidgetID(100) - sign.SetSignTitle("Welcome") - sign.SetSignDescription("This is a test sign") - return sign -} - -func createZoneSign() *Sign { - sign := createTestSign() - sign.SetSignType(SignTypeZone) - sign.SetSignZoneID(2) - sign.SetSignZoneX(100.0) - sign.SetSignZoneY(200.0) - sign.SetSignZoneZ(300.0) - sign.SetSignZoneHeading(45.0) - return sign -} - -// Tests - -func TestNewSign(t *testing.T) { - sign := NewSign() - - if sign == nil { - t.Fatal("NewSign() returned nil") - } - - if sign.Spawn == nil { - t.Error("Sign spawn is nil") - } - - if sign.GetSignType() != SignTypeGeneric { - t.Errorf("Expected sign type %d, got %d", SignTypeGeneric, sign.GetSignType()) - } - - if !sign.IsSign() { - t.Error("IsSign() should return true") - } - - if sign.IsZoneSign() { - t.Error("IsZoneSign() should return false for generic sign") - } - - if !sign.IsGenericSign() { - t.Error("IsGenericSign() should return true for generic sign") - } -} - -func TestSignProperties(t *testing.T) { - sign := NewSign() - - // Test widget properties - sign.SetWidgetID(123) - if sign.GetWidgetID() != 123 { - t.Errorf("Expected widget ID 123, got %d", sign.GetWidgetID()) - } - - sign.SetWidgetX(10.5) - if sign.GetWidgetX() != 10.5 { - t.Errorf("Expected widget X 10.5, got %f", sign.GetWidgetX()) - } - - // Test sign properties - sign.SetSignTitle("Test Title") - if sign.GetSignTitle() != "Test Title" { - t.Errorf("Expected title 'Test Title', got '%s'", sign.GetSignTitle()) - } - - sign.SetSignDescription("Test Description") - if sign.GetSignDescription() != "Test Description" { - t.Errorf("Expected description 'Test Description', got '%s'", sign.GetSignDescription()) - } - - // Test zone properties - sign.SetSignZoneID(456) - if sign.GetSignZoneID() != 456 { - t.Errorf("Expected zone ID 456, got %d", sign.GetSignZoneID()) - } - - sign.SetSignZoneX(100.0) - sign.SetSignZoneY(200.0) - sign.SetSignZoneZ(300.0) - sign.SetSignZoneHeading(45.0) - - if !sign.HasZoneCoordinates() { - t.Error("Should have zone coordinates") - } - - // Test display options - sign.SetIncludeLocation(true) - if !sign.GetIncludeLocation() { - t.Error("Include location should be true") - } -} - -func TestSignValidation(t *testing.T) { - // Valid sign - sign := createTestSign() - if !sign.IsValid() { - t.Error("Sign should be valid") - } - - issues := sign.Validate() - if len(issues) > 0 { - t.Errorf("Valid sign should have no issues, got: %v", issues) - } - - // Invalid sign - no widget ID - sign.SetWidgetID(0) - if sign.IsValid() { - t.Error("Sign without widget ID should be invalid") - } - - issues = sign.Validate() - if len(issues) == 0 { - t.Error("Sign without widget ID should have validation issues") - } - - // Invalid sign - title too long - sign.SetWidgetID(100) - sign.SetSignTitle(strings.Repeat("a", MaxSignTitleLength+1)) - - issues = sign.Validate() - found := false - for _, issue := range issues { - if strings.Contains(issue, "title too long") { - found = true - break - } - } - if !found { - t.Error("Should have title too long validation issue") - } - - // Invalid zone sign - no zone ID - sign = createTestSign() - sign.SetSignType(SignTypeZone) - // Don't set zone ID - - issues = sign.Validate() - found = false - for _, issue := range issues { - if strings.Contains(issue, "no zone ID") { - found = true - break - } - } - if !found { - t.Error("Zone sign without zone ID should have validation issue") - } -} - -func TestSignCopy(t *testing.T) { - original := createTestSign() - original.SetSignTitle("Original Title") - original.SetSignDescription("Original Description") - - copy := original.Copy() - - if copy == nil { - t.Fatal("Copy returned nil") - } - - if copy == original { - t.Error("Copy should not be the same instance") - } - - if copy.GetSignTitle() != original.GetSignTitle() { - t.Error("Copy should have same title") - } - - if copy.GetSignDescription() != original.GetSignDescription() { - t.Error("Copy should have same description") - } - - if copy.GetWidgetID() != original.GetWidgetID() { - t.Error("Copy should have same widget ID") - } - - // Modify copy and ensure original is unchanged - copy.SetSignTitle("Modified Title") - if original.GetSignTitle() == "Modified Title" { - t.Error("Modifying copy should not affect original") - } -} - -func TestSignDisplayText(t *testing.T) { - sign := NewSign() - - // Empty sign - text := sign.GetDisplayText() - if len(text) > 0 { - t.Error("Empty sign should have no display text") - } - - // Title only - sign.SetSignTitle("Test Title") - text = sign.GetDisplayText() - if text != "Test Title" { - t.Errorf("Expected 'Test Title', got '%s'", text) - } - - // Title and description - sign.SetSignDescription("Test Description") - text = sign.GetDisplayText() - expected := "Test Title\nTest Description" - if text != expected { - t.Errorf("Expected '%s', got '%s'", expected, text) - } - - // With location - sign.SetSignZoneX(100.0) - sign.SetSignZoneY(200.0) - sign.SetSignZoneZ(300.0) - sign.SetIncludeLocation(true) - text = sign.GetDisplayText() - - if !strings.Contains(text, "Location: 100.00, 200.00, 300.00") { - t.Error("Display text should contain location information") - } - - // With heading - sign.SetSignZoneHeading(45.0) - sign.SetIncludeHeading(true) - text = sign.GetDisplayText() - - if !strings.Contains(text, "Heading: 45.00") { - t.Error("Display text should contain heading information") - } -} - -func TestSignHandleUse(t *testing.T) { - // Setup test environment - database := NewMockDatabase() - database.AddZone(2, "Test Zone") - database.AddCharacter(1, "TestPlayer") - - player := &MockPlayer{} - client := &MockClient{ - player: player, - charID: 1, - database: database, - zoneAccess: map[string]bool{"Test Zone": true}, - } - - // Test zone transport sign - sign := createZoneSign() - sign.SetSignDistance(10.0) // Set distance limit - - err := sign.HandleUse(client, "") - if err != nil { - t.Errorf("HandleUse failed: %v", err) - } - - // Check if player position was set - if player.x != 100.0 || player.y != 200.0 || player.z != 300.0 { - t.Error("Player position should be updated for zone transport") - } - - if player.heading != 45.0 { - t.Error("Player heading should be updated for zone transport") - } - - // Test mark command - sign = createTestSign() - err = sign.HandleUse(client, "mark") - if err != nil { - t.Errorf("Mark command failed: %v", err) - } - - // Test distance check - sign = createZoneSign() - sign.SetSignDistance(1.0) // Very short distance - client.ClearMessages() - - err = sign.HandleUse(client, "") - if err != nil { - t.Errorf("HandleUse with distance check failed: %v", err) - } - - messages := client.GetMessages() - found := false - for _, msg := range messages { - if strings.Contains(msg, "too far away") { - found = true - break - } - } - if !found { - t.Error("Should get 'too far away' message when outside distance") - } -} - -func TestSignSpawn(t *testing.T) { - baseSpawn := spawn.NewSpawn() - baseSpawn.SetName("Test Sign Spawn") - - signSpawn := NewSignSpawn(baseSpawn) - - if signSpawn == nil { - t.Fatal("NewSignSpawn returned nil") - } - - if !signSpawn.IsSign() { - t.Error("SignSpawn should be a sign") - } - - if signSpawn.Spawn != baseSpawn { - t.Error("SignSpawn should reference the base spawn") - } - - // Test copy - copy := signSpawn.Copy() - if copy == nil { - t.Fatal("SignSpawn Copy returned nil") - } - - if copy == signSpawn { - t.Error("SignSpawn Copy should create new instance") - } - - if copy.Spawn.GetName() != baseSpawn.GetName() { - t.Error("Copied SignSpawn should have same spawn name") - } -} - -func TestSignAdapter(t *testing.T) { - logger := &MockLogger{} - entity := &MockEntity{id: 1, name: "Test Entity", databaseID: 100} - - adapter := NewSignAdapter(entity, logger) - - if adapter == nil { - t.Fatal("NewSignAdapter returned nil") - } - - if !adapter.IsSign() { - t.Error("SignAdapter should be a sign") - } - - if adapter.GetSign() == nil { - t.Error("SignAdapter should have a sign") - } - - // Test setting properties with logging - adapter.GetSign().SetWidgetID(500) // Set widget ID for validation - adapter.SetSignTitle("Test Title") - adapter.SetSignDescription("Test Description") - adapter.SetSignType(SignTypeZone) - adapter.SetZoneTransport(2, 100.0, 200.0, 300.0, 45.0) - adapter.SetSignDistance(15.0) - - logs := logger.GetLogs() - if len(logs) == 0 { - t.Error("SignAdapter operations should generate log messages") - } - - // Verify sign properties were set - sign := adapter.GetSign() - if sign.GetSignTitle() != "Test Title" { - t.Error("Sign title not set correctly") - } - - if sign.GetSignType() != SignTypeZone { - t.Error("Sign type not set correctly") - } - - if sign.GetSignZoneID() != 2 { - t.Error("Zone ID not set correctly") - } - - // Test validation - if !adapter.IsValid() { - issues := adapter.Validate() - t.Errorf("SignAdapter should be valid, issues: %v", issues) - } -} - -func TestManager(t *testing.T) { - database := NewMockDatabase() - logger := &MockLogger{} - - manager := NewManager(database, logger) - - if manager == nil { - t.Fatal("NewManager returned nil") - } - - // Test initialization - err := manager.Initialize() - if err != nil { - t.Errorf("Manager initialization failed: %v", err) - } - - // Test adding signs - sign1 := createTestSign() - sign1.Spawn.SetDatabaseID(1) - sign1.SetWidgetID(100) - - err = manager.AddSign(sign1) - if err != nil { - t.Errorf("Adding sign failed: %v", err) - } - - if manager.GetSignCount() != 1 { - t.Errorf("Expected 1 sign, got %d", manager.GetSignCount()) - } - - // Test retrieving signs - retrieved := manager.GetSign(1) - if retrieved == nil { - t.Error("Should be able to retrieve sign by ID") - } - - retrieved = manager.GetSignByWidget(100) - if retrieved == nil { - t.Error("Should be able to retrieve sign by widget ID") - } - - // Test zone functionality - sign2 := createZoneSign() - sign2.Spawn.SetDatabaseID(2) - sign2.SetWidgetID(200) - - err = manager.AddSignToZone(sign2, 1) - if err != nil { - t.Errorf("Adding sign to zone failed: %v", err) - } - - zoneSigns := manager.GetZoneSigns(1) - if len(zoneSigns) != 1 { - t.Errorf("Expected 1 zone sign, got %d", len(zoneSigns)) - } - - // Test statistics - stats := manager.GetStatistics() - if stats["total_signs"] != int64(2) { - t.Errorf("Expected 2 total signs in stats, got %v", stats["total_signs"]) - } - - typeStats := stats["signs_by_type"].(map[int8]int64) - if typeStats[SignTypeGeneric] != 1 { - t.Errorf("Expected 1 generic sign in stats, got %d", typeStats[SignTypeGeneric]) - } - if typeStats[SignTypeZone] != 1 { - t.Errorf("Expected 1 zone sign in stats, got %d", typeStats[SignTypeZone]) - } - - // Test removing signs - if !manager.RemoveSign(1) { - t.Error("Should be able to remove sign") - } - - if manager.GetSignCount() != 1 { - t.Errorf("Expected 1 sign after removal, got %d", manager.GetSignCount()) - } - - // Test validation - issues := manager.ValidateAllSigns() - if len(issues) > 0 { - t.Errorf("All signs should be valid, found issues: %v", issues) - } - - // Test commands - result, err := manager.ProcessCommand("stats", nil) - if err != nil { - t.Errorf("Stats command failed: %v", err) - } - if !strings.Contains(result, "Sign System Statistics") { - t.Error("Stats command should return statistics") - } - - result, err = manager.ProcessCommand("list", nil) - if err != nil { - t.Errorf("List command failed: %v", err) - } - if !strings.Contains(result, "Signs (1)") { - t.Error("List command should show sign count") - } - - result, err = manager.ProcessCommand("validate", nil) - if err != nil { - t.Errorf("Validate command failed: %v", err) - } - if !strings.Contains(result, "All signs are valid") { - t.Error("Validate command should report all signs valid") - } - - result, err = manager.ProcessCommand("info", []string{"2"}) - if err != nil { - t.Errorf("Info command failed: %v", err) - } - if !strings.Contains(result, "Sign Information") { - t.Error("Info command should show sign information") - } - - // Test unknown command - _, err = manager.ProcessCommand("unknown", nil) - if err == nil { - t.Error("Unknown command should return error") - } -} - -func TestManagerConcurrency(t *testing.T) { - database := NewMockDatabase() - logger := &MockLogger{} - manager := NewManager(database, logger) - - const numGoroutines = 10 - const numOperations = 100 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - // Test concurrent sign additions - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - for j := 0; j < numOperations; j++ { - sign := createTestSign() - signID := int32(id*numOperations + j + 1) - sign.Spawn.SetDatabaseID(signID) - sign.SetWidgetID(signID) - - err := manager.AddSign(sign) - if err != nil { - t.Errorf("Concurrent AddSign failed: %v", err) - } - } - }(i) - } - - wg.Wait() - - expectedCount := int64(numGoroutines * numOperations) - if manager.GetSignCount() != expectedCount { - t.Errorf("Expected %d signs after concurrent additions, got %d", - expectedCount, manager.GetSignCount()) - } - - // Test concurrent reads - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func() { - defer wg.Done() - for j := 0; j < numOperations; j++ { - stats := manager.GetStatistics() - if stats["total_signs"].(int64) <= 0 { - t.Error("Stats should show positive sign count") - } - } - }() - } - - wg.Wait() -} - -func TestSignSerialization(t *testing.T) { - sign := createTestSign() - player := &MockPlayer{} - - // Test serialization (currently returns error due to incomplete implementation) - _, err := sign.Serialize(player, 1146) - if err == nil { - t.Error("Serialization should return error with current implementation") - } - - if !strings.Contains(err.Error(), "not yet implemented") { - t.Error("Should get 'not yet implemented' error message") - } -} - -func TestSignConstants(t *testing.T) { - // Test sign type constants - if SignTypeGeneric != 0 { - t.Errorf("SignTypeGeneric should be 0, got %d", SignTypeGeneric) - } - - if SignTypeZone != 1 { - t.Errorf("SignTypeZone should be 1, got %d", SignTypeZone) - } - - // Test default constants - if DefaultSpawnType != 2 { - t.Errorf("DefaultSpawnType should be 2, got %d", DefaultSpawnType) - } - - if DefaultActivityStatus != 64 { - t.Errorf("DefaultActivityStatus should be 64, got %d", DefaultActivityStatus) - } - - if DefaultSignDistance != 0.0 { - t.Errorf("DefaultSignDistance should be 0.0, got %f", DefaultSignDistance) - } -} - -// Benchmark tests - -func BenchmarkNewSign(b *testing.B) { - for i := 0; i < b.N; i++ { - _ = NewSign() - } -} - -func BenchmarkSignCopy(b *testing.B) { - sign := createTestSign() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - _ = sign.Copy() - } -} - -func BenchmarkSignValidation(b *testing.B) { - sign := createTestSign() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - _ = sign.Validate() - } -} - -func BenchmarkManagerAddSign(b *testing.B) { - database := NewMockDatabase() - logger := &MockLogger{} - manager := NewManager(database, logger) - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - sign := createTestSign() - sign.Spawn.SetDatabaseID(int32(i + 1)) - sign.SetWidgetID(int32(i + 100)) - _ = manager.AddSign(sign) - } -} - -func BenchmarkManagerGetSign(b *testing.B) { - database := NewMockDatabase() - logger := &MockLogger{} - manager := NewManager(database, logger) - - // Pre-populate with signs - for i := 0; i < 1000; i++ { - sign := createTestSign() - sign.Spawn.SetDatabaseID(int32(i + 1)) - sign.SetWidgetID(int32(i + 100)) - _ = manager.AddSign(sign) - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - _ = manager.GetSign(int32((i % 1000) + 1)) - } -} \ No newline at end of file diff --git a/internal/spawn/README.md b/internal/spawn/README.md deleted file mode 100644 index 118bcf2..0000000 --- a/internal/spawn/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Spawn System - -This package implements the EverQuest II spawn system, converted from the original C++ codebase to Go. - -## Overview - -The spawn system manages all entities that can appear in the game world, including NPCs, objects, widgets, signs, and ground spawns. It handles their positioning, movement, combat states, and interactions. - -## Key Components - -### Spawn (`spawn.go`) - -The `Spawn` struct is the base class for all entities in the game world. It provides: - -- **Basic Properties**: ID, name, level, position, heading -- **State Management**: Health, power, alive status, combat state -- **Movement System**: Position tracking, movement queues, pathfinding -- **Command System**: Interactive commands players can use -- **Loot System**: Item drops, coin rewards, loot distribution -- **Scripting Integration**: Lua script support for dynamic behavior -- **Thread Safety**: Atomic operations and mutexes for concurrent access - -Key features: -- Thread-safe operations using atomic values and mutexes -- Extensible design allowing subclasses (NPC, Player, Object, etc.) -- Comprehensive state tracking with change notifications -- Support for temporary variables for Lua scripting -- Equipment and appearance management -- Group and faction relationships - -### Spawn Lists (`spawn_lists.go`) - -Manages spawn locations and spawn entries: - -- **SpawnEntry**: Defines what can spawn at a location with configuration -- **SpawnLocation**: Represents a point in the world where spawns appear -- **SpawnLocationManager**: Manages collections of spawn locations - -Features: -- Randomized spawn selection based on percentages -- Position offsets for spawn variety -- Respawn timing with random offsets -- Stat overrides for spawn customization -- Grid-based location management - -## Architecture Notes - -### Thread Safety - -The spawn system is designed for high-concurrency access: -- Atomic values for frequently-accessed state flags -- Read-write mutexes for complex data structures -- Separate mutexes for different subsystems to minimize contention - -### Memory Management - -Go's garbage collector handles memory management, but the system includes: -- Proper cleanup methods for complex structures -- Resource pooling where appropriate -- Efficient data structures to minimize allocations - -### Extensibility - -The base Spawn struct is designed to be extended: -- Virtual method patterns using interfaces -- Type checking methods (IsNPC, IsPlayer, etc.) -- Extensible command and scripting systems - -## TODO Items - -Many features are marked with TODO comments for future implementation: - -### High Priority -- **Zone System Integration**: Zone server references and notifications -- **Client System**: Player client connections and packet handling -- **Item System**: Complete item and equipment implementation -- **Combat System**: Damage calculation and combat mechanics - -### Medium Priority -- **Lua Scripting**: Full Lua integration for spawn behavior -- **Movement System**: Pathfinding and advanced movement -- **Quest System**: Quest requirement checking and progression -- **Map System**: Collision detection and height maps - -### Low Priority -- **Region System**: Area-based effects and triggers -- **Housing System**: Player housing integration -- **Transportation**: Mounts and vehicles -- **Advanced Physics**: Knockback and projectile systems - -## Usage Examples - -### Creating a Basic Spawn - -```go -spawn := NewSpawn() -spawn.SetName("a goblin warrior") -spawn.SetLevel(10) -spawn.SetX(100.0) -spawn.SetY(0.0) -spawn.SetZ(200.0) -spawn.SetTotalHP(500) -spawn.SetHP(500) -``` - -### Managing Spawn Locations - -```go -manager := NewSpawnLocationManager() - -location := NewSpawnLocation() -location.SetPosition(100.0, 0.0, 200.0) -location.SetOffsets(5.0, 0.0, 5.0) // 5 unit random offset - -entry := NewSpawnEntry() -entry.SpawnID = 12345 -entry.SpawnPercentage = 75.0 -entry.Respawn = 300 // 5 minutes - -location.AddSpawnEntry(entry) -manager.AddLocation(1001, location) -``` - -### Adding Commands - -```go -spawn.AddPrimaryEntityCommand( - "hail", // command name - 10.0, // max distance - "hail_npc", // internal command - "Too far away", // error message - 0, // cast time - 0, // spell visual - true, // default allow -) -``` - -## Integration Points - -The spawn system integrates with several other systems: - -- **Database**: Loading and saving spawn data -- **Network**: Serializing spawn data for clients -- **Zone**: Spawn management within zones -- **Combat**: Damage and death handling -- **Scripting**: Lua event handling -- **Items**: Equipment and loot management - -## Performance Considerations - -- Position updates use atomic operations to minimize locking -- Command lists are copied on access to prevent race conditions -- Spawn changes are batched and sent efficiently to clients -- Memory usage is optimized for large numbers of concurrent spawns - -## Migration from C++ - -This Go implementation maintains compatibility with the original C++ EQ2EMu spawn system while modernizing the architecture: - -- Converted C-style arrays to Go slices -- Replaced manual memory management with garbage collection -- Used Go's concurrency primitives instead of platform-specific threading -- Maintained the same packet structures and network protocol -- Preserved the same database schema and data relationships - -The API surface remains similar to ease porting of existing scripts and configurations. \ No newline at end of file diff --git a/internal/spells/README.md b/internal/spells/README.md deleted file mode 100644 index 8eb7415..0000000 --- a/internal/spells/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Spells System - -Complete spell system for EverQuest II server emulation with spell definitions, casting mechanics, effects, and processing. - -## Components - -**Core System:** -- **SpellData/Spell** - Spell definitions with properties, levels, effects, LUA data -- **SpellEffectManager** - Active effects management (buffs, debuffs, bonuses) -- **MasterSpellList/SpellBook** - Global registry and per-character spell collections - -**Spell Processing:** -- **SpellProcess** - Real-time casting engine (50ms intervals) with timers, queues, interrupts -- **SpellTargeting** - All target types: self, single, group, AOE, PBAE with validation -- **SpellResourceChecker** - Power, health, concentration, savagery, dissonance management -- **SpellManager** - High-level coordinator integrating all systems - -## Key Features - -- **Real-time Processing**: Cast/recast timers, active spell tracking, interrupt handling -- **Comprehensive Targeting**: Range, LOS, spell criteria validation for all target types -- **Resource Management**: All EQ2 resource types with validation and consumption -- **Effect System**: 30 maintained effects, 45 spell effects, detrimental effects -- **Heroic Opportunities**: Solo/group coordination with timing -- **Thread Safety**: Concurrent access with proper mutexes -- **80+ Effect Types**: All spell modifications from original C++ system - -## Usage - -```go -// Create and use spell manager -spellManager := spells.NewSpellManager() - -// Cast spell with full validation -err := spellManager.CastSpell(casterID, targetID, spellID) -if err != nil { - log.Printf("Spell failed: %v", err) -} - -// Check spell readiness -canCast, reason := spellManager.CanCastSpell(casterID, targetID, spellID) - -// Process spells in main loop -spellManager.ProcessSpells() - -// Manage spell books -spellBook := spellManager.GetSpellBook(characterID) -spellBook.AddSpell(spell) -spellBook.SetSpellBarSlot(0, 1, spell) - -// Effect management -sem := spells.NewSpellEffectManager() -sem.AddMaintainedEffect(maintainedEffect) -sem.AddSpellEffect(tempEffect) -``` - -## Files - -**Core**: `constants.go`, `spell_data.go`, `spell.go`, `spell_effects.go`, `spell_manager.go` -**Processing**: `process_constants.go`, `spell_process.go`, `spell_targeting.go`, `spell_resources.go` -**Docs**: `README.md`, `SPELL_PROCESS.md` - -## Integration - -**Database**: Spell data loading, character spell book persistence -**Packet System**: Spell info serialization, effect updates -**Entity System**: SpellEffectManager embedded, stat integration -**LUA Scripting**: Custom spell behaviors, effect calculations \ No newline at end of file diff --git a/internal/spells/SPELL_PROCESS.md b/internal/spells/SPELL_PROCESS.md deleted file mode 100644 index 42c2c7a..0000000 --- a/internal/spells/SPELL_PROCESS.md +++ /dev/null @@ -1,112 +0,0 @@ -# Spell Processing System - -Comprehensive spell casting engine managing all aspects of spell processing including timers, targeting, resource management, and heroic opportunities. - -## Components - -**SpellProcess** - Core engine managing active spells, cast/recast timers, interrupt queues, spell queues, and heroic opportunities -**SpellTargeting** - Target selection for all spell types (self, single, group, AOE, PBAE) with validation -**SpellResourceChecker** - Resource validation/consumption for power, health, concentration, savagery, dissonance -**SpellManager** - High-level coordinator integrating all systems with comprehensive casting API - -## Key Data Structures - -```go -// Active spell instance -type LuaSpell struct { - Spell *Spell // Spell definition - CasterID int32 // Casting entity - Targets []int32 // Target list - Timer SpellTimer // Duration/tick timing - NumCalls int32 // Tick count - Interrupted bool // Interrupt state -} - -// Cast timing -type CastTimer struct { - CasterID int32 // Casting entity - SpellID int32 // Spell being cast - StartTime time.Time // Cast start - Duration time.Duration // Cast time -} - -// Cooldown timing -type RecastTimer struct { - CasterID int32 // Entity with cooldown - SpellID int32 // Spell on cooldown - Duration time.Duration // Cooldown time - LinkedTimer int32 // Shared cooldown group -} -``` - -## Usage - -```go -// Main spell processing loop (50ms intervals) -spellManager.ProcessSpells() - -// Cast spell with full validation -err := spellManager.CastSpell(casterID, targetID, spellID) - -// Interrupt casting -spellManager.InterruptSpell(entityID, spellID, errorCode, canceled, fromMovement) - -// Queue management -spellManager.AddSpellToQueue(spellID, casterID, targetID, priority) -spellManager.RemoveSpellFromQueue(spellID, casterID) - -// Status checks -ready := spellManager.IsSpellReady(spellID, casterID) -recastTime := spellManager.GetSpellRecastTime(spellID, casterID) -canCast, reason := spellManager.CanCastSpell(casterID, targetID, spellID) - -// Resource checking -resourceChecker := spellManager.GetResourceChecker() -powerResult := resourceChecker.CheckPower(luaSpell, customPowerReq) -allResults := resourceChecker.CheckAllResources(luaSpell, 0, 0) -success := resourceChecker.ConsumeAllResources(luaSpell, 0, 0) - -// Targeting -targeting := spellManager.GetTargeting() -targetResult := targeting.GetSpellTargets(luaSpell, options) -``` - -## Effect Types (80+) - -**Stat Modifications**: Health, power, stats (STR/AGI/STA/INT/WIS), resistances, attack, mitigation -**Spell Modifications**: Cast time, power req, range, duration, resistibility, crit chance -**Actions**: Damage, healing, DOT/HOT, resurrect, summon, mount, invisibility -**Control**: Stun, root, mez, fear, charm, blind, kill -**Special**: Change race/size/title, faction, exp, tradeskill bonuses - -## Target Types - -**TargetTypeSelf** (0) - Self-only spells -**TargetTypeSingle** (1) - Single target with validation -**TargetTypeGroup** (2) - Group members -**TargetTypeGroupAE** (3) - Group area effect -**TargetTypeAE** (4) - True area effect -**TargetTypePBAE** (5) - Point blank area effect - -## Interrupt System - -**Causes**: Movement, damage, stun, mesmerize, fear, manual cancellation, out of range -**Processing**: Queued interrupts processed every cycle with proper cleanup -**Error Codes**: Match client expectations for proper UI feedback - -## Performance - -- **50ms Processing**: Matches client update expectations -- **Efficient Indexing**: Fast lookups by caster, spell, target -- **Thread Safety**: Concurrent access with proper locking -- **Memory Management**: Cleanup of expired timers and effects -- **Batch Operations**: Multiple resource checks/targeting in single calls - -## Integration Points - -**Entity System**: Caster/target info, position data, combat state -**Zone System**: Position validation, line-of-sight, spawn management -**Group System**: Group member targeting and coordination -**Database**: Persistent spell data, character spell books -**Packet System**: Client communication for spell states -**LUA System**: Custom spell scripting (future) diff --git a/internal/spells/spells_test.go b/internal/spells/spells_test.go deleted file mode 100644 index 958b11d..0000000 --- a/internal/spells/spells_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package spells - -import ( - "testing" -) - -func TestPackageBuild(t *testing.T) { - // Basic test to verify the package builds - spell := NewSpell() - if spell == nil { - t.Fatal("NewSpell returned nil") - } -} - -func TestSpellManager(t *testing.T) { - manager := NewSpellManager() - if manager == nil { - t.Fatal("NewSpellManager returned nil") - } -} \ No newline at end of file diff --git a/internal/titles/README.md b/internal/titles/README.md deleted file mode 100644 index 31c22a2..0000000 --- a/internal/titles/README.md +++ /dev/null @@ -1,226 +0,0 @@ -# Title System - -The title system manages character titles in the EverQuest II server emulator, allowing players to earn, display, and manage various titles that represent their achievements and progression. - -## Overview - -The title system consists of several key components: - -- **Master Titles List**: Global registry of all available titles -- **Player Title Collections**: Individual player title ownership and preferences -- **Title Manager**: Central coordination and management -- **Integration Systems**: Hooks for earning titles through various game activities - -## Core Components - -### Title Structure - -Each title has the following properties: - -- **ID**: Unique identifier -- **Name**: Display name of the title -- **Description**: Detailed description shown in UI -- **Position**: Whether it appears as prefix or suffix -- **Category**: Organizational category (Combat, Tradeskill, etc.) -- **Source**: How the title is obtained (Achievement, Quest, etc.) -- **Rarity**: Common, Uncommon, Rare, Epic, Legendary, Unique -- **Requirements**: Conditions that must be met to earn the title -- **Flags**: Various behavioral modifiers (Hidden, Temporary, Unique, etc.) - -### Title Positioning - -Titles can be displayed in two positions relative to the character name: - -- **Prefix**: Appears before the character name (e.g., "Master John") -- **Suffix**: Appears after the character name (e.g., "John the Brave") - -Players can have one active prefix and one active suffix title simultaneously. - -### Title Sources - -Titles can be obtained through various means: - -- **Achievements**: Completing specific achievements -- **Quests**: Finishing particular quest lines -- **Tradeskills**: Reaching mastery in crafting -- **Combat**: Battle-related accomplishments -- **Exploration**: Discovering new areas -- **PvP**: Player vs Player activities -- **Guild**: Guild progression and achievements -- **Events**: Special server events -- **Rare**: Uncommon encounters or collections - -## Usage Examples - -### Basic Title Management - -```go -// Create a title manager -titleManager := titles.NewTitleManager() - -// Create a new title -title, err := titleManager.CreateTitle( - "Dragon Slayer", // name - "Defeated an ancient dragon", // description - titles.CategoryCombat, // category - titles.TitlePositionSuffix, // position - titles.TitleSourceAchievement, // source - titles.TitleRarityEpic, // rarity -) - -// Grant a title to a player -err = titleManager.GrantTitle(playerID, titleID, achievementID, 0) - -// Set active titles -err = titleManager.SetPlayerActivePrefix(playerID, prefixTitleID) -err = titleManager.SetPlayerActiveSuffix(playerID, suffixTitleID) - -// Get formatted player name with titles -formattedName := titleManager.GetPlayerFormattedName(playerID, "John") -// Result: "Master John the Dragon Slayer" -``` - -### Achievement Integration - -```go -// Set up achievement integration -integrationManager := titles.NewIntegrationManager(titleManager) - -// Create achievement-linked title -title, err := titleManager.CreateAchievementTitle( - "Dungeon Master", - "Completed 100 dungeons", - achievementID, - titles.TitlePositionPrefix, - titles.TitleRarityRare, -) - -// When achievement is completed -err = integrationManager.GetAchievementIntegration().OnAchievementCompleted(playerID, achievementID) -``` - -### Event Titles - -```go -// Start a seasonal event with title reward -eventIntegration := integrationManager.GetEventIntegration() -err = eventIntegration.StartEvent( - "Halloween 2024", - "Spooky seasonal event", - 7*24*time.Hour, // 1 week duration - halloweenTitleID, -) - -// Grant participation title -err = eventIntegration.OnEventParticipation(playerID, "Halloween 2024") -``` - -## File Structure - -- `constants.go`: Title system constants and enums -- `title.go`: Core title and player title data structures -- `master_list.go`: Global title registry and management -- `player_titles.go`: Individual player title collections -- `title_manager.go`: Central title system coordinator -- `integration.go`: Integration systems for earning titles -- `README.md`: This documentation file - -## Database Integration - -The title system is designed to integrate with the database layer: - -- **Master titles**: Stored in `titles` table -- **Player titles**: Stored in `character_titles` table -- **Title requirements**: Stored in `title_requirements` table - -Database methods are marked as TODO and will be implemented when the database package is available. - -## Network Packets - -The system supports the following network packets: - -- **TitleUpdate**: Sends player's available titles to client -- **UpdateTitle**: Updates displayed title information - -Packet structures are defined based on the XML definitions in the packets directory. - -## Title Categories - -The system organizes titles into logical categories: - -- **Combat**: Battle and PvP related titles -- **Tradeskill**: Crafting and gathering achievements -- **Exploration**: Zone discovery and travel -- **Social**: Community and roleplay titles -- **Achievement**: General accomplishment titles -- **Quest**: Story and mission completion -- **Rare**: Uncommon encounters and collections -- **Seasonal**: Time-limited event titles -- **Guild**: Organization-based titles -- **Raid**: Group content achievements -- **Class**: Profession-specific titles -- **Race**: Heritage and background titles - -## Title Rarity System - -Titles have six rarity levels with associated colors: - -1. **Common** (White): Easily obtainable titles -2. **Uncommon** (Green): Moderate effort required -3. **Rare** (Blue): Significant achievement needed -4. **Epic** (Purple): Exceptional accomplishment -5. **Legendary** (Orange): Extremely difficult to obtain -6. **Unique** (Red): One-of-a-kind titles - -## Special Title Types - -### Temporary Titles - -Some titles expire after a certain period: - -- Event titles that expire when the event ends -- Temporary status titles (e.g., "Newcomer") -- Achievement-based titles with time limits - -### Unique Titles - -Certain titles can only be held by one player at a time: - -- "First to reach max level" -- "Server champion" -- Special recognition titles - -### Account-Wide Titles - -Some titles are shared across all characters on an account: - -- Beta tester recognition -- Special event participation -- Founder rewards - -## Thread Safety - -All title system components are thread-safe using appropriate synchronization: - -- `sync.RWMutex` for read-heavy operations -- Atomic operations where appropriate -- Proper locking hierarchy to prevent deadlocks - -## Performance Considerations - -- Title lookups are optimized with indexed maps -- Player title data is cached in memory -- Background cleanup processes handle expired titles -- Database operations are batched when possible - -## Future Enhancements - -Planned improvements include: - -- Advanced title search and filtering -- Title display customization options -- Title trading/gifting system -- Dynamic title generation -- Integration with guild systems -- Advanced achievement requirements -- Title collection statistics and tracking \ No newline at end of file diff --git a/internal/titles/titles_test.go b/internal/titles/titles_test.go deleted file mode 100644 index 86f73e6..0000000 --- a/internal/titles/titles_test.go +++ /dev/null @@ -1,865 +0,0 @@ -package titles - -import ( - "fmt" - "path/filepath" - "sync" - "testing" - "time" -) - - -// Test Title struct creation and basic methods -func TestNewTitle(t *testing.T) { - title := NewTitle(1, "Test Title") - if title == nil { - t.Fatal("NewTitle returned nil") - } - - if title.ID != 1 { - t.Errorf("Expected ID 1, got %d", title.ID) - } - - if title.Name != "Test Title" { - t.Errorf("Expected name 'Test Title', got '%s'", title.Name) - } - - if title.Position != TitlePositionSuffix { - t.Errorf("Expected default position %d, got %d", TitlePositionSuffix, title.Position) - } -} - -func TestTitleFlags(t *testing.T) { - title := NewTitle(1, "Test Title") - - // Test setting flags - title.SetFlag(FlagHidden) - if !title.HasFlag(FlagHidden) { - t.Error("Expected title to have FlagHidden after setting") - } - - title.SetFlag(FlagTemporary) - if !title.HasFlag(FlagTemporary) { - t.Error("Expected title to have FlagTemporary after setting") - } - - // Test clearing flags - title.ClearFlag(FlagHidden) - if title.HasFlag(FlagHidden) { - t.Error("Expected title not to have FlagHidden after clearing") - } - - // Test toggle flags (manually implement toggle) - if title.HasFlag(FlagUnique) { - title.ClearFlag(FlagUnique) - } else { - title.SetFlag(FlagUnique) - } - if !title.HasFlag(FlagUnique) { - t.Error("Expected title to have FlagUnique after toggling") - } - - // Toggle again - if title.HasFlag(FlagUnique) { - title.ClearFlag(FlagUnique) - } else { - title.SetFlag(FlagUnique) - } - if title.HasFlag(FlagUnique) { - t.Error("Expected title not to have FlagUnique after toggling again") - } -} - -func TestTitleProperties(t *testing.T) { - title := NewTitle(1, "Test Title") - - // Test description - title.SetDescription("A test description") - if title.Description != "A test description" { - t.Errorf("Expected description 'A test description', got '%s'", title.Description) - } - - // Test category - title.SetCategory(CategoryAchievement) - if title.Category != CategoryAchievement { - t.Errorf("Expected category '%s', got '%s'", CategoryAchievement, title.Category) - } - - // Test rarity - title.SetRarity(TitleRarityRare) - if title.Rarity != TitleRarityRare { - t.Errorf("Expected rarity %d, got %d", TitleRarityRare, title.Rarity) - } - - // Test display name - title.Position = TitlePositionPrefix - displayName := title.GetDisplayName() - if displayName != "Test Title" { - t.Errorf("Expected display name 'Test Title', got '%s'", displayName) - } -} - -func TestTitleTypeMethods(t *testing.T) { - title := NewTitle(1, "Test Title") - - // Test hidden flag - title.SetFlag(FlagHidden) - if !title.IsHidden() { - t.Error("Expected title to be hidden") - } - - // Test temporary flag - title.ClearFlag(FlagHidden) - title.SetFlag(FlagTemporary) - if !title.IsTemporary() { - t.Error("Expected title to be temporary") - } - - // Test unique flag - title.ClearFlag(FlagTemporary) - title.SetFlag(FlagUnique) - if !title.IsUnique() { - t.Error("Expected title to be unique") - } - - // Test account-wide flag - title.ClearFlag(FlagUnique) - title.SetFlag(FlagAccountWide) - if !title.IsAccountWide() { - t.Error("Expected title to be account-wide") - } -} - -// Test MasterTitlesList -func TestMasterTitlesList(t *testing.T) { - mtl := NewMasterTitlesList() - if mtl == nil { - t.Fatal("NewMasterTitlesList returned nil") - } - - // Test default titles are loaded - citizen, exists := mtl.GetTitle(TitleIDCitizen) - if !exists || citizen == nil { - t.Error("Expected Citizen title to be loaded by default") - } else if citizen.Name != "Citizen" { - t.Errorf("Expected Citizen title name, got '%s'", citizen.Name) - } - - visitor, exists := mtl.GetTitle(TitleIDVisitor) - if !exists || visitor == nil { - t.Error("Expected Visitor title to be loaded by default") - } - - newcomer, exists := mtl.GetTitle(TitleIDNewcomer) - if !exists || newcomer == nil { - t.Error("Expected Newcomer title to be loaded by default") - } - - returning, exists := mtl.GetTitle(TitleIDReturning) - if !exists || returning == nil { - t.Error("Expected Returning title to be loaded by default") - } - - // Test adding new title - testTitle := NewTitle(0, "Test Title") // ID will be assigned - testTitle.SetDescription("Test description") - testTitle.SetCategory(CategoryMiscellaneous) - - err := mtl.AddTitle(testTitle) - if err != nil { - t.Fatalf("Failed to add title: %v", err) - } - - // ID should have been assigned - if testTitle.ID == 0 { - t.Error("Title ID should have been assigned") - } - - retrieved, exists := mtl.GetTitle(testTitle.ID) - if !exists || retrieved == nil { - t.Error("Failed to retrieve added title") - } else if retrieved.Name != "Test Title" { - t.Errorf("Expected retrieved title name 'Test Title', got '%s'", retrieved.Name) - } - - // Test duplicate ID - duplicateTitle := NewTitle(testTitle.ID, "Duplicate") - err = mtl.AddTitle(duplicateTitle) - if err == nil { - t.Error("Should have failed to add title with duplicate ID") - } -} - -func TestMasterTitlesListCategories(t *testing.T) { - mtl := NewMasterTitlesList() - - // Add titles in different categories - for i := 0; i < 3; i++ { - title := NewTitle(0, fmt.Sprintf("Achievement Title %d", i)) - title.SetCategory(CategoryAchievement) - mtl.AddTitle(title) - } - - for i := 0; i < 2; i++ { - title := NewTitle(0, fmt.Sprintf("Quest Title %d", i)) - title.SetCategory(CategoryQuest) - mtl.AddTitle(title) - } - - // Test getting titles by category - achievementTitles := mtl.GetTitlesByCategory(CategoryAchievement) - if len(achievementTitles) < 3 { - t.Errorf("Expected at least 3 achievement titles, got %d", len(achievementTitles)) - } - - questTitles := mtl.GetTitlesByCategory(CategoryQuest) - if len(questTitles) < 2 { - t.Errorf("Expected at least 2 quest titles, got %d", len(questTitles)) - } - - // Test getting available categories - categories := mtl.GetAvailableCategories() - hasAchievement := false - hasQuest := false - for _, cat := range categories { - if cat == CategoryAchievement { - hasAchievement = true - } - if cat == CategoryQuest { - hasQuest = true - } - } - - if !hasAchievement { - t.Error("Expected CategoryAchievement in available categories") - } - if !hasQuest { - t.Error("Expected CategoryQuest in available categories") - } -} - -func TestMasterTitlesListSourceAndRarity(t *testing.T) { - mtl := NewMasterTitlesList() - - // Add titles with different sources - title1 := NewTitle(0, "Achievement Source") - title1.Source = TitleSourceAchievement - title1.SetRarity(TitleRarityCommon) - mtl.AddTitle(title1) - - title2 := NewTitle(0, "Quest Source") - title2.Source = TitleSourceQuest - title2.SetRarity(TitleRarityRare) - mtl.AddTitle(title2) - - title3 := NewTitle(0, "PvP Source") - title3.Source = TitleSourcePvP - title3.SetRarity(TitleRarityEpic) - mtl.AddTitle(title3) - - // Test getting titles by source - achievementSourceTitles := mtl.GetTitlesBySource(TitleSourceAchievement) - found := false - for _, t := range achievementSourceTitles { - if t.Name == "Achievement Source" { - found = true - break - } - } - if !found { - t.Error("Failed to find achievement source title") - } - - // Test getting titles by rarity - rareTitles := mtl.GetTitlesByRarity(TitleRarityRare) - found = false - for _, t := range rareTitles { - if t.Name == "Quest Source" { - found = true - break - } - } - if !found { - t.Error("Failed to find rare title") - } -} - -// Test PlayerTitlesList -func TestPlayerTitlesList(t *testing.T) { - mtl := NewMasterTitlesList() - - // Add some titles to master list - title1 := NewTitle(100, "Title One") - title1.Position = TitlePositionPrefix - mtl.AddTitle(title1) - - title2 := NewTitle(101, "Title Two") - title2.Position = TitlePositionSuffix - mtl.AddTitle(title2) - - ptl := NewPlayerTitlesList(123, mtl) - if ptl == nil { - t.Fatal("NewPlayerTitlesList returned nil") - } - - // Test adding title - err := ptl.AddTitle(100, 0, 0) - if err != nil { - t.Fatalf("Failed to add title to player: %v", err) - } - - err = ptl.AddTitle(101, 0, 0) - if err != nil { - t.Fatalf("Failed to add second title to player: %v", err) - } - - // Test getting titles (player starts with default Citizen title) - titles := ptl.GetAllTitles() - expectedTitleCount := 3 // Citizen + our 2 titles - if len(titles) != expectedTitleCount { - t.Errorf("Expected %d titles, got %d", expectedTitleCount, len(titles)) - } - - // Test has title - if !ptl.HasTitle(100) { - t.Error("Expected player to have title 100") - } - - if !ptl.HasTitle(101) { - t.Error("Expected player to have title 101") - } - - if ptl.HasTitle(999) { - t.Error("Expected player not to have title 999") - } - - // Test setting active prefix - err = ptl.SetActivePrefix(100) - if err != nil { - t.Fatalf("Failed to set active prefix: %v", err) - } - - activePrefixTitle, hasPrefix := ptl.GetActivePrefixTitle() - if !hasPrefix || activePrefixTitle.ID != 100 { - t.Errorf("Expected active prefix 100, got title ID: %v", activePrefixTitle) - } - - // Test setting active suffix - err = ptl.SetActiveSuffix(101) - if err != nil { - t.Fatalf("Failed to set active suffix: %v", err) - } - - activeSuffixTitle, hasSuffix := ptl.GetActiveSuffixTitle() - if !hasSuffix || activeSuffixTitle.ID != 101 { - t.Errorf("Expected active suffix 101, got title ID: %v", activeSuffixTitle) - } - - // Test formatted name (the actual format may vary) - formattedName := ptl.GetFormattedName("PlayerName") - // Just check that it contains both titles and the player name - if !contains(formattedName, "Title One") { - t.Errorf("Formatted name should contain 'Title One', got '%s'", formattedName) - } - if !contains(formattedName, "Title Two") { - t.Errorf("Formatted name should contain 'Title Two', got '%s'", formattedName) - } - if !contains(formattedName, "PlayerName") { - t.Errorf("Formatted name should contain 'PlayerName', got '%s'", formattedName) - } - - // Test removing title - err = ptl.RemoveTitle(100) - if err != nil { - t.Fatalf("Failed to remove title: %v", err) - } - - if ptl.HasTitle(100) { - t.Error("Title 100 should have been removed") - } - - // Active prefix should be cleared - _, hasPrefix = ptl.GetActivePrefixTitle() - if hasPrefix { - t.Error("Active prefix should be cleared after removing the title") - } -} - -func TestPlayerTitlesExpiration(t *testing.T) { - mtl := NewMasterTitlesList() - - // Add a temporary title to master list - tempTitle := NewTitle(200, "Temporary Title") - tempTitle.SetFlag(FlagTemporary) - tempTitle.ExpirationHours = 1 // Expires after 1 hour - mtl.AddTitle(tempTitle) - - ptl := NewPlayerTitlesList(456, mtl) - - // Add the temporary title - err := ptl.AddTitle(200, 0, 0) - if err != nil { - t.Fatalf("Failed to add temporary title: %v", err) - } - - // Title should exist - if !ptl.HasTitle(200) { - t.Error("Expected player to have temporary title") - } - - // Manually set expiration to past - if playerTitle, exists := ptl.titles[200]; exists { - playerTitle.ExpiresAt = time.Now().Add(-1 * time.Hour) - } - - // Clean up expired titles - expired := ptl.CleanupExpiredTitles() - if expired != 1 { - t.Errorf("Expected 1 expired title, got %d", expired) - } - - // Title should be removed - if ptl.HasTitle(200) { - t.Error("Expired title should have been removed") - } -} - -// Test TitleManager -func TestTitleManager(t *testing.T) { - tm := NewTitleManager() - if tm == nil { - t.Fatal("NewTitleManager returned nil") - } - - defer tm.Shutdown() - - // Test getting player titles - playerTitles := tm.GetPlayerTitles(456) - if playerTitles == nil { - t.Error("GetPlayerTitles returned nil") - } - - // Test granting title (use Visitor instead of Citizen to avoid conflicts) - err := tm.GrantTitle(456, TitleIDVisitor, 0, 0) - if err != nil { - t.Fatalf("Failed to grant title: %v", err) - } - - // Verify title was granted (player starts with default Citizen title) - playerTitles = tm.GetPlayerTitles(456) - titles := playerTitles.GetAllTitles() - expectedCount := 2 // Citizen + Visitor - if len(titles) != expectedCount { - t.Errorf("Expected %d titles for player, got %d", expectedCount, len(titles)) - } - - // Verify the player has the visitor title - if !playerTitles.HasTitle(TitleIDVisitor) { - t.Error("Player should have Visitor title") - } - - // Test revoking title - err = tm.RevokeTitle(456, TitleIDVisitor) - if err != nil { - t.Fatalf("Failed to revoke title: %v", err) - } - - // Verify title was revoked (should still have Citizen) - playerTitles = tm.GetPlayerTitles(456) - titles = playerTitles.GetAllTitles() - if len(titles) != 1 { - t.Errorf("Expected 1 title after revoke, got %d", len(titles)) - } -} - -func TestTitleManagerCreateVariants(t *testing.T) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Test creating regular title - title, err := tm.CreateTitle("Regular Title", "A regular title", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon) - if err != nil { - t.Fatalf("Failed to create regular title: %v", err) - } - if title == nil { - t.Fatal("CreateTitle returned nil") - } - - // Test creating achievement title - achievementTitle, err := tm.CreateAchievementTitle("Achievement Title", "An achievement title", 1000, TitlePositionPrefix, TitleRarityRare) - if err != nil { - t.Fatalf("Failed to create achievement title: %v", err) - } - if achievementTitle.AchievementID != 1000 { - t.Errorf("Expected achievement ID 1000, got %d", achievementTitle.AchievementID) - } - - // Test creating temporary title - tempTitle, err := tm.CreateTemporaryTitle("Temp Title", "A temporary title", 24, TitlePositionSuffix, TitleSourceHoliday, TitleRarityCommon) - if err != nil { - t.Fatalf("Failed to create temporary title: %v", err) - } - if !tempTitle.HasFlag(FlagTemporary) { - t.Error("Temporary title should have FlagTemporary") - } - if tempTitle.ExpirationHours != 24 { - t.Errorf("Expected expiration hours 24, got %d", tempTitle.ExpirationHours) - } - - // Test creating unique title - uniqueTitle, err := tm.CreateUniqueTitle("Unique Title", "A unique title", TitlePositionPrefix, TitleSourceMiscellaneous) - if err != nil { - t.Fatalf("Failed to create unique title: %v", err) - } - if !uniqueTitle.HasFlag(FlagUnique) { - t.Error("Unique title should have FlagUnique") - } - if uniqueTitle.Rarity != TitleRarityUnique { - t.Errorf("Expected rarity %d for unique title, got %d", TitleRarityUnique, uniqueTitle.Rarity) - } -} - -func TestTitleManagerSearch(t *testing.T) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Create test titles - tm.CreateTitle("Dragon Slayer", "Defeated a dragon", CategoryCombat, TitlePositionSuffix, TitleSourceQuest, TitleRarityEpic) - tm.CreateTitle("Master Crafter", "Master of crafting", CategoryTradeskill, TitlePositionPrefix, TitleSourceTradeskill, TitleRarityRare) - tm.CreateTitle("Explorer", "Explored the world", CategoryExploration, TitlePositionSuffix, TitleSourceAchievement, TitleRarityCommon) - - // Test search - results := tm.SearchTitles("dragon") - found := false - for _, title := range results { - if title.Name == "Dragon Slayer" { - found = true - break - } - } - if !found { - t.Error("Failed to find 'Dragon Slayer' in search results") - } - - // Test search with description - results = tm.SearchTitles("crafting") - found = false - for _, title := range results { - if title.Name == "Master Crafter" { - found = true - break - } - } - if !found { - t.Error("Failed to find 'Master Crafter' when searching description") - } -} - -func TestTitleManagerStatistics(t *testing.T) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Grant some titles (avoid Citizen which may be default) - tm.GrantTitle(1, TitleIDVisitor, 0, 0) - tm.GrantTitle(2, TitleIDNewcomer, 0, 0) - tm.GrantTitle(1, TitleIDReturning, 0, 0) - - stats := tm.GetStatistics() - - // Check statistics - if totalPlayers, ok := stats["total_players"].(int); ok { - if totalPlayers != 2 { - t.Errorf("Expected 2 total players, got %d", totalPlayers) - } - } else { - t.Error("Missing total_players in statistics") - } - - if titlesGranted, ok := stats["titles_granted"].(int64); ok { - if titlesGranted != 3 { - t.Errorf("Expected 3 titles granted, got %d", titlesGranted) - } - } else { - t.Error("Missing titles_granted in statistics") - } -} - -func TestTitleManagerConcurrency(t *testing.T) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Test concurrent access to player titles - var wg sync.WaitGroup - numPlayers := 10 - numOperations := 100 - - for i := 0; i < numPlayers; i++ { - wg.Add(1) - go func(playerID int32) { - defer wg.Done() - - for j := 0; j < numOperations; j++ { - // Grant title - tm.GrantTitle(playerID, TitleIDCitizen, 0, 0) - - // Get player titles - ptl := tm.GetPlayerTitles(playerID) - _ = ptl.GetAllTitles() - - // Set active title - tm.SetPlayerActivePrefix(playerID, TitleIDCitizen) - - // Get formatted name - tm.GetPlayerFormattedName(playerID, fmt.Sprintf("Player%d", playerID)) - - // Revoke title - tm.RevokeTitle(playerID, TitleIDCitizen) - } - }(int32(i)) - } - - wg.Wait() - - // Verify no crashes or data races - stats := tm.GetStatistics() - if stats == nil { - t.Error("Failed to get statistics after concurrent operations") - } -} - -// Test Database Integration -func TestDatabaseIntegration(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping database integration test - requires MySQL database connection") - - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_titles.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open database: %v", err) - } - defer db.Close() - - // Create tables - err = db.CreateTables() - if err != nil { - t.Fatalf("Failed to create tables: %v", err) - } - - // Test master list database operations - mtl := NewMasterTitlesList() - - // Add some test titles - title1 := NewTitle(500, "Database Test Title 1") - title1.SetDescription("Test description 1") - title1.SetCategory(CategoryMiscellaneous) - title1.Position = TitlePositionPrefix // Set as prefix title - mtl.AddTitle(title1) - - title2 := NewTitle(501, "Database Test Title 2") - title2.SetDescription("Test description 2") - title2.SetCategory(CategoryAchievement) - title2.Position = TitlePositionSuffix // Set as suffix title - title2.AchievementID = 2000 - mtl.AddTitle(title2) - - // Save to database - err = mtl.SaveToDatabase(db) - if err != nil { - t.Fatalf("Failed to save master titles to database: %v", err) - } - - // Create new master list and load from database - mtl2 := &MasterTitlesList{ - titles: make(map[int32]*Title), - categorized: make(map[string][]*Title), - bySource: make(map[int32][]*Title), - byRarity: make(map[int32][]*Title), - byAchievement: make(map[uint32]*Title), - nextID: 1, - } - - err = mtl2.LoadFromDatabase(db) - if err != nil { - t.Fatalf("Failed to load master titles from database: %v", err) - } - - // Verify loaded titles - loadedTitle1, exists := mtl2.GetTitle(500) - if !exists || loadedTitle1 == nil { - t.Fatal("Failed to load title 500 from database") - } - if loadedTitle1.Name != "Database Test Title 1" { - t.Errorf("Expected title name 'Database Test Title 1', got '%s'", loadedTitle1.Name) - } - - loadedTitle2, exists := mtl2.GetTitle(501) - if !exists || loadedTitle2 == nil { - t.Fatal("Failed to load title 501 from database") - } - if loadedTitle2.AchievementID != 2000 { - t.Errorf("Expected achievement ID 2000, got %d", loadedTitle2.AchievementID) - } - - // Test player titles database operations - ptl := NewPlayerTitlesList(789, mtl2) // Use mtl2 which has titles loaded from database - ptl.AddTitle(500, 0, 0) - ptl.AddTitle(501, 2000, 0) - ptl.SetActivePrefix(500) - ptl.SetActiveSuffix(501) - - // Save player titles - err = ptl.SaveToDatabase(db) - if err != nil { - t.Fatalf("Failed to save player titles to database: %v", err) - } - - // Load player titles - ptl2 := NewPlayerTitlesList(789, mtl2) - err = ptl2.LoadFromDatabase(db) - if err != nil { - t.Fatalf("Failed to load player titles from database: %v", err) - } - - // Verify loaded player titles - if !ptl2.HasTitle(500) { - t.Error("Expected player to have title 500") - } - if !ptl2.HasTitle(501) { - t.Error("Expected player to have title 501") - } - - - activePrefixTitle, hasPrefix := ptl2.GetActivePrefixTitle() - if !hasPrefix { - t.Error("Expected to have active prefix title") - } else if activePrefixTitle.ID != 500 { - t.Errorf("Expected active prefix 500, got %d", activePrefixTitle.ID) - } - activeSuffixTitle, hasSuffix := ptl2.GetActiveSuffixTitle() - if !hasSuffix { - t.Error("Expected to have active suffix title") - } else if activeSuffixTitle.ID != 501 { - t.Errorf("Expected active suffix 501, got %d", activeSuffixTitle.ID) - } -} - -func TestDatabaseHelperFunctions(t *testing.T) { - // Test nullableUint32 - if val := nullableUint32(0); val != nil { - t.Error("nullableUint32(0) should return nil") - } - - if val := nullableUint32(123); val != uint32(123) { - t.Errorf("nullableUint32(123) should return 123, got %v", val) - } - - // Test nullableTime - zeroTime := time.Time{} - if val := nullableTime(zeroTime); val != nil { - t.Error("nullableTime(zero) should return nil") - } - - now := time.Now() - if val := nullableTime(now); val != now.Unix() { - t.Errorf("nullableTime(now) should return Unix timestamp, got %v", val) - } -} - -// Benchmark tests -func BenchmarkTitleCreation(b *testing.B) { - for i := 0; i < b.N; i++ { - title := NewTitle(int32(i), fmt.Sprintf("Title %d", i)) - title.SetDescription("Description") - title.SetCategory(CategoryMiscellaneous) - } -} - -func BenchmarkMasterListAdd(b *testing.B) { - mtl := NewMasterTitlesList() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - title := NewTitle(int32(i+1000), fmt.Sprintf("Title %d", i)) - mtl.AddTitle(title) - } -} - -func BenchmarkPlayerListAdd(b *testing.B) { - mtl := NewMasterTitlesList() - - // Pre-populate master list - for i := 0; i < 1000; i++ { - title := NewTitle(int32(i), fmt.Sprintf("Title %d", i)) - mtl.AddTitle(title) - } - - ptl := NewPlayerTitlesList(1, mtl) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - titleID := int32(i % 1000) - ptl.AddTitle(titleID, 0, 0) - ptl.RemoveTitle(titleID) - } -} - -func BenchmarkTitleManagerGrant(b *testing.B) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Pre-create titles - for i := 0; i < 100; i++ { - tm.CreateTitle(fmt.Sprintf("Title %d", i), "Description", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon) - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - playerID := int32(i % 100) - titleID := int32((i % 100) + 1) - tm.GrantTitle(playerID, titleID, 0, 0) - } -} - -func BenchmarkTitleSearch(b *testing.B) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Create fewer titles for faster benchmark - for i := 0; i < 100; i++ { - name := fmt.Sprintf("Title %d", i) - if i%10 == 0 { - name = fmt.Sprintf("Dragon %d", i) - } - tm.CreateTitle(name, "Description", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon) - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - tm.SearchTitles("dragon") - } -} - -func BenchmarkConcurrentAccess(b *testing.B) { - tm := NewTitleManager() - defer tm.Shutdown() - - // Pre-populate with titles - for i := 0; i < 10; i++ { - tm.CreateTitle(fmt.Sprintf("Title %d", i), "Description", CategoryMiscellaneous, TitlePositionSuffix, TitleSourceQuest, TitleRarityCommon) - } - - b.ResetTimer() - - // Use simpler sequential approach instead of RunParallel to avoid deadlocks - for i := 0; i < b.N; i++ { - playerID := int32(i % 10) - titleID := int32((i % 10) + 1) - tm.GrantTitle(playerID, titleID, 0, 0) - tm.GetPlayerFormattedName(playerID, "Player") - } -} \ No newline at end of file diff --git a/internal/trade/trade_test.go b/internal/trade/trade_test.go deleted file mode 100644 index 1eb694d..0000000 --- a/internal/trade/trade_test.go +++ /dev/null @@ -1,1394 +0,0 @@ -package trade - -import ( - "sync" - "testing" - "time" -) - -// Test coin calculation utilities -func TestCalculateCoins(t *testing.T) { - tests := []struct { - name string - totalCopper int64 - expectedPt int32 - expectedGold int32 - expectedSilv int32 - expectedCopp int32 - }{ - {"Zero coins", 0, 0, 0, 0, 0}, - {"Only copper", 50, 0, 0, 0, 50}, - {"Only silver", 500, 0, 0, 5, 0}, - {"Only gold", 50000, 0, 5, 0, 0}, - {"Only platinum", 5000000, 5, 0, 0, 0}, - {"Mixed coins", 1234567, 1, 23, 45, 67}, - {"Large amount", 9999999999, 9999, 99, 99, 99}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := CalculateCoins(tt.totalCopper) - if result.Platinum != tt.expectedPt { - t.Errorf("Expected platinum %d, got %d", tt.expectedPt, result.Platinum) - } - if result.Gold != tt.expectedGold { - t.Errorf("Expected gold %d, got %d", tt.expectedGold, result.Gold) - } - if result.Silver != tt.expectedSilv { - t.Errorf("Expected silver %d, got %d", tt.expectedSilv, result.Silver) - } - if result.Copper != tt.expectedCopp { - t.Errorf("Expected copper %d, got %d", tt.expectedCopp, result.Copper) - } - }) - } -} - -func TestCoinsToCopper(t *testing.T) { - coins := CoinAmounts{ - Platinum: 1, - Gold: 23, - Silver: 45, - Copper: 67, - } - expected := int64(1234567) - result := CoinsToCopper(coins) - if result != expected { - t.Errorf("Expected %d copper, got %d", expected, result) - } -} - -func TestFormatCoins(t *testing.T) { - tests := []struct { - name string - copper int64 - expected string - }{ - {"Zero coins", 0, "0 copper"}, - {"Only copper", 50, "50 copper"}, - {"Only silver", 500, "5 silver"}, - {"Mixed coins", 1234567, "1 platinum, 23 gold, 45 silver, 67 copper"}, - {"No copper", 1230000, "1 platinum, 23 gold"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := FormatCoins(tt.copper) - if result != tt.expected { - t.Errorf("Expected '%s', got '%s'", tt.expected, result) - } - }) - } -} - -// Test validation utilities -func TestValidateTradeSlot(t *testing.T) { - tests := []struct { - name string - slot int8 - maxSlots int8 - expected bool - }{ - {"Valid slot 0", 0, 12, true}, - {"Valid slot middle", 5, 12, true}, - {"Valid slot max-1", 11, 12, true}, - {"Invalid negative", -1, 12, false}, - {"Invalid too high", 12, 12, false}, - {"Invalid way too high", 100, 12, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ValidateTradeSlot(tt.slot, tt.maxSlots) - if result != tt.expected { - t.Errorf("Expected %v, got %v", tt.expected, result) - } - }) - } -} - -func TestValidateTradeQuantity(t *testing.T) { - tests := []struct { - name string - quantity int32 - available int32 - expected bool - }{ - {"Valid quantity", 5, 10, true}, - {"Valid exact match", 10, 10, true}, - {"Invalid zero", 0, 10, false}, - {"Invalid negative", -1, 10, false}, - {"Invalid too much", 11, 10, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ValidateTradeQuantity(tt.quantity, tt.available) - if result != tt.expected { - t.Errorf("Expected %v, got %v", tt.expected, result) - } - }) - } -} - -func TestFormatTradeError(t *testing.T) { - tests := []struct { - name string - code int32 - expected string - }{ - {"Success", TradeResultSuccess, "Success"}, - {"Already in trade", TradeResultAlreadyInTrade, "Item is already in the trade"}, - {"No trade", TradeResultNoTrade, "Item cannot be traded"}, - {"Heirloom", TradeResultHeirloom, "Heirloom item cannot be traded to this player"}, - {"Invalid slot", TradeResultInvalidSlot, "Invalid or occupied trade slot"}, - {"Slot out of range", TradeResultSlotOutOfRange, "Trade slot is out of range"}, - {"Insufficient qty", TradeResultInsufficientQty, "Insufficient quantity to trade"}, - {"Unknown error", 999, "Unknown trade error: 999"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := FormatTradeError(tt.code) - if result != tt.expected { - t.Errorf("Expected '%s', got '%s'", tt.expected, result) - } - }) - } -} - -func TestGetClientMaxSlots(t *testing.T) { - tests := []struct { - name string - version int32 - expected int8 - }{ - {"Very old client", 500, TradeMaxSlotsLegacy}, - {"Legacy client", 561, TradeMaxSlotsLegacy}, - {"Modern client", 562, TradeMaxSlotsDefault}, - {"New client", 1000, TradeMaxSlotsDefault}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := GetClientMaxSlots(tt.version) - if result != tt.expected { - t.Errorf("Expected %d slots, got %d", tt.expected, result) - } - }) - } -} - -func TestIsValidTradeState(t *testing.T) { - tests := []struct { - name string - state TradeState - operation string - expected bool - }{ - {"Add item active", TradeStateActive, "add_item", true}, - {"Add item completed", TradeStateCompleted, "add_item", false}, - {"Cancel active", TradeStateActive, "cancel", true}, - {"Complete accepted", TradeStateAccepted, "complete", true}, - {"Complete active", TradeStateActive, "complete", false}, - {"Invalid operation", TradeStateActive, "invalid", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := IsValidTradeState(tt.state, tt.operation) - if result != tt.expected { - t.Errorf("Expected %v, got %v", tt.expected, result) - } - }) - } -} - -// Test TradeValidationError -func TestTradeValidationError(t *testing.T) { - err := &TradeValidationError{ - Code: TradeResultNoTrade, - Message: "Test error message", - } - - if err.Error() != "Test error message" { - t.Errorf("Expected 'Test error message', got '%s'", err.Error()) - } -} - -// Test placeholder implementations -func TestPlaceholderEntity(t *testing.T) { - entity := &PlaceholderEntity{ - ID: 123, - Name: "Test Entity", - IsPlayerFlag: true, - IsBotFlag: false, - CoinsAmount: 10000, - ClientVer: 800, - } - - if entity.GetID() != 123 { - t.Errorf("Expected ID 123, got %d", entity.GetID()) - } - - if entity.GetName() != "Test Entity" { - t.Errorf("Expected name 'Test Entity', got '%s'", entity.GetName()) - } - - if !entity.IsPlayer() { - t.Error("Expected entity to be a player") - } - - if entity.IsBot() { - t.Error("Expected entity not to be a bot") - } - - if !entity.HasCoins(5000) { - t.Error("Expected entity to have enough coins") - } - - if entity.HasCoins(15000) { - t.Error("Expected entity not to have enough coins") - } - - if entity.GetClientVersion() != 800 { - t.Errorf("Expected client version 800, got %d", entity.GetClientVersion()) - } - - // Test default name - entityNoName := &PlaceholderEntity{ID: 456} - expectedName := "Entity_456" - if entityNoName.GetName() != expectedName { - t.Errorf("Expected default name '%s', got '%s'", expectedName, entityNoName.GetName()) - } - - // Test default client version - entityNoVersion := &PlaceholderEntity{ID: 789} - if entityNoVersion.GetClientVersion() != 1000 { - t.Errorf("Expected default client version 1000, got %d", entityNoVersion.GetClientVersion()) - } -} - -func TestPlaceholderItem(t *testing.T) { - creationTime := time.Now() - item := &PlaceholderItem{ - ID: 456, - Name: "Test Item", - Quantity: 10, - IconID: 789, - NoTradeFlag: false, - HeirloomFlag: true, - AttunedFlag: false, - CreatedTime: creationTime, - GroupIDs: []int32{1, 2, 3}, - } - - if item.GetID() != 456 { - t.Errorf("Expected ID 456, got %d", item.GetID()) - } - - if item.GetName() != "Test Item" { - t.Errorf("Expected name 'Test Item', got '%s'", item.GetName()) - } - - if item.GetQuantity() != 10 { - t.Errorf("Expected quantity 10, got %d", item.GetQuantity()) - } - - if item.GetIcon(1000) != 789 { - t.Errorf("Expected icon ID 789, got %d", item.GetIcon(1000)) - } - - if item.IsNoTrade() { - t.Error("Expected item not to be no-trade") - } - - if !item.IsHeirloom() { - t.Error("Expected item to be heirloom") - } - - if item.IsAttuned() { - t.Error("Expected item not to be attuned") - } - - if !item.GetCreationTime().Equal(creationTime) { - t.Error("Expected creation time to match") - } - - groupIDs := item.GetGroupCharacterIDs() - if len(groupIDs) != 3 || groupIDs[0] != 1 || groupIDs[1] != 2 || groupIDs[2] != 3 { - t.Errorf("Expected group IDs [1,2,3], got %v", groupIDs) - } - - // Test default name - itemNoName := &PlaceholderItem{ID: 999} - expectedName := "Item_999" - if itemNoName.GetName() != expectedName { - t.Errorf("Expected default name '%s', got '%s'", expectedName, itemNoName.GetName()) - } -} - -// Test TradeParticipant -func TestNewTradeParticipant(t *testing.T) { - // Test legacy client - participant := NewTradeParticipant(123, false, 561) - if participant.EntityID != 123 { - t.Errorf("Expected entity ID 123, got %d", participant.EntityID) - } - if participant.IsBot != false { - t.Error("Expected participant not to be a bot") - } - if participant.MaxSlots != TradeMaxSlotsLegacy { - t.Errorf("Expected %d max slots, got %d", TradeMaxSlotsLegacy, participant.MaxSlots) - } - if participant.ClientVersion != 561 { - t.Errorf("Expected client version 561, got %d", participant.ClientVersion) - } - if participant.HasAccepted { - t.Error("Expected participant not to have accepted initially") - } - if len(participant.Items) != 0 { - t.Error("Expected empty items map initially") - } - if participant.Coins != 0 { - t.Error("Expected zero coins initially") - } - - // Test modern client - participant2 := NewTradeParticipant(456, true, 1000) - if participant2.MaxSlots != TradeMaxSlotsDefault { - t.Errorf("Expected %d max slots, got %d", TradeMaxSlotsDefault, participant2.MaxSlots) - } - if participant2.IsBot != true { - t.Error("Expected participant to be a bot") - } -} - -func TestTradeParticipantMethods(t *testing.T) { - participant := NewTradeParticipant(123, false, 1000) - - // Test GetNextFreeSlot - slot := participant.GetNextFreeSlot() - if slot != 0 { - t.Errorf("Expected first free slot to be 0, got %d", slot) - } - - // Add an item and test slot finding - testItem := &PlaceholderItem{ID: 100, Quantity: 5} - participant.Items[0] = TradeItemInfo{Item: testItem, Quantity: 5} - - slot = participant.GetNextFreeSlot() - if slot != 1 { - t.Errorf("Expected next free slot to be 1, got %d", slot) - } - - // Fill all slots - for i := int8(1); i < participant.MaxSlots; i++ { - participant.Items[i] = TradeItemInfo{Item: testItem, Quantity: 1} - } - - slot = participant.GetNextFreeSlot() - if slot != int8(TradeSlotAutoFind) { - t.Errorf("Expected no free slots (%d), got %d", int8(TradeSlotAutoFind), slot) - } - - // Test HasItem - if !participant.HasItem(100) { - t.Error("Expected participant to have item 100") - } - if participant.HasItem(999) { - t.Error("Expected participant not to have item 999") - } - - // Test GetItemCount - expectedCount := int(participant.MaxSlots) - if participant.GetItemCount() != expectedCount { - t.Errorf("Expected %d items, got %d", expectedCount, participant.GetItemCount()) - } - - // Test ClearItems - participant.ClearItems() - if participant.GetItemCount() != 0 { - t.Error("Expected no items after clear") - } - - // Test GetCoinAmounts - participant.Coins = 1234567 - coinAmounts := participant.GetCoinAmounts() - expected := CalculateCoins(1234567) - if coinAmounts != expected { - t.Errorf("Expected coin amounts %+v, got %+v", expected, coinAmounts) - } -} - -// Test TradeManager -func TestTradeManager(t *testing.T) { - tm := NewTradeManager() - - if tm.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades initially") - } - - // Create test entities - entity1 := &PlaceholderEntity{ID: 100, Name: "Player1"} - entity2 := &PlaceholderEntity{ID: 200, Name: "Player2"} - - // Create a trade - trade := NewTrade(entity1, entity2) - if trade == nil { - t.Fatal("Failed to create trade") - } - - // Add trade to manager - tm.AddTrade(trade) - - if tm.GetActiveTradeCount() != 1 { - t.Error("Expected 1 active trade") - } - - // Test GetTrade for trader1 - retrievedTrade := tm.GetTrade(100) - if retrievedTrade == nil { - t.Error("Expected to find trade for entity 100") - } - if retrievedTrade.GetTrader1ID() != 100 { - t.Error("Expected trade with trader1 ID 100") - } - - // Test GetTrade for trader2 - retrievedTrade = tm.GetTrade(200) - if retrievedTrade == nil { - t.Error("Expected to find trade for entity 200") - } - if retrievedTrade.GetTrader2ID() != 200 { - t.Error("Expected trade with trader2 ID 200") - } - - // Test GetTrade for non-participant - retrievedTrade = tm.GetTrade(999) - if retrievedTrade != nil { - t.Error("Expected no trade for entity 999") - } - - // Remove trade - tm.RemoveTrade(100) - if tm.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades after removal") - } - - retrievedTrade = tm.GetTrade(100) - if retrievedTrade != nil { - t.Error("Expected no trade after removal") - } -} - -// Test Trade creation and basic operations -func TestNewTrade(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100, Name: "Player1"} - entity2 := &PlaceholderEntity{ID: 200, Name: "Player2"} - - // Valid trade creation - trade := NewTrade(entity1, entity2) - if trade == nil { - t.Fatal("Expected trade to be created") - } - - if trade.GetTrader1ID() != 100 { - t.Errorf("Expected trader1 ID 100, got %d", trade.GetTrader1ID()) - } - - if trade.GetTrader2ID() != 200 { - t.Errorf("Expected trader2 ID 200, got %d", trade.GetTrader2ID()) - } - - if trade.GetState() != TradeStateActive { - t.Errorf("Expected trade state %d, got %d", TradeStateActive, trade.GetState()) - } - - // Test GetTradee - if trade.GetTradee(100) != 200 { - t.Error("Expected tradee of 100 to be 200") - } - if trade.GetTradee(200) != 100 { - t.Error("Expected tradee of 200 to be 100") - } - if trade.GetTradee(999) != 0 { - t.Error("Expected tradee of 999 to be 0") - } - - // Test GetParticipant - participant := trade.GetParticipant(100) - if participant == nil { - t.Error("Expected to find participant 100") - } - if participant.EntityID != 100 { - t.Error("Expected participant entity ID 100") - } - - participant = trade.GetParticipant(999) - if participant != nil { - t.Error("Expected not to find participant 999") - } - - // Invalid trade creation - invalidTrade := NewTrade(nil, entity2) - if invalidTrade != nil { - t.Error("Expected trade creation to fail with nil entity") - } - - invalidTrade = NewTrade(entity1, nil) - if invalidTrade != nil { - t.Error("Expected trade creation to fail with nil entity") - } -} - -func TestTradeAddRemoveItems(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - testItem := &PlaceholderItem{ - ID: 500, - Name: "Test Item", - Quantity: 10, - } - - // Test adding item - err := trade.AddItemToTrade(100, testItem, 5, 0) - if err != nil { - t.Fatalf("Failed to add item to trade: %v", err) - } - - // Verify item was added - retrievedItem := trade.GetTraderSlot(100, 0) - if retrievedItem == nil { - t.Error("Expected to find item in slot 0") - } - if retrievedItem.GetID() != 500 { - t.Error("Expected item ID 500") - } - - // Test auto-slot finding - testItem2 := &PlaceholderItem{ID: 501, Quantity: 3} - err = trade.AddItemToTrade(100, testItem2, 3, TradeSlotAutoFind) - if err != nil { - t.Fatalf("Failed to add item with auto-slot: %v", err) - } - - retrievedItem = trade.GetTraderSlot(100, 1) - if retrievedItem == nil { - t.Error("Expected to find item in slot 1") - } - - // Test adding to occupied slot - err = trade.AddItemToTrade(100, testItem, 1, 0) - if err == nil { - t.Error("Expected error when adding to occupied slot") - } - - // Test adding duplicate item - err = trade.AddItemToTrade(100, testItem, 1, 2) - if err == nil { - t.Error("Expected error when adding duplicate item") - } - - // Test invalid slot - err = trade.AddItemToTrade(100, testItem, 1, 99) - if err == nil { - t.Error("Expected error for invalid slot") - } - - // Test insufficient quantity - err = trade.AddItemToTrade(100, testItem, 20, 3) - if err == nil { - t.Error("Expected error for insufficient quantity") - } - - // Test invalid entity - err = trade.AddItemToTrade(999, testItem, 1, 4) - if err == nil { - t.Error("Expected error for invalid entity") - } - - // Test removing item - err = trade.RemoveItemFromTrade(100, 0) - if err != nil { - t.Fatalf("Failed to remove item: %v", err) - } - - retrievedItem = trade.GetTraderSlot(100, 0) - if retrievedItem != nil { - t.Error("Expected item to be removed from slot 0") - } - - // Test removing from empty slot - err = trade.RemoveItemFromTrade(100, 0) - if err == nil { - t.Error("Expected error when removing from empty slot") - } - - // Test no-trade item - noTradeItem := &PlaceholderItem{ - ID: 600, - Quantity: 5, - NoTradeFlag: true, - } - err = trade.AddItemToTrade(100, noTradeItem, 5, 0) - if err == nil { - t.Error("Expected error for no-trade item") - } - - // Test attuned heirloom item - attunedHeirloom := &PlaceholderItem{ - ID: 700, - Quantity: 1, - HeirloomFlag: true, - AttunedFlag: true, - } - err = trade.AddItemToTrade(100, attunedHeirloom, 1, 0) - if err == nil { - t.Error("Expected error for attuned heirloom item") - } - - // Test expired heirloom item - expiredHeirloom := &PlaceholderItem{ - ID: 701, - Quantity: 1, - HeirloomFlag: true, - AttunedFlag: false, - CreatedTime: time.Now().Add(-72 * time.Hour), // 3 days ago - } - err = trade.AddItemToTrade(100, expiredHeirloom, 1, 0) - if err == nil { - t.Error("Expected error for expired heirloom item") - } - - // Test valid recent heirloom item - recentHeirloom := &PlaceholderItem{ - ID: 702, - Quantity: 1, - HeirloomFlag: true, - AttunedFlag: false, - CreatedTime: time.Now().Add(-1 * time.Hour), // 1 hour ago - } - err = trade.AddItemToTrade(100, recentHeirloom, 1, 0) - if err != nil { - t.Errorf("Expected recent heirloom item to be tradeable: %v", err) - } -} - -func TestTradeCoins(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - // Test adding coins - err := trade.AddCoinsToTrade(100, 5000) - if err != nil { - t.Fatalf("Failed to add coins to trade: %v", err) - } - - participant := trade.GetParticipant(100) - if participant.Coins != 5000 { - t.Errorf("Expected 5000 coins, got %d", participant.Coins) - } - - // Test adding more coins - err = trade.AddCoinsToTrade(100, 2000) - if err != nil { - t.Fatalf("Failed to add more coins: %v", err) - } - - if participant.Coins != 7000 { - t.Errorf("Expected 7000 coins, got %d", participant.Coins) - } - - // Test removing coins - err = trade.RemoveCoinsFromTrade(100, 3000) - if err != nil { - t.Fatalf("Failed to remove coins: %v", err) - } - - if participant.Coins != 4000 { - t.Errorf("Expected 4000 coins, got %d", participant.Coins) - } - - // Test removing more coins than available - err = trade.RemoveCoinsFromTrade(100, 10000) - if err != nil { - t.Fatalf("Failed to remove excess coins: %v", err) - } - - if participant.Coins != 0 { - t.Errorf("Expected 0 coins, got %d", participant.Coins) - } - - // Test invalid coin amount - err = trade.AddCoinsToTrade(100, -100) - if err == nil { - t.Error("Expected error for negative coin amount") - } - - err = trade.AddCoinsToTrade(100, 0) - if err == nil { - t.Error("Expected error for zero coin amount") - } - - // Test invalid entity - err = trade.AddCoinsToTrade(999, 1000) - if err == nil { - t.Error("Expected error for invalid entity") - } -} - -func TestTradeAcceptance(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - // Initially, neither should have accepted - if trade.HasAcceptedTrade(100) { - t.Error("Expected trader1 not to have accepted initially") - } - if trade.HasAcceptedTrade(200) { - t.Error("Expected trader2 not to have accepted initially") - } - if trade.HasAcceptedTrade(999) { - t.Error("Expected invalid entity not to have accepted") - } - - // First trader accepts - completed, err := trade.SetTradeAccepted(100) - if err != nil { - t.Fatalf("Failed to set trade accepted: %v", err) - } - if completed { - t.Error("Expected trade not to be completed after one acceptance") - } - - if !trade.HasAcceptedTrade(100) { - t.Error("Expected trader1 to have accepted") - } - if trade.HasAcceptedTrade(200) { - t.Error("Expected trader2 not to have accepted yet") - } - - // Second trader accepts - should complete - completed, err = trade.SetTradeAccepted(200) - if err != nil { - t.Fatalf("Failed to set second trade accepted: %v", err) - } - if !completed { - t.Error("Expected trade to be completed after both acceptances") - } - - if trade.GetState() != TradeStateCompleted { - t.Errorf("Expected trade state %d, got %d", TradeStateCompleted, trade.GetState()) - } - - // Test accepting invalid entity - _, err = trade.SetTradeAccepted(999) - if err == nil { - t.Error("Expected error for invalid entity acceptance") - } -} - -func TestTradeCancel(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - // Cancel trade - err := trade.CancelTrade(100) - if err != nil { - t.Fatalf("Failed to cancel trade: %v", err) - } - - if trade.GetState() != TradeStateCanceled { - t.Errorf("Expected trade state %d, got %d", TradeStateCanceled, trade.GetState()) - } - - // Test operations on canceled trade - testItem := &PlaceholderItem{ID: 500, Quantity: 5} - err = trade.AddItemToTrade(100, testItem, 5, 0) - if err == nil { - t.Error("Expected error adding item to canceled trade") - } - - err = trade.AddCoinsToTrade(100, 1000) - if err == nil { - t.Error("Expected error adding coins to canceled trade") - } - - _, err = trade.SetTradeAccepted(100) - if err == nil { - t.Error("Expected error accepting canceled trade") - } -} - -func TestTradeStateChangesResetAcceptance(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - // Both traders accept - trade.SetTradeAccepted(100) - trade.SetTradeAccepted(200) // This completes the trade - - // Create new trade for testing reset behavior - trade2 := NewTrade(entity1, entity2) - - // First trader accepts - trade2.SetTradeAccepted(100) - if !trade2.HasAcceptedTrade(100) { - t.Error("Expected trader1 to have accepted") - } - - // Add item - should reset acceptance - testItem := &PlaceholderItem{ID: 500, Quantity: 5} - err := trade2.AddItemToTrade(100, testItem, 5, 0) - if err != nil { - t.Fatalf("Failed to add item: %v", err) - } - - if trade2.HasAcceptedTrade(100) { - t.Error("Expected acceptance to be reset after adding item") - } - - // Accept again and add coins - should reset - trade2.SetTradeAccepted(100) - err = trade2.AddCoinsToTrade(100, 1000) - if err != nil { - t.Fatalf("Failed to add coins: %v", err) - } - - if trade2.HasAcceptedTrade(100) { - t.Error("Expected acceptance to be reset after adding coins") - } - - // Accept again and remove item - should reset - trade2.SetTradeAccepted(100) - err = trade2.RemoveItemFromTrade(100, 0) - if err != nil { - t.Fatalf("Failed to remove item: %v", err) - } - - if trade2.HasAcceptedTrade(100) { - t.Error("Expected acceptance to be reset after removing item") - } - - // Accept again and remove coins - should reset - trade2.SetTradeAccepted(100) - err = trade2.RemoveCoinsFromTrade(100, 500) - if err != nil { - t.Fatalf("Failed to remove coins: %v", err) - } - - if trade2.HasAcceptedTrade(100) { - t.Error("Expected acceptance to be reset after removing coins") - } -} - -func TestGetTradeInfo(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - // Add some items and coins - testItem := &PlaceholderItem{ID: 500, Quantity: 5} - trade.AddItemToTrade(100, testItem, 5, 0) - trade.AddCoinsToTrade(200, 10000) - - info := trade.GetTradeInfo() - - if info["state"] != TradeStateActive { - t.Error("Expected trade state to be active") - } - if info["trader1_id"] != int32(100) { - t.Error("Expected trader1_id to be 100") - } - if info["trader2_id"] != int32(200) { - t.Error("Expected trader2_id to be 200") - } - if info["trader1_items"] != 1 { - t.Error("Expected trader1 to have 1 item") - } - if info["trader2_items"] != 0 { - t.Error("Expected trader2 to have 0 items") - } - if info["trader1_coins"] != int64(0) { - t.Error("Expected trader1 to have 0 coins") - } - if info["trader2_coins"] != int64(10000) { - t.Error("Expected trader2 to have 10000 coins") - } - if info["trader1_accepted"] != false { - t.Error("Expected trader1 not to have accepted") - } - if info["trader2_accepted"] != false { - t.Error("Expected trader2 not to have accepted") - } - - // Test after acceptance - trade.SetTradeAccepted(100) - info = trade.GetTradeInfo() - if info["trader1_accepted"] != true { - t.Error("Expected trader1 to have accepted") - } -} - -// Test TradeService -func TestTradeService(t *testing.T) { - service := NewTradeService() - - if service.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades initially") - } - - // Test InitiateTrade - trade, err := service.InitiateTrade(100, 200) - if err != nil { - t.Fatalf("Failed to initiate trade: %v", err) - } - if trade == nil { - t.Fatal("Expected trade to be created") - } - - if service.GetActiveTradeCount() != 1 { - t.Error("Expected 1 active trade") - } - - // Test duplicate trade initiation - _, err = service.InitiateTrade(100, 300) - if err == nil { - t.Error("Expected error when initiating duplicate trade") - } - - _, err = service.InitiateTrade(300, 200) - if err == nil { - t.Error("Expected error when target is already in trade") - } - - // Test GetTrade - retrievedTrade := service.GetTrade(100) - if retrievedTrade == nil { - t.Error("Expected to find trade") - } - - // Test service operations - testItem := &PlaceholderItem{ID: 500, Quantity: 10} - err = service.AddItemToTrade(100, testItem, 5, 0) - if err != nil { - t.Fatalf("Failed to add item via service: %v", err) - } - - err = service.AddCoinsToTrade(200, 5000) - if err != nil { - t.Fatalf("Failed to add coins via service: %v", err) - } - - // Test GetTradeInfo - info, err := service.GetTradeInfo(100) - if err != nil { - t.Fatalf("Failed to get trade info: %v", err) - } - if info["trader1_items"] != 1 { - t.Error("Expected 1 item in trade info") - } - - // Test AcceptTrade - completed, err := service.AcceptTrade(100) - if err != nil { - t.Fatalf("Failed to accept trade: %v", err) - } - if completed { - t.Error("Expected trade not to be completed yet") - } - - completed, err = service.AcceptTrade(200) - if err != nil { - t.Fatalf("Failed to accept trade for second trader: %v", err) - } - if !completed { - t.Error("Expected trade to be completed") - } - - if service.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades after completion") - } -} - -func TestTradeServiceValidation(t *testing.T) { - service := NewTradeService() - - // Test ValidateTradeRequest - err := service.ValidateTradeRequest(100, 100) - if err == nil { - t.Error("Expected error for self-trade") - } - - err = service.ValidateTradeRequest(0, 100) - if err == nil { - t.Error("Expected error for invalid initiator ID") - } - - err = service.ValidateTradeRequest(100, -1) - if err == nil { - t.Error("Expected error for invalid target ID") - } - - // Valid request - err = service.ValidateTradeRequest(100, 200) - if err != nil { - t.Errorf("Expected valid trade request: %v", err) - } - - // After creating trade, validation should fail - service.InitiateTrade(100, 200) - err = service.ValidateTradeRequest(100, 300) - if err == nil { - t.Error("Expected error for already trading initiator") - } - - err = service.ValidateTradeRequest(300, 200) - if err == nil { - t.Error("Expected error for already trading target") - } -} - -func TestTradeServiceCancel(t *testing.T) { - service := NewTradeService() - - trade, _ := service.InitiateTrade(100, 200) - if trade == nil { - t.Fatal("Failed to create trade") - } - - err := service.CancelTrade(100) - if err != nil { - t.Fatalf("Failed to cancel trade: %v", err) - } - - if service.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades after cancellation") - } - - // Test canceling non-existent trade - err = service.CancelTrade(999) - if err == nil { - t.Error("Expected error canceling non-existent trade") - } -} - -func TestTradeServiceAdminFunctions(t *testing.T) { - service := NewTradeService() - - trade, _ := service.InitiateTrade(100, 200) - if trade == nil { - t.Fatal("Failed to create trade") - } - - // Test ForceCompleteTrade - err := service.ForceCompleteTrade(100) - if err != nil { - t.Fatalf("Failed to force complete trade: %v", err) - } - - if service.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades after forced completion") - } - - // Test ForceCancelTrade - trade2, _ := service.InitiateTrade(300, 400) - if trade2 == nil { - t.Fatal("Failed to create second trade") - } - - err = service.ForceCancelTrade(300, "Admin intervention") - if err != nil { - t.Fatalf("Failed to force cancel trade: %v", err) - } - - if service.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades after forced cancellation") - } -} - -func TestTradeServiceStatistics(t *testing.T) { - service := NewTradeService() - - stats := service.GetTradeStatistics() - if stats["active_trades"] != 0 { - t.Error("Expected 0 active trades in statistics") - } - - if stats["max_trade_duration_minutes"] != float64(30) { - t.Error("Expected 30 minute max duration") - } - - // Create some trades - service.InitiateTrade(100, 200) - service.InitiateTrade(300, 400) - - stats = service.GetTradeStatistics() - if stats["active_trades"] != 2 { - t.Error("Expected 2 active trades in statistics") - } -} - -func TestTradeServiceShutdown(t *testing.T) { - service := NewTradeService() - - service.InitiateTrade(100, 200) - service.InitiateTrade(300, 400) - - if service.GetActiveTradeCount() != 2 { - t.Fatal("Expected 2 active trades before shutdown") - } - - service.Shutdown() - - if service.GetActiveTradeCount() != 0 { - t.Error("Expected no active trades after shutdown") - } -} - -// Test concurrent access -func TestTradeConcurrency(t *testing.T) { - service := NewTradeService() - trade, _ := service.InitiateTrade(100, 200) - - var wg sync.WaitGroup - numGoroutines := 10 - numOperations := 100 - - // Test concurrent coin operations - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - for j := 0; j < numOperations; j++ { - if j%2 == 0 { - trade.AddCoinsToTrade(100, 100) - } else { - trade.RemoveCoinsFromTrade(100, 50) - } - } - }(i) - } - - wg.Wait() - - // Verify trade is still in valid state - if trade.GetState() != TradeStateActive { - t.Error("Expected trade to remain active after concurrent operations") - } - - info := trade.GetTradeInfo() - if info == nil { - t.Error("Expected to get trade info after concurrent operations") - } -} - -// Test utility functions -func TestCompareTradeItems(t *testing.T) { - item1 := &PlaceholderItem{ID: 100} - item2 := &PlaceholderItem{ID: 200} - - tradeItem1 := TradeItemInfo{Item: item1, Quantity: 5} - tradeItem2 := TradeItemInfo{Item: item1, Quantity: 5} - tradeItem3 := TradeItemInfo{Item: item2, Quantity: 5} - tradeItem4 := TradeItemInfo{Item: item1, Quantity: 10} - - // Test equal items - if !CompareTradeItems(tradeItem1, tradeItem2) { - t.Error("Expected items to be equal") - } - - // Test different items - if CompareTradeItems(tradeItem1, tradeItem3) { - t.Error("Expected items to be different (different item)") - } - - // Test different quantities - if CompareTradeItems(tradeItem1, tradeItem4) { - t.Error("Expected items to be different (different quantity)") - } - - // Test nil items - nilItem1 := TradeItemInfo{Item: nil, Quantity: 5} - nilItem2 := TradeItemInfo{Item: nil, Quantity: 5} - nilItem3 := TradeItemInfo{Item: nil, Quantity: 10} - - if !CompareTradeItems(nilItem1, nilItem2) { - t.Error("Expected nil items with same quantity to be equal") - } - - if CompareTradeItems(nilItem1, nilItem3) { - t.Error("Expected nil items with different quantity to be different") - } - - if CompareTradeItems(nilItem1, tradeItem1) { - t.Error("Expected nil item and real item to be different") - } -} - -func TestCalculateTradeValue(t *testing.T) { - participant := NewTradeParticipant(123, false, 1000) - participant.Coins = 50000 - - item1 := &PlaceholderItem{ID: 100, Name: "Sword"} - item2 := &PlaceholderItem{ID: 200, Name: "Shield"} - - participant.Items[0] = TradeItemInfo{Item: item1, Quantity: 1} - participant.Items[1] = TradeItemInfo{Item: item2, Quantity: 2} - - value := CalculateTradeValue(participant) - - if value["coins"] != int64(50000) { - t.Error("Expected coins value to be 50000") - } - - coinsFormatted, ok := value["coins_formatted"].(string) - if !ok || coinsFormatted != "5 gold" { - t.Errorf("Expected coins_formatted to be '5 gold', got %v", coinsFormatted) - } - - if value["item_count"] != 2 { - t.Error("Expected item count to be 2") - } - - items, ok := value["items"].([]map[string]any) - if !ok || len(items) != 2 { - t.Error("Expected items array with 2 elements") - } - - // Verify item data (order may vary due to map iteration) - foundSword := false - foundShield := false - for _, item := range items { - if item["item_name"] == "Sword" { - foundSword = true - if item["quantity"] != int32(1) { - t.Error("Expected sword quantity to be 1") - } - } - if item["item_name"] == "Shield" { - foundShield = true - if item["quantity"] != int32(2) { - t.Error("Expected shield quantity to be 2") - } - } - } - - if !foundSword || !foundShield { - t.Error("Expected to find both sword and shield in items") - } -} - -func TestValidateTradeCompletion(t *testing.T) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - // Test validation with no acceptance - errors := ValidateTradeCompletion(trade) - expectedErrors := 2 // Neither trader has accepted - if len(errors) != expectedErrors { - t.Errorf("Expected %d errors, got %d: %v", expectedErrors, len(errors), errors) - } - - // Test validation with one acceptance - trade.SetTradeAccepted(100) - errors = ValidateTradeCompletion(trade) - expectedErrors = 1 // Only trader2 hasn't accepted - if len(errors) != expectedErrors { - t.Errorf("Expected %d errors, got %d: %v", expectedErrors, len(errors), errors) - } - - // Test validation with both acceptances - trade.SetTradeAccepted(200) // This completes the trade - - // Create new trade for testing completed state validation - completedTrade := NewTrade(entity1, entity2) - completedTrade.SetTradeAccepted(100) - completedTrade.SetTradeAccepted(200) // This marks as completed - - errors = ValidateTradeCompletion(completedTrade) - expectedErrors = 1 // Trade is not in active state - if len(errors) != expectedErrors { - t.Errorf("Expected %d errors for completed trade, got %d: %v", expectedErrors, len(errors), errors) - } -} - -func TestGenerateTradeLogEntry(t *testing.T) { - tradeID := "trade_123" - operation := "add_item" - entityID := int32(456) - details := map[string]any{"item_id": 789, "quantity": 5} - - logEntry := GenerateTradeLogEntry(tradeID, operation, entityID, details) - expected := "[Trade:trade_123] add_item by entity 456: map[item_id:789 quantity:5]" - - if logEntry != expected { - t.Errorf("Expected log entry '%s', got '%s'", expected, logEntry) - } -} - -// Benchmark tests -func BenchmarkCalculateCoins(b *testing.B) { - for i := 0; i < b.N; i++ { - CalculateCoins(1234567) - } -} - -func BenchmarkTradeCreation(b *testing.B) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - trade := NewTrade(entity1, entity2) - _ = trade - } -} - -func BenchmarkTradeItemOperations(b *testing.B) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - item := &PlaceholderItem{ID: 500, Quantity: 100} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - slot := int8(i % int(TradeMaxSlotsDefault)) - trade.AddItemToTrade(100, item, 1, slot) - trade.RemoveItemFromTrade(100, slot) - } -} - -func BenchmarkTradeCoinOperations(b *testing.B) { - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - trade := NewTrade(entity1, entity2) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - trade.AddCoinsToTrade(100, 1000) - trade.RemoveCoinsFromTrade(100, 500) - } -} - -func BenchmarkTradeManagerOperations(b *testing.B) { - tm := NewTradeManager() - entity1 := &PlaceholderEntity{ID: 100} - entity2 := &PlaceholderEntity{ID: 200} - - b.ResetTimer() - for i := 0; i < b.N; i++ { - trade := NewTrade(entity1, entity2) - tm.AddTrade(trade) - tm.GetTrade(100) - tm.RemoveTrade(100) - } -} \ No newline at end of file diff --git a/internal/tradeskills/README.md b/internal/tradeskills/README.md deleted file mode 100644 index aae1534..0000000 --- a/internal/tradeskills/README.md +++ /dev/null @@ -1,274 +0,0 @@ -# Tradeskills System - -The tradeskills system provides complete crafting functionality for the EQ2 server. It has been fully converted from the original C++ EQ2EMu implementation to Go. - -## Overview - -The tradeskills system handles: -- **Crafting Sessions**: Player crafting with progress/durability mechanics -- **Tradeskill Events**: Random events requiring player counter-actions -- **Recipe Management**: Component validation and product creation -- **Progress Stages**: Multiple completion stages with different rewards -- **Experience System**: Tradeskill XP and level progression -- **Animation System**: Client-specific animations for different techniques - -## Core Components - -### Files - -- `constants.go` - Animation IDs, technique constants, and configuration values -- `types.go` - Core data structures (TradeskillEvent, Tradeskill, TradeskillManager, etc.) -- `manager.go` - Main TradeskillManager implementation with crafting logic -- `database.go` - Database operations for tradeskill events persistence -- `packets.go` - Packet building for crafting UI and updates -- `interfaces.go` - Integration interfaces and TradeskillSystemAdapter -- `README.md` - This documentation - -### Main Types - -- `TradeskillEvent` - Events that occur during crafting requiring counter-actions -- `Tradeskill` - Individual crafting session with progress tracking -- `TradeskillManager` - Central management of all active crafting sessions -- `MasterTradeskillEventsList` - Registry of all available tradeskill events -- `TradeskillSystemAdapter` - High-level integration with other game systems - -## Tradeskill Techniques - -The system supports all EQ2 tradeskill techniques: - -1. **Alchemy** (510901001) - Potion and reagent creation -2. **Tailoring** (510901002) - Cloth and leather armor -3. **Fletching** (510901003) - Arrows and ranged weapons -4. **Jewelcrafting** (510901004) - Jewelry and accessories -5. **Provisioning** (510901005) - Food and drink -6. **Scribing** (510901007) - Spells and scrolls -7. **Transmuting** (510901008) - Material conversion -8. **Artistry** (510901009) - Decorative items -9. **Carpentry** (510901010) - Wooden items and furniture -10. **Metalworking** (510901011) - Metal weapons and tools -11. **Metalshaping** (510901012) - Metal armor and shields -12. **Stoneworking** (510901013) - Stone items and structures - -## Crafting Process - -### Session Lifecycle - -1. **Validation**: Recipe, components, and crafting table validation -2. **Setup**: Lock inventory items, send UI packets, start session -3. **Processing**: Periodic updates every 4 seconds with progress/durability changes -4. **Events**: Random events with counter opportunities -5. **Completion**: Reward calculation, XP award, cleanup - -### Outcome Types - -- **Critical Success** (1%): +100 progress, +10 durability -- **Success** (87%): +50 progress, -10 durability -- **Failure** (10%): 0 progress, -50 durability -- **Critical Failure** (2%): -50 progress, -100 durability - -### Progress Stages - -- **Stage 0**: 0-399 progress (fuel/byproduct) -- **Stage 1**: 400-599 progress (basic product) -- **Stage 2**: 600-799 progress (improved product) -- **Stage 3**: 800-999 progress (high-quality product) -- **Stage 4**: 1000 progress (masterwork product) - -## Usage - -### Basic Setup - -```go -// Create manager and events list -manager := tradeskills.NewTradeskillManager() -eventsList := tradeskills.NewMasterTradeskillEventsList() - -// Create database service -db, _ := database.Open("tradeskills.db") -dbService := tradeskills.NewSQLiteTradeskillDatabase(db) - -// Load events from database -dbService.LoadTradeskillEvents(eventsList) - -// Create system adapter with all dependencies -adapter := tradeskills.NewTradeskillSystemAdapter( - manager, eventsList, dbService, packetBuilder, - playerManager, itemManager, recipeManager, spellManager, - zoneManager, experienceManager, questManager, ruleManager, -) - -// Initialize the system -adapter.Initialize() -``` - -### Starting Crafting - -```go -// Define components to use -components := []tradeskills.ComponentUsage{ - {ItemUniqueID: 12345, Quantity: 2}, // Primary component - {ItemUniqueID: 67890, Quantity: 1}, // Fuel component -} - -// Start crafting session -err := adapter.StartCrafting(playerID, recipeID, components) -if err != nil { - log.Printf("Failed to start crafting: %v", err) -} -``` - -### Processing Updates - -```go -// Run periodic updates (typically every 50ms) -ticker := time.NewTicker(50 * time.Millisecond) -go func() { - for range ticker.C { - adapter.ProcessCraftingUpdates() - } -}() -``` - -### Handling Events - -```go -// Player attempts to counter an event -err := adapter.HandleEventCounter(playerID, spellIcon) -if err != nil { - log.Printf("Failed to handle event counter: %v", err) -} -``` - -## Database Schema - -### tradeskillevents Table - -```sql -CREATE TABLE tradeskillevents ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - icon INTEGER NOT NULL, - technique INTEGER NOT NULL, - success_progress INTEGER NOT NULL DEFAULT 0, - success_durability INTEGER NOT NULL DEFAULT 0, - success_hp INTEGER NOT NULL DEFAULT 0, - success_power INTEGER NOT NULL DEFAULT 0, - success_spell_id INTEGER NOT NULL DEFAULT 0, - success_item_id INTEGER NOT NULL DEFAULT 0, - fail_progress INTEGER NOT NULL DEFAULT 0, - fail_durability INTEGER NOT NULL DEFAULT 0, - fail_hp INTEGER NOT NULL DEFAULT 0, - fail_power INTEGER NOT NULL DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(name, technique) -); -``` - -## Configuration - -The system uses configurable rules for outcome chances: - -- **Success Rate**: 87% (configurable via rules) -- **Critical Success Rate**: 2% (configurable via rules) -- **Failure Rate**: 10% (configurable via rules) -- **Critical Failure Rate**: 1% (configurable via rules) -- **Event Chance**: 30% (configurable via rules) - -## Client Animations - -The system provides client-version-specific animations: - -### Animation Types -- **Success**: Played on successful crafting outcomes -- **Failure**: Played on failed crafting outcomes -- **Idle**: Played during active crafting -- **Miss Target**: Played on targeting errors -- **Kill Miss Target**: Played on critical targeting errors - -### Version Support -- **Version ≤ 561**: Legacy animation IDs -- **Version > 561**: Modern animation IDs - -## Integration Interfaces - -The system integrates with other game systems through well-defined interfaces: - -- `PlayerManager` - Player operations and messaging -- `ItemManager` - Inventory and item operations -- `RecipeManager` - Recipe validation and tracking -- `SpellManager` - Tradeskill spell management -- `ZoneManager` - Spawn and animation operations -- `ExperienceManager` - XP calculation and awards -- `QuestManager` - Quest update integration -- `RuleManager` - Configuration and rules access - -## Thread Safety - -All operations are thread-safe using Go's sync.RWMutex for optimal read performance during frequent access patterns. - -## Performance - -- Crafting updates: ~1ms per active session -- Event processing: ~500μs per event -- Database operations: Optimized with proper indexing -- Memory usage: ~1KB per active crafting session - -## Testing - -Run the test suite: - -```bash -go test ./internal/tradeskills/ -v -``` - -## Migration from C++ - -This is a complete conversion from the original C++ implementation: - -- `Tradeskills.h` → `constants.go` + `types.go` -- `Tradeskills.cpp` → `manager.go` -- `TradeskillsDB.cpp` → `database.go` -- `TradeskillsPackets.cpp` → `packets.go` - -All functionality has been preserved with Go-native patterns and improvements: -- Better error handling and logging -- Type safety with strongly-typed interfaces -- Comprehensive integration system -- Modern testing practices -- Performance optimizations -- Thread-safe concurrent access - -## Key Features - -### Crafting Mechanics -- Real-time progress and durability tracking -- Skill-based success/failure calculations -- Component consumption and validation -- Multi-stage completion system - -### Event System -- Random event generation during crafting -- Player counter-action requirements -- Success/failure rewards and penalties -- Icon-based event identification - -### Animation System -- Technique-specific animations -- Client version compatibility -- Success/failure/idle animations -- Visual state management - -### Experience System -- Recipe level-based XP calculation -- Stage-based XP multipliers -- Tradeskill level progression -- Quest integration for crafting updates - -### UI Integration -- Complex recipe selection UI -- Real-time crafting progress UI -- Component selection and validation -- Mass production support - -The tradeskills system provides a complete, production-ready crafting implementation that maintains full compatibility with the original EQ2 client while offering modern Go development practices. \ No newline at end of file diff --git a/internal/tradeskills/tradeskills_test.go b/internal/tradeskills/tradeskills_test.go deleted file mode 100644 index 62f7e30..0000000 --- a/internal/tradeskills/tradeskills_test.go +++ /dev/null @@ -1,428 +0,0 @@ -package tradeskills - -import ( - "testing" - "time" -) - -func TestTradeskillEvent(t *testing.T) { - event := &TradeskillEvent{ - Name: "Test Event", - Icon: 1234, - Technique: TechniqueSkillAlchemy, - SuccessProgress: 25, - SuccessDurability: 0, - SuccessHP: 0, - SuccessPower: -10, - SuccessSpellID: 0, - SuccessItemID: 0, - FailProgress: -10, - FailDurability: -25, - FailHP: 0, - FailPower: 0, - } - - // Test Copy method - copied := event.Copy() - if copied == nil { - t.Fatal("Copy returned nil") - } - - if copied.Name != event.Name { - t.Errorf("Expected name %s, got %s", event.Name, copied.Name) - } - - if copied.Technique != event.Technique { - t.Errorf("Expected technique %d, got %d", event.Technique, copied.Technique) - } - - // Test Copy with nil - var nilEvent *TradeskillEvent - copiedNil := nilEvent.Copy() - if copiedNil != nil { - t.Error("Copy of nil should return nil") - } -} - -func TestTradeskillManager(t *testing.T) { - manager := NewTradeskillManager() - if manager == nil { - t.Fatal("NewTradeskillManager returned nil") - } - - // Test initial state - if manager.IsClientCrafting(12345) { - t.Error("New manager should not have any active crafting sessions") - } - - // Test begin crafting - request := CraftingRequest{ - PlayerID: 12345, - RecipeID: 67890, - TableSpawnID: 11111, - Components: []ComponentUsage{ - {ItemUniqueID: 22222, Quantity: 2}, - {ItemUniqueID: 33333, Quantity: 1}, - }, - Quantity: 1, - } - - err := manager.BeginCrafting(request) - if err != nil { - t.Fatalf("BeginCrafting failed: %v", err) - } - - // Test client is now crafting - if !manager.IsClientCrafting(12345) { - t.Error("Client should be crafting after BeginCrafting") - } - - // Test get tradeskill - tradeskill := manager.GetTradeskill(12345) - if tradeskill == nil { - t.Fatal("GetTradeskill returned nil") - } - - if tradeskill.PlayerID != 12345 { - t.Errorf("Expected player ID 12345, got %d", tradeskill.PlayerID) - } - - if tradeskill.RecipeID != 67890 { - t.Errorf("Expected recipe ID 67890, got %d", tradeskill.RecipeID) - } - - // Test stop crafting - err = manager.StopCrafting(12345) - if err != nil { - t.Fatalf("StopCrafting failed: %v", err) - } - - // Test client is no longer crafting - if manager.IsClientCrafting(12345) { - t.Error("Client should not be crafting after StopCrafting") - } -} - -func TestTradeskillSession(t *testing.T) { - now := time.Now() - tradeskill := &Tradeskill{ - PlayerID: 12345, - TableSpawnID: 11111, - RecipeID: 67890, - CurrentProgress: 500, - CurrentDurability: 800, - NextUpdateTime: now.Add(time.Second), - UsedComponents: []ComponentUsage{ - {ItemUniqueID: 22222, Quantity: 2}, - }, - StartTime: now, - LastUpdate: now, - } - - // Test completion check - if tradeskill.IsComplete() { - t.Error("Tradeskill with 500 progress should not be complete") - } - - tradeskill.CurrentProgress = MaxProgress - if !tradeskill.IsComplete() { - t.Error("Tradeskill with max progress should be complete") - } - - // Test failure check - if tradeskill.IsFailed() { - t.Error("Tradeskill with 800 durability should not be failed") - } - - tradeskill.CurrentDurability = MinDurability - if !tradeskill.IsFailed() { - t.Error("Tradeskill with min durability should be failed") - } - - // Test update check - tradeskill.NextUpdateTime = now.Add(-time.Second) // Past time - if !tradeskill.NeedsUpdate() { - t.Error("Tradeskill with past update time should need update") - } - - // Test reset - tradeskill.Reset() - if tradeskill.CurrentProgress != MinProgress { - t.Errorf("Expected progress %d after reset, got %d", MinProgress, tradeskill.CurrentProgress) - } - - if tradeskill.CurrentDurability != MaxDurability { - t.Errorf("Expected durability %d after reset, got %d", MaxDurability, tradeskill.CurrentDurability) - } -} - -func TestMasterTradeskillEventsList(t *testing.T) { - eventsList := NewMasterTradeskillEventsList() - if eventsList == nil { - t.Fatal("NewMasterTradeskillEventsList returned nil") - } - - // Test initial state - if eventsList.Size() != 0 { - t.Error("New events list should be empty") - } - - // Test add event - event := &TradeskillEvent{ - Name: "Test Event", - Icon: 1234, - Technique: TechniqueSkillAlchemy, - } - - eventsList.AddEvent(event) - if eventsList.Size() != 1 { - t.Errorf("Expected size 1 after adding event, got %d", eventsList.Size()) - } - - // Test get by technique - events := eventsList.GetEventByTechnique(TechniqueSkillAlchemy) - if len(events) != 1 { - t.Errorf("Expected 1 event for alchemy, got %d", len(events)) - } - - if events[0].Name != "Test Event" { - t.Errorf("Expected event name 'Test Event', got %s", events[0].Name) - } - - // Test get by non-existent technique - noEvents := eventsList.GetEventByTechnique(TechniqueSkillFletching) - if len(noEvents) != 0 { - t.Errorf("Expected 0 events for fletching, got %d", len(noEvents)) - } - - // Test add nil event - eventsList.AddEvent(nil) - if eventsList.Size() != 1 { - t.Error("Adding nil event should not change size") - } - - // Test clear - eventsList.Clear() - if eventsList.Size() != 0 { - t.Error("Events list should be empty after Clear") - } -} - -func TestValidTechnique(t *testing.T) { - validTechniques := []uint32{ - TechniqueSkillAlchemy, - TechniqueSkillTailoring, - TechniqueSkillFletching, - TechniqueSkillJewelcrafting, - TechniqueSkillProvisioning, - TechniqueSkillScribing, - TechniqueSkillTransmuting, - TechniqueSkillArtistry, - TechniqueSkillCarpentry, - TechniqueSkillMetalworking, - TechniqueSkillMetalshaping, - TechniqueSkillStoneworking, - } - - for _, technique := range validTechniques { - if !IsValidTechnique(technique) { - t.Errorf("Technique %d should be valid", technique) - } - } - - // Test invalid technique - if IsValidTechnique(999999) { - t.Error("Invalid technique should not be valid") - } -} - -func TestDatabaseOperations(t *testing.T) { - t.Skip("Database operations require actual database connection - skipping for basic validation") - - // This test would work with a real database connection - // For now, just test that the interface methods exist and compile - - // Mock database service - var dbService DatabaseService - _ = dbService // Ensure interface compiles -} - -func TestAnimationMethods(t *testing.T) { - manager := NewTradeskillManager() - - testCases := []struct { - technique uint32 - version int16 - expectNonZero bool - }{ - {TechniqueSkillAlchemy, 500, true}, - {TechniqueSkillAlchemy, 1000, true}, - {TechniqueSkillTailoring, 500, true}, - {TechniqueSkillFletching, 1000, true}, - {TechniqueSkillScribing, 500, false}, // No animations for scribing - {999999, 500, false}, // Invalid technique - } - - for _, tc := range testCases { - successAnim := manager.GetTechniqueSuccessAnim(tc.version, tc.technique) - failureAnim := manager.GetTechniqueFailureAnim(tc.version, tc.technique) - idleAnim := manager.GetTechniqueIdleAnim(tc.version, tc.technique) - - if tc.expectNonZero { - if successAnim == 0 { - t.Errorf("Expected non-zero success animation for technique %d, version %d", tc.technique, tc.version) - } - if failureAnim == 0 { - t.Errorf("Expected non-zero failure animation for technique %d, version %d", tc.technique, tc.version) - } - if idleAnim == 0 { - t.Errorf("Expected non-zero idle animation for technique %d, version %d", tc.technique, tc.version) - } - } - } - - // Test miss target animations - missAnim := manager.GetMissTargetAnim(500) - if missAnim == 0 { - t.Error("Expected non-zero miss target animation for version 500") - } - - killMissAnim := manager.GetKillMissTargetAnim(1000) - if killMissAnim == 0 { - t.Error("Expected non-zero kill miss target animation for version 1000") - } -} - -func TestConfigurationUpdate(t *testing.T) { - manager := NewTradeskillManager() - - // Test valid configuration - err := manager.UpdateConfiguration(1.0, 2.0, 10.0, 87.0, 30.0) - if err != nil { - t.Errorf("Valid configuration update failed: %v", err) - } - - // Test invalid configuration (doesn't add to 100%) - err = manager.UpdateConfiguration(1.0, 2.0, 10.0, 80.0, 30.0) // Only adds to 93% - if err != nil { - t.Errorf("Invalid configuration should not return error, should use defaults: %v", err) - } -} - -func TestPacketHelper(t *testing.T) { - helper := &PacketHelper{} - - // Test progress stage calculation - testCases := []struct { - progress int32 - expected int8 - }{ - {0, 0}, - {300, 0}, - {400, 1}, - {550, 1}, - {600, 2}, - {750, 2}, - {800, 3}, - {950, 3}, - {1000, 4}, - {1100, 4}, // Clamped to max - } - - for _, tc := range testCases { - result := helper.CalculateProgressStage(tc.progress) - if result != tc.expected { - t.Errorf("Progress %d: expected stage %d, got %d", tc.progress, tc.expected, result) - } - } - - // Test mass production quantities - quantities := helper.GetMassProductionQuantities(3) - if len(quantities) == 0 { - t.Error("Should return at least base quantity") - } - - if quantities[0] != 1 { - t.Error("First quantity should always be 1") - } - - // Test component validation - components := []ComponentUsage{ - {ItemUniqueID: 12345, Quantity: 2}, - {ItemUniqueID: 67890, Quantity: 1}, - } - - err := helper.ValidateRecipeComponents(12345, components) - if err != nil { - t.Errorf("Valid components should pass validation: %v", err) - } - - // Test invalid components - invalidComponents := []ComponentUsage{ - {ItemUniqueID: 0, Quantity: 2}, // Invalid unique ID - } - - err = helper.ValidateRecipeComponents(12345, invalidComponents) - if err == nil { - t.Error("Invalid components should fail validation") - } - - // Test packet type calculation - packetType := helper.CalculateItemPacketType(500) - if packetType == 0 { - t.Error("Should return non-zero packet type") - } - - // Test value clamping - clampedProgress := helper.ClampProgress(-100) - if clampedProgress != MinProgress { - t.Errorf("Expected clamped progress %d, got %d", MinProgress, clampedProgress) - } - - clampedDurability := helper.ClampDurability(2000) - if clampedDurability != MaxDurability { - t.Errorf("Expected clamped durability %d, got %d", MaxDurability, clampedDurability) - } -} - -func BenchmarkTradeskillManagerProcess(b *testing.B) { - manager := NewTradeskillManager() - - // Add some test sessions - for i := 0; i < 10; i++ { - request := CraftingRequest{ - PlayerID: uint32(i + 1), - RecipeID: 67890, - TableSpawnID: 11111, - Components: []ComponentUsage{ - {ItemUniqueID: 22222, Quantity: 2}, - }, - Quantity: 1, - } - manager.BeginCrafting(request) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.Process() - } -} - -func BenchmarkEventListAccess(b *testing.B) { - eventsList := NewMasterTradeskillEventsList() - - // Add test events - for i := 0; i < 100; i++ { - event := &TradeskillEvent{ - Name: "Test Event", - Icon: int16(i), - Technique: TechniqueSkillAlchemy, - } - eventsList.AddEvent(event) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - eventsList.GetEventByTechnique(TechniqueSkillAlchemy) - } -} diff --git a/internal/traits/README.md b/internal/traits/README.md deleted file mode 100644 index 8a10a4d..0000000 --- a/internal/traits/README.md +++ /dev/null @@ -1,338 +0,0 @@ -# Traits System - -The traits system provides character advancement through selectable abilities and focuses. It has been fully converted from the original C++ EQ2EMu implementation to Go. - -## Overview - -The traits system handles: -- **Character Traits**: Universal abilities available to all classes and races -- **Class Training**: Specialized abilities specific to each class -- **Racial Traditions**: Abilities specific to each race -- **Innate Racial Abilities**: Passive racial abilities -- **Focus Effects**: Advanced abilities available at higher levels -- **Tiered Selection**: Progressive trait selection based on player choices - -## Core Components - -### Files - -- `constants.go` - Trait categories, level requirements, and configuration constants -- `types.go` - Core data structures (TraitData, MasterTraitList, PlayerTraitState, etc.) -- `manager.go` - Main trait management with selection logic and validation -- `packets.go` - Packet building for trait UI and selection -- `interfaces.go` - Integration interfaces and TraitSystemAdapter -- `traits_test.go` - Comprehensive test coverage -- `README.md` - This documentation - -### Main Types - -- `TraitData` - Individual trait definition with requirements and properties -- `MasterTraitList` - Registry of all available traits in the system -- `PlayerTraitState` - Individual player's trait selections and state -- `TraitManager` - High-level trait operations and player state management -- `TraitSystemAdapter` - Complete integration with other game systems - -## Trait Categories - -The system supports six trait categories: - -1. **Attributes** (0) - Attribute-based traits (STR, STA, etc.) -2. **Combat** (1) - Combat-related traits and abilities -3. **Noncombat** (2) - Non-combat utility traits -4. **Pools** (3) - Health/Power/Concentration pool traits -5. **Resist** (4) - Resistance-based traits -6. **Tradeskill** (5) - Tradeskill-related traits - -## Trait Types - -### Character Traits -- Available to all classes and races (ClassReq=255, RaceReq=255) -- Selectable based on character level progression -- Organized by group and level for UI display - -### Class Training -- Specific to each adventure class -- Available at specific levels based on class progression -- Enhances class-specific abilities - -### Racial Traditions -- Specific to each race -- Both active abilities and passive bonuses -- Reflects racial heritage and culture - -### Innate Racial Abilities -- Passive abilities automatically granted by race -- Cannot be unselected once granted -- Represent inherent racial characteristics - -### Focus Effects -- Advanced abilities available at higher levels -- Require specialized knowledge and experience -- Often provide significant combat or utility benefits - -## Classic Trait Progression - -The system implements the classic EverQuest II trait progression schedule: - -``` -Level 8: Personal Trait (1st) -Level 10: Training (1st) -Level 12: Enemy Tactic (1st) -Level 14: Personal Trait (2nd) -Level 16: Enemy Tactic (2nd) -Level 18: Racial Tradition (1st) -Level 20: Training (2nd) -Level 22: Personal Trait (3rd) -Level 24: Enemy Tactic (3rd) -Level 26: Racial Tradition (2nd) -Level 28: Personal Trait (4th) -Level 30: Training (3rd) -Level 32: Enemy Tactic (4th) -Level 34: Racial Tradition (3rd) -Level 36: Personal Trait (5th) -Level 38: Enemy Tactic (5th) -Level 40: Training (4th) -Level 42: Personal Trait (6th) -Level 44: Racial Tradition (4th) -Level 46: Personal Trait (7th) -Level 48: Personal Trait (8th) -Level 50: Training (5th) -``` - -### Level Requirements - -- **Personal Traits**: Levels 8, 14, 22, 28, 36, 42, 46, 48 -- **Training**: Levels 10, 20, 30, 40, 50 -- **Racial Traditions**: Levels 18, 26, 34, 44 -- **Enemy Tactics**: Levels 12, 16, 24, 32, 38 - -## Usage - -### Basic Setup - -```go -// Create master trait list and manager -masterList := traits.NewMasterTraitList() -config := &traits.TraitSystemConfig{ - TieringSelection: true, - UseClassicLevelTable: true, - FocusSelectLevel: 9, - TrainingSelectLevel: 10, - RaceSelectLevel: 10, - CharacterSelectLevel: 4, -} -manager := traits.NewTraitManager(masterList, config) - -// Create system adapter -adapter := traits.NewTraitSystemAdapter( - masterList, manager, packetBuilder, - spellManager, itemManager, playerManager, - packetManager, ruleManager, databaseService, -) - -// Initialize the system -adapter.Initialize() -``` - -### Adding Traits - -```go -// Create a new trait -trait := &traits.TraitData{ - SpellID: 12345, - Level: 10, - ClassReq: traits.UniversalClassReq, // Available to all classes - RaceReq: traits.UniversalRaceReq, // Available to all races - IsTrait: true, - IsInnate: false, - IsFocusEffect: false, - IsTraining: false, - Tier: 1, - Group: traits.TraitsCombat, - ItemID: 0, -} - -// Add to system -err := adapter.AddTrait(trait) -if err != nil { - log.Printf("Failed to add trait: %v", err) -} -``` - -### Player Trait Operations - -```go -// Get trait list packet for player -packetData, err := adapter.GetTraitListPacket(playerID) -if err != nil { - log.Printf("Failed to get trait list: %v", err) -} - -// Check if player can select a trait -allowed, err := adapter.IsPlayerAllowedTrait(playerID, spellID) -if err != nil { - log.Printf("Failed to check trait allowance: %v", err) -} - -// Process trait selections -selectedSpells := []uint32{12345, 67890} -err = adapter.SelectTraits(playerID, selectedSpells) -if err != nil { - log.Printf("Failed to select traits: %v", err) -} - -// Get player trait statistics -stats, err := adapter.GetPlayerTraitStats(playerID) -if err != nil { - log.Printf("Failed to get player stats: %v", err) -} -``` - -### Event Handling - -```go -// Create event handler -eventHandler := traits.NewTraitEventHandler(adapter) - -// Handle level up -err := eventHandler.OnPlayerLevelUp(playerID, newLevel) -if err != nil { - log.Printf("Failed to handle level up: %v", err) -} - -// Handle login -err = eventHandler.OnPlayerLogin(playerID) -if err != nil { - log.Printf("Failed to handle login: %v", err) -} - -// Handle logout -eventHandler.OnPlayerLogout(playerID) -``` - -## Configuration - -The system uses configurable rules for trait selection: - -- **TieringSelection**: Enable/disable tiered trait selection logic -- **UseClassicLevelTable**: Use classic EQ2 level requirements vs. interval-based -- **FocusSelectLevel**: Level interval for focus effect availability (default: 9) -- **TrainingSelectLevel**: Level interval for training availability (default: 10) -- **RaceSelectLevel**: Level interval for racial trait availability (default: 10) -- **CharacterSelectLevel**: Level interval for character trait availability (default: 4) - -## Packet System - -### Trait List Packet (WS_TraitsList) - -Contains comprehensive trait information for client display: - -- **Character Traits**: Organized by level with up to 5 traits per level -- **Class Training**: Specialized abilities for the player's class -- **Racial Traits**: Grouped by category (Attributes, Combat, etc.) -- **Innate Abilities**: Automatic racial abilities -- **Focus Effects**: Advanced abilities (client version >= 1188) - -### Trait Reward Packet (WS_QuestRewardPackMsg) - -Used for trait selection during level-up: - -- **Selection Rewards**: Available trait choices -- **Item Rewards**: Associated items for trait selection -- **Packet Type**: Determines UI presentation (0-3) - -## Integration Interfaces - -The system integrates with other game systems through well-defined interfaces: - -- `SpellManager` - Spell information and player spell management -- `ItemManager` - Item operations for trait-associated items -- `PlayerManager` - Player information and messaging -- `PacketManager` - Client communication and versioning -- `RuleManager` - Configuration and rule access -- `DatabaseService` - Trait persistence and player state - -## Tiered Selection Logic - -The system supports sophisticated tiered selection logic: - -1. **Group Processing**: Traits are processed by group to ensure balanced selection -2. **Spell Matching**: Previously selected spells influence future availability -3. **Priority System**: Different trait types have selection priority -4. **Validation**: Level and prerequisite requirements are enforced - -## Thread Safety - -All operations are thread-safe using Go's sync.RWMutex for optimal read performance during frequent access patterns. - -## Performance - -- Trait access: ~200ns per operation -- Trait list generation: ~50μs per player -- Memory usage: ~2KB per active player trait state -- Packet building: ~100μs per comprehensive trait packet - -## Testing - -Run the comprehensive test suite: - -```bash -go test ./internal/traits/ -v -``` - -Benchmarks are included for performance-critical operations: - -```bash -go test ./internal/traits/ -bench=. -``` - -## Migration from C++ - -This is a complete conversion from the original C++ implementation: - -- `Traits.h` → `constants.go` + `types.go` -- `Traits.cpp` → `manager.go` + `packets.go` - -All functionality has been preserved with Go-native patterns and improvements: - -- Better error handling with typed errors -- Type safety with strongly-typed interfaces -- Comprehensive integration system -- Modern testing practices with benchmarks -- Performance optimizations for concurrent access -- Thread-safe operations with proper mutex usage - -## Key Features - -### Trait Management -- Complete trait registry with validation -- Player-specific trait state management -- Level-based trait availability calculations -- Classic EQ2 progression table support - -### Selection Logic -- Tiered selection with group-based processing -- Prerequisite validation and enforcement -- Spell matching for progression continuity -- Multiple trait type support - -### Packet System -- Comprehensive trait list packet building -- Client version compatibility -- Trait reward selection packets -- Empty slot filling for consistent UI - -### Integration -- Seamless spell system integration -- Item association for trait rewards -- Player management integration -- Rule-based configuration system -- Database persistence for trait state - -### Event System -- Level-up trait availability notifications -- Login/logout state management -- Automatic trait selection opportunities -- Player progression tracking - -The traits system provides a complete, production-ready character advancement implementation that maintains full compatibility with the original EQ2 client while offering modern Go development practices and performance optimizations. \ No newline at end of file diff --git a/internal/traits/traits_test.go b/internal/traits/traits_test.go deleted file mode 100644 index ee78cbf..0000000 --- a/internal/traits/traits_test.go +++ /dev/null @@ -1,584 +0,0 @@ -package traits - -import ( - "fmt" - "testing" -) - -func TestTraitData(t *testing.T) { - trait := &TraitData{ - SpellID: 12345, - Level: 10, - ClassReq: UniversalClassReq, - RaceReq: UniversalRaceReq, - IsTrait: true, - IsInnate: false, - IsFocusEffect: false, - IsTraining: false, - Tier: 1, - Group: TraitsCombat, - ItemID: 0, - } - - // Test Copy method - copied := trait.Copy() - if copied == nil { - t.Fatal("Copy returned nil") - } - - if copied.SpellID != trait.SpellID { - t.Errorf("Expected SpellID %d, got %d", trait.SpellID, copied.SpellID) - } - - if copied.Level != trait.Level { - t.Errorf("Expected Level %d, got %d", trait.Level, copied.Level) - } - - // Test Copy with nil - var nilTrait *TraitData - copiedNil := nilTrait.Copy() - if copiedNil != nil { - t.Error("Copy of nil should return nil") - } - - // Test IsUniversalTrait - if !trait.IsUniversalTrait() { - t.Error("Trait should be universal") - } - - // Test IsForClass - if !trait.IsForClass(5) { - t.Error("Universal trait should be available for any class") - } - - // Test IsForRace - if !trait.IsForRace(3) { - t.Error("Universal trait should be available for any race") - } - - // Test GetTraitType - traitType := trait.GetTraitType() - if traitType != "Character Trait" { - t.Errorf("Expected 'Character Trait', got '%s'", traitType) - } - - // Test Validate - err := trait.Validate() - if err != nil { - t.Errorf("Valid trait should pass validation: %v", err) - } - - // Test invalid trait - invalidTrait := &TraitData{ - SpellID: 0, // Invalid - Level: -1, // Invalid - Group: 10, // Invalid - } - - err = invalidTrait.Validate() - if err == nil { - t.Error("Invalid trait should fail validation") - } -} - -func TestMasterTraitList(t *testing.T) { - masterList := NewMasterTraitList() - if masterList == nil { - t.Fatal("NewMasterTraitList returned nil") - } - - // Test initial state - if masterList.Size() != 0 { - t.Error("New master list should be empty") - } - - // Test AddTrait - trait := &TraitData{ - SpellID: 12345, - Level: 10, - ClassReq: UniversalClassReq, - RaceReq: UniversalRaceReq, - IsTrait: true, - IsInnate: false, - IsFocusEffect: false, - IsTraining: false, - Tier: 1, - Group: TraitsCombat, - ItemID: 67890, - } - - err := masterList.AddTrait(trait) - if err != nil { - t.Fatalf("AddTrait failed: %v", err) - } - - if masterList.Size() != 1 { - t.Errorf("Expected size 1 after adding trait, got %d", masterList.Size()) - } - - // Test GetTrait - retrieved := masterList.GetTrait(12345) - if retrieved == nil { - t.Fatal("GetTrait returned nil") - } - - if retrieved.SpellID != 12345 { - t.Errorf("Expected SpellID 12345, got %d", retrieved.SpellID) - } - - // Test GetTraitByItemID - retrievedByItem := masterList.GetTraitByItemID(67890) - if retrievedByItem == nil { - t.Fatal("GetTraitByItemID returned nil") - } - - if retrievedByItem.ItemID != 67890 { - t.Errorf("Expected ItemID 67890, got %d", retrievedByItem.ItemID) - } - - // Test GetTrait with non-existent ID - nonExistent := masterList.GetTrait(99999) - if nonExistent != nil { - t.Error("GetTrait should return nil for non-existent trait") - } - - // Test AddTrait with nil - err = masterList.AddTrait(nil) - if err == nil { - t.Error("AddTrait should fail with nil trait") - } - - // Test DestroyTraits - masterList.DestroyTraits() - if masterList.Size() != 0 { - t.Error("Size should be 0 after DestroyTraits") - } -} - -func TestPlayerTraitState(t *testing.T) { - playerState := NewPlayerTraitState(12345, 25, 1, 2) - if playerState == nil { - t.Fatal("NewPlayerTraitState returned nil") - } - - if playerState.PlayerID != 12345 { - t.Errorf("Expected PlayerID 12345, got %d", playerState.PlayerID) - } - - // Test UpdateLevel - playerState.UpdateLevel(30) - if playerState.Level != 30 { - t.Errorf("Expected level 30, got %d", playerState.Level) - } - - if !playerState.NeedTraitUpdate { - t.Error("Should need trait update after level change") - } - - // Test trait selection - playerState.SelectTrait(11111) - if !playerState.HasTrait(11111) { - t.Error("Player should have selected trait") - } - - if playerState.GetSelectedTraitCount() != 1 { - t.Errorf("Expected 1 selected trait, got %d", playerState.GetSelectedTraitCount()) - } - - // Test trait unselection - playerState.UnselectTrait(11111) - if playerState.HasTrait(11111) { - t.Error("Player should not have unselected trait") - } - - if playerState.GetSelectedTraitCount() != 0 { - t.Errorf("Expected 0 selected traits, got %d", playerState.GetSelectedTraitCount()) - } -} - -func TestTraitLists(t *testing.T) { - traitLists := NewTraitLists() - if traitLists == nil { - t.Fatal("NewTraitLists returned nil") - } - - // Test initial state - if len(traitLists.SortedTraitList) != 0 { - t.Error("SortedTraitList should be empty initially") - } - - // Test Clear - traitLists.SortedTraitList[0] = make(map[int8][]*TraitData) - traitLists.Clear() - - if len(traitLists.SortedTraitList) != 0 { - t.Error("SortedTraitList should be empty after Clear") - } -} - -func TestTraitSelectionContext(t *testing.T) { - context := NewTraitSelectionContext(true) - if context == nil { - t.Fatal("NewTraitSelectionContext returned nil") - } - - if !context.TieredSelection { - t.Error("TieredSelection should be true") - } - - if context.GroupToApply != UnassignedGroupID { - t.Error("GroupToApply should be UnassignedGroupID initially") - } - - // Test Reset - context.GroupToApply = 5 - context.FoundSpellMatch = true - context.Reset() - - if context.GroupToApply != UnassignedGroupID { - t.Error("GroupToApply should be reset to UnassignedGroupID") - } - - if context.FoundSpellMatch { - t.Error("FoundSpellMatch should be reset to false") - } -} - -func TestTraitSystemConfig(t *testing.T) { - config := &TraitSystemConfig{ - TieringSelection: true, - UseClassicLevelTable: false, - FocusSelectLevel: 9, - TrainingSelectLevel: 10, - RaceSelectLevel: 10, - CharacterSelectLevel: 4, - } - - if !config.TieringSelection { - t.Error("TieringSelection should be true") - } - - if config.FocusSelectLevel != 9 { - t.Errorf("Expected FocusSelectLevel 9, got %d", config.FocusSelectLevel) - } -} - -func TestGenerateTraitLists(t *testing.T) { - masterList := NewMasterTraitList() - - // Add test traits - traits := []*TraitData{ - { - SpellID: 1001, - Level: 10, - ClassReq: UniversalClassReq, - RaceReq: UniversalRaceReq, - IsTrait: true, - Group: TraitsCombat, - }, - { - SpellID: 1002, - Level: 15, - ClassReq: 5, // Specific class - IsTraining: true, - Group: TraitsAttributes, - }, - { - SpellID: 1003, - Level: 20, - RaceReq: 3, // Specific race - Group: TraitsNoncombat, - }, - { - SpellID: 1004, - Level: 25, - RaceReq: 3, - IsInnate: true, - Group: TraitsPools, - }, - { - SpellID: 1005, - Level: 30, - ClassReq: 5, - IsFocusEffect: true, - Group: TraitsResist, - }, - } - - for _, trait := range traits { - masterList.AddTrait(trait) - } - - playerState := NewPlayerTraitState(12345, 50, 5, 3) - - // Test GenerateTraitLists - success := masterList.GenerateTraitLists(playerState, 50, UnassignedGroupID) - if !success { - t.Fatal("GenerateTraitLists should succeed") - } - - // Check that traits were categorized correctly - - // Should have 1 character trait (universal) - characterTraitFound := false - for _, levelMap := range playerState.TraitLists.SortedTraitList { - if len(levelMap) > 0 { - characterTraitFound = true - break - } - } - if !characterTraitFound { - t.Error("Should have character traits") - } - - // Should have 1 class training trait - if len(playerState.TraitLists.ClassTraining) == 0 { - t.Error("Should have class training traits") - } - - // Should have 1 racial trait - if len(playerState.TraitLists.RaceTraits) == 0 { - t.Error("Should have racial traits") - } - - // Should have 1 innate racial trait - if len(playerState.TraitLists.InnateRaceTraits) == 0 { - t.Error("Should have innate racial traits") - } - - // Should have 1 focus effect - if len(playerState.TraitLists.FocusEffects) == 0 { - t.Error("Should have focus effects") - } -} - -func TestIsPlayerAllowedTrait(t *testing.T) { - masterList := NewMasterTraitList() - playerState := NewPlayerTraitState(12345, 20, 5, 3) - - config := &TraitSystemConfig{ - TieringSelection: false, - UseClassicLevelTable: false, - FocusSelectLevel: 9, - TrainingSelectLevel: 10, - RaceSelectLevel: 10, - CharacterSelectLevel: 4, - } - - trait := &TraitData{ - SpellID: 1001, - Level: 10, - ClassReq: UniversalClassReq, - RaceReq: UniversalRaceReq, - IsTrait: true, - Group: TraitsCombat, - } - - masterList.AddTrait(trait) - - // Generate trait lists - masterList.GenerateTraitLists(playerState, 50, UnassignedGroupID) - - // Test trait allowance - allowed := masterList.IsPlayerAllowedTrait(playerState, trait, config) - if !allowed { - t.Error("Player should be allowed this trait") - } -} - -func TestTraitManager(t *testing.T) { - masterList := NewMasterTraitList() - config := &TraitSystemConfig{ - TieringSelection: false, - UseClassicLevelTable: false, - FocusSelectLevel: 9, - TrainingSelectLevel: 10, - RaceSelectLevel: 10, - CharacterSelectLevel: 4, - } - - manager := NewTraitManager(masterList, config) - if manager == nil { - t.Fatal("NewTraitManager returned nil") - } - - // Test GetPlayerState - playerState := manager.GetPlayerState(12345, 25, 5, 3) - if playerState == nil { - t.Fatal("GetPlayerState returned nil") - } - - if playerState.PlayerID != 12345 { - t.Errorf("Expected PlayerID 12345, got %d", playerState.PlayerID) - } - - // Test level update - playerState2 := manager.GetPlayerState(12345, 30, 5, 3) - if playerState2.Level != 30 { - t.Errorf("Expected level 30, got %d", playerState2.Level) - } - - // Test ClearPlayerState - manager.ClearPlayerState(12345) - - // Should create new state after clearing - playerState3 := manager.GetPlayerState(12345, 25, 5, 3) - if playerState3 == playerState { - t.Error("Should create new state after clearing") - } -} - -func TestTraitPacketHelper(t *testing.T) { - helper := NewTraitPacketHelper() - if helper == nil { - t.Fatal("NewTraitPacketHelper returned nil") - } - - // Test FormatTraitFieldName - fieldName := helper.FormatTraitFieldName("trait", 2, "_icon") - if fieldName != "trait2_icon" { - t.Errorf("Expected 'trait2_icon', got '%s'", fieldName) - } - - // Test GetPacketTypeForTrait - trait := &TraitData{ - ClassReq: UniversalClassReq, - RaceReq: UniversalRaceReq, - IsTrait: true, - IsTraining: false, - } - - packetType := helper.GetPacketTypeForTrait(trait, 5, 3) - if packetType != PacketTypeCharacterTrait { - t.Errorf("Expected PacketTypeCharacterTrait (%d), got %d", PacketTypeCharacterTrait, packetType) - } - - // Test CalculateAvailableSelections - available := helper.CalculateAvailableSelections(30, 2, 10) - if available != 1 { - t.Errorf("Expected 1 available selection, got %d", available) - } - - // Test GetClassicLevelRequirement - levelReq := helper.GetClassicLevelRequirement(PersonalTraitLevelLimits, 2) - if levelReq != PersonalTraitLevelLimits[2] { - t.Errorf("Expected %d, got %d", PersonalTraitLevelLimits[2], levelReq) - } - - // Test BuildEmptyTraitSlot - emptySlot := helper.BuildEmptyTraitSlot() - if emptySlot.SpellID != EmptyTraitID { - t.Errorf("Expected EmptyTraitID (%d), got %d", EmptyTraitID, emptySlot.SpellID) - } - - // Test CountSelectedTraits - traits := []TraitInfo{ - {Selected: true}, - {Selected: false}, - {Selected: true}, - } - - count := helper.CountSelectedTraits(traits) - if count != 2 { - t.Errorf("Expected 2 selected traits, got %d", count) - } -} - -func TestTraitErrors(t *testing.T) { - // Test TraitError - err := NewTraitError("test error") - if err == nil { - t.Fatal("NewTraitError returned nil") - } - - if err.Error() != "test error" { - t.Errorf("Expected 'test error', got '%s'", err.Error()) - } - - // Test IsTraitError - if !IsTraitError(err) { - t.Error("Should identify as trait error") - } - - // Test with non-trait error - if IsTraitError(fmt.Errorf("not a trait error")) { - t.Error("Should not identify as trait error") - } -} - -func TestConstants(t *testing.T) { - // Test trait group constants - if TraitsAttributes != 0 { - t.Errorf("Expected TraitsAttributes to be 0, got %d", TraitsAttributes) - } - - if TraitsTradeskill != 5 { - t.Errorf("Expected TraitsTradeskill to be 5, got %d", TraitsTradeskill) - } - - // Test level limits - if len(PersonalTraitLevelLimits) != 9 { - t.Errorf("Expected 9 personal trait level limits, got %d", len(PersonalTraitLevelLimits)) - } - - if PersonalTraitLevelLimits[1] != 8 { - t.Errorf("Expected first personal trait at level 8, got %d", PersonalTraitLevelLimits[1]) - } - - // Test trait group names - if TraitGroupNames[TraitsAttributes] != "Attributes" { - t.Errorf("Expected 'Attributes', got '%s'", TraitGroupNames[TraitsAttributes]) - } - - // Test constants - if MaxTraitsPerLine != 5 { - t.Errorf("Expected MaxTraitsPerLine to be 5, got %d", MaxTraitsPerLine) - } - - if UniversalClassReq != -1 { - t.Errorf("Expected UniversalClassReq to be -1, got %d", UniversalClassReq) - } -} - -func BenchmarkMasterTraitListAccess(b *testing.B) { - masterList := NewMasterTraitList() - - // Add test traits - for i := 0; i < 1000; i++ { - trait := &TraitData{ - SpellID: uint32(i + 1000), - Level: int8((i % 50) + 1), - Group: int8(i % 6), - } - masterList.AddTrait(trait) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - masterList.GetTrait(uint32((i % 1000) + 1000)) - } -} - -func BenchmarkGenerateTraitLists(b *testing.B) { - masterList := NewMasterTraitList() - - // Add test traits - for i := 0; i < 100; i++ { - trait := &TraitData{ - SpellID: uint32(i + 1000), - Level: int8((i % 50) + 1), - ClassReq: UniversalClassReq, - RaceReq: UniversalRaceReq, - IsTrait: true, - Group: int8(i % 6), - } - masterList.AddTrait(trait) - } - - playerState := NewPlayerTraitState(12345, 50, 5, 3) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - masterList.GenerateTraitLists(playerState, 50, UnassignedGroupID) - } -} diff --git a/internal/transmute/transmute_test.go b/internal/transmute/transmute_test.go deleted file mode 100644 index da6ccf0..0000000 --- a/internal/transmute/transmute_test.go +++ /dev/null @@ -1,1302 +0,0 @@ -package transmute - -import ( - "path/filepath" - "sync" - "testing" -) - -// Mock implementations for testing - -// MockItem implements the Item interface for testing -type MockItem struct { - id int32 - uniqueID int32 - name string - adventureDefaultLevel int32 - itemFlags int32 - itemFlags2 int32 - tier int32 - stackCount int32 - count int32 -} - -func NewMockItem(id, uniqueID int32, name string, level int32) *MockItem { - return &MockItem{ - id: id, - uniqueID: uniqueID, - name: name, - adventureDefaultLevel: level, - tier: ItemTagLegendary, - stackCount: 1, - count: 1, - } -} - -func (m *MockItem) GetID() int32 { return m.id } -func (m *MockItem) GetUniqueID() int32 { return m.uniqueID } -func (m *MockItem) GetName() string { return m.name } -func (m *MockItem) GetAdventureDefaultLevel() int32 { return m.adventureDefaultLevel } -func (m *MockItem) GetItemFlags() int32 { return m.itemFlags } -func (m *MockItem) GetItemFlags2() int32 { return m.itemFlags2 } -func (m *MockItem) GetTier() int32 { return m.tier } -func (m *MockItem) GetStackCount() int32 { return m.stackCount } -func (m *MockItem) SetCount(count int32) { m.count = count } -func (m *MockItem) CreateItemLink(version int32, detailed bool) string { - return "[" + m.name + "]" -} - -// MockPlayer implements the Player interface for testing -type MockPlayer struct { - items map[int32]Item - skills map[string]Skill - stats map[int32]int32 - name string - zone Zone -} - -func NewMockPlayer(name string) *MockPlayer { - return &MockPlayer{ - items: make(map[int32]Item), - skills: make(map[string]Skill), - stats: make(map[int32]int32), - name: name, - } -} - -func (m *MockPlayer) GetItemList() map[int32]Item { return m.items } -func (m *MockPlayer) GetItemFromUniqueID(uniqueID int32) Item { return m.items[uniqueID] } -func (m *MockPlayer) GetSkillByName(skillName string) Skill { return m.skills[skillName] } -func (m *MockPlayer) GetStat(statType int32) int32 { return m.stats[statType] } -func (m *MockPlayer) GetName() string { return m.name } -func (m *MockPlayer) GetZone() Zone { return m.zone } -func (m *MockPlayer) RemoveItem(item Item, deleteItem bool) bool { delete(m.items, item.GetUniqueID()); return true } -func (m *MockPlayer) AddItem(item Item) (bool, error) { m.items[item.GetUniqueID()] = item; return true, nil } -func (m *MockPlayer) IncreaseSkill(skillName string, amount int32) error { return nil } - -// MockClient implements the Client interface for testing -type MockClient struct { - version int32 - transmuteID int32 - packets [][]byte - messages []string - mutex sync.Mutex -} - -func NewMockClient(version int32) *MockClient { - return &MockClient{ - version: version, - packets: make([][]byte, 0), - messages: make([]string, 0), - } -} - -func (m *MockClient) GetVersion() int32 { return m.version } -func (m *MockClient) GetTransmuteID() int32 { return m.transmuteID } -func (m *MockClient) SetTransmuteID(id int32) { m.transmuteID = id } -func (m *MockClient) QueuePacket(packet []byte) { - m.mutex.Lock() - m.packets = append(m.packets, packet) - m.mutex.Unlock() -} -func (m *MockClient) SimpleMessage(channel int32, message string) { - m.mutex.Lock() - m.messages = append(m.messages, message) - m.mutex.Unlock() -} -func (m *MockClient) Message(channel int32, format string, args ...any) { - // Simple implementation for testing - m.SimpleMessage(channel, format) -} -func (m *MockClient) AddItem(item Item, itemDeleted *bool) error { - *itemDeleted = false - return nil -} - -func (m *MockClient) GetPackets() [][]byte { - m.mutex.Lock() - defer m.mutex.Unlock() - return append([][]byte(nil), m.packets...) -} - -func (m *MockClient) GetMessages() []string { - m.mutex.Lock() - defer m.mutex.Unlock() - return append([]string(nil), m.messages...) -} - -// MockSkill implements the Skill interface for testing -type MockSkill struct { - current int32 - max int32 -} - -func NewMockSkill(current, max int32) *MockSkill { - return &MockSkill{current: current, max: max} -} - -func (m *MockSkill) GetCurrentValue() int32 { return m.current } -func (m *MockSkill) GetMaxValue() int32 { return m.max } - -// MockZone implements the Zone interface for testing -type MockZone struct{} - -func (m *MockZone) ProcessSpell(spell Spell, caster Player) error { - // For testing, we'll simulate spell processing - return nil -} - -// MockSpell implements the Spell interface for testing -type MockSpell struct { - id int32 - name string -} - -func (m *MockSpell) GetID() int32 { return m.id } -func (m *MockSpell) GetName() string { return m.name } - -// MockSpellMaster implements the SpellMaster interface for testing -type MockSpellMaster struct { - spells map[int32]Spell -} - -func NewMockSpellMaster() *MockSpellMaster { - return &MockSpellMaster{ - spells: make(map[int32]Spell), - } -} - -func (m *MockSpellMaster) GetSpell(spellID int32, tier int32) Spell { - return m.spells[spellID] -} - -// MockItemMaster implements the ItemMaster interface for testing -type MockItemMaster struct { - items map[int32]Item -} - -func NewMockItemMaster() *MockItemMaster { - return &MockItemMaster{ - items: make(map[int32]Item), - } -} - -func (m *MockItemMaster) GetItem(itemID int32) Item { - return m.items[itemID] -} - -func (m *MockItemMaster) CreateItem(itemID int32) Item { - if item, exists := m.items[itemID]; exists { - // Return a copy - mockItem := item.(*MockItem) - return &MockItem{ - id: mockItem.id, - uniqueID: mockItem.uniqueID, - name: mockItem.name, - adventureDefaultLevel: mockItem.adventureDefaultLevel, - itemFlags: mockItem.itemFlags, - itemFlags2: mockItem.itemFlags2, - tier: mockItem.tier, - stackCount: mockItem.stackCount, - count: 1, - } - } - // Create a default item if not found - return NewMockItem(itemID, itemID, "Mock Item", 50) -} - -// Test database functionality -func TestDatabaseOperations(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping database operations test - requires MySQL database connection") - - // Create temporary database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_transmute.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - // Test LoadTransmutingTiers (should populate default tiers) - tiers, err := db.LoadTransmutingTiers() - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - if len(tiers) == 0 { - t.Fatal("Expected default tiers to be loaded") - } - - expectedTiers := 10 // Should have 10 default tiers (1-9, 10-19, ..., 90-100) - if len(tiers) != expectedTiers { - t.Errorf("Expected %d default tiers, got %d", expectedTiers, len(tiers)) - } - - // Verify first tier - firstTier := tiers[0] - if firstTier.MinLevel != 1 || firstTier.MaxLevel != 9 { - t.Errorf("Expected first tier to be 1-9, got %d-%d", firstTier.MinLevel, firstTier.MaxLevel) - } - - // Test GetTransmutingTierByLevel - tier, err := db.GetTransmutingTierByLevel(5) - if err != nil { - t.Fatalf("Failed to get tier by level: %v", err) - } - - if tier.MinLevel != 1 || tier.MaxLevel != 9 { - t.Errorf("Expected tier 1-9 for level 5, got %d-%d", tier.MinLevel, tier.MaxLevel) - } - - // Test non-existent level - _, err = db.GetTransmutingTierByLevel(200) - if err == nil { - t.Error("Expected error for non-existent tier level") - } - - // Test SaveTransmutingTier - newTier := &TransmutingTier{ - MinLevel: 101, - MaxLevel: 110, - FragmentID: 2001, - PowderID: 2002, - InfusionID: 2003, - ManaID: 2004, - } - - err = db.SaveTransmutingTier(newTier) - if err != nil { - t.Fatalf("Failed to save new tier: %v", err) - } - - // Verify it was saved - savedTier, err := db.GetTransmutingTierByLevel(105) - if err != nil { - t.Fatalf("Failed to get saved tier: %v", err) - } - - if savedTier.FragmentID != 2001 { - t.Errorf("Expected fragment ID 2001, got %d", savedTier.FragmentID) - } - - // Test TransmutingTierExists - exists, err := db.TransmutingTierExists(101, 110) - if err != nil { - t.Fatalf("Failed to check tier existence: %v", err) - } - - if !exists { - t.Error("Expected tier 101-110 to exist") - } - - exists, err = db.TransmutingTierExists(999, 1000) - if err != nil { - t.Fatalf("Failed to check non-existent tier: %v", err) - } - - if exists { - t.Error("Expected tier 999-1000 not to exist") - } - - // Test UpdateTransmutingTier - updatedTier := &TransmutingTier{ - MinLevel: 101, - MaxLevel: 110, - FragmentID: 3001, - PowderID: 3002, - InfusionID: 3003, - ManaID: 3004, - } - - err = db.UpdateTransmutingTier(101, 110, updatedTier) - if err != nil { - t.Fatalf("Failed to update tier: %v", err) - } - - // Verify update - updated, err := db.GetTransmutingTierByLevel(105) - if err != nil { - t.Fatalf("Failed to get updated tier: %v", err) - } - - if updated.FragmentID != 3001 { - t.Errorf("Expected updated fragment ID 3001, got %d", updated.FragmentID) - } - - // Test DeleteTransmutingTier - err = db.DeleteTransmutingTier(101, 110) - if err != nil { - t.Fatalf("Failed to delete tier: %v", err) - } - - // Verify deletion - _, err = db.GetTransmutingTierByLevel(105) - if err == nil { - t.Error("Expected error after deleting tier") - } -} - -func TestDatabaseValidation(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping database validation test - requires MySQL database connection") - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_validation.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - // Test saving nil tier - err = db.SaveTransmutingTier(nil) - if err == nil { - t.Error("Expected error when saving nil tier") - } - - // Test invalid level range - invalidTier := &TransmutingTier{ - MinLevel: -1, - MaxLevel: 10, - } - - err = db.SaveTransmutingTier(invalidTier) - if err == nil { - t.Error("Expected error for invalid level range") - } - - // Test min > max - invalidTier.MinLevel = 20 - invalidTier.MaxLevel = 10 - - err = db.SaveTransmutingTier(invalidTier) - if err == nil { - t.Error("Expected error when min level > max level") - } - - // Test invalid material IDs - invalidTier.MinLevel = 10 - invalidTier.MaxLevel = 20 - invalidTier.FragmentID = 0 - - err = db.SaveTransmutingTier(invalidTier) - if err == nil { - t.Error("Expected error for invalid material ID") - } -} - -// Test transmutation logic -func TestTransmuter(t *testing.T) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - // Add some test materials to item master - itemMaster.items[1001] = NewMockItem(1001, 1001, "Fragment", 10) - itemMaster.items[1002] = NewMockItem(1002, 1002, "Powder", 10) - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping transmuter test - requires MySQL database connection") - - // Create test database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_transmuter.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - // Load tiers - err = transmuter.LoadTransmutingTiers(db) - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - tiers := transmuter.GetTransmutingTiers() - if len(tiers) == 0 { - t.Fatal("Expected transmuting tiers to be loaded") - } - - // Test item transmutability - transmutableItem := NewMockItem(500, 500, "Legendary Sword", 25) - transmutableItem.tier = ItemTagLegendary - - if !transmuter.IsItemTransmutable(transmutableItem) { - t.Error("Expected legendary item to be transmutable") - } - - // Test non-transmutable item (wrong tier) - nonTransmutableItem := NewMockItem(501, 501, "Common Sword", 25) - nonTransmutableItem.tier = ItemTagTreasured - - if transmuter.IsItemTransmutable(nonTransmutableItem) { - t.Error("Expected treasured item not to be transmutable") - } - - // Test flagged item - flaggedItem := NewMockItem(502, 502, "No-Trade Sword", 25) - flaggedItem.tier = ItemTagLegendary - flaggedItem.itemFlags = NoTransmute - - if transmuter.IsItemTransmutable(flaggedItem) { - t.Error("Expected no-transmute item not to be transmutable") - } - - // Test stacked item - stackedItem := NewMockItem(503, 503, "Stacked Item", 25) - stackedItem.tier = ItemTagLegendary - stackedItem.stackCount = 5 - - if transmuter.IsItemTransmutable(stackedItem) { - t.Error("Expected stacked item not to be transmutable") - } -} - -func TestCreateItemRequest(t *testing.T) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping create item request test - requires MySQL database connection") - - // Set up database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_request.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - err = transmuter.LoadTransmutingTiers(db) - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - // Create test player with transmutable items - player := NewMockPlayer("TestPlayer") - transmutableItem1 := NewMockItem(600, 600, "Legendary Sword", 25) - transmutableItem1.tier = ItemTagLegendary - transmutableItem2 := NewMockItem(601, 601, "Fabled Shield", 35) - transmutableItem2.tier = ItemTagFabled - - player.items[600] = transmutableItem1 - player.items[601] = transmutableItem2 - - // Add non-transmutable item - nonTransmutableItem := NewMockItem(602, 602, "Common Helm", 25) - nonTransmutableItem.tier = ItemTagTreasured - player.items[602] = nonTransmutableItem - - client := NewMockClient(1000) - - // Create item request - requestID, err := transmuter.CreateItemRequest(client, player) - if err != nil { - t.Fatalf("Failed to create item request: %v", err) - } - - if requestID == 0 { - t.Error("Expected non-zero request ID") - } - - if client.GetTransmuteID() != requestID { - t.Error("Expected client transmute ID to match request ID") - } - - // Verify request was stored - request := transmuter.GetActiveRequest(requestID) - if request == nil { - t.Error("Expected active request to be stored") - } - - if request.Phase != PhaseItemSelection { - t.Errorf("Expected phase %d, got %d", PhaseItemSelection, request.Phase) - } -} - -func TestHandleItemResponse(t *testing.T) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping handle item response test - requires MySQL database connection") - - // Set up database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_response.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - err = transmuter.LoadTransmutingTiers(db) - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - // Create test player with transmuting skill - player := NewMockPlayer("TestPlayer") - transmutingSkill := NewMockSkill(100, 300) - player.skills["Transmuting"] = transmutingSkill - - transmutableItem := NewMockItem(700, 700, "Legendary Weapon", 25) - transmutableItem.tier = ItemTagLegendary - player.items[700] = transmutableItem - - client := NewMockClient(1000) - - // Create initial request - requestID, err := transmuter.CreateItemRequest(client, player) - if err != nil { - t.Fatalf("Failed to create initial request: %v", err) - } - - // Handle item response - err = transmuter.HandleItemResponse(client, player, requestID, 700) - if err != nil { - t.Fatalf("Failed to handle item response: %v", err) - } - - // Verify request was updated - request := transmuter.GetActiveRequest(requestID) - if request == nil { - t.Fatal("Expected request to still exist") - } - - if request.Phase != PhaseConfirmation { - t.Errorf("Expected phase %d, got %d", PhaseConfirmation, request.Phase) - } - - if request.ItemID != 700 { - t.Errorf("Expected item ID 700, got %d", request.ItemID) - } - - // Test insufficient skill - lowSkillPlayer := NewMockPlayer("LowSkillPlayer") - lowSkill := NewMockSkill(1, 300) - lowSkillPlayer.skills["Transmuting"] = lowSkill - - highLevelItem := NewMockItem(701, 701, "High Level Item", 80) - highLevelItem.tier = ItemTagLegendary - lowSkillPlayer.items[701] = highLevelItem - - requestID2, err := transmuter.CreateItemRequest(client, lowSkillPlayer) - if err != nil { - t.Fatalf("Failed to create second request: %v", err) - } - - err = transmuter.HandleItemResponse(client, lowSkillPlayer, requestID2, 701) - if err == nil { - t.Error("Expected error for insufficient transmuting skill") - } - - // Test non-existent item - err = transmuter.HandleItemResponse(client, player, requestID, 9999) - if err == nil { - t.Error("Expected error for non-existent item") - } - - // Test non-transmutable item - nonTransmutableItem := NewMockItem(702, 702, "Common Item", 25) - nonTransmutableItem.tier = ItemTagTreasured - player.items[702] = nonTransmutableItem - - err = transmuter.HandleItemResponse(client, player, requestID, 702) - if err == nil { - t.Error("Expected error for non-transmutable item") - } -} - -func TestHandleConfirmResponse(t *testing.T) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - // Add transmute spell to spell master - transmuteSpell := &MockSpell{id: TransmuteItemSpellID, name: "Transmute Item"} - spellMaster.spells[TransmuteItemSpellID] = transmuteSpell - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping handle confirm response test - requires MySQL database connection") - - // Set up database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_confirm.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - err = transmuter.LoadTransmutingTiers(db) - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - // Create test setup - player := NewMockPlayer("TestPlayer") - zone := &MockZone{} - player.zone = zone - - transmutableItem := NewMockItem(800, 800, "Legendary Item", 25) - transmutableItem.tier = ItemTagLegendary - player.items[800] = transmutableItem - - client := NewMockClient(1000) - client.SetTransmuteID(800) - - // Test successful confirmation - err = transmuter.HandleConfirmResponse(client, player, 800) - if err != nil { - t.Fatalf("Failed to handle confirm response: %v", err) - } - - // Test non-existent item - err = transmuter.HandleConfirmResponse(client, player, 9999) - if err == nil { - t.Error("Expected error for non-existent item") - } - - // Test player without zone - playerNoZone := NewMockPlayer("NoZonePlayer") - playerNoZone.items[800] = transmutableItem - - err = transmuter.HandleConfirmResponse(client, playerNoZone, 800) - if err == nil { - t.Error("Expected error for player without zone") - } -} - -// Test material calculation logic -func TestCalculateTransmuteResult(t *testing.T) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - // Add test materials to item master - itemMaster.items[1001] = NewMockItem(1001, 1001, "Fragment", 10) // Tier 1 fragment - itemMaster.items[1002] = NewMockItem(1002, 1002, "Powder", 10) // Tier 1 powder - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping calculate transmute result test - requires MySQL database connection") - - // Set up database and load tiers - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_materials.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - err = transmuter.LoadTransmutingTiers(db) - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - // Test legendary item (should give powder/infusion) - legendaryItem := NewMockItem(900, 900, "Legendary Item", 25) - legendaryItem.tier = ItemTagLegendary - - // We can't easily test the random result, but we can test the structure - result, err := transmuter.calculateTransmuteResult(legendaryItem) - if err != nil { - t.Fatalf("Failed to calculate transmute result: %v", err) - } - - if !result.Success { - t.Errorf("Expected successful result, got error: %s", result.ErrorMessage) - } - - // Test item with no tier match - highLevelItem := NewMockItem(901, 901, "High Level Item", 200) - highLevelItem.tier = ItemTagLegendary - - result, err = transmuter.calculateTransmuteResult(highLevelItem) - if err != nil { - t.Fatalf("Failed to calculate result for high level item: %v", err) - } - - if result.Success { - t.Error("Expected failure for item with no tier match") - } -} - -func TestCompleteTransmutation(t *testing.T) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - // Add test materials - fragment := NewMockItem(1001, 1001, "Fragment", 10) - powder := NewMockItem(1002, 1002, "Powder", 10) - itemMaster.items[1001] = fragment - itemMaster.items[1002] = powder - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping complete transmutation test - requires MySQL database connection") - - // Set up database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_complete.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - err = transmuter.LoadTransmutingTiers(db) - if err != nil { - t.Fatalf("Failed to load transmuting tiers: %v", err) - } - - // Create test setup - player := NewMockPlayer("TestPlayer") - transmutingSkill := NewMockSkill(100, 300) - player.skills["Transmuting"] = transmutingSkill - - transmutableItem := NewMockItem(1000, 1000, "Test Item", 25) - transmutableItem.tier = ItemTagLegendary - player.items[1000] = transmutableItem - - client := NewMockClient(1000) - client.SetTransmuteID(1000) - - // Complete transmutation - err = transmuter.CompleteTransmutation(client, player) - if err != nil { - t.Fatalf("Failed to complete transmutation: %v", err) - } - - // Verify item was removed from player - if _, exists := player.items[1000]; exists { - t.Error("Expected transmuted item to be removed from player") - } - - // Verify messages were sent - messages := client.GetMessages() - if len(messages) == 0 { - t.Error("Expected completion messages to be sent") - } - - // Test non-existent item - client.SetTransmuteID(9999) - err = transmuter.CompleteTransmutation(client, player) - if err == nil { - t.Error("Expected error for non-existent item") - } -} - -// Test Manager functionality -func TestManager(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping manager test - requires MySQL database connection") - - // Create test database - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_manager.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - manager := NewManager(db, itemMaster, spellMaster, packetBuilder) - defer manager.Shutdown() - - // Test initialization - err = manager.Initialize() - if err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - tiers := manager.GetTransmutingTiers() - if len(tiers) == 0 { - t.Error("Expected tiers to be loaded after initialization") - } - - // Test statistics - stats := manager.GetStatistics() - if stats["total_transmutes"] != int64(0) { - t.Error("Expected zero transmutes initially") - } - - // Test validation - issues := manager.ValidateTransmutingSetup() - if len(issues) > 0 { - t.Errorf("Expected no validation issues, got: %v", issues) - } - - // Test GetTierForItemLevel - tier := manager.GetTierForItemLevel(25) - if tier == nil { - t.Error("Expected to find tier for level 25") - } - - if tier.MinLevel > 25 || tier.MaxLevel < 25 { - t.Errorf("Expected tier to contain level 25, got %d-%d", tier.MinLevel, tier.MaxLevel) - } - - // Test GetTierForItemLevel for non-existent level - noTier := manager.GetTierForItemLevel(200) - if noTier != nil { - t.Error("Expected no tier for level 200") - } - - // Test CalculateRequiredSkill - item := NewMockItem(1100, 1100, "Test Item", 25) - requiredSkill := manager.CalculateRequiredSkill(item) - expectedSkill := (25 - 5) * 5 // (level - 5) * 5 = 100 - if requiredSkill != int32(expectedSkill) { - t.Errorf("Expected required skill %d, got %d", expectedSkill, requiredSkill) - } - - // Test low level item - lowLevelItem := NewMockItem(1101, 1101, "Low Level Item", 3) - requiredSkill = manager.CalculateRequiredSkill(lowLevelItem) - if requiredSkill != 0 { - t.Errorf("Expected required skill 0 for low level item, got %d", requiredSkill) - } -} - -func TestManagerPlayerOperations(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping manager player operations test - requires MySQL database connection") - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_player_ops.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - manager := NewManager(db, itemMaster, spellMaster, packetBuilder) - defer manager.Shutdown() - - err = manager.Initialize() - if err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - // Create test player - player := NewMockPlayer("TestPlayer") - transmutingSkill := NewMockSkill(100, 300) - player.skills["Transmuting"] = transmutingSkill - - // Add transmutable and non-transmutable items - transmutableItem := NewMockItem(1200, 1200, "Legendary Sword", 25) - transmutableItem.tier = ItemTagLegendary - player.items[1200] = transmutableItem - - nonTransmutableItem := NewMockItem(1201, 1201, "Common Sword", 25) - nonTransmutableItem.tier = ItemTagTreasured - player.items[1201] = nonTransmutableItem - - // Test GetTransmutableItems - transmutableItems := manager.GetTransmutableItems(player) - if len(transmutableItems) != 1 { - t.Errorf("Expected 1 transmutable item, got %d", len(transmutableItems)) - } - - if transmutableItems[0].GetID() != 1200 { - t.Errorf("Expected transmutable item ID 1200, got %d", transmutableItems[0].GetID()) - } - - // Test CanPlayerTransmuteItem - canTransmute, reason := manager.CanPlayerTransmuteItem(player, transmutableItem) - if !canTransmute { - t.Errorf("Expected player to be able to transmute item, reason: %s", reason) - } - - // Test with insufficient skill - highLevelItem := NewMockItem(1202, 1202, "High Level Item", 80) - highLevelItem.tier = ItemTagLegendary - - canTransmute, reason = manager.CanPlayerTransmuteItem(player, highLevelItem) - if canTransmute { - t.Error("Expected player not to be able to transmute high level item") - } - - if reason == "" { - t.Error("Expected reason for inability to transmute") - } - - // Test with non-transmutable item - canTransmute, reason = manager.CanPlayerTransmuteItem(player, nonTransmutableItem) - if canTransmute { - t.Error("Expected player not to be able to transmute non-transmutable item") - } -} - -func TestManagerCommandProcessing(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping manager command processing test - requires MySQL database connection") - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_commands.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - manager := NewManager(db, itemMaster, spellMaster, packetBuilder) - defer manager.Shutdown() - - err = manager.Initialize() - if err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - client := NewMockClient(1000) - player := NewMockPlayer("TestPlayer") - - // Test stats command - result, err := manager.ProcessCommand("stats", []string{}, client, player) - if err != nil { - t.Fatalf("Failed to process stats command: %v", err) - } - - if result == "" { - t.Error("Expected stats command to return result") - } - - // Test validate command - result, err = manager.ProcessCommand("validate", []string{}, client, player) - if err != nil { - t.Fatalf("Failed to process validate command: %v", err) - } - - if result == "" { - t.Error("Expected validate command to return result") - } - - // Test tiers command - result, err = manager.ProcessCommand("tiers", []string{}, client, player) - if err != nil { - t.Fatalf("Failed to process tiers command: %v", err) - } - - if result == "" { - t.Error("Expected tiers command to return result") - } - - // Test reload command - result, err = manager.ProcessCommand("reload", []string{}, client, player) - if err != nil { - t.Fatalf("Failed to process reload command: %v", err) - } - - if result == "" { - t.Error("Expected reload command to return result") - } - - // Test unknown command - _, err = manager.ProcessCommand("unknown", []string{}, client, player) - if err == nil { - t.Error("Expected error for unknown command") - } -} - -func TestManagerStatistics(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping manager statistics test - requires MySQL database connection") - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_stats.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - manager := NewManager(db, itemMaster, spellMaster, packetBuilder) - defer manager.Shutdown() - - // Test initial statistics - stats := manager.GetStatistics() - if stats["total_transmutes"] != int64(0) { - t.Error("Expected zero total transmutes initially") - } - - // Test RecordMaterialProduced - manager.RecordMaterialProduced(1001, 5) - manager.RecordMaterialProduced(1002, 3) - manager.RecordMaterialProduced(1001, 2) // Add more of the same material - - count := manager.GetMaterialProductionCount(1001) - if count != 7 { - t.Errorf("Expected material 1001 count 7, got %d", count) - } - - count = manager.GetMaterialProductionCount(1002) - if count != 3 { - t.Errorf("Expected material 1002 count 3, got %d", count) - } - - // Test statistics after recording - stats = manager.GetStatistics() - materialStats := stats["material_counts"].(map[int32]int64) - - if materialStats[1001] != 7 { - t.Errorf("Expected material 1001 in stats to be 7, got %d", materialStats[1001]) - } - - // Test ResetStatistics - manager.ResetStatistics() - - stats = manager.GetStatistics() - if stats["total_transmutes"] != int64(0) { - t.Error("Expected statistics to be reset") - } - - count = manager.GetMaterialProductionCount(1001) - if count != 0 { - t.Error("Expected material count to be reset") - } -} - -// Test concurrent operations -func TestConcurrency(t *testing.T) { - // Skip this test as it requires a MySQL database connection - t.Skip("Skipping concurrency test - requires MySQL database connection") - - tempDir := t.TempDir() - dbPath := filepath.Join(tempDir, "test_concurrency.db") - - db, err := OpenDB(dbPath) - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - manager := NewManager(db, itemMaster, spellMaster, packetBuilder) - defer manager.Shutdown() - - err = manager.Initialize() - if err != nil { - t.Fatalf("Failed to initialize manager: %v", err) - } - - var wg sync.WaitGroup - numGoroutines := 10 - numOperations := 100 - - // Test concurrent material recording - wg.Add(numGoroutines) - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - for j := 0; j < numOperations; j++ { - materialID := int32(1000 + (id % 5)) // Use 5 different materials - manager.RecordMaterialProduced(materialID, 1) - } - }(i) - } - - wg.Wait() - - // Verify concurrent operations worked correctly - stats := manager.GetStatistics() - materialStats := stats["material_counts"].(map[int32]int64) - - totalRecorded := int64(0) - for _, count := range materialStats { - totalRecorded += count - } - - expectedTotal := int64(numGoroutines * numOperations) - if totalRecorded != expectedTotal { - t.Errorf("Expected total recorded %d, got %d", expectedTotal, totalRecorded) - } -} - -// Test packet builder -func TestPacketBuilder(t *testing.T) { - builder := NewPacketBuilder() - - // Test BuildItemRequestPacket - items := []int32{100, 200, 300} - packet, err := builder.BuildItemRequestPacket(12345, items, 1000) - if err != nil { - t.Fatalf("Failed to build item request packet: %v", err) - } - - if packet == nil { - t.Error("Expected packet to be non-nil") - } - - // Test empty items list - _, err = builder.BuildItemRequestPacket(12345, []int32{}, 1000) - if err == nil { - t.Error("Expected error for empty items list") - } - - // Test BuildConfirmationPacket - item := NewMockItem(400, 400, "Test Item", 50) - packet, err = builder.BuildConfirmationPacket(12345, item, 1000) - if err != nil { - t.Fatalf("Failed to build confirmation packet: %v", err) - } - - if packet == nil { - t.Error("Expected packet to be non-nil") - } - - // Test nil item - _, err = builder.BuildConfirmationPacket(12345, nil, 1000) - if err == nil { - t.Error("Expected error for nil item") - } - - // Test BuildRewardPacket - rewardItems := []Item{ - NewMockItem(500, 500, "Reward 1", 30), - NewMockItem(501, 501, "Reward 2", 30), - } - - packet, err = builder.BuildRewardPacket(rewardItems, 1000) - if err != nil { - t.Fatalf("Failed to build reward packet: %v", err) - } - - if packet == nil { - t.Error("Expected packet to be non-nil") - } - - // Test empty rewards - _, err = builder.BuildRewardPacket([]Item{}, 1000) - if err == nil { - t.Error("Expected error for empty rewards list") - } -} - -// Benchmark tests -func BenchmarkIsItemTransmutable(b *testing.B) { - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - transmuter := NewTransmuter(itemMaster, spellMaster, packetBuilder) - item := NewMockItem(1000, 1000, "Test Item", 50) - item.tier = ItemTagLegendary - - b.ResetTimer() - for i := 0; i < b.N; i++ { - transmuter.IsItemTransmutable(item) - } -} - -func BenchmarkDatabaseOperations(b *testing.B) { - // Skip this benchmark as it requires a MySQL database connection - b.Skip("Skipping database operations benchmark - requires MySQL database connection") - - tempDir := b.TempDir() - dbPath := filepath.Join(tempDir, "bench_db.db") - - db, err := OpenDB(dbPath) - if err != nil { - b.Fatalf("Failed to open benchmark database: %v", err) - } - defer db.Close() - - // Load initial tiers - _, err = db.LoadTransmutingTiers() - if err != nil { - b.Fatalf("Failed to load initial tiers: %v", err) - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := db.GetTransmutingTierByLevel(int32(25 + (i % 75))) // Test levels 25-99 - if err != nil { - b.Fatalf("Failed to get tier by level: %v", err) - } - } -} - -func BenchmarkManagerOperations(b *testing.B) { - // Skip this benchmark as it requires a MySQL database connection - b.Skip("Skipping manager operations benchmark - requires MySQL database connection") - - tempDir := b.TempDir() - dbPath := filepath.Join(tempDir, "bench_manager.db") - - db, err := OpenDB(dbPath) - if err != nil { - b.Fatalf("Failed to open benchmark database: %v", err) - } - defer db.Close() - - itemMaster := NewMockItemMaster() - spellMaster := NewMockSpellMaster() - packetBuilder := NewPacketBuilder() - - manager := NewManager(db, itemMaster, spellMaster, packetBuilder) - defer manager.Shutdown() - - err = manager.Initialize() - if err != nil { - b.Fatalf("Failed to initialize manager: %v", err) - } - - item := NewMockItem(1000, 1000, "Benchmark Item", 50) - item.tier = ItemTagLegendary - - b.ResetTimer() - for i := 0; i < b.N; i++ { - manager.IsItemTransmutable(item) - } -} \ No newline at end of file diff --git a/internal/zone/README.md b/internal/zone/README.md deleted file mode 100644 index ac43cc5..0000000 --- a/internal/zone/README.md +++ /dev/null @@ -1,372 +0,0 @@ -# EverQuest II Zone System - -This package implements a comprehensive zone management system for the EverQuest II server emulator, converted from the original C++ implementation while leveraging Go's concurrency and type safety features. - -## Overview - -The zone system handles: -- **Zone Management**: Loading, initialization, and lifecycle management of game zones -- **Instance Management**: Creating and managing instanced zones for groups, raids, and solo play -- **Spawn Management**: NPCs, objects, widgets, signs, and ground spawns with spatial optimization -- **Movement System**: NPC movement with pathfinding, stuck detection, and multiple movement modes -- **Position System**: 3D position calculations, distance functions, and EQ2-specific heading math -- **Weather System**: Dynamic weather with patterns, severity controls, and client synchronization -- **Database Integration**: Persistent storage of zone configuration, spawns, and player data -- **Client Management**: Player connections, spawn visibility, and packet communication -- **Grid System**: Spatial partitioning for efficient range queries and spawn management - -## Architecture - -### Core Components - -#### ZoneServer (`zone_server.go`) -- Central coordinator for all zone functionality -- Manages clients, spawns, timers, and processing loops -- Handles zone initialization, configuration, and shutdown -- Thread-safe operations using sync.RWMutex and atomic values -- Supports both regular zones and instances - -#### ZoneManager (`zone_manager.go`) -- High-level management of multiple zones and instances -- Automatic loading/unloading based on demand -- Instance creation with type-specific player limits -- Statistics collection and monitoring -- Cleanup of inactive instances - -#### MobMovementManager (`movement_manager.go`) -- Advanced NPC movement system with command queuing -- Pathfinding integration with multiple backends -- Stuck detection and recovery mechanisms -- Multiple movement modes (walk, run, swim, fly) -- Thread-safe processing with delta time calculations - -#### Position System (`position.go`) -- EverQuest II specific 3D math utilities -- Distance calculations (2D, 3D, 4D with heading) -- Heading conversion and normalization (512-unit circle) -- Bounding box and cylinder collision detection -- Interpolation and random position generation - -#### Database Layer (`database.go`) -- Complete zone data persistence with prepared statements -- Transaction support for atomic updates -- Efficient loading of zone configuration and spawn data -- Support for spawn locations, groups, and associations -- Thread-safe operations with connection pooling - -### Key Features - -#### Spatial Optimization -- **Grid System**: Spatial partitioning for efficient spawn queries -- **Range-based Updates**: Only process spawns within client visibility -- **Distance Culling**: Automatic spawn loading/unloading based on distance -- **Grid-based Indexing**: Fast lookup of nearby spawns and objects - -#### Instance System -- **Multiple Instance Types**: Group, raid, solo, tradeskill, housing, quest instances -- **Automatic Limits**: Type-specific player count restrictions -- **Lifecycle Management**: Automatic creation and cleanup -- **Persistence Options**: Lockout vs persistent instances - -#### Movement System -- **Command Queuing**: Sequential movement command execution -- **Pathfinding Integration**: Multiple pathfinding backends (navmesh, waypoint, null) -- **Stuck Detection**: Position-based stuck detection with recovery strategies -- **Smooth Movement**: Delta-time based position interpolation -- **Multi-mode Support**: Walking, running, swimming, flying - -#### Weather System -- **Dynamic Patterns**: Normal, dynamic, random, and chaotic weather types -- **Severity Control**: Min/max bounds with configurable change rates -- **Pattern Support**: Increasing, decreasing, and random severity patterns -- **Client Synchronization**: Automatic weather updates to all clients - -## Usage Examples - -### Basic Zone Creation and Management - -```go -// Create zone server -zoneServer := NewZoneServer("qeynos") - -// Configure zone -config := &ZoneServerConfig{ - ZoneName: "qeynos", - ZoneFile: "qeynos.zone", - ZoneDescription: "Qeynos: Capitol of Antonica", - ZoneID: 100, - InstanceID: 0, - InstanceType: InstanceTypeNone, - MaxPlayers: 200, - MinLevel: 1, - MaxLevel: 100, - SafeX: 830.0, - SafeY: -25.0, - SafeZ: -394.0, - SafeHeading: 0.0, - LoadMaps: true, - EnableWeather: true, - EnablePathfinding: true, -} - -// Initialize zone -err := zoneServer.Initialize(config) -if err != nil { - log.Fatal(err) -} - -// Add client to zone -err = zoneServer.AddClient(client) -if err != nil { - log.Printf("Failed to add client: %v", err) -} -``` - -### Zone Manager Usage - -```go -// Create zone manager -config := &ZoneManagerConfig{ - MaxZones: 50, - MaxInstanceZones: 500, - ProcessInterval: time.Millisecond * 100, - CleanupInterval: time.Minute * 5, - EnableWeather: true, - EnablePathfinding: true, -} - -zoneManager := NewZoneManager(config, database) - -// Start zone manager -err := zoneManager.Start() -if err != nil { - log.Fatal(err) -} - -// Load a zone -zone, err := zoneManager.LoadZone(100) // Qeynos -if err != nil { - log.Printf("Failed to load zone: %v", err) -} - -// Create an instance -instance, err := zoneManager.CreateInstance(100, InstanceTypeGroupLockout, playerID) -if err != nil { - log.Printf("Failed to create instance: %v", err) -} -``` - -### Movement System Usage - -```go -// Get movement manager from zone -movementMgr := zoneServer.movementMgr - -// Add NPC to movement tracking -spawnID := int32(1001) -movementMgr.AddMovementSpawn(spawnID) - -// Command NPC to move -err := movementMgr.MoveTo(spawnID, 100.0, 200.0, 0.0, DefaultRunSpeed) -if err != nil { - log.Printf("Movement command failed: %v", err) -} - -// Queue multiple commands -movementMgr.MoveTo(spawnID, 150.0, 250.0, 0.0, DefaultWalkSpeed) -movementMgr.RotateTo(spawnID, 256.0, 90.0) // Turn around -movementMgr.MoveTo(spawnID, 100.0, 200.0, 0.0, DefaultRunSpeed) // Return - -// Check if moving -if movementMgr.IsMoving(spawnID) { - state := movementMgr.GetMovementState(spawnID) - log.Printf("NPC %d is moving at speed %.2f", spawnID, state.Speed) -} -``` - -### Position Calculations - -```go -// Calculate distance between two points -distance := Distance3D(0, 0, 0, 100, 100, 100) -log.Printf("Distance: %.2f", distance) - -// Calculate heading from one point to another -heading := CalculateHeading(0, 0, 100, 100) -log.Printf("Heading: %.2f", heading) - -// Work with positions -pos1 := NewPosition(10.0, 20.0, 30.0, 128.0) -pos2 := NewPosition(50.0, 60.0, 30.0, 256.0) - -distance = pos1.DistanceTo3D(pos2) -log.Printf("Position distance: %.2f", distance) - -// Check if positions are within range -if IsWithinRange(pos1, pos2, 100.0) { - log.Println("Positions are within range") -} - -// Create bounding box and test containment -bbox := NewBoundingBox(0, 0, 0, 100, 100, 100) -if bbox.ContainsPosition(pos1) { - log.Println("Position is inside bounding box") -} -``` - -### Weather System - -```go -// Set rain level -zoneServer.SetRain(0.8) // Heavy rain - -// Weather is processed automatically, but can be triggered manually -zoneServer.ProcessWeather() - -// Configure weather (typically done during initialization) -zoneServer.weatherEnabled = true -zoneServer.weatherType = WeatherTypeDynamic -zoneServer.weatherFrequency = 600 // 10 minutes -zoneServer.weatherMinSeverity = 0.0 -zoneServer.weatherMaxSeverity = 1.0 -zoneServer.weatherChangeAmount = 0.1 -zoneServer.weatherChangeChance = 75 // 75% chance of change -``` - -## Database Schema - -The zone system uses several database tables: - -### Core Zone Configuration -```sql -CREATE TABLE zones ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - file TEXT, - description TEXT, - safe_x REAL DEFAULT 0, - safe_y REAL DEFAULT 0, - safe_z REAL DEFAULT 0, - safe_heading REAL DEFAULT 0, - underworld REAL DEFAULT -1000, - min_level INTEGER DEFAULT 0, - max_level INTEGER DEFAULT 0, - max_players INTEGER DEFAULT 100, - instance_type INTEGER DEFAULT 0, - expansion_flag INTEGER DEFAULT 0, - weather_allowed INTEGER DEFAULT 1, - -- ... additional fields -); -``` - -### Spawn Locations -```sql -CREATE TABLE spawn_location_placement ( - id INTEGER PRIMARY KEY, - zone_id INTEGER, - x REAL, - y REAL, - z REAL, - heading REAL, - spawn_type INTEGER, - respawn_time INTEGER DEFAULT 600, - conditions INTEGER DEFAULT 0, - spawn_percentage REAL DEFAULT 100.0 -); -``` - -### Spawn Groups -```sql -CREATE TABLE spawn_location_group ( - group_id INTEGER, - location_id INTEGER, - zone_id INTEGER -); -``` - -## Configuration - -### Key Constants - -- **Distance Constants**: `SendSpawnDistance` (250), `RemoveSpawnDistance` (300) -- **Movement Speeds**: `DefaultWalkSpeed` (2.5), `DefaultRunSpeed` (7.0) -- **Timer Intervals**: Configurable processing intervals for different systems -- **Capacity Limits**: `MaxSpawnsPerGrid` (100), `MaxClientsPerZone` (200) - -### Zone Rules - -Zones can be configured with various rules: -- Player level restrictions (min/max) -- Client version requirements -- PvP enablement -- Weather settings -- Instance type and capacity -- Expansion and holiday flags - -## Thread Safety - -All zone operations are designed to be thread-safe: -- **RWMutex Usage**: Separate read/write locks for different data structures -- **Atomic Operations**: For simple flags and counters -- **Channel Communication**: For cross-goroutine messaging -- **Immutable Data**: Where possible, data structures are immutable -- **Copy-on-Read**: Returns copies of data to prevent race conditions - -## Performance Considerations - -- **Spatial Indexing**: Grid-based partitioning reduces O(n) to O(1) for range queries -- **Prepared Statements**: All database queries use prepared statements -- **Object Pooling**: Reuse of frequently allocated objects -- **Lazy Loading**: Zone data loaded on demand -- **Concurrent Processing**: Multiple goroutines for different subsystems -- **Memory Management**: Regular cleanup of expired objects and timers - -## Error Handling - -The zone system provides comprehensive error handling: -- **Graceful Degradation**: Systems continue operating when non-critical components fail -- **Detailed Logging**: All errors logged with appropriate prefixes and context -- **Recovery Mechanisms**: Automatic recovery from common error conditions -- **Validation**: Input validation at all API boundaries -- **Timeouts**: All operations have appropriate timeouts - -## Testing - -Comprehensive test suite includes: -- Unit tests for all major components -- Integration tests for database operations -- Performance benchmarks for critical paths -- Mock implementations for testing isolation -- Property-based testing for mathematical functions - -Run tests with: -```bash -go test ./internal/zone/... -go test -race ./internal/zone/... # Race condition detection -go test -bench=. ./internal/zone/ # Performance benchmarks -``` - -## Migration from C++ - -This Go implementation maintains compatibility with the original C++ EQ2EMu zone system: - -- **Database Schema**: Identical table structure and relationships -- **Protocol Compatibility**: Same client communication protocols -- **Algorithmic Equivalence**: Math functions produce identical results -- **Configuration Format**: Compatible configuration files and settings -- **Performance**: Comparable or improved performance through Go's concurrency - -## Dependencies - -- **Standard Library**: sync, time, database/sql, math -- **Internal Packages**: database, spawn, common -- **External**: SQLite driver (zombiezen.com/go/sqlite) - -## Future Enhancements - -Planned improvements include: -- **Advanced Pathfinding**: Integration with Detour navigation mesh -- **Lua Scripting**: Full Lua integration for spawn behaviors -- **Physics Engine**: Advanced collision detection and physics -- **Clustering**: Multi-server zone distribution -- **Hot Reloading**: Dynamic configuration updates without restart \ No newline at end of file