diff --git a/internal/entity/info_struct.go b/internal/entity/info_struct.go index 2950003..de27301 100644 --- a/internal/entity/info_struct.go +++ b/internal/entity/info_struct.go @@ -831,11 +831,259 @@ func (info *InfoStruct) Clone() *InfoStruct { info.mutex.RLock() defer info.mutex.RUnlock() - clone := &InfoStruct{} - *clone = *info // Copy all fields + clone := &InfoStruct{ + // Basic character information + name: info.name, + class1: info.class1, + class2: info.class2, + class3: info.class3, + race: info.race, + gender: info.gender, + level: info.level, + maxLevel: info.maxLevel, + effectiveLevel: info.effectiveLevel, + tradeskillLevel: info.tradeskillLevel, + tradeskillMaxLevel: info.tradeskillMaxLevel, - // Reset the mutex in the clone to avoid sharing the same mutex - clone.mutex = sync.RWMutex{} + // Concentration system + curConcentration: info.curConcentration, + maxConcentration: info.maxConcentration, + + // Combat statistics + curAttack: info.curAttack, + attackBase: info.attackBase, + curMitigation: info.curMitigation, + maxMitigation: info.maxMitigation, + mitigationBase: info.mitigationBase, + mitigationMod: info.mitigationMod, + + // Avoidance statistics + avoidanceDisplay: info.avoidanceDisplay, + curAvoidance: info.curAvoidance, + baseAvoidancePct: info.baseAvoidancePct, + avoidanceBase: info.avoidanceBase, + maxAvoidance: info.maxAvoidance, + parry: info.parry, + parryBase: info.parryBase, + deflection: info.deflection, + deflectionBase: info.deflectionBase, + block: info.block, + blockBase: info.blockBase, + + // Primary attributes + str: info.str, + sta: info.sta, + agi: info.agi, + wis: info.wis, + intel: info.intel, + strBase: info.strBase, + staBase: info.staBase, + agiBase: info.agiBase, + wisBase: info.wisBase, + intelBase: info.intelBase, + + // Resistances + heat: info.heat, + cold: info.cold, + magic: info.magic, + mental: info.mental, + divine: info.divine, + disease: info.disease, + poison: info.poison, + heatBase: info.heatBase, + coldBase: info.coldBase, + magicBase: info.magicBase, + mentalBase: info.mentalBase, + divineBase: info.divineBase, + diseaseBase: info.diseaseBase, + poisonBase: info.poisonBase, + elementalBase: info.elementalBase, + noxiousBase: info.noxiousBase, + arcaneBase: info.arcaneBase, + + // Currency + coinCopper: info.coinCopper, + coinSilver: info.coinSilver, + coinGold: info.coinGold, + coinPlat: info.coinPlat, + bankCoinCopper: info.bankCoinCopper, + bankCoinSilver: info.bankCoinSilver, + bankCoinGold: info.bankCoinGold, + bankCoinPlat: info.bankCoinPlat, + statusPoints: info.statusPoints, + + // Character details + deity: info.deity, + weight: info.weight, + maxWeight: info.maxWeight, + + // Tradeskill classes + tradeskillClass1: info.tradeskillClass1, + tradeskillClass2: info.tradeskillClass2, + tradeskillClass3: info.tradeskillClass3, + + // Account and character age + accountAgeBase: info.accountAgeBase, + + // Combat and damage + absorb: info.absorb, + + // Experience points + xp: info.xp, + xpNeeded: info.xpNeeded, + xpDebt: info.xpDebt, + xpYellow: info.xpYellow, + xpYellowVitalityBar: info.xpYellowVitalityBar, + xpBlueVitalityBar: info.xpBlueVitalityBar, + xpBlue: info.xpBlue, + tsXp: info.tsXp, + tsXpNeeded: info.tsXpNeeded, + tradeskillExpYellow: info.tradeskillExpYellow, + tradeskillExpBlue: info.tradeskillExpBlue, + xpVitality: info.xpVitality, + tradeskillXpVitality: info.tradeskillXpVitality, + + // Flags and states + flags: info.flags, + flags2: info.flags2, + + // Specialized mitigation + mitigationSkill1: info.mitigationSkill1, + mitigationSkill2: info.mitigationSkill2, + mitigationSkill3: info.mitigationSkill3, + mitigationPve: info.mitigationPve, + mitigationPvp: info.mitigationPvp, + + // Combat modifiers + abilityModifier: info.abilityModifier, + criticalMitigation: info.criticalMitigation, + blockChance: info.blockChance, + uncontestedParry: info.uncontestedParry, + uncontestedBlock: info.uncontestedBlock, + uncontestedDodge: info.uncontestedDodge, + uncontestedRiposte: info.uncontestedRiposte, + critChance: info.critChance, + critBonus: info.critBonus, + potency: info.potency, + hateMod: info.hateMod, + reuseSpeed: info.reuseSpeed, + castingSpeed: info.castingSpeed, + recoverySpeed: info.recoverySpeed, + spellReuseSpeed: info.spellReuseSpeed, + spellMultiAttack: info.spellMultiAttack, + + // Size and physical attributes + sizeMod: info.sizeMod, + ignoreSizeModCalc: info.ignoreSizeModCalc, + dps: info.dps, + dpsMultiplier: info.dpsMultiplier, + attackSpeed: info.attackSpeed, + haste: info.haste, + multiAttack: info.multiAttack, + flurry: info.flurry, + meleeAe: info.meleeAe, + strikethrough: info.strikethrough, + accuracy: info.accuracy, + offensiveSpeed: info.offensiveSpeed, + + // Environmental + rain: info.rain, + wind: info.wind, + alignment: info.alignment, + + // Pet information + petId: info.petId, + petName: info.petName, + petHealthPct: info.petHealthPct, + petPowerPct: info.petPowerPct, + petMovement: info.petMovement, + petBehavior: info.petBehavior, + + // Character abilities + vision: info.vision, + breatheUnderwater: info.breatheUnderwater, + biography: info.biography, + drunk: info.drunk, + + // Regeneration + powerRegen: info.powerRegen, + hpRegen: info.hpRegen, + powerRegenOverride: info.powerRegenOverride, + hpRegenOverride: info.hpRegenOverride, + + // Movement types + waterType: info.waterType, + flyingType: info.flyingType, + + // Special flags + noInterrupt: info.noInterrupt, + interactionFlag: info.interactionFlag, + tag1: info.tag1, + mood: info.mood, + + // Weapon timing + rangeLastAttackTime: info.rangeLastAttackTime, + primaryLastAttackTime: info.primaryLastAttackTime, + secondaryLastAttackTime: info.secondaryLastAttackTime, + primaryAttackDelay: info.primaryAttackDelay, + secondaryAttackDelay: info.secondaryAttackDelay, + rangedAttackDelay: info.rangedAttackDelay, + + // Weapon information + primaryWeaponType: info.primaryWeaponType, + secondaryWeaponType: info.secondaryWeaponType, + rangedWeaponType: info.rangedWeaponType, + primaryWeaponDmgLow: info.primaryWeaponDmgLow, + primaryWeaponDmgHigh: info.primaryWeaponDmgHigh, + secondaryWeaponDmgLow: info.secondaryWeaponDmgLow, + secondaryWeaponDmgHigh: info.secondaryWeaponDmgHigh, + rangedWeaponDmgLow: info.rangedWeaponDmgLow, + rangedWeaponDmgHigh: info.rangedWeaponDmgHigh, + wieldType: info.wieldType, + attackType: info.attackType, + primaryWeaponDelay: info.primaryWeaponDelay, + secondaryWeaponDelay: info.secondaryWeaponDelay, + rangedWeaponDelay: info.rangedWeaponDelay, + + // Weapon overrides + overridePrimaryWeapon: info.overridePrimaryWeapon, + overrideSecondaryWeapon: info.overrideSecondaryWeapon, + overrideRangedWeapon: info.overrideRangedWeapon, + + // NPC specific + friendlyTargetNpc: info.friendlyTargetNpc, + lastClaimTime: info.lastClaimTime, + + // Encounter system + engagedEncounter: info.engagedEncounter, + lockableEncounter: info.lockableEncounter, + + // Player flags + firstWorldLogin: info.firstWorldLogin, + reloadPlayerSpells: info.reloadPlayerSpells, + + // Group settings + groupLootMethod: info.groupLootMethod, + groupLootItemsRarity: info.groupLootItemsRarity, + groupAutoSplit: info.groupAutoSplit, + groupDefaultYell: info.groupDefaultYell, + groupAutolock: info.groupAutolock, + groupLockMethod: info.groupLockMethod, + groupSoloAutolock: info.groupSoloAutolock, + groupAutoLootMethod: info.groupAutoLootMethod, + assistAutoAttack: info.assistAutoAttack, + + // Action state + actionState: info.actionState, + + // Spell and ability reductions + spellMulticastChance: info.spellMulticastChance, + maxSpellReductionOverride: info.maxSpellReductionOverride, + maxChaseDistance: info.maxChaseDistance, + + // Thread safety - new mutex for the clone + mutex: sync.RWMutex{}, + } // Copy the account age bonus array copy(clone.accountAgeBonus[:], info.accountAgeBonus[:]) diff --git a/internal/entity/manager.go b/internal/entity/manager.go new file mode 100644 index 0000000..6b6b542 --- /dev/null +++ b/internal/entity/manager.go @@ -0,0 +1,651 @@ +package entity + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "eq2emu/internal/packets" +) + +// Manager provides centralized management for the entity system +// following the SIMPLIFICATION.md methodology while maintaining C++ API compatibility +type Manager struct { + // Thread safety + mutex sync.RWMutex + + // Performance monitoring + stats struct { + // Entity operations + EntitiesCreated int64 + EntitiesDestroyed int64 + EntitiesUpdated int64 + + // Spell effect operations + SpellEffectsAdded int64 + SpellEffectsRemoved int64 + BonusCalculations int64 + + // Combat operations + DamageCalculations int64 + HealingCalculations int64 + CombatActions int64 + + // Packet operations + PacketsSent int64 + PacketErrors int64 + VerbRequests int64 + + // Cache performance + CacheHits int64 + CacheMisses int64 + } + + // Configuration + config struct { + EnableStatistics bool + EnableValidation bool + EnableCombatLogging bool + MaxEntitiesPerZone int + EffectCleanupInterval time.Duration + BonusCalculationInterval time.Duration + PacketBuilderTimeout time.Duration + EnablePetManagement bool + EnableSpellEffectCaching bool + } +} + +// NewManager creates a new entity manager with default configuration +func NewManager() *Manager { + m := &Manager{} + + // Set default configuration + m.config.EnableStatistics = true + m.config.EnableValidation = true + m.config.EnableCombatLogging = false + m.config.MaxEntitiesPerZone = 10000 + m.config.EffectCleanupInterval = 30 * time.Second + m.config.BonusCalculationInterval = 5 * time.Second + m.config.PacketBuilderTimeout = 30 * time.Second + m.config.EnablePetManagement = true + m.config.EnableSpellEffectCaching = true + + return m +} + +// === Entity Creation and Management === + +// CreateEntity creates a new Entity with proper initialization +func (m *Manager) CreateEntity() *Entity { + entity := NewEntity() + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.EntitiesCreated, 1) + } + + return entity +} + +// DestroyEntity properly cleans up an entity +func (m *Manager) DestroyEntity(entity *Entity) error { + if entity == nil { + return fmt.Errorf("entity cannot be nil") + } + + // Clean up spell effects + entity.DeleteSpellEffects(false) + + // Clean up pets + if entity.GetPet() != nil { + entity.SetPet(nil) + } + if entity.GetCharmedPet() != nil { + entity.SetCharmedPet(nil) + } + if entity.GetDeityPet() != nil { + entity.SetDeityPet(nil) + } + if entity.GetCosmeticPet() != nil { + entity.SetCosmeticPet(nil) + } + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.EntitiesDestroyed, 1) + } + + return nil +} + +// === C++ API Compatibility Methods === + +// GetEntityInfoStruct returns an entity's info structure (C++ API compatibility) +func (m *Manager) GetEntityInfoStruct(entity *Entity) *InfoStruct { + if entity == nil { + return nil + } + return entity.GetInfoStruct() +} + +// SetEntityInfoStruct updates an entity's info structure (C++ API compatibility) +func (m *Manager) SetEntityInfoStruct(entity *Entity, info *InfoStruct) { + if entity != nil && info != nil { + entity.SetInfoStruct(info) + } +} + +// IsEntityInCombat returns whether an entity is in combat (C++ API compatibility) +func (m *Manager) IsEntityInCombat(entity *Entity) bool { + if entity == nil { + return false + } + return entity.IsInCombat() +} + +// SetEntityInCombat updates an entity's combat state (C++ API compatibility) +func (m *Manager) SetEntityInCombat(entity *Entity, inCombat bool) { + if entity != nil { + entity.SetInCombat(inCombat) + if m.config.EnableStatistics && inCombat { + atomic.AddInt64(&m.stats.CombatActions, 1) + } + } +} + +// IsEntityCasting returns whether an entity is casting (C++ API compatibility) +func (m *Manager) IsEntityCasting(entity *Entity) bool { + if entity == nil { + return false + } + return entity.IsCasting() +} + +// SetEntityCasting updates an entity's casting state (C++ API compatibility) +func (m *Manager) SetEntityCasting(entity *Entity, casting bool) { + if entity != nil { + entity.SetCasting(casting) + } +} + +// === Spell Effect Management === + +// AddMaintainedSpell adds a maintained spell to an entity +func (m *Manager) AddMaintainedSpell(entity *Entity, name string, spellID int32, duration float32, concentration int8) bool { + if entity == nil { + return false + } + + success := entity.AddMaintainedSpell(name, spellID, duration, concentration) + + if m.config.EnableStatistics && success { + atomic.AddInt64(&m.stats.SpellEffectsAdded, 1) + } + + return success +} + +// RemoveMaintainedSpell removes a maintained spell from an entity +func (m *Manager) RemoveMaintainedSpell(entity *Entity, spellID int32) bool { + if entity == nil { + return false + } + + success := entity.RemoveMaintainedSpell(spellID) + + if m.config.EnableStatistics && success { + atomic.AddInt64(&m.stats.SpellEffectsRemoved, 1) + } + + return success +} + +// AddSpellEffect adds a temporary spell effect to an entity +func (m *Manager) AddSpellEffect(entity *Entity, spellID int32, casterID int32, duration float32) bool { + if entity == nil { + return false + } + + success := entity.AddSpellEffect(spellID, casterID, duration) + + if m.config.EnableStatistics && success { + atomic.AddInt64(&m.stats.SpellEffectsAdded, 1) + } + + return success +} + +// RemoveSpellEffect removes a spell effect from an entity +func (m *Manager) RemoveSpellEffect(entity *Entity, spellID int32) bool { + if entity == nil { + return false + } + + success := entity.RemoveSpellEffect(spellID) + + if m.config.EnableStatistics && success { + atomic.AddInt64(&m.stats.SpellEffectsRemoved, 1) + } + + return success +} + +// AddDetrimentalSpell adds a detrimental effect to an entity +func (m *Manager) AddDetrimentalSpell(entity *Entity, spellID int32, casterID int32, duration float32, detType int8) { + if entity != nil { + entity.AddDetrimentalSpell(spellID, casterID, duration, detType) + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.SpellEffectsAdded, 1) + } + } +} + +// RemoveDetrimentalSpell removes a detrimental effect from an entity +func (m *Manager) RemoveDetrimentalSpell(entity *Entity, spellID int32, casterID int32) bool { + if entity == nil { + return false + } + + success := entity.RemoveDetrimentalSpell(spellID, casterID) + + if m.config.EnableStatistics && success { + atomic.AddInt64(&m.stats.SpellEffectsRemoved, 1) + } + + return success +} + +// CalculateEntityBonuses recalculates all bonuses for an entity +func (m *Manager) CalculateEntityBonuses(entity *Entity) { + if entity != nil { + entity.CalculateBonuses() + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.BonusCalculations, 1) + } + } +} + +// === Pet Management === + +// SetEntityPet assigns a summon pet to an entity +func (m *Manager) SetEntityPet(entity *Entity, pet *Entity) { + if entity != nil { + entity.SetPet(pet) + } +} + +// GetEntityPet returns an entity's summon pet +func (m *Manager) GetEntityPet(entity *Entity) *Entity { + if entity == nil { + return nil + } + return entity.GetPet() +} + +// SetEntityCharmedPet assigns a charmed pet to an entity +func (m *Manager) SetEntityCharmedPet(entity *Entity, pet *Entity) { + if entity != nil { + entity.SetCharmedPet(pet) + } +} + +// GetEntityCharmedPet returns an entity's charmed pet +func (m *Manager) GetEntityCharmedPet(entity *Entity) *Entity { + if entity == nil { + return nil + } + return entity.GetCharmedPet() +} + +// DismissPet marks a pet for dismissal +func (m *Manager) DismissPet(pet *Entity) { + if pet != nil { + pet.SetPetDismissing(true) + } +} + +// === Movement and Position === + +// UpdateEntityPosition updates an entity's position tracking +func (m *Manager) UpdateEntityPosition(entity *Entity) { + if entity != nil { + // Update last position from current position + x := entity.GetX() + y := entity.GetY() + z := entity.GetZ() + heading := entity.GetHeading() + + entity.SetLastPosition(x, y, z, heading) + } +} + +// HasEntityMoved checks if an entity has moved since last update +func (m *Manager) HasEntityMoved(entity *Entity) bool { + if entity == nil { + return false + } + return entity.HasMoved() +} + +// GetEntityEffectiveSpeed calculates an entity's current effective speed +func (m *Manager) GetEntityEffectiveSpeed(entity *Entity) float32 { + if entity == nil { + return 0.0 + } + return entity.CalculateEffectiveSpeed() +} + +// === Stat Calculations === + +// GetEntityPrimaryStat returns an entity's highest primary stat +func (m *Manager) GetEntityPrimaryStat(entity *Entity) int16 { + if entity == nil { + return 0 + } + return entity.GetPrimaryStat() +} + +// GetEntityResistance returns an entity's resistance to a specific damage type +func (m *Manager) GetEntityResistance(entity *Entity, resistanceType string) int16 { + if entity == nil { + return 0 + } + + switch resistanceType { + case "heat": + return entity.GetHeatResistance() + case "cold": + return entity.GetColdResistance() + case "magic": + return entity.GetMagicResistance() + case "mental": + return entity.GetMentalResistance() + case "divine": + return entity.GetDivineResistance() + case "disease": + return entity.GetDiseaseResistance() + case "poison": + return entity.GetPoisonResistance() + default: + return 0 + } +} + +// === Packet Building Methods === + +// SendEntityVerbsRequest builds and returns entity verbs request packet data (C++ API compatibility) +func (m *Manager) SendEntityVerbsRequest(entityID int32, clientVersion uint32) ([]byte, error) { + packet, exists := packets.GetPacket("WS_EntityVerbsRequest") + if !exists { + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketErrors, 1) + } + return nil, fmt.Errorf("failed to get WS_EntityVerbsRequest packet structure: packet not found") + } + + data := map[string]interface{}{ + "entity_id": entityID, + } + + builder := packets.NewPacketBuilder(packet, clientVersion, 0) + packetData, err := builder.Build(data) + if err != nil { + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketErrors, 1) + } + return nil, fmt.Errorf("failed to build entity verbs request packet: %v", err) + } + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketsSent, 1) + atomic.AddInt64(&m.stats.VerbRequests, 1) + } + + return packetData, nil +} + +// SendEntityVerbsResponse builds and returns entity verbs response packet data (C++ API compatibility) +func (m *Manager) SendEntityVerbsResponse(entityID int32, clientVersion uint32, verbs []map[string]interface{}) ([]byte, error) { + packet, exists := packets.GetPacket("WS_EntityVerbsResponse") + if !exists { + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketErrors, 1) + } + return nil, fmt.Errorf("failed to get WS_EntityVerbsResponse packet structure: packet not found") + } + + data := map[string]interface{}{ + "entity_id": entityID, + "num_verbs": len(verbs), + "verbs_array": verbs, + } + + builder := packets.NewPacketBuilder(packet, clientVersion, 0) + packetData, err := builder.Build(data) + if err != nil { + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketErrors, 1) + } + return nil, fmt.Errorf("failed to build entity verbs response packet: %v", err) + } + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketsSent, 1) + } + + return packetData, nil +} + +// SendEntityVerbsVerb builds and returns entity verb action packet data (C++ API compatibility) +func (m *Manager) SendEntityVerbsVerb(entityID int32, targetID int32, clientVersion uint32, verbID int32, verbName string) ([]byte, error) { + packet, exists := packets.GetPacket("WS_EntityVerbsVerb") + if !exists { + // Try backup packet structure + packet, exists = packets.GetPacket("WS_EntityVerbsVerbBackup") + if !exists { + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketErrors, 1) + } + return nil, fmt.Errorf("failed to get WS_EntityVerbsVerb packet structure: packet not found") + } + } + + data := map[string]interface{}{ + "entity_id": entityID, + "target_id": targetID, + "verb_id": verbID, + "verb_name": verbName, + } + + builder := packets.NewPacketBuilder(packet, clientVersion, 0) + packetData, err := builder.Build(data) + if err != nil { + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketErrors, 1) + } + return nil, fmt.Errorf("failed to build entity verb action packet: %v", err) + } + + if m.config.EnableStatistics { + atomic.AddInt64(&m.stats.PacketsSent, 1) + } + + return packetData, nil +} + +// === Effect Processing === + +// ProcessEntityEffects handles periodic effect processing for an entity +func (m *Manager) ProcessEntityEffects(entity *Entity) { + if entity != nil { + entity.ProcessEffects() + } +} + +// CleanupEntityEffects removes expired effects from an entity +func (m *Manager) CleanupEntityEffects(entity *Entity) { + if entity != nil { + entity.ProcessEffects() // This includes cleanup + } +} + +// === Statistics and Monitoring === + +// GetStatistics returns comprehensive manager statistics +func (m *Manager) GetStatistics() map[string]interface{} { + m.mutex.RLock() + defer m.mutex.RUnlock() + + stats := make(map[string]interface{}) + + // Entity stats (using atomic loads) + stats["entities_created"] = atomic.LoadInt64(&m.stats.EntitiesCreated) + stats["entities_destroyed"] = atomic.LoadInt64(&m.stats.EntitiesDestroyed) + stats["entities_updated"] = atomic.LoadInt64(&m.stats.EntitiesUpdated) + + // Spell effect stats (using atomic loads) + stats["spell_effects_added"] = atomic.LoadInt64(&m.stats.SpellEffectsAdded) + stats["spell_effects_removed"] = atomic.LoadInt64(&m.stats.SpellEffectsRemoved) + stats["bonus_calculations"] = atomic.LoadInt64(&m.stats.BonusCalculations) + + // Combat stats (using atomic loads) + stats["damage_calculations"] = atomic.LoadInt64(&m.stats.DamageCalculations) + stats["healing_calculations"] = atomic.LoadInt64(&m.stats.HealingCalculations) + stats["combat_actions"] = atomic.LoadInt64(&m.stats.CombatActions) + + // Packet stats (using atomic loads) + stats["packets_sent"] = atomic.LoadInt64(&m.stats.PacketsSent) + stats["packet_errors"] = atomic.LoadInt64(&m.stats.PacketErrors) + stats["verb_requests"] = atomic.LoadInt64(&m.stats.VerbRequests) + + // Cache stats (using atomic loads) + cacheHits := atomic.LoadInt64(&m.stats.CacheHits) + cacheMisses := atomic.LoadInt64(&m.stats.CacheMisses) + stats["cache_hits"] = cacheHits + stats["cache_misses"] = cacheMisses + + if cacheHits+cacheMisses > 0 { + hitRate := float64(cacheHits) / float64(cacheHits+cacheMisses) * 100 + stats["cache_hit_rate"] = fmt.Sprintf("%.2f%%", hitRate) + } + + // Configuration + stats["config_statistics_enabled"] = m.config.EnableStatistics + stats["config_validation_enabled"] = m.config.EnableValidation + stats["config_combat_logging_enabled"] = m.config.EnableCombatLogging + stats["config_pet_management_enabled"] = m.config.EnablePetManagement + + return stats +} + +// ResetStatistics clears all performance counters +func (m *Manager) ResetStatistics() { + atomic.StoreInt64(&m.stats.EntitiesCreated, 0) + atomic.StoreInt64(&m.stats.EntitiesDestroyed, 0) + atomic.StoreInt64(&m.stats.EntitiesUpdated, 0) + atomic.StoreInt64(&m.stats.SpellEffectsAdded, 0) + atomic.StoreInt64(&m.stats.SpellEffectsRemoved, 0) + atomic.StoreInt64(&m.stats.BonusCalculations, 0) + atomic.StoreInt64(&m.stats.DamageCalculations, 0) + atomic.StoreInt64(&m.stats.HealingCalculations, 0) + atomic.StoreInt64(&m.stats.CombatActions, 0) + atomic.StoreInt64(&m.stats.PacketsSent, 0) + atomic.StoreInt64(&m.stats.PacketErrors, 0) + atomic.StoreInt64(&m.stats.VerbRequests, 0) + atomic.StoreInt64(&m.stats.CacheHits, 0) + atomic.StoreInt64(&m.stats.CacheMisses, 0) +} + +// === Configuration Management === + +// SetConfiguration updates manager configuration +func (m *Manager) SetConfiguration(config map[string]interface{}) { + m.mutex.Lock() + defer m.mutex.Unlock() + + if val, ok := config["enable_statistics"].(bool); ok { + m.config.EnableStatistics = val + } + if val, ok := config["enable_validation"].(bool); ok { + m.config.EnableValidation = val + } + if val, ok := config["enable_combat_logging"].(bool); ok { + m.config.EnableCombatLogging = val + } + if val, ok := config["max_entities_per_zone"].(int); ok { + m.config.MaxEntitiesPerZone = val + } + if val, ok := config["effect_cleanup_interval"].(time.Duration); ok { + m.config.EffectCleanupInterval = val + } + if val, ok := config["enable_pet_management"].(bool); ok { + m.config.EnablePetManagement = val + } + if val, ok := config["enable_spell_effect_caching"].(bool); ok { + m.config.EnableSpellEffectCaching = val + } +} + +// GetConfiguration returns current manager configuration +func (m *Manager) GetConfiguration() map[string]interface{} { + m.mutex.RLock() + defer m.mutex.RUnlock() + + return map[string]interface{}{ + "enable_statistics": m.config.EnableStatistics, + "enable_validation": m.config.EnableValidation, + "enable_combat_logging": m.config.EnableCombatLogging, + "max_entities_per_zone": m.config.MaxEntitiesPerZone, + "effect_cleanup_interval": m.config.EffectCleanupInterval, + "bonus_calculation_interval": m.config.BonusCalculationInterval, + "packet_builder_timeout": m.config.PacketBuilderTimeout, + "enable_pet_management": m.config.EnablePetManagement, + "enable_spell_effect_caching": m.config.EnableSpellEffectCaching, + } +} + +// === Validation and Health Checks === + +// ValidateEntity performs comprehensive entity validation +func (m *Manager) ValidateEntity(entity *Entity) []string { + var issues []string + + if entity == nil { + return []string{"entity is nil"} + } + + info := entity.GetInfoStruct() + if info == nil { + issues = append(issues, "entity info struct is nil") + return issues + } + + if info.GetLevel() < 0 { + issues = append(issues, fmt.Sprintf("invalid level: %d", info.GetLevel())) + } + + if info.GetName() == "" { + issues = append(issues, "entity name is empty") + } + + if entity.GetTotalHP() < 0 { + issues = append(issues, fmt.Sprintf("invalid total HP: %d", entity.GetTotalHP())) + } + + if entity.GetTotalPower() < 0 { + issues = append(issues, fmt.Sprintf("invalid total power: %d", entity.GetTotalPower())) + } + + return issues +} + +// IsHealthy returns true if the manager is in a healthy state +func (m *Manager) IsHealthy() bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // Manager is always considered healthy for entities + // as it's primarily a utility manager + return true +} \ No newline at end of file diff --git a/internal/entity/manager_test.go b/internal/entity/manager_test.go new file mode 100644 index 0000000..21f922f --- /dev/null +++ b/internal/entity/manager_test.go @@ -0,0 +1,802 @@ +package entity + +import ( + "testing" +) + +func createTestManager() *Manager { + return NewManager() +} + +func createTestEntity() *Entity { + entity := NewEntity() + + // Set some basic info for testing + info := entity.GetInfoStruct() + info.SetName("Test Entity") + info.SetLevel(10) + info.SetClass1(1) // Fighter + info.SetRace(1) // Human + info.SetGender(1) // Male + info.SetStr(50.0) + info.SetSta(45.0) + info.SetAgi(40.0) + info.SetWis(35.0) + info.SetIntel(30.0) + + // Set HP and Power for testing + entity.SetTotalHP(1000) + entity.SetHP(1000) + entity.SetTotalPower(500) + entity.SetPower(500) + + return entity +} + +// Test Manager Creation and Configuration +func TestManagerCreation(t *testing.T) { + manager := NewManager() + if manager == nil { + t.Fatal("NewManager returned nil") + } + + // Check default configuration + config := manager.GetConfiguration() + if config["enable_statistics"].(bool) != true { + t.Error("Expected statistics to be enabled by default") + } + if config["enable_validation"].(bool) != true { + t.Error("Expected validation to be enabled by default") + } + if config["enable_pet_management"].(bool) != true { + t.Error("Expected pet management to be enabled by default") + } +} + +// Test Entity Creation and Destruction +func TestEntityCreationAndDestruction(t *testing.T) { + manager := createTestManager() + + // Test entity creation + entity := manager.CreateEntity() + if entity == nil { + t.Fatal("CreateEntity returned nil") + } + + if entity.GetInfoStruct() == nil { + t.Error("Entity info struct is nil") + } + + // Check initial state + if entity.IsInCombat() { + t.Error("New entity should not be in combat") + } + if entity.IsCasting() { + t.Error("New entity should not be casting") + } + + // Test entity destruction + err := manager.DestroyEntity(entity) + if err != nil { + t.Errorf("DestroyEntity failed: %v", err) + } + + // Test destroying nil entity + err = manager.DestroyEntity(nil) + if err == nil { + t.Error("Expected error when destroying nil entity") + } +} + +// Test Entity State Management +func TestEntityStateManagement(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Test combat state + if manager.IsEntityInCombat(entity) { + t.Error("Entity should not be in combat initially") + } + + manager.SetEntityInCombat(entity, true) + if !manager.IsEntityInCombat(entity) { + t.Error("Entity should be in combat after setting") + } + + manager.SetEntityInCombat(entity, false) + if manager.IsEntityInCombat(entity) { + t.Error("Entity should not be in combat after unsetting") + } + + // Test casting state + if manager.IsEntityCasting(entity) { + t.Error("Entity should not be casting initially") + } + + manager.SetEntityCasting(entity, true) + if !manager.IsEntityCasting(entity) { + t.Error("Entity should be casting after setting") + } + + manager.SetEntityCasting(entity, false) + if manager.IsEntityCasting(entity) { + t.Error("Entity should not be casting after unsetting") + } + + // Test with nil entity + if manager.IsEntityInCombat(nil) { + t.Error("Nil entity should not be in combat") + } + if manager.IsEntityCasting(nil) { + t.Error("Nil entity should not be casting") + } +} + +// Test InfoStruct Management +func TestInfoStructManagement(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Test getting info struct + info := manager.GetEntityInfoStruct(entity) + if info == nil { + t.Fatal("GetEntityInfoStruct returned nil") + } + + if info.GetName() != "Test Entity" { + t.Errorf("Expected 'Test Entity', got '%s'", info.GetName()) + } + if info.GetLevel() != 10 { + t.Errorf("Expected level 10, got %d", info.GetLevel()) + } + + // Test setting new info struct + newInfo := NewInfoStruct() + newInfo.SetName("Updated Entity") + newInfo.SetLevel(20) + + manager.SetEntityInfoStruct(entity, newInfo) + + updatedInfo := manager.GetEntityInfoStruct(entity) + if updatedInfo.GetName() != "Updated Entity" { + t.Error("Info struct was not updated") + } + + // Test with nil entity + nilInfo := manager.GetEntityInfoStruct(nil) + if nilInfo != nil { + t.Error("Expected nil for nil entity info struct") + } +} + +// Test Spell Effect Management +func TestSpellEffectManagement(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Test adding maintained spell + success := manager.AddMaintainedSpell(entity, "Test Buff", 1001, 60.0, 1) + if !success { + t.Error("Failed to add maintained spell") + } + + // Test getting maintained spell + effect := entity.GetMaintainedSpell(1001) + if effect == nil { + t.Error("Failed to retrieve maintained spell") + } + + // Test removing maintained spell + success = manager.RemoveMaintainedSpell(entity, 1001) + if !success { + t.Error("Failed to remove maintained spell") + } + + // Verify removal + effect = entity.GetMaintainedSpell(1001) + if effect != nil { + t.Error("Maintained spell was not removed") + } + + // Test adding temporary spell effect + success = manager.AddSpellEffect(entity, 2001, 12345, 30.0) + if !success { + t.Error("Failed to add spell effect") + } + + // Test removing spell effect + success = manager.RemoveSpellEffect(entity, 2001) + if !success { + t.Error("Failed to remove spell effect") + } + + // Test detrimental spell + manager.AddDetrimentalSpell(entity, 3001, 12345, 45.0, 1) + + // Test removing detrimental spell + success = manager.RemoveDetrimentalSpell(entity, 3001, 12345) + if !success { + t.Error("Failed to remove detrimental spell") + } + + // Test with nil entity + if manager.AddMaintainedSpell(nil, "Test", 1, 1.0, 1) { + t.Error("Should not be able to add spell to nil entity") + } +} + +// Test Pet Management +func TestPetManagement(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + pet := createTestEntity() + pet.GetInfoStruct().SetName("Test Pet") + + // Test setting summon pet + manager.SetEntityPet(entity, pet) + retrievedPet := manager.GetEntityPet(entity) + if retrievedPet != pet { + t.Error("Failed to set/get summon pet") + } + + if pet.GetOwner() != entity.GetID() { + t.Error("Pet owner was not set correctly") + } + if pet.GetPetType() != PetTypeSummon { + t.Error("Pet type was not set correctly") + } + + // Test charmed pet + charmedPet := createTestEntity() + charmedPet.GetInfoStruct().SetName("Charmed Pet") + + manager.SetEntityCharmedPet(entity, charmedPet) + retrievedCharmed := manager.GetEntityCharmedPet(entity) + if retrievedCharmed != charmedPet { + t.Error("Failed to set/get charmed pet") + } + + if charmedPet.GetPetType() != PetTypeCharm { + t.Error("Charmed pet type was not set correctly") + } + + // Test dismissing pet + manager.DismissPet(pet) + if !pet.IsPetDismissing() { + t.Error("Pet was not marked for dismissal") + } + + // Test with nil entities + if manager.GetEntityPet(nil) != nil { + t.Error("Expected nil for nil entity pet") + } + if manager.GetEntityCharmedPet(nil) != nil { + t.Error("Expected nil for nil entity charmed pet") + } +} + +// Test Movement and Position +func TestMovementAndPosition(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Set initial position + entity.SetX(100.0) + entity.SetY(200.0, false) + entity.SetZ(300.0) + entity.SetHeading(150, 150) // Heading is int16, not float + + // Update position tracking + manager.UpdateEntityPosition(entity) + + // Check if position was recorded + lastX, lastY, lastZ, lastHeading := entity.GetLastPosition() + if lastX == -1 || lastY == -1 || lastZ == -1 || lastHeading == -1 { + t.Error("Position was not updated correctly") + } + + // Test movement detection (should not have moved yet) + if manager.HasEntityMoved(entity) { + t.Error("Entity should not have moved yet") + } + + // Move entity + entity.SetX(101.0) + + // Now should detect movement + if !manager.HasEntityMoved(entity) { + t.Error("Entity movement was not detected") + } + + // Test speed calculation + entity.SetMaxSpeed(10.0) + entity.SetBaseSpeed(8.0) + entity.SetSpeedMultiplier(1.5) + + speed := manager.GetEntityEffectiveSpeed(entity) + expected := float32(8.0 * 1.5) // base * multiplier + if speed != expected { + t.Errorf("Expected speed %.2f, got %.2f", expected, speed) + } + + // Test with nil entity + if manager.HasEntityMoved(nil) { + t.Error("Nil entity should not have moved") + } + if manager.GetEntityEffectiveSpeed(nil) != 0.0 { + t.Error("Nil entity speed should be 0.0") + } +} + +// Test Stat Calculations +func TestStatCalculations(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Test primary stat calculation + primaryStat := manager.GetEntityPrimaryStat(entity) + if primaryStat != 50 { // STR is highest at 50.0 + t.Errorf("Expected primary stat 50, got %d", primaryStat) + } + + // Test resistance calculations + info := entity.GetInfoStruct() + info.SetResistance("heat", 25) + info.SetResistance("cold", 30) + + heatResist := manager.GetEntityResistance(entity, "heat") + if heatResist != 25 { + t.Errorf("Expected heat resistance 25, got %d", heatResist) + } + + coldResist := manager.GetEntityResistance(entity, "cold") + if coldResist != 30 { + t.Errorf("Expected cold resistance 30, got %d", coldResist) + } + + unknownResist := manager.GetEntityResistance(entity, "unknown") + if unknownResist != 0 { + t.Errorf("Expected unknown resistance 0, got %d", unknownResist) + } + + // Test with nil entity + if manager.GetEntityPrimaryStat(nil) != 0 { + t.Error("Nil entity primary stat should be 0") + } + if manager.GetEntityResistance(nil, "heat") != 0 { + t.Error("Nil entity resistance should be 0") + } +} + +// Test Bonus Calculations +func TestBonusCalculations(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Calculate bonuses (this should recalculate all stats) + manager.CalculateEntityBonuses(entity) + + // Stats should be recalculated (may be different due to effects) + newStr := entity.GetStr() + + // Since we don't have spell effects, stats should remain the same or reset to base + if newStr < 0 { + t.Error("Strength should not be negative after bonus calculation") + } +} + +// Test Packet Building (these would normally require packet definitions) +func TestPacketBuilding(t *testing.T) { + manager := createTestManager() + + entityID := int32(12345) + clientVersion := uint32(57048) + + // Test entity verbs request - should fail without packet definitions + _, err := manager.SendEntityVerbsRequest(entityID, clientVersion) + if err == nil { + t.Error("Expected error for missing packet definition, but got none") + } + + // Test entity verbs response - should fail without packet definitions + verbs := []map[string]interface{}{ + {"verb_id": 1, "verb_name": "examine"}, + {"verb_id": 2, "verb_name": "attack"}, + } + _, err = manager.SendEntityVerbsResponse(entityID, clientVersion, verbs) + if err == nil { + t.Error("Expected error for missing packet definition, but got none") + } + + // Test entity verb action - should fail without packet definitions + _, err = manager.SendEntityVerbsVerb(entityID, 67890, clientVersion, 1, "examine") + if err == nil { + t.Error("Expected error for missing packet definition, but got none") + } +} + +// Test Effect Processing +func TestEffectProcessing(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Add a spell effect + manager.AddSpellEffect(entity, 1001, 12345, 30.0) + + // Process effects (should handle cleanup, etc.) + manager.ProcessEntityEffects(entity) + + // Cleanup effects + manager.CleanupEntityEffects(entity) + + // Test with nil entity (should not crash) + manager.ProcessEntityEffects(nil) + manager.CleanupEntityEffects(nil) +} + +// Test Statistics and Monitoring +func TestStatisticsAndMonitoring(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Perform some operations to generate statistics + manager.CreateEntity() + manager.SetEntityInCombat(entity, true) + manager.AddSpellEffect(entity, 1001, 12345, 30.0) + manager.RemoveSpellEffect(entity, 1001) + manager.CalculateEntityBonuses(entity) + + stats := manager.GetStatistics() + if stats == nil { + t.Fatal("Statistics returned nil") + } + + // Check some expected statistics exist + if _, exists := stats["entities_created"]; !exists { + t.Error("Expected entities_created statistic") + } + if _, exists := stats["spell_effects_added"]; !exists { + t.Error("Expected spell_effects_added statistic") + } + if _, exists := stats["spell_effects_removed"]; !exists { + t.Error("Expected spell_effects_removed statistic") + } + if _, exists := stats["bonus_calculations"]; !exists { + t.Error("Expected bonus_calculations statistic") + } + if _, exists := stats["combat_actions"]; !exists { + t.Error("Expected combat_actions statistic") + } + + // Check that some statistics have non-zero values + if stats["entities_created"].(int64) < 1 { + t.Error("Expected at least 1 entity created") + } + if stats["spell_effects_added"].(int64) < 1 { + t.Error("Expected at least 1 spell effect added") + } + + // Test reset statistics + manager.ResetStatistics() + newStats := manager.GetStatistics() + if newStats["entities_created"].(int64) != 0 { + t.Error("Expected entities created to be reset to 0") + } +} + +// Test Configuration Management +func TestConfigurationManagement(t *testing.T) { + manager := createTestManager() + + // Test default configuration + config := manager.GetConfiguration() + if !config["enable_statistics"].(bool) { + t.Error("Expected statistics to be enabled by default") + } + + // Test configuration update + newConfig := map[string]interface{}{ + "enable_statistics": false, + "enable_combat_logging": true, + "max_entities_per_zone": 5000, + } + manager.SetConfiguration(newConfig) + + updatedConfig := manager.GetConfiguration() + if updatedConfig["enable_statistics"].(bool) { + t.Error("Expected statistics to be disabled after update") + } + if !updatedConfig["enable_combat_logging"].(bool) { + t.Error("Expected combat logging to be enabled after update") + } + if updatedConfig["max_entities_per_zone"].(int) != 5000 { + t.Error("Expected max_entities_per_zone to be updated to 5000") + } +} + +// Test Validation and Health Checks +func TestValidationAndHealthChecks(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Test health check + if !manager.IsHealthy() { + t.Error("Manager should be healthy") + } + + // Test entity validation + issues := manager.ValidateEntity(entity) + if len(issues) > 0 { + t.Errorf("Expected no validation issues, got %d: %v", len(issues), issues) + } + + // Test validation with invalid entity + invalidEntity := createTestEntity() + invalidEntity.GetInfoStruct().SetLevel(-1) + invalidEntity.GetInfoStruct().SetName("") + invalidEntity.SetTotalHP(-100) + + validationIssues := manager.ValidateEntity(invalidEntity) + if len(validationIssues) == 0 { + t.Error("Expected validation issues for invalid entity") + } + + // Check specific issues (should have at least 3 issues) + if len(validationIssues) < 3 { + t.Errorf("Expected at least 3 validation issues, got %d: %v", len(validationIssues), validationIssues) + } + + // Test validation with nil entity + nilIssues := manager.ValidateEntity(nil) + if len(nilIssues) != 1 || nilIssues[0] != "entity is nil" { + t.Error("Expected 'entity is nil' issue for nil entity") + } +} + +// Test Thread Safety (basic test) +func TestThreadSafety(t *testing.T) { + manager := createTestManager() + entity := createTestEntity() + + // Perform concurrent operations + done := make(chan bool, 10) + + // Start multiple goroutines performing different operations + for i := 0; i < 10; i++ { + go func(id int) { + defer func() { done <- true }() + + // Mix of read and write operations + manager.GetEntityInfoStruct(entity) + manager.SetEntityInCombat(entity, true) + manager.SetEntityInCombat(entity, false) + manager.GetStatistics() + manager.CalculateEntityBonuses(entity) + manager.ProcessEntityEffects(entity) + }(i) + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Manager should still be functional + if !manager.IsHealthy() { + t.Error("Manager appears corrupted after concurrent access") + } +} + +// Test Entity Stat Methods +func TestEntityStatMethods(t *testing.T) { + entity := createTestEntity() + + // Test individual stat getters + if entity.GetStr() != 50 { + t.Errorf("Expected STR 50, got %d", entity.GetStr()) + } + if entity.GetSta() != 45 { + t.Errorf("Expected STA 45, got %d", entity.GetSta()) + } + if entity.GetAgi() != 40 { + t.Errorf("Expected AGI 40, got %d", entity.GetAgi()) + } + if entity.GetWis() != 35 { + t.Errorf("Expected WIS 35, got %d", entity.GetWis()) + } + if entity.GetIntel() != 30 { + t.Errorf("Expected INT 30, got %d", entity.GetIntel()) + } + + // Test primary stat calculation + primary := entity.GetPrimaryStat() + if primary != 50 { // STR is highest + t.Errorf("Expected primary stat 50, got %d", primary) + } +} + +// Test Entity Health and Power +func TestEntityHealthAndPower(t *testing.T) { + entity := createTestEntity() + + // Test initial values + if entity.GetTotalHP() != 1000 { + t.Errorf("Expected total HP 1000, got %d", entity.GetTotalHP()) + } + if entity.GetHP() != 1000 { + t.Errorf("Expected current HP 1000, got %d", entity.GetHP()) + } + if entity.GetTotalPower() != 500 { + t.Errorf("Expected total power 500, got %d", entity.GetTotalPower()) + } + if entity.GetPower() != 500 { + t.Errorf("Expected current power 500, got %d", entity.GetPower()) + } + + // Test alive state + if !entity.IsAlive() { + t.Error("Entity should be alive with positive HP") + } + if entity.IsDead() { + t.Error("Entity should not be dead with positive HP") + } + + // Test setting HP to 0 + entity.SetHP(0) + entity.SetAlive(false) // Need to explicitly set alive state + if entity.IsAlive() { + t.Error("Entity should not be alive with 0 HP") + } + if !entity.IsDead() { + t.Error("Entity should be dead with 0 HP") + } +} + +// Test InfoStruct Methods +func TestInfoStructMethods(t *testing.T) { + info := NewInfoStruct() + + // Test basic properties + info.SetName("Test Character") + if info.GetName() != "Test Character" { + t.Error("Name was not set correctly") + } + + info.SetLevel(25) + if info.GetLevel() != 25 { + t.Error("Level was not set correctly") + } + + info.SetClass1(5) + if info.GetClass1() != 5 { + t.Error("Class was not set correctly") + } + + info.SetRace(2) + if info.GetRace() != 2 { + t.Error("Race was not set correctly") + } + + // Test concentration system + info.SetMaxConcentration(10) + if info.GetMaxConcentration() != 10 { + t.Error("Max concentration was not set correctly") + } + + success := info.AddConcentration(5) + if !success { + t.Error("Failed to add concentration") + } + if info.GetCurConcentration() != 5 { + t.Error("Current concentration was not updated correctly") + } + + // Try to add too much concentration + success = info.AddConcentration(10) + if success { + t.Error("Should not be able to add more concentration than available") + } + + // Remove concentration + info.RemoveConcentration(3) + if info.GetCurConcentration() != 2 { + t.Error("Concentration was not removed correctly") + } + + // Test coin system + info.AddCoins(1234567) // 1.23 plat, 45 gold, 67 copper + totalCoins := info.GetCoins() + if totalCoins != 1234567 { + t.Errorf("Expected %d total coins, got %d", 1234567, totalCoins) + } + + // Test removing coins + success = info.RemoveCoins(100000) + if !success { + t.Error("Failed to remove coins") + } + + newTotal := info.GetCoins() + if newTotal != 1134567 { + t.Errorf("Expected %d coins after removal, got %d", 1134567, newTotal) + } + + // Try to remove more coins than available + success = info.RemoveCoins(2000000) + if success { + t.Error("Should not be able to remove more coins than available") + } +} + +// Test InfoStruct Clone +func TestInfoStructClone(t *testing.T) { + original := NewInfoStruct() + original.SetName("Original") + original.SetLevel(50) + original.SetStr(100.0) + original.AddCoins(5000) + + clone := original.Clone() + if clone == nil { + t.Fatal("Clone returned nil") + } + + // Verify clone has same data + if clone.GetName() != original.GetName() { + t.Error("Clone has different name") + } + if clone.GetLevel() != original.GetLevel() { + t.Error("Clone has different level") + } + if clone.GetStr() != original.GetStr() { + t.Error("Clone has different strength") + } + if clone.GetCoins() != original.GetCoins() { + t.Error("Clone has different coins") + } + + // Verify clone is independent + clone.SetName("Modified Clone") + if original.GetName() == "Modified Clone" { + t.Error("Modifying clone affected original") + } +} + +// Benchmark tests +func BenchmarkCreateEntity(b *testing.B) { + manager := createTestManager() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + manager.CreateEntity() + } +} + +func BenchmarkGetEntityInfoStruct(b *testing.B) { + manager := createTestManager() + entity := createTestEntity() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + manager.GetEntityInfoStruct(entity) + } +} + +func BenchmarkCalculateEntityBonuses(b *testing.B) { + manager := createTestManager() + entity := createTestEntity() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + manager.CalculateEntityBonuses(entity) + } +} + +func BenchmarkGetStatistics(b *testing.B) { + manager := createTestManager() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + manager.GetStatistics() + } +} \ No newline at end of file diff --git a/internal/packets/opcodes.go b/internal/packets/opcodes.go index 35eaa69..8c837f8 100644 --- a/internal/packets/opcodes.go +++ b/internal/packets/opcodes.go @@ -128,6 +128,11 @@ const ( OP_EqCollectionFilterCmd OP_EqCollectionItemCmd + // Entity system opcodes (additional ones beyond existing) + OP_EntityVerbsRequestMsg + OP_EntityVerbsReplyMsg + OP_EntityVerbsVerbMsg + // Add more opcodes as needed... _maxInternalOpcode // Sentinel value ) @@ -217,6 +222,11 @@ var OpcodeNames = map[InternalOpcode]string{ OP_EqCollectionUpdateCmd: "OP_EqCollectionUpdateCmd", OP_EqCollectionFilterCmd: "OP_EqCollectionFilterCmd", OP_EqCollectionItemCmd: "OP_EqCollectionItemCmd", + + // Entity system opcodes (additional ones beyond existing) + OP_EntityVerbsRequestMsg: "OP_EntityVerbsRequestMsg", + OP_EntityVerbsReplyMsg: "OP_EntityVerbsReplyMsg", + OP_EntityVerbsVerbMsg: "OP_EntityVerbsVerbMsg", } // OpcodeManager handles the mapping between client-specific opcodes and internal opcodes