Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions src/server/game/Entities/Unit/Unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/server/game/Spells/Spell.h
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ class Spell
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;
Expand Down
34 changes: 34 additions & 0 deletions src/test/mocks/ProcChanceTestHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,40 @@ class ProcChanceTestHelper
}
}

// =============================================================================
// 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
// =============================================================================
Expand Down
12 changes: 12 additions & 0 deletions src/test/mocks/SpellInfoTestHelper.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ class TestSpellEntryHelper
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)
Expand Down Expand Up @@ -183,6 +189,12 @@ class SpellInfoBuilder
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);
Expand Down
213 changes: 213 additions & 0 deletions src/test/server/game/Spells/CascadeProcSuppressionTest.cpp
Original file line number Diff line number Diff line change
@@ -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";
}