diff --git a/Source/Game/Entities/Unit/Unit.Fields.cs b/Source/Game/Entities/Unit/Unit.Fields.cs index 87e76082d..54142526e 100644 --- a/Source/Game/Entities/Unit/Unit.Fields.cs +++ b/Source/Game/Entities/Unit/Unit.Fields.cs @@ -77,7 +77,8 @@ namespace Game.Entities MultiMap[] m_spellImmune = new MultiMap[(int)SpellImmunity.Max]; SpellAuraInterruptFlags m_interruptMask; SpellAuraInterruptFlags2 m_interruptMask2; - protected int m_procDeep; + int m_procDeep; // tracked for proc system correctness (what spells should proc what) + int m_procChainLength; // tracked to protect against infinite proc loops (hard limit, will disallow procs even if they should happen) SpellHistory _spellHistory; //Auras diff --git a/Source/Game/Entities/Unit/Unit.Spells.cs b/Source/Game/Entities/Unit/Unit.Spells.cs index b4e0f94c9..636377257 100644 --- a/Source/Game/Entities/Unit/Unit.Spells.cs +++ b/Source/Game/Entities/Unit/Unit.Spells.cs @@ -1480,6 +1480,13 @@ namespace Game.Entities public static void ProcSkillsAndAuras(Unit actor, Unit actionTarget, ProcFlagsInit typeMaskActor, ProcFlagsInit typeMaskActionTarget, ProcFlagsSpellType spellTypeMask, ProcFlagsSpellPhase spellPhaseMask, ProcFlagsHit hitMask, Spell spell, DamageInfo damageInfo, HealInfo healInfo) { + int ProcChainHardLimit = 10; + if (spell != null && spell.GetProcChainLength() >= ProcChainHardLimit) + { + Log.outError(LogFilter.Spells, $"Unit::ProcSkillsAndAuras: Possible infinite proc loop detected, current triggering spell {spell.GetDebugInfo()}"); + return; + } + WeaponAttackType attType = damageInfo != null ? damageInfo.GetAttackType() : WeaponAttackType.BaseAttack; if (typeMaskActor && actor != null) actor.ProcSkillsAndReactives(false, actionTarget, typeMaskActor, hitMask, attType); @@ -1625,6 +1632,11 @@ namespace Game.Entities { Spell triggeringSpell = eventInfo.GetProcSpell(); bool disableProcs = triggeringSpell && triggeringSpell.IsProcDisabled(); + + + int oldProcChainLength = m_procChainLength; + m_procChainLength = Math.Max(m_procChainLength + 1, triggeringSpell != null ? triggeringSpell.GetProcChainLength() : 0); + if (disableProcs) SetCantProc(true); @@ -1638,6 +1650,8 @@ namespace Game.Entities if (disableProcs) SetCantProc(false); + + m_procChainLength = oldProcChainLength; } void SetCantProc(bool apply) @@ -3877,6 +3891,8 @@ namespace Game.Entities public bool CanProc() { return m_procDeep == 0; } + public int GetProcChainLength() { return m_procChainLength; } + public void _ApplyAuraEffect(Aura aura, uint effIndex) { Cypher.Assert(aura != null); diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 7182840b2..a46dce1e6 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -84,6 +84,7 @@ namespace Game.Spells m_castId = ObjectGuid.Create(HighGuid.Cast, SpellCastSource.Normal, m_caster.GetMapId(), m_spellInfo.Id, m_caster.GetMap().GenerateLowGuid(HighGuid.Cast)); m_originalCastId = originalCastId; m_SpellVisual.SpellXSpellVisualID = caster.GetCastSpellXSpellVisualId(m_spellInfo); + m_procChainLength = caster.IsUnit() ? caster.ToUnit().GetProcChainLength() : 0; //Auto Shot & Shoot (wand) m_autoRepeat = m_spellInfo.IsAutoRepeatRangedSpell(); @@ -7991,6 +7992,8 @@ namespace Game.Spells public bool IsProcDisabled() { return _triggeredCastFlags.HasAnyFlag(TriggerCastFlags.DisallowProcEvents); } public bool IsChannelActive() { return m_caster.IsUnit() && m_caster.ToUnit().GetChannelSpellId() != 0; } + public int GetProcChainLength() { return m_procChainLength; } + public bool IsDeletable() { return !m_referencedFromCurrentSpell && !m_executedCurrently; @@ -8161,6 +8164,7 @@ namespace Game.Spells // we can't store original aura link to prevent access to deleted auras // and in same time need aura data and after aura deleting. public SpellInfo m_triggeredByAuraSpell; + int m_procChainLength; #endregion } diff --git a/Source/Game/Spells/SpellManager.cs b/Source/Game/Spells/SpellManager.cs index a9b04b175..51307fe96 100644 --- a/Source/Game/Spells/SpellManager.cs +++ b/Source/Game/Spells/SpellManager.cs @@ -1518,6 +1518,7 @@ namespace Game.Entities procEntry.SpellPhaseMask = ProcFlagsSpellPhase.Hit; procEntry.HitMask = ProcFlagsHit.None; // uses default proc @see SpellMgr::CanSpellTriggerProcOnEvent + bool triggersSpell = false; foreach (var spellEffectInfo in spellInfo.GetEffects()) { if (!spellEffectInfo.IsAura()) @@ -1543,6 +1544,10 @@ namespace Game.Entities if (spellEffectInfo.CalcValue() <= -100) procEntry.HitMask = ProcFlagsHit.Miss; break; + case AuraType.ProcTriggerSpell: + case AuraType.ProcTriggerSpellWithValue: + triggersSpell = spellEffectInfo.TriggerSpell != 0; + break; default: continue; } @@ -1561,6 +1566,20 @@ namespace Game.Entities procEntry.Cooldown = spellInfo.ProcCooldown; procEntry.Charges = spellInfo.ProcCharges; + if (spellInfo.HasAttribute(SpellAttr3.CanProcFromProcs) && !procEntry.SpellFamilyMask + && procEntry.Chance >= 100 + && spellInfo.ProcBasePPM <= 0.0f + && procEntry.Cooldown <= 0 + && procEntry.Charges <= 0 + && procEntry.ProcFlags.HasFlag(ProcFlags.DealMeleeAbility | ProcFlags.DealRangedAttack | ProcFlags.DealRangedAbility | ProcFlags.DealHelpfulAbility + | ProcFlags.DealHarmfulAbility | ProcFlags.DealHelpfulSpell | ProcFlags.DealHarmfulSpell | ProcFlags.DealHarmfulPeriodic | ProcFlags.DealHelpfulPeriodic) + && triggersSpell) + { + Log.outError(LogFilter.Sql, $"Spell Id {spellInfo.Id} has SPELL_ATTR3_CAN_PROC_FROM_PROCS attribute and no restriction on what spells can cause it to proc and no cooldown. " + + "This spell can cause infinite proc loops. Proc data for this spell was not generated, data in `spell_proc` table is required for it to function!"); + continue; + } + mSpellProcMap[(spellInfo.Id, spellInfo.Difficulty)] = procEntry; ++count; }