diff --git a/src/server/game/Combat/ThreatManager.cpp b/src/server/game/Combat/ThreatManager.cpp index ce37f961c..5559b8674 100644 --- a/src/server/game/Combat/ThreatManager.cpp +++ b/src/server/game/Combat/ThreatManager.cpp @@ -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( diff --git a/src/server/game/Combat/ThreatManager.h b/src/server/game/Combat/ThreatManager.h index 261679371..f111da619 100644 --- a/src/server/game/Combat/ThreatManager.h +++ b/src/server/game/Combat/ThreatManager.h @@ -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); diff --git a/src/test/server/game/Combat/ThreatManagerTest.cpp b/src/test/server/game/Combat/ThreatManagerTest.cpp index df29e0495..af4e48aaf 100644 --- a/src/test/server/game/Combat/ThreatManagerTest.cpp +++ b/src/test/server/game/Combat/ThreatManagerTest.cpp @@ -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::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