fix(Core/Spells): revert CAST proc ordering and add ApplySpellMod recursion guard (#24990)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-03-03 16:45:06 -06:00
committed by GitHub
parent a013968436
commit cccd52dab8
8 changed files with 36 additions and 390 deletions

View File

@@ -820,6 +820,17 @@ void AuraEffect::ApplySpellMod(Unit* target, bool apply)
// Auras with charges do not mod amount of passive auras
if (GetBase()->IsUsingCharges())
return;
// Guard against infinite recursion: a spell mod recalculating an aura that
// triggers ApplySpellMod again (self-referencing or mutual spell mods).
if (m_isRecalculatingPassiveAuras)
{
LOG_DEBUG("spells.aura", "AuraEffect::ApplySpellMod: Recursion detected for spell {} effect {}, skipping passive aura recalculation",
GetId(), GetEffIndex());
return;
}
m_isRecalculatingPassiveAuras = true;
// reapply some passive spells after add/remove related spellmods
// Warning: it is a dead loop if 2 auras each other amount-shouldn't happen
switch (GetMiscValue())
@@ -906,6 +917,8 @@ void AuraEffect::ApplySpellMod(Unit* target, bool apply)
default:
break;
}
m_isRecalculatingPassiveAuras = false;
}
void AuraEffect::Update(uint32 diff, Unit* caster)

View File

@@ -144,6 +144,7 @@ private:
uint8 const m_effIndex;
bool m_canBeRecalculated;
bool m_isPeriodic;
bool m_isRecalculatingPassiveAuras = false;
private:
float CalcPeriodicCritChance(Unit const* caster, Unit const* target) const;

View File

@@ -2354,20 +2354,7 @@ void Aura::ConsumeProcCharges(SpellProcEntry const* procEntry)
else if (IsUsingCharges())
{
if (!GetCharges())
{
// Defer removal while spell mods are being consumed,
// cleaned up in Spell::_cast() after handle_immediate()
if (GetType() == UNIT_AURA_TYPE
&& (HasEffectType(SPELL_AURA_ADD_FLAT_MODIFIER)
|| HasEffectType(SPELL_AURA_ADD_PCT_MODIFIER)))
{
if (Player* player = GetUnitOwner()->ToPlayer())
if (player->m_spellModTakingSpell)
return;
}
Remove();
}
}
}

View File

@@ -3951,70 +3951,8 @@ void Spell::_cast(bool skipCheck)
}
else
{
// CAST phase procs for immediate spells (including channeled)
if (m_originalCaster)
{
uint32 procAttacker = m_procAttacker;
if (!procAttacker)
{
bool IsPositive = m_spellInfo->IsPositive();
if (m_spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MAGIC)
{
procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG;
}
else
{
procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_NEG;
}
}
uint32 hitMask = PROC_HIT_NORMAL;
for (std::list<TargetInfo>::iterator ihit = m_UniqueTargetInfo.begin(); ihit != m_UniqueTargetInfo.end(); ++ihit)
{
if (ihit->missCondition != SPELL_MISS_NONE)
continue;
if (!ihit->crit)
continue;
hitMask |= PROC_HIT_CRITICAL;
break;
}
Unit::ProcSkillsAndAuras(m_originalCaster, m_originalCaster, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo,
m_triggeredByAuraSpell.effectIndex, this, nullptr, nullptr, PROC_SPELL_PHASE_CAST);
}
// Immediate spell, no big deal
handle_immediate();
// Clean up deferred 0-charge spell modifier auras
// Copy to vector first — aura->Remove() can modify m_appliedMods
std::vector<Aura*> appliedModsCopy(m_appliedMods.begin(), m_appliedMods.end());
for (Aura* aura : appliedModsCopy)
{
if (!aura->IsRemoved() && aura->IsUsingCharges()
&& !aura->GetCharges())
aura->Remove();
}
// Also clean up deferred modifier auras not in m_appliedMods
if (Unit* caster = m_caster)
{
std::vector<Aura*> deferred;
for (auto const& [id, aura] : caster->GetOwnedAuras())
{
if (!aura->IsRemoved() && aura->IsUsingCharges()
&& !aura->GetCharges()
&& (aura->HasEffectType(SPELL_AURA_ADD_FLAT_MODIFIER)
|| aura->HasEffectType(SPELL_AURA_ADD_PCT_MODIFIER)))
deferred.push_back(aura);
}
for (Aura* aura : deferred)
if (!aura->IsRemoved())
aura->Remove();
}
}
if (resetAttackTimers)
@@ -4042,9 +3980,13 @@ void Spell::_cast(bool skipCheck)
if (modOwner)
modOwner->SetSpellModTakingSpell(this, false);
// CAST phase procs for delayed spells
if (m_spellState == SPELL_STATE_DELAYED
&& m_originalCaster)
// Handle procs on cast - only for non-triggered spells
// Triggered spells (from auras, items, etc.) should not fire CAST phase procs
// as they are not player-initiated casts. This prevents issues like Arcane Potency
// charges being consumed by periodic damage effects (e.g., Blizzard ticks).
// Must be called AFTER handle_immediate() so spell mods (like Missile Barrage's
// duration reduction) are applied before the aura is consumed by the proc.
if (m_originalCaster && !IsTriggered())
{
uint32 procAttacker = m_procAttacker;
if (!procAttacker)

View File

@@ -959,11 +959,23 @@ class spell_mage_fingers_of_frost : public AuraScript
void PrepareProc(ProcEventInfo& eventInfo)
{
// Block channeled spells (e.g. Blizzard channel start) from consuming charges.
// All other filtering is handled by SpellPhaseMask=1 (CAST only) in spell_proc.
if (Spell const* spell = eventInfo.GetProcSpell())
if (spell->GetSpellInfo()->IsChanneled())
{
bool isTriggered = spell->IsTriggered();
bool isCastPhase = (eventInfo.GetSpellPhaseMask() & PROC_SPELL_PHASE_CAST) != 0;
bool isChanneled = spell->GetSpellInfo()->IsChanneled();
bool prevent = false;
if (isTriggered)
prevent = false;
else if (isChanneled)
prevent = true;
else if (!isCastPhase)
prevent = true;
if (prevent)
PreventDefaultAction();
}
}
void OnRemove(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/)