fix(Core/Spells): Port SPELL_ATTR3_INSTANT_TARGET_PROCS cascade proc suppression from TrinityCore (#24936)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: QAston <126822+QAston@users.noreply.github.com>
This commit is contained in:
blinkysc
2026-03-02 06:34:59 -06:00
committed by GitHub
parent ed78bfe5a6
commit dd6f32d54d
5 changed files with 279 additions and 2 deletions

View File

@@ -13058,11 +13058,28 @@ void Unit::TriggerAurasProcOnEvent(std::list<AuraApplication*>* myProcAuras, std
void Unit::TriggerAurasProcOnEvent(ProcEventInfo& eventInfo, AuraApplicationProcContainer& aurasTriggeringProc)
{
Spell const* triggeringSpell = eventInfo.GetProcSpell();
bool const disableProcs = triggeringSpell && triggeringSpell->IsProcDisabled();
if (disableProcs)
SetCantProc(true);
for (auto const& [procEffectMask, aurApp] : aurasTriggeringProc)
{
if (!aurApp->GetRemoveMode())
aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo);
if (aurApp->GetRemoveMode())
continue;
SpellInfo const* spellInfo = aurApp->GetBase()->GetSpellInfo();
if (spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
SetCantProc(true);
aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo);
if (spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
SetCantProc(false);
}
if (disableProcs)
SetCantProc(false);
}
Player* Unit::GetSpellModOwner() const

View File

@@ -565,6 +565,7 @@ public:
bool IsNextMeleeSwingSpell() const;
bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); };
bool HasTriggeredCastFlag(TriggerCastFlags flag) const { return _triggeredCastFlags & flag; };
[[nodiscard]] bool IsProcDisabled() const { return HasTriggeredCastFlag(TRIGGERED_DISALLOW_PROC_EVENTS); }
bool IsChannelActive() const { return m_caster->GetUInt32Value(UNIT_CHANNEL_SPELL) != 0; }
bool IsAutoActionResetSpell() const;
bool IsIgnoringCooldowns() const;

View File

@@ -492,6 +492,40 @@ public:
}
}
// =============================================================================
// Cascade Proc Suppression - simulates Unit.cpp TriggerAurasProcOnEvent
// =============================================================================
/**
* @brief Configuration for simulating cascade proc suppression
*
* Models the two paths in TriggerAurasProcOnEvent that call SetCantProc():
* 1. Outer check: triggering spell has TRIGGERED_DISALLOW_PROC_EVENTS
* 2. Per-aura check: aura has SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000)
*/
struct CascadeProcConfig
{
bool triggeringSpellIsProcDisabled = false; // Spell::IsProcDisabled()
bool auraHasDisableProcAttr = false; // SpellInfo::HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS)
};
/**
* @brief Returns true if cascading procs should be suppressed for this aura
*
* @param config Cascade proc configuration
* @return true if SetCantProc(true) would be active during this aura's proc
*/
static bool ShouldSuppressCascadingProc(CascadeProcConfig const& config)
{
// Outer check: triggering spell disables all cascading procs
if (config.triggeringSpellIsProcDisabled)
return true;
// Per-aura check: aura itself suppresses cascading
if (config.auraHasDisableProcAttr)
return true;
return false;
}
// =============================================================================
// Conditions System - simulates SpellAuras.cpp:2232-2236
// =============================================================================

View File

@@ -96,6 +96,12 @@ public:
return *this;
}
TestSpellEntryHelper& WithAttributesEx3(uint32 attr)
{
_entry.AttributesEx3 = attr;
return *this;
}
TestSpellEntryHelper& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
{
if (effIndex < MAX_SPELL_EFFECTS)
@@ -183,6 +189,12 @@ public:
return *this;
}
SpellInfoBuilder& WithAttributesEx3(uint32 attr)
{
_entryHelper.WithAttributesEx3(attr);
return *this;
}
SpellInfoBuilder& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
{
_entryHelper.WithEffect(effIndex, effect, auraType);

View File

@@ -0,0 +1,213 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file CascadeProcSuppressionTest.cpp
* @brief Unit tests for cascade proc suppression via SPELL_ATTR3_INSTANT_TARGET_PROCS
*
* Tests the logic from Unit.cpp TriggerAurasProcOnEvent:
* - Outer check: Spell::IsProcDisabled() (TRIGGERED_DISALLOW_PROC_EVENTS) suppresses all cascade procs
* - Per-aura check: SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000) suppresses cascade for that aura
* - Normal spells/auras without these flags allow cascading
* - Both flags set simultaneously still suppresses correctly
*/
#include "ProcChanceTestHelper.h"
#include "SpellInfoTestHelper.h"
#include "gtest/gtest.h"
using namespace testing;
class CascadeProcSuppressionTest : public ::testing::Test
{
protected:
ProcChanceTestHelper::CascadeProcConfig MakeConfig(
bool isProcDisabled, bool hasDisableProcAttr)
{
ProcChanceTestHelper::CascadeProcConfig config;
config.triggeringSpellIsProcDisabled = isProcDisabled;
config.auraHasDisableProcAttr = hasDisableProcAttr;
return config;
}
};
// =============================================================================
// Normal behavior (no suppression)
// =============================================================================
TEST_F(CascadeProcSuppressionTest, NormalSpellNormalAura_NotSuppressed)
{
auto config = MakeConfig(false, false);
EXPECT_FALSE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Normal spell + normal aura should not suppress cascading procs";
}
// =============================================================================
// IsProcDisabled (outer check - TRIGGERED_DISALLOW_PROC_EVENTS)
// =============================================================================
TEST_F(CascadeProcSuppressionTest, ProcDisabledSpell_NormalAura_Suppressed)
{
auto config = MakeConfig(true, false);
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Triggered spell with DISALLOW_PROC_EVENTS should suppress all cascading procs";
}
TEST_F(CascadeProcSuppressionTest, ProcDisabledSpell_WithAttr_Suppressed)
{
auto config = MakeConfig(true, true);
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Both flags set should still suppress (double-suppress doesn't break)";
}
// =============================================================================
// SPELL_ATTR3_INSTANT_TARGET_PROCS (per-aura check)
// =============================================================================
TEST_F(CascadeProcSuppressionTest, NormalSpell_AuraWithAttr_Suppressed)
{
auto config = MakeConfig(false, true);
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Aura with SPELL_ATTR3_INSTANT_TARGET_PROCS should suppress cascading procs";
}
// =============================================================================
// SpellInfo attribute verification via SpellInfoBuilder
// =============================================================================
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithAttr_HasAttributeReturnsTrue)
{
auto spellInfo = SpellInfoBuilder()
.WithId(99001)
.WithAttributesEx3(SPELL_ATTR3_INSTANT_TARGET_PROCS)
.BuildUnique();
EXPECT_TRUE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
<< "SpellInfo built with 0x80000 should report HasAttribute true";
}
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithoutAttr_HasAttributeReturnsFalse)
{
auto spellInfo = SpellInfoBuilder()
.WithId(99002)
.WithAttributesEx3(0)
.BuildUnique();
EXPECT_FALSE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
<< "SpellInfo built with 0 should report HasAttribute false";
}
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithMixedBits_HasAttributeReturnsTrue)
{
// 0x80001 = SPELL_ATTR3_INSTANT_TARGET_PROCS | SPELL_ATTR3_PVP_ENABLING (bit 0)
auto spellInfo = SpellInfoBuilder()
.WithId(99003)
.WithAttributesEx3(0x00080001)
.BuildUnique();
EXPECT_TRUE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
<< "Other bits in AttributesEx3 should not interfere with attribute detection";
}
// =============================================================================
// Real spell scenarios (data-driven)
// These spells have SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000) in DBC
// =============================================================================
struct RealSpellTestCase
{
const char* name;
uint32 spellId;
bool hasAttr; // Whether the spell has SPELL_ATTR3_INSTANT_TARGET_PROCS
};
class CascadeProcRealSpellTest : public ::testing::TestWithParam<RealSpellTestCase> {};
TEST_P(CascadeProcRealSpellTest, VerifySuppressionForRealSpell)
{
auto const& tc = GetParam();
// Build a SpellInfo mimicking the real spell's AttributesEx3
auto spellInfo = SpellInfoBuilder()
.WithId(tc.spellId)
.WithAttributesEx3(tc.hasAttr ? SPELL_ATTR3_INSTANT_TARGET_PROCS : 0)
.BuildUnique();
// Verify attribute detection matches expectation
EXPECT_EQ(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS), tc.hasAttr)
<< tc.name << " (spell " << tc.spellId << ") attribute detection mismatch";
// Verify cascade suppression matches attribute presence
ProcChanceTestHelper::CascadeProcConfig config;
config.triggeringSpellIsProcDisabled = false;
config.auraHasDisableProcAttr = tc.hasAttr;
EXPECT_EQ(ProcChanceTestHelper::ShouldSuppressCascadingProc(config), tc.hasAttr)
<< tc.name << " (spell " << tc.spellId << ") cascade suppression mismatch";
}
INSTANTIATE_TEST_SUITE_P(
CascadeProcSuppression,
CascadeProcRealSpellTest,
::testing::Values(
// Spells WITH SPELL_ATTR3_INSTANT_TARGET_PROCS
RealSpellTestCase{"Seal Fate", 14195, true},
RealSpellTestCase{"Sword Specialization", 12281, true},
RealSpellTestCase{"Reckoning", 20178, true},
RealSpellTestCase{"Flurry", 16257, true},
// Counter-example: spell WITHOUT the attribute
RealSpellTestCase{"Eviscerate", 26865, false}
),
[](testing::TestParamInfo<RealSpellTestCase> const& info) {
// Generate readable test name from spell name (replace spaces)
std::string name = info.param.name;
std::replace(name.begin(), name.end(), ' ', '_');
return name;
}
);
// =============================================================================
// Nesting behavior - both flags simultaneously
// =============================================================================
TEST_F(CascadeProcSuppressionTest, BothFlagsSet_StillSuppressed)
{
auto config = MakeConfig(true, true);
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Both IsProcDisabled and INSTANT_TARGET_PROCS set should still suppress";
}
TEST_F(CascadeProcSuppressionTest, OnlyOuterFlag_Suppressed)
{
auto config = MakeConfig(true, false);
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Only IsProcDisabled should be sufficient to suppress";
}
TEST_F(CascadeProcSuppressionTest, OnlyPerAuraFlag_Suppressed)
{
auto config = MakeConfig(false, true);
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
<< "Only INSTANT_TARGET_PROCS should be sufficient to suppress";
}