From 0388396b0d605ff447f905326c4df460b95a90ce Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Fri, 1 Aug 2025 20:29:21 -0500 Subject: [PATCH] implement tests and fixes for entity package --- internal/entity/benchmark_test.go | 506 ++++++++++++++++++++++ internal/entity/concurrency_test.go | 445 +++++++++++++++++++ internal/entity/entity.go | 29 +- internal/entity/entity_test.go | 641 +++++++++++++++++++++++++++- internal/entity/info_struct.go | 3 + internal/entity/info_struct_test.go | 514 ++++++++++++++++++++++ internal/spawn/spawn.go | 8 +- internal/spells/spell_targeting.go | 2 +- 8 files changed, 2134 insertions(+), 14 deletions(-) create mode 100644 internal/entity/benchmark_test.go create mode 100644 internal/entity/concurrency_test.go create mode 100644 internal/entity/info_struct_test.go diff --git a/internal/entity/benchmark_test.go b/internal/entity/benchmark_test.go new file mode 100644 index 0000000..f11c3d1 --- /dev/null +++ b/internal/entity/benchmark_test.go @@ -0,0 +1,506 @@ +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 new file mode 100644 index 0000000..506ac68 --- /dev/null +++ b/internal/entity/concurrency_test.go @@ -0,0 +1,445 @@ +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") + } +} \ No newline at end of file diff --git a/internal/entity/entity.go b/internal/entity/entity.go index 161d368..1c48192 100644 --- a/internal/entity/entity.go +++ b/internal/entity/entity.go @@ -88,6 +88,7 @@ type Entity struct { detrimentalMutex sync.RWMutex commandMutex sync.Mutex bonusCalculationMutex sync.RWMutex + petMutex sync.RWMutex } // NewEntity creates a new Entity with default values @@ -253,35 +254,35 @@ func (e *Entity) HasMoved() bool { // GetStr returns the effective strength stat func (e *Entity) GetStr() int16 { base := int16(e.infoStruct.GetStr()) - bonus := int16(e.spellEffectManager.GetBonusValue(1, 0, e.infoStruct.GetRace(), 0)) // Stat type 1 = STR + bonus := int16(e.spellEffectManager.GetBonusValue(1, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 1 = STR return base + bonus } // GetSta returns the effective stamina stat func (e *Entity) GetSta() int16 { base := int16(e.infoStruct.GetSta()) - bonus := int16(e.spellEffectManager.GetBonusValue(2, 0, e.infoStruct.GetRace(), 0)) // Stat type 2 = STA + bonus := int16(e.spellEffectManager.GetBonusValue(2, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 2 = STA return base + bonus } // GetAgi returns the effective agility stat func (e *Entity) GetAgi() int16 { base := int16(e.infoStruct.GetAgi()) - bonus := int16(e.spellEffectManager.GetBonusValue(3, 0, e.infoStruct.GetRace(), 0)) // Stat type 3 = AGI + bonus := int16(e.spellEffectManager.GetBonusValue(3, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 3 = AGI return base + bonus } // GetWis returns the effective wisdom stat func (e *Entity) GetWis() int16 { base := int16(e.infoStruct.GetWis()) - bonus := int16(e.spellEffectManager.GetBonusValue(4, 0, e.infoStruct.GetRace(), 0)) // Stat type 4 = WIS + bonus := int16(e.spellEffectManager.GetBonusValue(4, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 4 = WIS return base + bonus } // GetIntel returns the effective intelligence stat func (e *Entity) GetIntel() int16 { base := int16(e.infoStruct.GetIntel()) - bonus := int16(e.spellEffectManager.GetBonusValue(5, 0, e.infoStruct.GetRace(), 0)) // Stat type 5 = INT + bonus := int16(e.spellEffectManager.GetBonusValue(5, 0, int16(e.infoStruct.GetRace()), 0)) // Stat type 5 = INT return base + bonus } @@ -469,7 +470,7 @@ func (e *Entity) CalculateBonuses() { e.infoStruct.ResetEffects() entityClass := int64(1 << e.infoStruct.GetClass1()) // Convert class to bitmask - race := e.infoStruct.GetRace() + race := int16(e.infoStruct.GetRace()) factionID := int32(e.GetFactionID()) // Apply stat bonuses @@ -508,11 +509,15 @@ func (e *Entity) CalculateBonuses() { // GetPet returns the summon pet func (e *Entity) GetPet() *Entity { + e.petMutex.RLock() + defer e.petMutex.RUnlock() return e.pet } // SetPet sets the summon pet func (e *Entity) SetPet(pet *Entity) { + e.petMutex.Lock() + defer e.petMutex.Unlock() e.pet = pet if pet != nil { pet.owner = e.GetID() @@ -522,11 +527,15 @@ func (e *Entity) SetPet(pet *Entity) { // GetCharmedPet returns the charmed pet func (e *Entity) GetCharmedPet() *Entity { + e.petMutex.RLock() + defer e.petMutex.RUnlock() return e.charmedPet } // SetCharmedPet sets the charmed pet func (e *Entity) SetCharmedPet(pet *Entity) { + e.petMutex.Lock() + defer e.petMutex.Unlock() e.charmedPet = pet if pet != nil { pet.owner = e.GetID() @@ -536,11 +545,15 @@ func (e *Entity) SetCharmedPet(pet *Entity) { // GetDeityPet returns the deity pet func (e *Entity) GetDeityPet() *Entity { + e.petMutex.RLock() + defer e.petMutex.RUnlock() return e.deityPet } // SetDeityPet sets the deity pet func (e *Entity) SetDeityPet(pet *Entity) { + e.petMutex.Lock() + defer e.petMutex.Unlock() e.deityPet = pet if pet != nil { pet.owner = e.GetID() @@ -550,11 +563,15 @@ func (e *Entity) SetDeityPet(pet *Entity) { // GetCosmeticPet returns the cosmetic pet func (e *Entity) GetCosmeticPet() *Entity { + e.petMutex.RLock() + defer e.petMutex.RUnlock() return e.cosmeticPet } // SetCosmeticPet sets the cosmetic pet func (e *Entity) SetCosmeticPet(pet *Entity) { + e.petMutex.Lock() + defer e.petMutex.Unlock() e.cosmeticPet = pet if pet != nil { pet.owner = e.GetID() diff --git a/internal/entity/entity_test.go b/internal/entity/entity_test.go index 0882036..4a850d0 100644 --- a/internal/entity/entity_test.go +++ b/internal/entity/entity_test.go @@ -1,22 +1,657 @@ package entity import ( + "sync" "testing" + "time" ) -func TestPackageBuild(t *testing.T) { - // Basic test to verify the package builds +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 TestEntityStats(t *testing.T) { +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(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 ControlEffectStun == 0 && ControlEffectRoot == 0 { + t.Error("Control effect constants should be non-zero") + } } \ No newline at end of file diff --git a/internal/entity/info_struct.go b/internal/entity/info_struct.go index 4376e68..2950003 100644 --- a/internal/entity/info_struct.go +++ b/internal/entity/info_struct.go @@ -834,6 +834,9 @@ func (info *InfoStruct) Clone() *InfoStruct { clone := &InfoStruct{} *clone = *info // Copy all fields + // Reset the mutex in the clone to avoid sharing the same mutex + clone.mutex = sync.RWMutex{} + // Copy the account age bonus array copy(clone.accountAgeBonus[:], info.accountAgeBonus[:]) diff --git a/internal/entity/info_struct_test.go b/internal/entity/info_struct_test.go new file mode 100644 index 0000000..a3699bb --- /dev/null +++ b/internal/entity/info_struct_test.go @@ -0,0 +1,514 @@ +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") + } +} \ No newline at end of file diff --git a/internal/spawn/spawn.go b/internal/spawn/spawn.go index 747be14..4ebc7f6 100644 --- a/internal/spawn/spawn.go +++ b/internal/spawn/spawn.go @@ -252,8 +252,8 @@ func NextID() int32 { for { id := atomic.AddInt32(&nextSpawnID, 1) - // Handle wraparound - if id == 0xFFFFFFFE { + // Handle wraparound (using math.MaxInt32 - 1) + if id >= 2147483646 { atomic.StoreInt32(&nextSpawnID, 1) continue } @@ -623,7 +623,7 @@ func (s *Spawn) SetName(name string) { // GetLevel returns the spawn's level func (s *Spawn) GetLevel() int16 { - return s.appearance.Level + return int16(s.appearance.Level) } // SetLevel updates the spawn's level and marks info as changed @@ -631,7 +631,7 @@ func (s *Spawn) SetLevel(level int16) { s.updateMutex.Lock() defer s.updateMutex.Unlock() - s.appearance.Level = level + s.appearance.Level = int8(level) s.infoChanged.Store(true) s.changed.Store(true) s.addChangedZoneSpawn() diff --git a/internal/spells/spell_targeting.go b/internal/spells/spell_targeting.go index 9666bdd..cc96dba 100644 --- a/internal/spells/spell_targeting.go +++ b/internal/spells/spell_targeting.go @@ -154,7 +154,7 @@ func (st *SpellTargeting) getAETargets(luaSpell *LuaSpell, result *TargetingResu // 5. Add valid targets to the list // For now, implement basic logic - spellData := luaSpell.Spell.GetSpellData() + _ = luaSpell.Spell.GetSpellData() // TODO: Use spell data when needed maxTargets := int32(10) // TODO: Use spellData.AOENodeNumber when field exists if maxTargets <= 0 { maxTargets = 10 // Default limit