simplify/enhance entity
This commit is contained in:
parent
4b32b0e3ee
commit
2b233535aa
@ -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[:])
|
||||
|
651
internal/entity/manager.go
Normal file
651
internal/entity/manager.go
Normal file
@ -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
|
||||
}
|
802
internal/entity/manager_test.go
Normal file
802
internal/entity/manager_test.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user