diff --git a/Source/Game/AI/CoreAI/CreatureAI.cs b/Source/Game/AI/CoreAI/CreatureAI.cs index b446bc2c8..a1ec8e2ac 100644 --- a/Source/Game/AI/CoreAI/CreatureAI.cs +++ b/Source/Game/AI/CoreAI/CreatureAI.cs @@ -29,6 +29,7 @@ namespace Game.AI { public class CreatureAI : UnitAI { + bool _isEngaged; bool _moveInLOSLocked; List _boundary = new(); bool _negateBoundary; @@ -201,6 +202,12 @@ namespace Game.AI } } + public override void JustEnteredCombat(Unit who) + { + if (!IsEngaged() && !me.CanHaveThreatList()) + EngagementStart(who); + } + // Called for reaction at stopping attack at no attackers or targets public virtual void EnterEvadeMode(EvadeReason why = EvadeReason.Other) { @@ -231,7 +238,7 @@ namespace Game.AI public bool UpdateVictim() { - if (!me.IsEngaged()) + if (!IsEngaged()) return false; if (!me.HasReactState(ReactStates.Passive)) @@ -254,23 +261,53 @@ namespace Game.AI return true; } + public void EngagementStart(Unit who) + { + if (_isEngaged) + { + //Log.outError(LogFilter.ScriptsAi, $"CreatureAI::EngagementStart called even though creature is already engaged. Creature debug info:\n{me.GetDebugInfo()}"); + return; + } + _isEngaged = true; + + me.AtEngage(who); + } + + public void EngagementOver() + { + if (!_isEngaged) + { + //Log.outError(LogFilter.ScriptsAi, $"CreatureAI::EngagementOver called even though creature is not currently engaged. Creature debug info:\n{me.GetDebugInfo()}"); + return; + } + _isEngaged = false; + + me.AtDisengage(); + } + public bool _EnterEvadeMode(EvadeReason why = EvadeReason.Other) { - if (!me.IsAlive()) + if (me.IsInEvadeMode()) return false; + if (!me.IsAlive()) + { + EngagementOver(); + return false; + } + me.RemoveAurasOnEvade(); // sometimes bosses stuck in combat? me.CombatStop(true); - me.GetThreatManager().NotifyDisengaged(); me.SetLootRecipient(null); me.ResetPlayerDamageReq(); me.SetLastDamagedTime(0); me.SetCannotReachTarget(false); me.DoNotReacquireTarget(); + EngagementOver(); - return !me.IsInEvadeMode(); + return true; } public CypherStrings VisualizeBoundary(int duration, Unit owner = null, bool fill = false) @@ -404,11 +441,22 @@ namespace Game.AI me.DoImmediateBoundaryCheck(); } + // Called for reaction whenever a new non-offline unit is added to the threat list + public virtual void JustStartedThreateningMe(Unit who) + { + if (!IsEngaged()) + EngagementStart(who); + } + // Called for reaction when initially engaged - this will always happen _after_ JustEnteredCombat public virtual void JustEngagedWith(Unit who) { } // Called when the creature is killed - public virtual void JustDied(Unit killer) { } + public virtual void JustDied(Unit killer) + { + if (IsEngaged()) + EngagementOver(); + } // Called when the creature kills a unit public virtual void KilledUnit(Unit victim) { } @@ -521,6 +569,8 @@ namespace Game.AI public virtual bool IsEscortNPC(bool onlyIfActive) { return false; } public List GetBoundary() { return _boundary; } + + public bool IsEngaged() { return _isEngaged; } } public class AISpellInfoType diff --git a/Source/Game/AI/CoreAI/GuardAI.cs b/Source/Game/AI/CoreAI/GuardAI.cs index 463422e82..1d5a0c6d9 100644 --- a/Source/Game/AI/CoreAI/GuardAI.cs +++ b/Source/Game/AI/CoreAI/GuardAI.cs @@ -48,7 +48,7 @@ namespace Game.AI { me.GetMotionMaster().MoveIdle(); me.CombatStop(true); - me.GetThreatManager().NotifyDisengaged(); + EngagementOver(); return; } @@ -56,7 +56,7 @@ namespace Game.AI me.RemoveAllAuras(); me.CombatStop(true); - me.GetThreatManager().NotifyDisengaged(); + EngagementOver(); me.GetMotionMaster().MoveTargetedHome(); } diff --git a/Source/Game/AI/CoreAI/PassiveAI.cs b/Source/Game/AI/CoreAI/PassiveAI.cs index c664c5e51..cfcc143fa 100644 --- a/Source/Game/AI/CoreAI/PassiveAI.cs +++ b/Source/Game/AI/CoreAI/PassiveAI.cs @@ -77,6 +77,21 @@ namespace Game.AI public override void MoveInLineOfSight(Unit who) { } + public override void JustEnteredCombat(Unit who) + { + EngagementStart(who); + } + + public override void JustExitedCombat() + { + EngagementOver(); + } + + public override void JustStartedThreateningMe(Unit who) + { + + } + public override void EnterEvadeMode(EvadeReason why) { } } @@ -89,6 +104,8 @@ namespace Game.AI public override void MoveInLineOfSight(Unit unit) { } public override void AttackStart(Unit unit) { } + public override void JustStartedThreateningMe(Unit unit) { } + public override void JustEnteredCombat(Unit who) { } public override void UpdateAI(uint diff) { } public override void JustAppeared() { } public override void EnterEvadeMode(EvadeReason why) { } diff --git a/Source/Game/AI/CoreAI/TotemAI.cs b/Source/Game/AI/CoreAI/TotemAI.cs index 09a6af8b7..21239d154 100644 --- a/Source/Game/AI/CoreAI/TotemAI.cs +++ b/Source/Game/AI/CoreAI/TotemAI.cs @@ -21,7 +21,7 @@ using Game.Maps; namespace Game.AI { - public class TotemAI : CreatureAI + public class TotemAI : NullCreatureAI { ObjectGuid _victimGuid; @@ -30,15 +30,6 @@ namespace Game.AI _victimGuid = ObjectGuid.Empty; } - public override void MoveInLineOfSight(Unit who) { } - - public override void JustAppeared() { } - - public override void EnterEvadeMode(EvadeReason why) - { - me.CombatStop(true); - } - public override void UpdateAI(uint diff) { if (me.ToTotem().GetTotemType() != TotemType.Active) diff --git a/Source/Game/AI/CoreAI/UnitAI.cs b/Source/Game/AI/CoreAI/UnitAI.cs index c61bc8fd0..391ed173a 100644 --- a/Source/Game/AI/CoreAI/UnitAI.cs +++ b/Source/Game/AI/CoreAI/UnitAI.cs @@ -536,7 +536,7 @@ namespace Game.AI _me = unit; _dist = dist; _playerOnly = playerOnly; - _exception = !withTank ? _me.GetThreatManager().GetCurrentVictim() : null; + _exception = !withTank ? unit.GetThreatManager().GetLastVictim() : null; _aura = aura; } diff --git a/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs b/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs index 3060d32fe..2c253b8ca 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedEscortAI.cs @@ -135,9 +135,10 @@ namespace Game.AI { me.RemoveAllAuras(); me.CombatStop(true); - me.GetThreatManager().NotifyDisengaged(); me.SetLootRecipient(null); + EngagementOver(); + if (HasEscortState(EscortState.Escorting)) { AddEscortState(EscortState.Returning); diff --git a/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs b/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs index bfd740231..322593203 100644 --- a/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs +++ b/Source/Game/AI/ScriptedAI/ScriptedFollowerAI.cs @@ -123,15 +123,19 @@ namespace Game.AI public override void EnterEvadeMode(EvadeReason why) { if (!me.IsAlive()) + { + EngagementOver(); return; + } me.RemoveAllAuras(); me.CombatStop(true); - me.GetThreatManager().NotifyDisengaged(); me.SetLootRecipient(null); me.SetCannotReachTarget(false); me.DoNotReacquireTarget(); + EngagementOver(); + if (HasFollowState(FollowState.Inprogress)) { Log.outDebug(LogFilter.Scripts, "FollowerAI left combat, returning to CombatStartPosition."); diff --git a/Source/Game/Combat/CombatManager.cs b/Source/Game/Combat/CombatManager.cs index 3a8d81ce8..adaaa7a7b 100644 --- a/Source/Game/Combat/CombatManager.cs +++ b/Source/Game/Combat/CombatManager.cs @@ -234,15 +234,9 @@ namespace Game.Combat public static void NotifyAICombat(Unit me, Unit other) { - if (!me.IsAIEnabled()) - return; - - me.GetAI().JustEnteredCombat(other); - - Creature cMe = me.ToCreature(); - if (cMe != null) - if (!cMe.CanHaveThreatList()) - cMe.GetAI().JustEngagedWith(other); + UnitAI ai = me.GetAI(); + if (ai != null) + ai.JustEnteredCombat(other); } void PutReference(ObjectGuid guid, CombatReference refe) @@ -277,14 +271,14 @@ namespace Game.Combat { _owner.AddUnitFlag(UnitFlags.InCombat); _owner.AtEnterCombat(); - if (!_owner.CanHaveThreatList() && !_owner.IsEngaged()) + if (_owner.IsCreature()) _owner.AtEngage(GetAnyTarget()); } else { _owner.RemoveUnitFlag(UnitFlags.InCombat); _owner.AtExitCombat(); - if (_owner.IsEngaged() && !(_owner.IsCreature() && _owner.CanHaveThreatList() && _owner.ToCreature().IsAIEnabled())) + if (!_owner.IsCreature()) _owner.AtDisengage(); } diff --git a/Source/Game/Combat/ThreatManager.cs b/Source/Game/Combat/ThreatManager.cs index 9d09d8e03..7946a2d07 100644 --- a/Source/Game/Combat/ThreatManager.cs +++ b/Source/Game/Combat/ThreatManager.cs @@ -37,6 +37,7 @@ namespace Game.Combat uint _updateTimer; List _sortedThreatList = new(); Dictionary _myThreatListEntries = new(); + List _needsAIUpdate = new(); ThreatReference _currentVictimRef; ThreatReference _fixateRef; @@ -98,6 +99,12 @@ namespace Game.Combat if (_currentVictimRef == null || _currentVictimRef.ShouldBeOffline()) UpdateVictim(); + Cypher.Assert(_currentVictimRef == null || _currentVictimRef.IsAvailable()); + return _currentVictimRef?.GetVictim(); + } + + public Unit GetLastVictim() + { if (_currentVictimRef != null && !_currentVictimRef.ShouldBeOffline()) return _currentVictimRef.GetVictim(); @@ -294,7 +301,7 @@ namespace Game.Combat } } - if (targetRefe.IsOnline()) + if (targetRefe.IsOnline()) targetRefe.AddThreat(amount); return; } @@ -306,14 +313,13 @@ namespace Game.Combat // afterwards, we evaluate whether this is an online reference (it might not be an acceptable target, but we need to add it to our threat list before we check!) newRefe.UpdateOffline(); - if (newRefe.IsOnline()) // ...and if the ref is online it also gets the threat it should have + if (newRefe.IsOnline()) // we only add the threat if the ref is currently available newRefe.AddThreat(amount); - if (!_owner.IsEngaged()) - { - _owner.AtEngage(target); + if (_currentVictimRef == null) UpdateVictim(); - } + else + ProcessAIUpdates(); } void ScaleThreat(Unit target, float factor) @@ -398,18 +404,6 @@ namespace Game.Combat } } - /// - /// THIS SHOULD ONLY BE CALLED FROM A CREATURE'S AI (typically in EnterEvadeMode) - /// notify the unit that the AI has disengaged - /// - public void NotifyDisengaged() - { - // note: i don't really like having this here - // (maybe engage flag should be in creature ai? it's inherently an AI property...) - if (_owner.IsEngaged()) - _owner.AtDisengage(); - } - public void FixateTarget(Unit target) { if (target) @@ -446,6 +440,7 @@ namespace Game.Combat NeedClientUpdate = false; } + ProcessAIUpdates(); } ThreatReference ReselectVictim() @@ -454,7 +449,7 @@ namespace Game.Combat return null; foreach (var pair in _myThreatListEntries) - pair.Value.UpdateOffline(); + pair.Value.UpdateOffline(); // AI notifies are processed in ::UpdateVictim caller // fixated target is always preferred if (_fixateRef != null && _fixateRef.IsAvailable()) @@ -509,6 +504,16 @@ namespace Game.Combat return null; } + void ProcessAIUpdates() + { + CreatureAI ai = _owner.ToCreature().GetAI(); + List v = new(_needsAIUpdate); // _needClientUpdate is now empty in case this triggers a recursive call + if (ai == null) + return; + foreach (ThreatReference refe in v) + ai.JustStartedThreateningMe(refe.GetVictim()); + } + // returns true if a is LOWER on the threat list than b public static bool CompareReferencesLT(ThreatReference a, ThreatReference b, float aWeight) { @@ -563,17 +568,17 @@ namespace Game.Combat threat *= victimMgr._singleSchoolModifiers[(int)SpellSchools.Arcane]; break; default: + { + if (victimMgr._multiSchoolModifiers.TryGetValue(mask, out float value)) { - if (victimMgr._multiSchoolModifiers.TryGetValue(mask, out float value)) - { - threat *= value; - break; - } - float mod = victim.GetTotalAuraMultiplierByMiscMask(AuraType.ModThreat, (uint)mask); - victimMgr._multiSchoolModifiers[mask] = mod; - threat *= mod; + threat *= value; break; } + float mod = victim.GetTotalAuraMultiplierByMiscMask(AuraType.ModThreat, (uint)mask); + victimMgr._multiSchoolModifiers[mask] = mod; + threat *= mod; + break; + } } return threat; } @@ -671,7 +676,7 @@ namespace Game.Combat threatClear.UnitGUID = _owner.GetGUID(); _owner.SendMessageToSet(threatClear, false); } - + public void SendRemoveToClients(Unit victim) { ThreatRemove threatRemove = new(); @@ -797,6 +802,8 @@ namespace Game.Combat // Resets the specified unit's threat to zero public void ResetThreat(Unit target) { ScaleThreat(target, 0.0f); } + + public void RegisterForAIUpdate(ThreatReference refe) { _needsAIUpdate.Add(refe); } } public enum TauntState @@ -827,7 +834,7 @@ namespace Game.Combat _owner = mgr._owner as Creature; _mgr = mgr; _victim = victim; - Online = ShouldBeSuppressed() ? OnlineState.Suppressed : OnlineState.Online; + Online = OnlineState.Offline; } public void AddThreat(float amount) @@ -866,6 +873,7 @@ namespace Game.Combat { Online = ShouldBeSuppressed() ? OnlineState.Suppressed : OnlineState.Online; ListNotifyChanged(); + _mgr.RegisterForAIUpdate(this); } } diff --git a/Source/Game/Entities/Creature/Creature.cs b/Source/Game/Entities/Creature/Creature.cs index c818834ca..a4b1df281 100644 --- a/Source/Game/Entities/Creature/Creature.cs +++ b/Source/Game/Entities/Creature/Creature.cs @@ -1047,6 +1047,14 @@ namespace Game.Entities && !GetCreatureTemplate().FlagsExtra.HasAnyFlag(CreatureFlagsExtra.NoXpAtKill); } + public override bool IsEngaged() + { + CreatureAI ai = GetAI(); + if (ai != null) + return ai.IsEngaged(); + return false; + } + public override void AtEngage(Unit target) { base.AtEngage(target); @@ -1353,7 +1361,7 @@ namespace Game.Entities CreatureBaseStats stats = Global.ObjectMgr.GetCreatureBaseStats(level, cInfo.UnitClass); // health - float healthmod = _GetHealthMod(rank); + float healthmod = GetHealthMod(rank); uint basehp = (uint)GetMaxHealthByLevel(level); uint health = (uint)(basehp * healthmod); @@ -1416,7 +1424,7 @@ namespace Game.Entities } } - float _GetHealthMod(CreatureEliteType Rank) + public float GetHealthMod(CreatureEliteType Rank) { switch (Rank) // define rates for each elite rank { @@ -1567,7 +1575,7 @@ namespace Game.Entities curhealth = m_creatureData.curhealth; if (curhealth != 0) { - curhealth = (uint)(curhealth * _GetHealthMod(GetCreatureTemplate().Rank)); + curhealth = (uint)(curhealth * GetHealthMod(GetCreatureTemplate().Rank)); if (curhealth < 1) curhealth = 1; } diff --git a/Source/Game/Entities/Unit/Unit.Combat.cs b/Source/Game/Entities/Unit/Unit.Combat.cs index e637f8651..13f020d99 100644 --- a/Source/Game/Entities/Unit/Unit.Combat.cs +++ b/Source/Game/Entities/Unit/Unit.Combat.cs @@ -57,8 +57,9 @@ namespace Game.Entities RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.LeavingCombat); } - public virtual void AtEngage(Unit target) { m_isEngaged = true; } - public virtual void AtDisengage() { m_isEngaged = false; } + public virtual void AtEngage(Unit target) { } + + public virtual void AtDisengage() { } public void CombatStop(bool includingCast = false, bool mutualPvP = true) { @@ -147,10 +148,10 @@ namespace Game.Entities public bool CanHaveThreatList() { return m_threatManager.CanHaveThreatList(); } - // This value can be different from IsInCombat: + // This value can be different from IsInCombat, for example: // - when a projectile spell is midair against a creature (combat on launch - threat+aggro on impact) // - when the creature has no targets left, but the AI has not yet ceased engaged logic - public bool IsEngaged() { return m_isEngaged; } + public virtual bool IsEngaged() { return IsInCombat(); } public bool IsEngagedBy(Unit who) { return CanHaveThreatList() ? IsThreatenedBy(who) : IsInCombatWith(who); } @@ -230,7 +231,6 @@ namespace Game.Entities minion.StopAttackFaction(factionId); } - public void HandleProcExtraAttackFor(Unit victim) { while (ExtraAttacks != 0) diff --git a/Source/Game/Entities/Unit/Unit.Fields.cs b/Source/Game/Entities/Unit/Unit.Fields.cs index 1d3a63b65..ab654ee6f 100644 --- a/Source/Game/Entities/Unit/Unit.Fields.cs +++ b/Source/Game/Entities/Unit/Unit.Fields.cs @@ -56,8 +56,6 @@ namespace Game.Entities protected uint[] m_attackTimer = new uint[(int)WeaponAttackType.Max]; // Threat+combat management - bool m_isEngaged; - CombatManager m_combatManager; ThreatManager m_threatManager; diff --git a/Source/Scripts/Pets/Priest.cs b/Source/Scripts/Pets/Priest.cs index d657b8b32..90e05a8d5 100644 --- a/Source/Scripts/Pets/Priest.cs +++ b/Source/Scripts/Pets/Priest.cs @@ -45,7 +45,7 @@ namespace Scripts.Pets return; me.CombatStop(true); - me.GetThreatManager().NotifyDisengaged(); + EngagementOver(); me.ResetPlayerDamageReq(); } }