diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 9efc4fa62..0d70c952b 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -13058,11 +13058,28 @@ void Unit::TriggerAurasProcOnEvent(std::list* 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 diff --git a/src/server/game/Spells/Spell.h b/src/server/game/Spells/Spell.h index e41d27757..603a5c923 100644 --- a/src/server/game/Spells/Spell.h +++ b/src/server/game/Spells/Spell.h @@ -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; diff --git a/src/test/mocks/ProcChanceTestHelper.h b/src/test/mocks/ProcChanceTestHelper.h index 6ce878b8f..e4caf332f 100644 --- a/src/test/mocks/ProcChanceTestHelper.h +++ b/src/test/mocks/ProcChanceTestHelper.h @@ -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 // ============================================================================= diff --git a/src/test/mocks/SpellInfoTestHelper.h b/src/test/mocks/SpellInfoTestHelper.h index ce129b4d1..81e0c8b03 100644 --- a/src/test/mocks/SpellInfoTestHelper.h +++ b/src/test/mocks/SpellInfoTestHelper.h @@ -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); diff --git a/src/test/server/game/Spells/CascadeProcSuppressionTest.cpp b/src/test/server/game/Spells/CascadeProcSuppressionTest.cpp new file mode 100644 index 000000000..a04edbf2e --- /dev/null +++ b/src/test/server/game/Spells/CascadeProcSuppressionTest.cpp @@ -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 . + */ + +/** + * @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 {}; + +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 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"; +}