Skip to content

Commit dd6f32d

Browse files
blinkyscQAston
andauthored
fix(Core/Spells): Port SPELL_ATTR3_INSTANT_TARGET_PROCS cascade proc suppression from TrinityCore (azerothcore#24936)
Co-authored-by: blinkysc <blinkysc@users.noreply.github.com> Co-authored-by: QAston <126822+QAston@users.noreply.github.com>
1 parent ed78bfe commit dd6f32d

File tree

5 files changed

+279
-2
lines changed

5 files changed

+279
-2
lines changed

src/server/game/Entities/Unit/Unit.cpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13058,11 +13058,28 @@ void Unit::TriggerAurasProcOnEvent(std::list<AuraApplication*>* myProcAuras, std
1305813058

1305913059
void Unit::TriggerAurasProcOnEvent(ProcEventInfo& eventInfo, AuraApplicationProcContainer& aurasTriggeringProc)
1306013060
{
13061+
Spell const* triggeringSpell = eventInfo.GetProcSpell();
13062+
bool const disableProcs = triggeringSpell && triggeringSpell->IsProcDisabled();
13063+
if (disableProcs)
13064+
SetCantProc(true);
13065+
1306113066
for (auto const& [procEffectMask, aurApp] : aurasTriggeringProc)
1306213067
{
13063-
if (!aurApp->GetRemoveMode())
13064-
aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo);
13068+
if (aurApp->GetRemoveMode())
13069+
continue;
13070+
13071+
SpellInfo const* spellInfo = aurApp->GetBase()->GetSpellInfo();
13072+
if (spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
13073+
SetCantProc(true);
13074+
13075+
aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo);
13076+
13077+
if (spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
13078+
SetCantProc(false);
1306513079
}
13080+
13081+
if (disableProcs)
13082+
SetCantProc(false);
1306613083
}
1306713084

1306813085
Player* Unit::GetSpellModOwner() const

src/server/game/Spells/Spell.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ class Spell
565565
bool IsNextMeleeSwingSpell() const;
566566
bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); };
567567
bool HasTriggeredCastFlag(TriggerCastFlags flag) const { return _triggeredCastFlags & flag; };
568+
[[nodiscard]] bool IsProcDisabled() const { return HasTriggeredCastFlag(TRIGGERED_DISALLOW_PROC_EVENTS); }
568569
bool IsChannelActive() const { return m_caster->GetUInt32Value(UNIT_CHANNEL_SPELL) != 0; }
569570
bool IsAutoActionResetSpell() const;
570571
bool IsIgnoringCooldowns() const;

src/test/mocks/ProcChanceTestHelper.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,40 @@ class ProcChanceTestHelper
492492
}
493493
}
494494

495+
// =============================================================================
496+
// Cascade Proc Suppression - simulates Unit.cpp TriggerAurasProcOnEvent
497+
// =============================================================================
498+
499+
/**
500+
* @brief Configuration for simulating cascade proc suppression
501+
*
502+
* Models the two paths in TriggerAurasProcOnEvent that call SetCantProc():
503+
* 1. Outer check: triggering spell has TRIGGERED_DISALLOW_PROC_EVENTS
504+
* 2. Per-aura check: aura has SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000)
505+
*/
506+
struct CascadeProcConfig
507+
{
508+
bool triggeringSpellIsProcDisabled = false; // Spell::IsProcDisabled()
509+
bool auraHasDisableProcAttr = false; // SpellInfo::HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS)
510+
};
511+
512+
/**
513+
* @brief Returns true if cascading procs should be suppressed for this aura
514+
*
515+
* @param config Cascade proc configuration
516+
* @return true if SetCantProc(true) would be active during this aura's proc
517+
*/
518+
static bool ShouldSuppressCascadingProc(CascadeProcConfig const& config)
519+
{
520+
// Outer check: triggering spell disables all cascading procs
521+
if (config.triggeringSpellIsProcDisabled)
522+
return true;
523+
// Per-aura check: aura itself suppresses cascading
524+
if (config.auraHasDisableProcAttr)
525+
return true;
526+
return false;
527+
}
528+
495529
// =============================================================================
496530
// Conditions System - simulates SpellAuras.cpp:2232-2236
497531
// =============================================================================

src/test/mocks/SpellInfoTestHelper.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ class TestSpellEntryHelper
9696
return *this;
9797
}
9898

99+
TestSpellEntryHelper& WithAttributesEx3(uint32 attr)
100+
{
101+
_entry.AttributesEx3 = attr;
102+
return *this;
103+
}
104+
99105
TestSpellEntryHelper& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
100106
{
101107
if (effIndex < MAX_SPELL_EFFECTS)
@@ -183,6 +189,12 @@ class SpellInfoBuilder
183189
return *this;
184190
}
185191

192+
SpellInfoBuilder& WithAttributesEx3(uint32 attr)
193+
{
194+
_entryHelper.WithAttributesEx3(attr);
195+
return *this;
196+
}
197+
186198
SpellInfoBuilder& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
187199
{
188200
_entryHelper.WithEffect(effIndex, effect, auraType);
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12+
* more details.
13+
*
14+
* You should have received a copy of the GNU General Public License along
15+
* with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
/**
19+
* @file CascadeProcSuppressionTest.cpp
20+
* @brief Unit tests for cascade proc suppression via SPELL_ATTR3_INSTANT_TARGET_PROCS
21+
*
22+
* Tests the logic from Unit.cpp TriggerAurasProcOnEvent:
23+
* - Outer check: Spell::IsProcDisabled() (TRIGGERED_DISALLOW_PROC_EVENTS) suppresses all cascade procs
24+
* - Per-aura check: SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000) suppresses cascade for that aura
25+
* - Normal spells/auras without these flags allow cascading
26+
* - Both flags set simultaneously still suppresses correctly
27+
*/
28+
29+
#include "ProcChanceTestHelper.h"
30+
#include "SpellInfoTestHelper.h"
31+
#include "gtest/gtest.h"
32+
33+
using namespace testing;
34+
35+
class CascadeProcSuppressionTest : public ::testing::Test
36+
{
37+
protected:
38+
ProcChanceTestHelper::CascadeProcConfig MakeConfig(
39+
bool isProcDisabled, bool hasDisableProcAttr)
40+
{
41+
ProcChanceTestHelper::CascadeProcConfig config;
42+
config.triggeringSpellIsProcDisabled = isProcDisabled;
43+
config.auraHasDisableProcAttr = hasDisableProcAttr;
44+
return config;
45+
}
46+
};
47+
48+
// =============================================================================
49+
// Normal behavior (no suppression)
50+
// =============================================================================
51+
52+
TEST_F(CascadeProcSuppressionTest, NormalSpellNormalAura_NotSuppressed)
53+
{
54+
auto config = MakeConfig(false, false);
55+
56+
EXPECT_FALSE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
57+
<< "Normal spell + normal aura should not suppress cascading procs";
58+
}
59+
60+
// =============================================================================
61+
// IsProcDisabled (outer check - TRIGGERED_DISALLOW_PROC_EVENTS)
62+
// =============================================================================
63+
64+
TEST_F(CascadeProcSuppressionTest, ProcDisabledSpell_NormalAura_Suppressed)
65+
{
66+
auto config = MakeConfig(true, false);
67+
68+
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
69+
<< "Triggered spell with DISALLOW_PROC_EVENTS should suppress all cascading procs";
70+
}
71+
72+
TEST_F(CascadeProcSuppressionTest, ProcDisabledSpell_WithAttr_Suppressed)
73+
{
74+
auto config = MakeConfig(true, true);
75+
76+
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
77+
<< "Both flags set should still suppress (double-suppress doesn't break)";
78+
}
79+
80+
// =============================================================================
81+
// SPELL_ATTR3_INSTANT_TARGET_PROCS (per-aura check)
82+
// =============================================================================
83+
84+
TEST_F(CascadeProcSuppressionTest, NormalSpell_AuraWithAttr_Suppressed)
85+
{
86+
auto config = MakeConfig(false, true);
87+
88+
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
89+
<< "Aura with SPELL_ATTR3_INSTANT_TARGET_PROCS should suppress cascading procs";
90+
}
91+
92+
// =============================================================================
93+
// SpellInfo attribute verification via SpellInfoBuilder
94+
// =============================================================================
95+
96+
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithAttr_HasAttributeReturnsTrue)
97+
{
98+
auto spellInfo = SpellInfoBuilder()
99+
.WithId(99001)
100+
.WithAttributesEx3(SPELL_ATTR3_INSTANT_TARGET_PROCS)
101+
.BuildUnique();
102+
103+
EXPECT_TRUE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
104+
<< "SpellInfo built with 0x80000 should report HasAttribute true";
105+
}
106+
107+
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithoutAttr_HasAttributeReturnsFalse)
108+
{
109+
auto spellInfo = SpellInfoBuilder()
110+
.WithId(99002)
111+
.WithAttributesEx3(0)
112+
.BuildUnique();
113+
114+
EXPECT_FALSE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
115+
<< "SpellInfo built with 0 should report HasAttribute false";
116+
}
117+
118+
TEST_F(CascadeProcSuppressionTest, SpellInfo_WithMixedBits_HasAttributeReturnsTrue)
119+
{
120+
// 0x80001 = SPELL_ATTR3_INSTANT_TARGET_PROCS | SPELL_ATTR3_PVP_ENABLING (bit 0)
121+
auto spellInfo = SpellInfoBuilder()
122+
.WithId(99003)
123+
.WithAttributesEx3(0x00080001)
124+
.BuildUnique();
125+
126+
EXPECT_TRUE(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
127+
<< "Other bits in AttributesEx3 should not interfere with attribute detection";
128+
}
129+
130+
// =============================================================================
131+
// Real spell scenarios (data-driven)
132+
// These spells have SPELL_ATTR3_INSTANT_TARGET_PROCS (0x80000) in DBC
133+
// =============================================================================
134+
135+
struct RealSpellTestCase
136+
{
137+
const char* name;
138+
uint32 spellId;
139+
bool hasAttr; // Whether the spell has SPELL_ATTR3_INSTANT_TARGET_PROCS
140+
};
141+
142+
class CascadeProcRealSpellTest : public ::testing::TestWithParam<RealSpellTestCase> {};
143+
144+
TEST_P(CascadeProcRealSpellTest, VerifySuppressionForRealSpell)
145+
{
146+
auto const& tc = GetParam();
147+
148+
// Build a SpellInfo mimicking the real spell's AttributesEx3
149+
auto spellInfo = SpellInfoBuilder()
150+
.WithId(tc.spellId)
151+
.WithAttributesEx3(tc.hasAttr ? SPELL_ATTR3_INSTANT_TARGET_PROCS : 0)
152+
.BuildUnique();
153+
154+
// Verify attribute detection matches expectation
155+
EXPECT_EQ(spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS), tc.hasAttr)
156+
<< tc.name << " (spell " << tc.spellId << ") attribute detection mismatch";
157+
158+
// Verify cascade suppression matches attribute presence
159+
ProcChanceTestHelper::CascadeProcConfig config;
160+
config.triggeringSpellIsProcDisabled = false;
161+
config.auraHasDisableProcAttr = tc.hasAttr;
162+
163+
EXPECT_EQ(ProcChanceTestHelper::ShouldSuppressCascadingProc(config), tc.hasAttr)
164+
<< tc.name << " (spell " << tc.spellId << ") cascade suppression mismatch";
165+
}
166+
167+
INSTANTIATE_TEST_SUITE_P(
168+
CascadeProcSuppression,
169+
CascadeProcRealSpellTest,
170+
::testing::Values(
171+
// Spells WITH SPELL_ATTR3_INSTANT_TARGET_PROCS
172+
RealSpellTestCase{"Seal Fate", 14195, true},
173+
RealSpellTestCase{"Sword Specialization", 12281, true},
174+
RealSpellTestCase{"Reckoning", 20178, true},
175+
RealSpellTestCase{"Flurry", 16257, true},
176+
// Counter-example: spell WITHOUT the attribute
177+
RealSpellTestCase{"Eviscerate", 26865, false}
178+
),
179+
[](testing::TestParamInfo<RealSpellTestCase> const& info) {
180+
// Generate readable test name from spell name (replace spaces)
181+
std::string name = info.param.name;
182+
std::replace(name.begin(), name.end(), ' ', '_');
183+
return name;
184+
}
185+
);
186+
187+
// =============================================================================
188+
// Nesting behavior - both flags simultaneously
189+
// =============================================================================
190+
191+
TEST_F(CascadeProcSuppressionTest, BothFlagsSet_StillSuppressed)
192+
{
193+
auto config = MakeConfig(true, true);
194+
195+
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
196+
<< "Both IsProcDisabled and INSTANT_TARGET_PROCS set should still suppress";
197+
}
198+
199+
TEST_F(CascadeProcSuppressionTest, OnlyOuterFlag_Suppressed)
200+
{
201+
auto config = MakeConfig(true, false);
202+
203+
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
204+
<< "Only IsProcDisabled should be sufficient to suppress";
205+
}
206+
207+
TEST_F(CascadeProcSuppressionTest, OnlyPerAuraFlag_Suppressed)
208+
{
209+
auto config = MakeConfig(false, true);
210+
211+
EXPECT_TRUE(ProcChanceTestHelper::ShouldSuppressCascadingProc(config))
212+
<< "Only INSTANT_TARGET_PROCS should be sufficient to suppress";
213+
}

0 commit comments

Comments
 (0)