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:
@@ -13058,11 +13058,28 @@ void Unit::TriggerAurasProcOnEvent(std::list<AuraApplication*>* myProcAuras, std
|
|||||||
|
|
||||||
void Unit::TriggerAurasProcOnEvent(ProcEventInfo& eventInfo, AuraApplicationProcContainer& aurasTriggeringProc)
|
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)
|
for (auto const& [procEffectMask, aurApp] : aurasTriggeringProc)
|
||||||
{
|
{
|
||||||
if (!aurApp->GetRemoveMode())
|
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);
|
aurApp->GetBase()->TriggerProcOnEvent(procEffectMask, aurApp, eventInfo);
|
||||||
|
|
||||||
|
if (spellInfo->HasAttribute(SPELL_ATTR3_INSTANT_TARGET_PROCS))
|
||||||
|
SetCantProc(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (disableProcs)
|
||||||
|
SetCantProc(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Player* Unit::GetSpellModOwner() const
|
Player* Unit::GetSpellModOwner() const
|
||||||
|
|||||||
@@ -565,6 +565,7 @@ public:
|
|||||||
bool IsNextMeleeSwingSpell() const;
|
bool IsNextMeleeSwingSpell() const;
|
||||||
bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); };
|
bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); };
|
||||||
bool HasTriggeredCastFlag(TriggerCastFlags flag) const { return _triggeredCastFlags & flag; };
|
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 IsChannelActive() const { return m_caster->GetUInt32Value(UNIT_CHANNEL_SPELL) != 0; }
|
||||||
bool IsAutoActionResetSpell() const;
|
bool IsAutoActionResetSpell() const;
|
||||||
bool IsIgnoringCooldowns() const;
|
bool IsIgnoringCooldowns() const;
|
||||||
|
|||||||
@@ -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
|
// Conditions System - simulates SpellAuras.cpp:2232-2236
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -96,6 +96,12 @@ public:
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TestSpellEntryHelper& WithAttributesEx3(uint32 attr)
|
||||||
|
{
|
||||||
|
_entry.AttributesEx3 = attr;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
TestSpellEntryHelper& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
|
TestSpellEntryHelper& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
|
||||||
{
|
{
|
||||||
if (effIndex < MAX_SPELL_EFFECTS)
|
if (effIndex < MAX_SPELL_EFFECTS)
|
||||||
@@ -183,6 +189,12 @@ public:
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpellInfoBuilder& WithAttributesEx3(uint32 attr)
|
||||||
|
{
|
||||||
|
_entryHelper.WithAttributesEx3(attr);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
SpellInfoBuilder& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
|
SpellInfoBuilder& WithEffect(uint8 effIndex, uint32 effect, uint32 auraType = 0)
|
||||||
{
|
{
|
||||||
_entryHelper.WithEffect(effIndex, effect, auraType);
|
_entryHelper.WithEffect(effIndex, effect, auraType);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user