fix(Core/Combat): Immediately reselect victim after taunt aura update (#25254)

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
This commit is contained in:
blinkysc 2026-03-27 16:33:55 -05:00 committed by GitHub
parent 1520a6f055
commit 8ea9252d76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 250 additions and 0 deletions

View file

@ -540,6 +540,11 @@ void ThreatManager::TauntUpdate()
// taunt aura update also re-evaluates all suppressed states (retail behavior)
EvaluateSuppressed(true);
// immediately reselect victim so taunt takes effect without waiting
// for the next THREAT_UPDATE_INTERVAL (1 s) timer tick
UpdateVictim();
_updateTimer = THREAT_UPDATE_INTERVAL;
}
void ThreatManager::SetTauntStateForTesting(

View file

@ -182,6 +182,8 @@ public:
// Test-only: directly set taunt state on a threat ref
// Uses uint32 to avoid incomplete-type issue with ThreatReference
void SetTauntStateForTesting(Unit* target, uint32 state);
// Immediately reselect victim (bypasses 1-second Update timer)
void UpdateVictimForTesting() { UpdateVictim(); }
///== REDIRECT SYSTEM ==
void RegisterRedirectThreat(uint32 spellId, ObjectGuid const& victim, uint32 pct);

View file

@ -2313,4 +2313,247 @@ TEST_F(ThreatManagerIntegrationTest,
delete creatureC;
}
// ============================================================================
// TauntUpdate immediate victim reselection tests
// Verify that TauntUpdate() calls UpdateVictim() immediately (no 1-second
// delay), and that rapid/repeated calls do not cause recursion or crashes.
//
// Note: TauntUpdate() reads real auras via GetAuraEffectsByType(), which are
// not present in unit tests. Tests that need taunt state to persist use
// SetTauntStateForTesting() + UpdateVictim() to exercise the same code path
// (ReselectVictim → ProcessAIUpdates) that TauntUpdate now invokes.
// TauntUpdate()-specific tests verify no-crash / no-recursion behaviour.
// ============================================================================
TEST_F(ThreatManagerIntegrationTest,
UpdateVictim_ImmediateReselectAfterTauntState)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
// B has low threat, C has high threat
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureC);
// Set taunt on B, then call UpdateVictim directly (the call that
// TauntUpdate now makes internally) — WITHOUT waiting for the
// 1-second Update() timer
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_TAUNT);
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
// Victim should switch immediately to B (taunted)
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
_creatureB);
creatureC->CleanupCombatState();
delete creatureC;
}
TEST_F(ThreatManagerIntegrationTest,
UpdateVictim_Idempotent_MultipleCalls)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_TAUNT);
// Call UpdateVictim many times in a row — must be idempotent
for (int i = 0; i < 50; ++i)
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
_creatureB);
creatureC->CleanupCombatState();
delete creatureC;
}
TEST_F(ThreatManagerIntegrationTest,
UpdateVictim_RapidTauntToggle_NoCrash)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
// Rapidly toggle taunt state with immediate UpdateVictim each time
for (int i = 0; i < 50; ++i)
{
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_TAUNT);
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
_creatureB);
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_NONE);
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureC);
}
creatureC->CleanupCombatState();
delete creatureC;
}
TEST_F(ThreatManagerIntegrationTest,
UpdateVictim_WithConcurrentThreatChanges_NoCrash)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
// Simulate taunt landing while threat is also being modified
// (e.g. damage events arriving in the same server tick)
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_TAUNT);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f);
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
// B should be victim because taunt overrides threat
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
_creatureB);
creatureC->CleanupCombatState();
delete creatureC;
}
TEST_F(ThreatManagerIntegrationTest,
UpdateVictim_MultipleTaunters_HighestStateWins)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
TestCreature* creatureD = new TestCreature();
creatureD->SetupForCombatTest(_map, 4, 12348);
creatureD->SetFaction(90002);
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureD, 300.0f);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureD);
// Two taunters: B (state=TAUNT) and C (state=TAUNT+1, i.e. "last taunt")
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_TAUNT);
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
creatureC, static_cast<ThreatReference::TauntState>(
ThreatReference::TAUNT_STATE_TAUNT + 1));
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
// C has the higher taunt state — should be victim immediately
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureC);
creatureC->CleanupCombatState();
creatureD->CleanupCombatState();
delete creatureC;
delete creatureD;
}
TEST_F(ThreatManagerIntegrationTest,
UpdateVictim_FixateStillTakesPrecedence)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 200.0f);
// Fixate on C
_creatureA->TestGetThreatMgr().FixateTarget(creatureC);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureC);
// Taunt from B + immediate UpdateVictim — fixate should still win
_creatureA->TestGetThreatMgr().SetTauntStateForTesting(
_creatureB, ThreatReference::TAUNT_STATE_TAUNT);
_creatureA->TestGetThreatMgr().UpdateVictimForTesting();
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureC);
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetFixateTarget(),
creatureC);
creatureC->CleanupCombatState();
delete creatureC;
}
TEST_F(ThreatManagerIntegrationTest,
TauntUpdate_EmptyThreatList_NoCrash)
{
// TauntUpdate on a creature with no threats should not crash
// even though UpdateVictim is now called internally
_creatureA->TestGetThreatMgr().TauntUpdate();
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
nullptr);
}
TEST_F(ThreatManagerIntegrationTest,
TauntUpdate_RapidCalls_NoCrash)
{
TestCreature* creatureC = new TestCreature();
creatureC->SetupForCombatTest(_map, 3, 12347);
creatureC->SetFaction(90002);
_creatureA->TestGetThreatMgr().AddThreat(_creatureB, 100.0f);
_creatureA->TestGetThreatMgr().AddThreat(creatureC, 500.0f);
_creatureA->TestGetThreatMgr().Update(
ThreatManager::THREAT_UPDATE_INTERVAL);
// Rapidly call TauntUpdate (no real auras — clears taunt state each
// time and calls UpdateVictim). Must not crash or recurse.
for (int i = 0; i < 50; ++i)
_creatureA->TestGetThreatMgr().TauntUpdate();
// C remains victim (highest threat, no taunt active)
EXPECT_EQ(
_creatureA->TestGetThreatMgr().GetCurrentVictim(),
creatureC);
creatureC->CleanupCombatState();
delete creatureC;
}
} // namespace