Files
ThordekkCore/src/modules/Playerbot/Lifecycle/DeathRecoveryManager.cpp
T
2026-05-01 07:18:39 -03:00

1866 lines
72 KiB
C++

/*
* Copyright (C) 2025 TrinityCore <https://www.trinitycore.org/>
*
* 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.
*/
#include "DeathRecoveryManager.h"
#include "Player.h"
#include "Creature.h"
#include "Corpse.h"
#include "ObjectAccessor.h"
#include "ObjectMgr.h"
#include "Map.h"
#include "TerrainMgr.h"
#include "GameObject.h" // For TransportBase and transport handling in manual teleport completion
#include "WorldSession.h"
// MovementPackets.h removed - teleport ACK now handled by BotPacketSimulator
#include "MotionMaster.h"
#include "Log.h"
#include "Config/PlayerbotConfig.h"
#include "Config/PlayerbotConfig.h"
#include "AI/BotAI.h"
#include "Interaction/Core/InteractionManager.h"
#include "GridNotifiers.h"
#include "CellImpl.h"
#include "GameTime.h" // For corpse reclaim delay check
#include <sstream>
#include "../Spatial/SpatialGridManager.h" // Lock-free spatial grid for deadlock fix
#include "../Spatial/SpatialGridQueryHelpers.h" // Thread-safe spatial queries
#include "MiscPackets.h" // For ReclaimCorpse packet
#include "Opcodes.h" // For CMSG_RECLAIM_CORPSE
#include "../Session/BotSessionManager.h" // For BotSessionManager::GetBotSession (safe resurrection)
#include "../Session/BotSession.h" // For BotSession::QueueSafeResurrection (crash fix)
// PHASE 3 MIGRATION: Movement Arbiter Integration
#include "Movement/UnifiedMovementCoordinator.h"
#include "Movement/Arbiter/MovementRequest.h"
#include "Movement/Arbiter/MovementPriorityMapper.h"
namespace Playerbot
{
// ============================================================================
// DeathRecoveryConfig Implementation
// ============================================================================
DeathRecoveryConfig DeathRecoveryConfig::LoadFromConfig()
{
DeathRecoveryConfig config;
config.autoReleaseDelayMs = sPlayerbotConfig->GetInt("Playerbot.AutoReleaseDelay", 5) * 1000;
config.preferCorpseRun = sPlayerbotConfig->GetBool("Playerbot.PreferCorpseRun", true);
config.maxCorpseRunDistance = sPlayerbotConfig->GetFloat("Playerbot.MaxCorpseRunDistance", 200.0f);
config.autoSpiritHealer = sPlayerbotConfig->GetBool("Playerbot.AutoSpiritHealer", true);
config.allowBattleResurrection = sPlayerbotConfig->GetBool("Playerbot.AllowBattleResurrection", true);
config.navigationUpdateInterval = sPlayerbotConfig->GetInt("Playerbot.DeathRecovery.NavigationInterval", 500);
config.corpseDistanceCheckInterval = sPlayerbotConfig->GetInt("Playerbot.DeathRecovery.DistanceCheckInterval", 1000);
config.skipResurrectionSickness = sPlayerbotConfig->GetBool("Playerbot.SkipResurrectionSickness", false);
config.spiritHealerSearchRadius = sPlayerbotConfig->GetFloat("Playerbot.SpiritHealerSearchRadius", 150.0f);
config.resurrectionTimeout = sPlayerbotConfig->GetInt("Playerbot.ResurrectionTimeout", 300) * 1000;
config.logDebugInfo = sPlayerbotConfig->GetBool("Playerbot.LogDeathRecovery", true);
return config;
}
// ============================================================================
// DeathRecoveryStatistics Implementation
// ============================================================================
void DeathRecoveryStatistics::RecordDeath()
{
++totalDeaths;
}
void DeathRecoveryStatistics::RecordResurrection(ResurrectionMethod method, uint64 recoveryTimeMs, bool hadSickness)
{
switch (method)
{
case ResurrectionMethod::CORPSE_RUN:
++corpseResurrections;
break;
case ResurrectionMethod::SPIRIT_HEALER:
++spiritHealerResurrections;
if (hadSickness)
++resurrectionsWithSickness;
break;
case ResurrectionMethod::BATTLE_RESURRECTION:
++battleResurrections;
break;
default:
break;
}
totalRecoveryTimeMs += recoveryTimeMs;
uint32 totalResurrections = corpseResurrections + spiritHealerResurrections + battleResurrections;
if (totalResurrections > 0)
averageRecoveryTimeMs = totalRecoveryTimeMs / totalResurrections;
}
void DeathRecoveryStatistics::RecordFailure()
{
++failedResurrections;
}
::std::string DeathRecoveryStatistics::ToString() const
{
::std::ostringstream oss;
oss << "Death Recovery Statistics:\n"
<< " Total Deaths: " << totalDeaths << "\n"
<< " Corpse Resurrections: " << corpseResurrections << "\n"
<< " Spirit Healer Resurrections: " << spiritHealerResurrections << "\n"
<< " Battle Resurrections: " << battleResurrections << "\n"
<< " Failed Resurrections: " << failedResurrections << "\n"
<< " Average Recovery Time: " << (averageRecoveryTimeMs / 1000.0f) << "s\n"
<< " Resurrections with Sickness: " << resurrectionsWithSickness;
return oss.str();
}
// ============================================================================
// DeathRecoveryManager Implementation
// ============================================================================
DeathRecoveryManager::DeathRecoveryManager(Player* bot, BotAI* ai)
: m_bot(bot)
, m_ai(ai)
, m_state(DeathRecoveryState::NOT_DEAD)
, m_method(ResurrectionMethod::UNDECIDED)
, m_corpseDistance(-1.0f)
, m_spiritHealerGuid(ObjectGuid::Empty)
, m_navigationActive(false)
, m_releaseTimer(0)
, m_stateTimer(0)
, m_retryTimer(0)
, m_retryCount(0)
, m_needsTeleportAck(false)
{
m_config = DeathRecoveryConfig::LoadFromConfig();
// CRITICAL: No logging with bot->GetName() in constructor/destructor
// Player's m_name can be corrupted during concurrent access, causing ACCESS_VIOLATION
}
DeathRecoveryManager::~DeathRecoveryManager()
{
// CRITICAL: No logging in destructors - allocation can throw + player may be invalid
}
// ========================================================================
// LIFECYCLE EVENTS
// ========================================================================
void DeathRecoveryManager::OnDeath()
{
// CRITICAL DIAGNOSTIC: Log every death attempt with timestamp
static uint32 deathCounter = 0;
++deathCounter;
TC_LOG_ERROR("playerbot.death", "========================================");
TC_LOG_ERROR("playerbot.death", " OnDeath() CALLED #{} for bot {}! deathState={}, IsAlive={}, IsGhost={}",
deathCounter, m_bot ? m_bot->GetName() : "nullptr",
m_bot ? static_cast<int>(m_bot->getDeathState()) : -1,
m_bot ? (m_bot->IsAlive() ? "TRUE" : "FALSE") : "null",
IsGhost() ? "TRUE" : "FALSE");
TC_LOG_ERROR("playerbot.death", "========================================");
// No lock needed - death recovery state is per-bot instance data
if (!ValidateBotState())
{
TC_LOG_ERROR("playerbot.death", " OnDeath: ValidateBotState FAILED!");
return;
}
m_stats.RecordDeath();
m_deathTime = ::std::chrono::steady_clock::now();
m_method = ResurrectionMethod::UNDECIDED;
m_spiritHealerGuid = ObjectGuid::Empty;
m_navigationActive = false;
m_releaseTimer = m_config.autoReleaseDelayMs;
m_retryCount = 0;
// Check if bot is already a ghost (TrinityCore auto-releases spirit on death)
if (IsGhost())
{
// CRITICAL FIX: Clear combat references to prevent use-after-free crash in Map::Update()
// Ghost bots have invalid CombatReference pointers that cause ACCESS_VIOLATION
m_bot->GetCombatManager().EndAllPvECombat();
TC_LOG_ERROR("playerbot.death", " Bot {} DIED! Already a ghost, skipping spirit release. IsAlive={}, IsGhost={}",
m_bot->GetName(), m_bot->IsAlive(), IsGhost());
TransitionToState(DeathRecoveryState::GHOST_DECIDING, "Bot died as ghost");
}
else
{
// NOTE: Do NOT call EndAllPvECombat() here - BuildPlayerRepop() will handle cleanup
// Calling it here causes double-remove of auras ? ASSERT(!aura->IsRemoved()) failure
TC_LOG_ERROR("playerbot.death", " Bot {} DIED! Initiating death recovery. Auto-release in {}s. IsAlive={}, IsGhost={}",
m_bot->GetName(), m_config.autoReleaseDelayMs / 1000.0f, m_bot->IsAlive(), IsGhost());
TransitionToState(DeathRecoveryState::JUST_DIED, "Bot died");
}
}
void DeathRecoveryManager::OnResurrection()
{
// No lock needed - death recovery state is per-bot instance data
if (m_state == DeathRecoveryState::NOT_DEAD)
return;
uint64 recoveryTime = GetTimeSinceDeath();
bool hadSickness = m_method == ResurrectionMethod::SPIRIT_HEALER && WillReceiveResurrectionSickness();
m_stats.RecordResurrection(m_method, recoveryTime, hadSickness);
TC_LOG_INFO("playerbot.death", "Bot {} resurrected via {} in {:.2f}s",
m_bot ? m_bot->GetName() : "nullptr",
m_method == ResurrectionMethod::CORPSE_RUN ? "corpse" :
m_method == ResurrectionMethod::SPIRIT_HEALER ? "spirit healer" :
m_method == ResurrectionMethod::BATTLE_RESURRECTION ? "battle rez" : "unknown",
recoveryTime / 1000.0f);
Reset();
// CRITICAL FIX: Signal BotAI to reset AI state from DEAD to SOLO
// Without this, bots remain in DEAD state and do nothing after resurrection
if (m_ai)
{
m_ai->OnRespawn();
TC_LOG_INFO("playerbot.death", "Bot {} AI state reset via OnRespawn() - bot should now resume normal behavior",
m_bot ? m_bot->GetName() : "nullptr");
}
else
{
TC_LOG_ERROR("playerbot.death", "Bot {} has no AI reference - cannot reset AI state after resurrection!",
m_bot ? m_bot->GetName() : "nullptr");
}
}
void DeathRecoveryManager::Reset()
{
// No lock needed - death recovery state is per-bot instance data
m_state = DeathRecoveryState::NOT_DEAD;
m_method = ResurrectionMethod::UNDECIDED;
m_corpseDistance = -1.0f;
m_spiritHealerGuid = ObjectGuid::Empty;
m_navigationActive = false;
m_releaseTimer = 0;
m_stateTimer = 0;
m_retryTimer = 0;
m_retryCount = 0;
m_needsTeleportAck = false;
// OPTION 3 REFACTOR: No longer manage Ghost flag manually
// TrinityCore handles Ghost aura (spell 8326) and PLAYER_FLAGS_GHOST automatically
// We only need to reset our internal state tracking
LogDebug("Death recovery state reset");
}
// ========================================================================
// MAIN UPDATE LOOP
// ========================================================================
void DeathRecoveryManager::Update(uint32 diff)
{
// Only update if in death recovery
if (m_state == DeathRecoveryState::NOT_DEAD)
return;
// No lock needed - death recovery state is per-bot instance data
if (!ValidateBotState())
{
TC_LOG_ERROR("playerbot.death", " Bot {} Update: ValidateBotState FAILED!", m_bot ? m_bot->GetName() : "nullptr");
HandleResurrectionFailure("Bot state validation failed");
return;
}
// Check for timeout
if (IsResurrectionTimedOut())
{
TC_LOG_ERROR("playerbot.death", "? Bot {} Update: Resurrection TIMED OUT!", m_bot->GetName());
HandleResurrectionFailure("Resurrection timed out");
return;
}
// CLEANUP FIX: Detect bots stuck in old failed state before ghost flag fix
// Old bug signature: State=RESURRECTION_FAILED (10) with IsGhost=false
// These bots need to be force-resurrected immediately to unstick them
// Note: Don't check IsAlive() here because ForceResurrection will reset state to NOT_DEAD,
// and ResurrectPlayer() doesn't set IsAlive instantly, which would cause repeated triggering
if (m_state.load() == DeathRecoveryState::RESURRECTION_FAILED && !IsGhost())
{
TC_LOG_ERROR("playerbot.death", " Bot {} STUCK in old failed state (State=10, IsGhost=false) - FORCE RESURRECTING!",
m_bot->GetName());
ForceResurrection(ResurrectionMethod::SPIRIT_HEALER);
return;
}
// Log state every 5 seconds
static uint32 logTimer = 0;
logTimer += diff;
if (logTimer >= 5000)
{
logTimer = 0;
TC_LOG_ERROR("playerbot.death", " Bot {} Update: State={}, IsAlive={}, IsGhost={}",
m_bot->GetName(), static_cast<int>(m_state.load()), m_bot->IsAlive(), IsGhost());
}
// Handle current state
switch (m_state.load())
{
case DeathRecoveryState::JUST_DIED:
HandleJustDied(diff);
break;
case DeathRecoveryState::RELEASING_SPIRIT:
HandleReleasingSpirit(diff);
break;
case DeathRecoveryState::PENDING_TELEPORT_ACK:
HandlePendingTeleportAck(diff);
break;
case DeathRecoveryState::GHOST_DECIDING:
HandleGhostDeciding(diff);
break;
case DeathRecoveryState::RUNNING_TO_CORPSE:
HandleRunningToCorpse(diff);
break;
case DeathRecoveryState::AT_CORPSE:
HandleAtCorpse(diff);
break;
case DeathRecoveryState::FINDING_SPIRIT_HEALER:
HandleFindingSpiritHealer(diff);
break;
case DeathRecoveryState::MOVING_TO_SPIRIT_HEALER:
HandleMovingToSpiritHealer(diff);
break;
case DeathRecoveryState::AT_SPIRIT_HEALER:
HandleAtSpiritHealer(diff);
break;
case DeathRecoveryState::RESURRECTING:
HandleResurrecting(diff);
break;
case DeathRecoveryState::RESURRECTION_FAILED:
HandleResurrectionFailed(diff);
break;
default:
break;
}
}
// ========================================================================
// STATE MACHINE HANDLERS
// ========================================================================
void DeathRecoveryManager::HandleJustDied(uint32 diff)
{
// Wait for auto-release timer
if (m_releaseTimer > diff)
{
m_releaseTimer -= diff;
if (m_releaseTimer % 1000 < diff) // Log every second
{
TC_LOG_ERROR("playerbot.death", "? Bot {} waiting to release spirit... {:.1f}s remaining",
m_bot->GetName(), m_releaseTimer / 1000.0f);
}
return;
}
// Check if bot already released (manually or by another system)
if (IsGhost())
{
TC_LOG_ERROR("playerbot.death", " Bot {} already a ghost, proceeding to decision phase", m_bot->GetName());
TransitionToState(DeathRecoveryState::GHOST_DECIDING, "Already ghost");
return;
}
// Release spirit
TC_LOG_ERROR("playerbot.death", " Bot {} auto-release timer expired, releasing spirit...", m_bot->GetName());
TransitionToState(DeathRecoveryState::RELEASING_SPIRIT, "Auto-release timer expired");
}
void DeathRecoveryManager::HandleReleasingSpirit(uint32 diff)
{
TC_LOG_ERROR("playerbot.death", " Bot {} attempting to release spirit... IsGhost={}", m_bot->GetName(), IsGhost());
if (ExecuteReleaseSpirit())
{
TC_LOG_ERROR("playerbot.death", " Bot {} spirit released successfully! IsGhost={}", m_bot->GetName(), IsGhost());
// NOTE: ExecuteReleaseSpirit() may transition to PENDING_TELEPORT_ACK state
// If not (no teleport needed), it will fall through to GHOST_DECIDING
}
else
{
// Retry after delay
m_stateTimer += diff;
if (m_stateTimer > 2000) // Retry every 2 seconds
{
m_stateTimer = 0;
TC_LOG_ERROR("playerbot.death", " Bot {} retrying spirit release (IsGhost={})", m_bot->GetName(), IsGhost());
}
}
}
void DeathRecoveryManager::HandlePendingTeleportAck(uint32 diff)
{
// The BotPacketSimulator's ACK scanner (runs every 100ms from BotSession::Update(),
// BEFORE the AI update loop) automatically processes near-teleport ACKs when
// IsBeingTeleportedNear() is true. This provides the 100ms stabilization delay
// that prevents the Spell.cpp:603 crash (m_spellModTakingSpell assertion).
//
// This state handler just waits for the teleport to complete, then transitions.
auto now = ::std::chrono::steady_clock::now();
auto elapsed = ::std::chrono::duration_cast<::std::chrono::milliseconds>(
now - m_teleportAckTime).count();
// Wait minimum 200ms for BotPacketSimulator ACK scanner to process the teleport
// (ACK scanner runs every 100ms, so 200ms guarantees at least one full scan cycle)
if (elapsed < 200)
{
if (elapsed % 50 < diff)
{
TC_LOG_TRACE("playerbot.death", "Bot {} waiting for ACK scanner to process teleport... {}ms elapsed",
m_bot->GetName(), elapsed);
}
return;
}
// Check if teleport was successfully ACKed by BotPacketSimulator
if (m_bot && !m_bot->IsBeingTeleportedNear())
{
TC_LOG_DEBUG("playerbot.death", "Bot {} teleport ACK processed by BotPacketSimulator ({}ms)",
m_bot->GetName(), elapsed);
m_needsTeleportAck = false;
}
else if (elapsed > 5000)
{
// Safety timeout - teleport should have been ACKed within a few scan cycles
TC_LOG_ERROR("playerbot.death", "Bot {} teleport ACK timeout after {}ms, forcing transition",
m_bot->GetName(), elapsed);
m_needsTeleportAck = false;
}
else
{
// Still pending - wait for next update cycle
return;
}
// Transition to next state
TransitionToState(DeathRecoveryState::GHOST_DECIDING, "Teleport ack completed, proceeding to decision");
}
void DeathRecoveryManager::HandleGhostDeciding(uint32 /*diff*/)
{
TC_LOG_ERROR("playerbot.death", " Bot {} deciding resurrection method...", m_bot->GetName());
// Check for special cases first (battlegrounds, arenas, etc)
if (CheckSpecialResurrectionCases())
{
TC_LOG_ERROR("playerbot.death", " Bot {} in special zone, using special resurrection", m_bot->GetName());
return;
}
// Make resurrection decision
DecideResurrectionMethod();
if (m_method == ResurrectionMethod::CORPSE_RUN)
{
TC_LOG_ERROR("playerbot.death", " Bot {} chose CORPSE RUN (distance: {:.1f}y)", m_bot->GetName(), GetCorpseDistance());
TransitionToState(DeathRecoveryState::RUNNING_TO_CORPSE, "Chose corpse run");
}
else if (m_method == ResurrectionMethod::SPIRIT_HEALER)
{
TC_LOG_ERROR("playerbot.death", " Bot {} chose SPIRIT HEALER", m_bot->GetName());
TransitionToState(DeathRecoveryState::FINDING_SPIRIT_HEALER, "Chose spirit healer");
}
else
{
TC_LOG_ERROR("playerbot.death", " Bot {} FAILED to decide resurrection method!", m_bot->GetName());
HandleResurrectionFailure("Failed to decide resurrection method");
}
}
void DeathRecoveryManager::HandleRunningToCorpse(uint32 diff)
{
// Update corpse distance periodically
m_stateTimer += diff;
if (m_stateTimer >= m_config.corpseDistanceCheckInterval)
{
m_stateTimer = 0;
UpdateCorpseDistance();
if (m_corpseDistance < 0.0f)
{
TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: Lost corpse location during corpse run!", m_bot->GetName());
HandleResurrectionFailure("Lost corpse location");
return;
}
TC_LOG_INFO("playerbot.death", " Bot {} distance to corpse: {:.1f} yards (resurrection range: {})",
m_bot->GetName(), m_corpseDistance, CORPSE_RESURRECTION_RANGE);
}
// Check if in range
if (IsInCorpseRange())
{
TC_LOG_INFO("playerbot.death", " Bot {} reached corpse! Distance: {:.1f} yards",
m_bot->GetName(), m_corpseDistance);
TransitionToState(DeathRecoveryState::AT_CORPSE, "Reached corpse");
return;
}
// Update navigation periodically
auto now = ::std::chrono::steady_clock::now();
auto timeSinceLastNav = ::std::chrono::duration_cast<::std::chrono::milliseconds>(
now - m_lastNavigationUpdate).count();
if (timeSinceLastNav >= m_config.navigationUpdateInterval)
{
TC_LOG_DEBUG("playerbot.death", " Bot {} updating navigation to corpse (distance: {:.1f}y)",
m_bot->GetName(), m_corpseDistance);
if (NavigateToCorpse())
{
m_lastNavigationUpdate = now;
}
else
{
TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: Failed to navigate to corpse!",
m_bot->GetName());
HandleResurrectionFailure("Failed to navigate to corpse");
}
}
}
void DeathRecoveryManager::HandleAtCorpse(uint32 diff)
{
m_stateTimer += diff;
// ============================================================================
// ENTERPRISE-GRADE PACKET-BASED RESURRECTION SYSTEM (Option B)
// ============================================================================
//
// Design Rationale:
// -----------------
// Direct ResurrectPlayer() calls from bot worker threads cause crashes when
// UpdateAreaDependentAuras() ? CastSpell() is called during resurrection.
// GM .revive command (main thread) works perfectly with same auras.
//
// Solution: Queue CMSG_RECLAIM_CORPSE packet for main thread processing.
// This delegates ALL resurrection logic to TrinityCore's proven packet handler
// (HandleReclaimCorpse in MiscHandler.cpp) which runs on the main thread.
//
// Thread Safety:
// --------------
// - QueuePacket() uses LockedQueue<WorldPacket*> with mutex (thread-safe)
// - HandleReclaimCorpse() executes on main thread (no race conditions)
// - HandleResurrecting() polls IsAlive() to detect completion
//
// Validation Strategy:
// --------------------
// Perform basic pre-validation here (corpse exists, ghost flag, distance)
// HandleReclaimCorpse() performs its own comprehensive validation (7 checks)
// This prevents unnecessary packet queuing for obviously invalid cases
// ============================================================================
// VALIDATION 1: Bot must not be already alive
if (m_bot->IsAlive())
{
TC_LOG_INFO("playerbot.death", " Bot {} is already alive, no resurrection needed",
m_bot->GetName());
TransitionToState(DeathRecoveryState::NOT_DEAD, "Already alive");
return;
}
// VALIDATION 2: Must have corpse
Corpse* corpse = m_bot->GetCorpse();
if (!corpse)
{
TC_LOG_ERROR("playerbot.death", " Bot {} has no corpse! Cannot resurrect.",
m_bot->GetName());
// Timeout after 30 seconds
if (m_stateTimer > 30000)
{
TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: No corpse after 30 seconds!",
m_bot->GetName());
HandleResurrectionFailure("No corpse exists after 30 seconds");
}
return;
}
// VALIDATION 3: Must be in ghost form (spirit released)
if (!m_bot->HasPlayerFlag(PLAYER_FLAGS_GHOST))
{
TC_LOG_WARN("playerbot.death", " Bot {} does not have PLAYER_FLAGS_GHOST, cannot resurrect yet",
m_bot->GetName());
// Timeout after 30 seconds
if (m_stateTimer > 30000)
{
TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: No ghost flag after 30 seconds!",
m_bot->GetName());
HandleResurrectionFailure("Spirit not released after 30 seconds");
}
return;
}
// VALIDATION 4: Check distance to corpse (must be within 39 yards)
// HandleReclaimCorpse uses corpse->IsWithinDistInMap(player, 39.0f, true)
float distance = m_bot->GetDistance2d(corpse);
if (!corpse->IsWithinDistInMap(m_bot, CORPSE_RESURRECTION_RANGE, true))
{
TC_LOG_WARN("playerbot.death", " Bot {} too far from corpse ({:.1f}y > {:.1f}y), moving closer",
m_bot->GetName(), distance, CORPSE_RESURRECTION_RANGE);
// Bot moved out of range, return to RUNNING_TO_CORPSE
TransitionToState(DeathRecoveryState::RUNNING_TO_CORPSE, "Moved out of corpse range");
m_stateTimer = 0; // Reset timer for next attempt
return;
}
// VALIDATION 5: Check ghost time delay (prevent instant resurrection)
// HandleReclaimCorpse checks: ghostTime + delay <= currentTime
time_t ghostTime = corpse->GetGhostTime();
time_t currentTime = GameTime::GetGameTime();
uint32 corpseReclaimDelay = m_bot->GetCorpseReclaimDelay(
corpse->GetType() == CORPSE_RESURRECTABLE_PVP);
time_t requiredTime = ghostTime + corpseReclaimDelay;
if (requiredTime > currentTime)
{
uint32 remainingSeconds = static_cast<uint32>(requiredTime - currentTime);
// Log waiting status every 5 seconds
if (m_stateTimer % 5000 < diff)
{
TC_LOG_INFO("playerbot.death", "? Bot {} waiting for ghost time delay ({} seconds remaining)",
m_bot->GetName(), remainingSeconds);
}
return; // Wait for delay to expire
}
// ============================================================================
// ALL VALIDATIONS PASSED - Queue Safe Resurrection (SpawnCorpseBones Crash Fix)
// ============================================================================
// CRITICAL FIX: DO NOT use CMSG_RECLAIM_CORPSE packet!
// HandleReclaimCorpse ? SpawnCorpseBones ? Map::RemoveWorldObject crashes
// due to corrupted i_worldObjects tree structure (infinite loop in _Erase).
//
// Instead, we use QueueSafeResurrection() which calls ResurrectPlayer() directly
// on the main thread WITHOUT SpawnCorpseBones. The corpse will decay naturally.
// ============================================================================
TC_LOG_INFO("playerbot.death", " Bot {} passed all 5 validation checks, queuing SAFE resurrection (bypasses SpawnCorpseBones crash)",
m_bot->GetName());
TC_LOG_DEBUG("playerbot.death", " Bot {} resurrection validation details: IsAlive=false, Corpse=yes, "
"Ghost=true, Distance={:.1f}y<{:.1f}y, GhostDelay=expired",
m_bot->GetName(), distance, CORPSE_RESURRECTION_RANGE);
// Get BotSession for safe resurrection queue
BotSession* botSession = BotSessionManager::GetBotSession(m_bot->GetSession());
if (!botSession)
{
TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: BotSession not found! Cannot queue safe resurrection.",
m_bot->GetName());
HandleResurrectionFailure("BotSession not found for safe resurrection");
return;
}
// Queue safe resurrection (thread-safe atomic flag)
// ProcessPendingSafeResurrection() will execute on main thread:
// - Validates bot state (IsAlive, ghost flag, corpse distance)
// - Calls ResurrectPlayer() directly
// - Does NOT call SpawnCorpseBones (avoids crash)
// - Corpse will decay naturally via TrinityCore's corpse cleanup
botSession->QueueSafeResurrection();
TC_LOG_INFO("playerbot.death", " Bot {} queued SAFE resurrection for main thread "
"(distance: {:.1f}y, map: {}, corpseMap: {}) - NO SpawnCorpseBones!",
m_bot->GetName(), distance, m_bot->GetMapId(), corpse->GetMapId());
// Transition to RESURRECTING state
// HandleResurrecting() will poll IsAlive() to detect completion
// 30-second timeout will trigger HandleResurrectionFailure() if needed
TransitionToState(DeathRecoveryState::RESURRECTING, "Safe resurrection scheduled (no SpawnCorpseBones)");
}
void DeathRecoveryManager::HandleFindingSpiritHealer(uint32 diff)
{
Creature* spiritHealer = FindNearestSpiritHealer();
if (spiritHealer)
{
m_spiritHealerGuid = spiritHealer->GetGUID();
m_spiritHealerLocation = WorldLocation(spiritHealer->GetMapId(),
spiritHealer->GetPositionX(),
spiritHealer->GetPositionY(),
spiritHealer->GetPositionZ(),
spiritHealer->GetOrientation());
TransitionToState(DeathRecoveryState::MOVING_TO_SPIRIT_HEALER, "Found spirit healer");
}
else
{
m_stateTimer += diff;
if (m_stateTimer > 10000) // Retry every 10 seconds
{
m_stateTimer = 0;
LogDebug("No spirit healer found, retrying search");
// After multiple failures, try corpse run instead
if (++m_retryCount >= 3)
{
TC_LOG_WARN("playerbot.death", "Bot {} cannot find spirit healer, switching to corpse run",
m_bot->GetName());
m_method = ResurrectionMethod::CORPSE_RUN;
TransitionToState(DeathRecoveryState::RUNNING_TO_CORPSE, "Fallback to corpse run");
}
}
}
}
void DeathRecoveryManager::HandleMovingToSpiritHealer(uint32 /*diff*/)
{
// PHASE 5D: Thread-safe spatial grid validation
auto snapshot = SpatialGridQueryHelpers::FindCreatureByGuid(m_bot, m_spiritHealerGuid);
Creature* spiritHealer = nullptr;
if (snapshot && snapshot->IsAlive())
{
// Get Creature* for distance check (validated via snapshot first)
spiritHealer = ObjectAccessor::GetCreature(*m_bot, m_spiritHealerGuid);
}
if (!spiritHealer || !spiritHealer->IsAlive())
{
m_spiritHealerGuid = ObjectGuid::Empty;
TransitionToState(DeathRecoveryState::FINDING_SPIRIT_HEALER, "Spirit healer invalid");
return;
}
float distance = m_bot->GetDistance(spiritHealer);
if (distance <= SPIRIT_HEALER_INTERACTION_RANGE)
{
TransitionToState(DeathRecoveryState::AT_SPIRIT_HEALER, "Reached spirit healer");
return;
}
// Update navigation
auto now = ::std::chrono::steady_clock::now();
auto timeSinceLastNav = ::std::chrono::duration_cast<::std::chrono::milliseconds>(
now - m_lastNavigationUpdate).count();
if (timeSinceLastNav >= m_config.navigationUpdateInterval)
{
if (NavigateToSpiritHealer())
{
m_lastNavigationUpdate = now;
LogDebug("Navigating to spirit healer, distance: " + ::std::to_string(distance));
}
else
{
HandleResurrectionFailure("Failed to navigate to spirit healer");
}
}
}
void DeathRecoveryManager::HandleAtSpiritHealer(uint32 diff)
{
if (InteractWithSpiritHealer())
{
TransitionToState(DeathRecoveryState::RESURRECTING, "Interacting with spirit healer");
}
else
{
m_stateTimer += diff;
if (m_stateTimer > 5000) // Retry after 5 seconds
{
m_stateTimer = 0;
LogDebug("Retrying spirit healer interaction");
}
}
}
void DeathRecoveryManager::HandleResurrecting(uint32 diff)
{
// Check if bot is now alive
if (m_bot->IsAlive())
{
TC_LOG_INFO("playerbot.death", " Bot {} IS ALIVE! Calling OnResurrection()...",
m_bot->GetName());
OnResurrection();
return;
}
// Wait for resurrection to complete (max 30 seconds)
m_stateTimer += diff;
// Log waiting status every 5 seconds
if (m_stateTimer % 5000 < diff)
{
TC_LOG_WARN("playerbot.death", "? Bot {} waiting for resurrection... ({:.1f}s elapsed, IsAlive={})",
m_bot->GetName(), m_stateTimer / 1000.0f, m_bot->IsAlive());
}
if (m_stateTimer > 30000)
{
// BATTLEGROUND FIX: In BGs, bots are never auto-resurrected because the BG spirit guide
// requires client-side gossip interaction that bots don't perform. Instead of falling
// through to RESURRECTION_FAILED ? retry ? corpse run, actively force-resurrect.
// This prevents the pathological cycle where bots run to their corpse in BGs.
if (m_method == ResurrectionMethod::AUTO_RESURRECT && m_bot->InBattleground())
{
TC_LOG_INFO("playerbot.death", "Bot {} in BG auto-resurrect: forcing resurrection after 30s wave timer",
m_bot->GetName());
if (ForceResurrection(ResurrectionMethod::SPIRIT_HEALER))
return;
// ForceResurrection failed (no corpse or no BotSession) ? fall through to normal failure path
}
TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: Resurrection did not complete after 30 seconds! (IsAlive={})",
m_bot->GetName(), m_bot->IsAlive());
HandleResurrectionFailure("Resurrection did not complete");
}
}
void DeathRecoveryManager::HandleResurrectionFailed(uint32 diff)
{
m_retryTimer += diff;
if (m_retryTimer >= RETRY_DELAY_MS)
{
m_retryTimer = 0;
// BATTLEGROUND FIX: Never retry via GHOST_DECIDING in BGs.
// GHOST_DECIDING calls DecideResurrectionMethod() which may choose CORPSE_RUN
// if InBattleground() returns false (BG ended during wait). Force-resurrect directly.
if (m_bot && m_bot->InBattleground())
{
TC_LOG_INFO("playerbot.death", "Bot {} in BG resurrection retry: forcing resurrection directly",
m_bot->GetName());
ForceResurrection(ResurrectionMethod::SPIRIT_HEALER);
return;
}
if (++m_retryCount >= MAX_RETRY_ATTEMPTS)
{
TC_LOG_ERROR("playerbot.death", "Bot {} exhausted all resurrection attempts",
m_bot->GetName());
m_stats.RecordFailure();
// Force resurrection as last resort
ForceResurrection(ResurrectionMethod::SPIRIT_HEALER);
}
else
{
TC_LOG_WARN("playerbot.death", "Bot {} retrying resurrection (attempt {}/{})",
m_bot->GetName(), m_retryCount, MAX_RETRY_ATTEMPTS);
// Reset to decision phase
TransitionToState(DeathRecoveryState::GHOST_DECIDING, "Retry resurrection");
}
}
}
// ========================================================================
// DECISION LOGIC
// ========================================================================
void DeathRecoveryManager::DecideResurrectionMethod()
{
// Check configuration preference
if (ShouldDoCorpseRun())
{
m_method = ResurrectionMethod::CORPSE_RUN;
TC_LOG_DEBUG("playerbot.death", "Bot {} chose corpse run (distance: {:.1f}y)",
m_bot->GetName(), GetCorpseDistance());
}
else if (ShouldUseSpiritHealer())
{
m_method = ResurrectionMethod::SPIRIT_HEALER;
TC_LOG_DEBUG("playerbot.death", "Bot {} chose spirit healer",
m_bot->GetName());
}
else
{
// Default to corpse run
m_method = ResurrectionMethod::CORPSE_RUN;
TC_LOG_WARN("playerbot.death", "Bot {} defaulting to corpse run",
m_bot->GetName());
}
}
bool DeathRecoveryManager::ShouldDoCorpseRun() const
{
if (!m_config.preferCorpseRun)
return false;
UpdateCorpseDistance();
if (m_corpseDistance < 0.0f)
return false; // No corpse
if (m_corpseDistance > m_config.maxCorpseRunDistance)
return false; // Too far
return true;
}
bool DeathRecoveryManager::ShouldUseSpiritHealer() const
{
if (!m_config.autoSpiritHealer)
return false;
// Use spirit healer if corpse is too far or unreachable
UpdateCorpseDistance();
if (m_corpseDistance < 0.0f)
return true; // No corpse
if (m_corpseDistance > m_config.maxCorpseRunDistance)
return true; // Too far
return false;
}
bool DeathRecoveryManager::CheckSpecialResurrectionCases()
{
if (!m_bot)
return false;
// Battlegrounds/arenas have special resurrection mechanics
if (m_bot->InBattleground())
{
// Battlegrounds typically auto-resurrect
TC_LOG_DEBUG("playerbot.death", "Bot {} in battleground, using default BG resurrection",
m_bot->GetName());
m_method = ResurrectionMethod::AUTO_RESURRECT;
TransitionToState(DeathRecoveryState::RESURRECTING, "Battleground auto-resurrection");
return true;
}
if (m_bot->InArena())
{
// Arenas don't allow resurrection during match
TC_LOG_DEBUG("playerbot.death", "Bot {} in arena, waiting for match end",
m_bot->GetName());
return true; // Stay in current state
}
return false;
}
// ========================================================================
// RESURRECTION EXECUTION
// ========================================================================
bool DeathRecoveryManager::ExecuteReleaseSpirit()
{
if (!m_bot)
return false;
// Check if already a ghost
if (IsGhost())
return true;
// OPTION 3 REFACTOR: Let TrinityCore handle Ghost aura (spell 8326) and PLAYER_FLAGS_GHOST
// BuildPlayerRepop() automatically:
// 1. Creates corpse
// 2. Applies Ghost aura (spell 8326) which sets PLAYER_FLAGS_GHOST
// 3. Sets ghost state properly
// We never touch Ghost aura or flag manually - TrinityCore manages the entire lifecycle
// DEBUG: Log position BEFORE BuildPlayerRepop
Position posBeforeRepop = m_bot->GetPosition();
TC_LOG_ERROR("playerbot.death", " Bot {} BEFORE BuildPlayerRepop: Map={} Zone={} Pos=({:.2f}, {:.2f}, {:.2f}) Team={}",
m_bot->GetName(), m_bot->GetMapId(), m_bot->GetZoneId(),
posBeforeRepop.GetPositionX(), posBeforeRepop.GetPositionY(), posBeforeRepop.GetPositionZ(),
m_bot->GetTeam());
// CRITICAL FIX: Check if bot already has Ghost aura before calling BuildPlayerRepop()
// BuildPlayerRepop() casts Ghost spell (8326) at Player.cpp:4313, but will crash if aura already applied
// This can happen if bot dies while already a ghost or if resurrection failed
if (m_bot->HasAura(8326))
{
TC_LOG_WARN("playerbot.death",
" Bot {} already has Ghost aura (8326)! Skipping BuildPlayerRepop() to prevent assertion crash. "
"This can happen if bot died while already a ghost.",
m_bot->GetName());
// Bot is already a ghost - skip BuildPlayerRepop() and proceed to decision phase
// Return TRUE because this is a successful operation (we successfully avoided the crash)
TransitionToState(DeathRecoveryState::GHOST_DECIDING, "Bot already has Ghost aura, skipping BuildPlayerRepop()");
return true; // Success - we handled the Ghost aura case properly
}
// CRITICAL FIX: Prevent double BuildPlayerRepop() race condition with TrinityCore auto-release
// Problem: TrinityCore's Player::Update() has auto-release logic (Player.cpp:1075-1081):
// if (m_deathTimer > 0 && !Instanceable) { m_deathTimer = 0; BuildPlayerRepop(); RepopAtGraveyard(); }
// If TrinityCore's auto-release fires AFTER we call BuildPlayerRepop(), it will apply Ghost aura (8326) TWICE
// ? SpellAuras.cpp:168 assertion crash: "HasEffect(effIndex) == (!apply)"
//
// Solution: Check if deathTimer is about to expire (< 500ms). If yes, let TrinityCore handle it automatically.
// This prevents us from calling BuildPlayerRepop() manually just before TrinityCore does the same.
// Trade-off: Bot resurrection may be delayed by up to 500ms, but this prevents 100% of double-call crashes.
uint32 deathTimer = m_bot->GetDeathTimer();
if (deathTimer > 0 && deathTimer < 500)
{
TC_LOG_WARN("playerbot.death",
"? Bot {} has death timer {}ms < 500ms - letting TrinityCore auto-release handle BuildPlayerRepop() "
"to prevent double-call crash. Will wait for next update.",
m_bot->GetName(), deathTimer);
return false; // Try again next update after TrinityCore auto-release fires
}
// GHOST SPELL CRASH FIX: Queue CMSG_REPOP_REQUEST packet instead of calling BuildPlayerRepop() directly
// Direct call executes on bot worker thread ? race condition with Map::Update() during Ghost aura application
// Packet-based approach defers to main thread via PacketDeferralClassifier (already includes CMSG_REPOP_REQUEST)
//
// HandleRepopRequest (MiscHandler.cpp:82-83) will execute on main thread:
// 1. BuildPlayerRepop() - Creates corpse + applies Ghost spell 8326
// 2. RepopAtGraveyard() - Teleports to graveyard
//
// This serializes Ghost spell application with Map::Update(), preventing SpellAuras.cpp:168 crash
// TrinityCore 12.0: QueuePacket now takes WorldPacket&& instead of WorldPacket*
WorldPacket repopPacket(CMSG_REPOP_REQUEST, 1);
repopPacket << uint8(0); // CheckInstance = false (not in instance recovery)
// m_bot->GetSession()->QueuePacket(std::move(repopPacket));
TC_LOG_WARN("playerbot.death",
"Bot {} queued CMSG_REPOP_REQUEST packet for main thread execution (Ghost spell crash fix)",
m_bot->GetName());
// BOT AUTO-RESURRECTION DETECTION: Check if BotResurrectionScript auto-resurrected during BuildPlayerRepop()
// NOTE: With packet-based approach, BuildPlayerRepop() executes asynchronously on main thread
// We can't check IsAlive() immediately - the packet hasn't been processed yet
// Instead, we defer this check to next Update() cycle (see GHOST_DECIDING state handling)
// Old synchronous check removed:
// if (m_bot->IsAlive())
// {
// TC_LOG_INFO("playerbot.death",
// " Bot {} was auto-resurrected during BuildPlayerRepop() by BotResurrectionScript! "
// "Health={}/{}, Mana={}/{}, Position=({:.2f}, {:.2f}, {:.2f})",
// m_bot->GetName(),
// m_bot->GetHealth(), m_bot->GetMaxHealth(),
// m_bot->GetPower(POWER_MANA), m_bot->GetMaxPower(POWER_MANA),
// m_bot->GetPositionX(), m_bot->GetPositionY(), m_bot->GetPositionZ());
//
// TransitionToState(DeathRecoveryState::NOT_DEAD, "Auto-resurrected at corpse successfully");
// return true; // Exit HandleReleasingSpirit(), death recovery complete
// }
// PACKET-BASED APPROACH: HandleRepopRequest will execute asynchronously on main thread
// The handler will call BuildPlayerRepop() + RepopAtGraveyard() in the correct sequence
// We transition to PENDING_TELEPORT_ACK state and let the next Update() cycle check the results
//
// NOTE: All the following logic (corpse checks, zone validation, RepopAtGraveyard() call, teleport ack)
// is now handled by HandleRepopRequest on the main thread. We can't execute it here because the
// packet hasn't been processed yet - everything is asynchronous now.
//
// The state machine will handle teleport ack when IsBeingTeleportedNear() becomes true after
// HandleRepopRequest completes (see PENDING_TELEPORT_ACK state in Update() method)
TransitionToState(DeathRecoveryState::PENDING_TELEPORT_ACK,
"Waiting for CMSG_REPOP_REQUEST to execute on main thread");
TC_LOG_WARN("playerbot.death",
" Bot {} queued spirit release - waiting for main thread execution (async packet-based approach)",
m_bot->GetName());
return true;
}
bool DeathRecoveryManager::NavigateToCorpse()
{
if (!m_bot)
return false;
WorldLocation corpseLocation = GetCorpseLocation();
if (corpseLocation.GetMapId() == MAPID_INVALID)
return false;
// PHASE 3 MIGRATION: Use Movement Arbiter with DEATH_RECOVERY priority (255)
// Death recovery has the ABSOLUTE HIGHEST priority - must override ALL other movement
BotAI* botAI = dynamic_cast<BotAI*>(m_bot->GetAI());
if (botAI && botAI->GetUnifiedMovementCoordinator())
{
// CORPSE RUN MOVEMENT FIX: Throttle movement updates to 500ms to prevent spell mod crashes
// Rapid movement recalculation causes Spell.cpp:603 assertion failure (m_spellModTakingSpell corruption)
auto now = ::std::chrono::steady_clock::now();
auto timeSinceLastUpdate = ::std::chrono::duration_cast<::std::chrono::milliseconds>(now - m_lastNavigationUpdate);
if (m_navigationActive && timeSinceLastUpdate.count() < 500)
{
// Still within throttle window - don't update movement yet
TC_LOG_DEBUG("playerbot.death",
"Bot {} corpse run throttled: {}ms since last update (need 500ms)",
m_bot->GetName(), timeSinceLastUpdate.count());
return true; // Navigation still active, just throttled
}
// Get corpse object to chase
Corpse* corpse = m_bot->GetCorpse();
if (!corpse)
{
TC_LOG_ERROR("playerbot.death",
"DeathRecoveryManager: Bot {} has no corpse to navigate to",
m_bot->GetName());
return false;
}
// MOVEMENT FIX: Use RequestChaseMovement instead of RequestPointMovement
// This makes the bot continuously track the corpse GUID rather than moving to a static position
// Prevents instant resurrection appearance by providing visible corpse run with walking speed
bool accepted = botAI->RequestChaseMovement(
PlayerBotMovementPriority::DEATH_RECOVERY, // Priority 255 - HIGHEST
corpse->GetGUID(), // Chase the corpse by GUID
"Corpse run - death recovery (chase mode)",
"DeathRecoveryManager");
if (accepted)
{
TC_LOG_DEBUG("playerbot.movement.arbiter",
"DeathRecoveryManager: Bot {} requested CHASE movement to corpse {} with DEATH_RECOVERY priority (255)",
m_bot->GetName(),
corpse->GetGUID().ToString());
m_navigationActive = true;
m_lastNavigationUpdate = now; // Update timestamp for 500ms throttling
return true;
}
else
{
TC_LOG_WARN("playerbot.movement.arbiter",
"DeathRecoveryManager: Bot {} corpse run movement request FILTERED (duplicate detected)",
m_bot->GetName());
// Still consider navigation active if filtered (likely duplicate)
m_navigationActive = true;
return true;
}
}
else
{
// FALLBACK: Direct MotionMaster call if arbiter not available
// This should only happen during transition period
TC_LOG_WARN("playerbot.movement.arbiter",
"DeathRecoveryManager: Bot {} has no MovementArbiter - using legacy MovePoint() for corpse run",
m_bot->GetName());
// CRITICAL FIX: Clear MotionMaster before calling MovePoint (like mod-playerbot does)
// This prevents movement spam/cancellation that causes "teleporting" behavior
// Reference: mod-playerbot MovementActions.cpp:244-248
MotionMaster* mm = m_bot->GetMotionMaster();
mm->Clear();
// CORPSE RACE CONDITION FIX: Use walking speed to prevent instant teleportation
// Instant movement causes race condition with Map::SendObjectUpdates() when corpse
// gets deleted while still in _updateObjects set (crash at Map.cpp:1940)
// Walking speed adds natural delay (~3-5 seconds) between reaching corpse and resurrection
mm->MovePoint(0,
corpseLocation.GetPositionX(),
corpseLocation.GetPositionY(),
corpseLocation.GetPositionZ(),
true, // generatePath = true for proper pathfinding
{}, // finalOrient - default
2.5f); // speed - WALKING SPEED (default run is ~7.0) prevents instant teleport
m_navigationActive = true;
return true;
}
}
bool DeathRecoveryManager::InteractWithCorpse()
{
// RACE CONDITION FIX: Single mutex-protected critical section
// Replaces three-layer protection (mutex + debounce + atomic) that had race windows
::std::lock_guard<::std::timed_mutex> lock(_resurrectionMutex);
// Check atomic debounce inside mutex protection (prevents TOCTOU race)
uint64 now = GameTime::GetGameTimeMS();
uint64 lastAttempt = _lastResurrectionAttemptMs.load(::std::memory_order_acquire);
if (now - lastAttempt < RESURRECTION_DEBOUNCE_MS)
{
TC_LOG_WARN("playerbot.death", "Bot {} InteractWithCorpse: Too soon since last attempt ({}ms < {}ms), debouncing",
m_bot ? m_bot->GetName() : "nullptr", now - lastAttempt, RESURRECTION_DEBOUNCE_MS);
return false;
}
// Check atomic resurrection flag inside mutex protection (prevents concurrent resurrections)
bool expectedFalse = false;
if (!_resurrectionInProgress.compare_exchange_strong(expectedFalse, true, ::std::memory_order_acq_rel))
{
TC_LOG_WARN("playerbot.death", "Bot {} InteractWithCorpse: Resurrection flag already set, rejecting concurrent attempt",
m_bot ? m_bot->GetName() : "nullptr");
return false;
}
// Update last attempt timestamp atomically
_lastResurrectionAttemptMs.store(now, ::std::memory_order_release);
// RAII guard to reset resurrection flag on exit (exception-safe)
struct ResurrectionGuard {
::std::atomic<bool>& flag;
~ResurrectionGuard() { flag.store(false, ::std::memory_order_release); }
} guard{_resurrectionInProgress};
// Match TrinityCore's HandleReclaimCorpse validation checks
if (m_bot->IsAlive())
{
TC_LOG_WARN("playerbot.death", " Bot {} already alive, skipping corpse interaction", m_bot->GetName());
return true; // Not an error, just already alive
}
if (m_bot->InArena())
{
TC_LOG_DEBUG("playerbot.death", "Bot {} in arena, cannot resurrect at corpse", m_bot->GetName());
return false;
}
// OPTION 3 REFACTOR: No longer check Ghost flag manually
// TrinityCore's HandleReclaimCorpse will validate the ghost state internally
// We trust TrinityCore to handle resurrection validation
Corpse* corpse = m_bot->GetCorpse();
if (!corpse)
{
TC_LOG_ERROR("playerbot.death", " Bot {} has no corpse!", m_bot->GetName());
return false;
}
// Check corpse reclaim delay (30-second delay after death in PvP)
time_t ghostTime = corpse->GetGhostTime();
time_t reclaimDelay = m_bot->GetCorpseReclaimDelay(corpse->GetType() == CORPSE_RESURRECTABLE_PVP);
time_t currentTime = GameTime::GetGameTime();
if (time_t(ghostTime + reclaimDelay) > currentTime)
{
time_t remainingDelay = (ghostTime + reclaimDelay) - currentTime;
TC_LOG_WARN("playerbot.death", "? Bot {} corpse reclaim delay BLOCKING resurrection: {} seconds remaining (ghostTime={}, delay={}, current={})",
m_bot->GetName(), remainingDelay, ghostTime, reclaimDelay, currentTime);
return false; // Must wait for delay to expire
}
else
{
TC_LOG_INFO("playerbot.death", " Bot {} corpse reclaim delay check PASSED (ghostTime={}, delay={}, current={})",
m_bot->GetName(), ghostTime, reclaimDelay, currentTime);
}
if (!IsInCorpseRange())
{
TC_LOG_ERROR("playerbot.death", " Bot {} InteractWithCorpse FAILED: Not in corpse range! Distance: {:.1f} yards (need <= {})",
m_bot->GetName(), m_corpseDistance, CORPSE_RESURRECTION_RANGE);
return false;
}
TC_LOG_WARN("playerbot.death", " Bot {} using TrinityCore-mirrored resurrection at corpse (distance: {:.1f}y, deathState BEFORE: {})...",
m_bot->GetName(), m_corpseDistance, static_cast<int>(m_bot->getDeathState()));
// CRITICAL FIX: Mirror TrinityCore's HandleReclaimCorpse logic (MiscHandler.cpp:417-446)
// We can't use the packet handler directly because WorldPackets::Misc::ReclaimCorpse has no default constructor
// Instead, we replicate the EXACT logic from HandleReclaimCorpse:
//
// Handler logic (MiscHandler.cpp:442-445):
// _player->ResurrectPlayer(_player->InBattleground() ? 1.0f : 0.5f);
// _player->SpawnCorpseBones();
//
// This is the PROVEN, tested resurrection flow in TrinityCore
// OPTION 3 REFACTOR: ResurrectPlayer() automatically handles:
// - setDeathState(ALIVE) which properly transitions death state
// - UpdateObjectVisibility() which fixes visibility issues
// - RemovePlayerFlag(PLAYER_FLAGS_GHOST) via RemoveAurasDueToSpell(8326)
// - All Ghost aura (spell 8326) lifecycle management
// We never touch Ghost aura or flag - TrinityCore handles everything
//
// Clear movement before resurrection to prevent conflicts (like mod-playerbot does)
m_bot->GetMotionMaster()->Clear();
m_bot->StopMoving();
// DIAGNOSTIC: Log health BEFORE resurrection
uint32 healthBefore = m_bot->GetHealth();
uint32 maxHealth = m_bot->GetMaxHealth();
float restorePercent = m_bot->InBattleground() ? 1.0f : 0.5f;
DeathState deathStateBefore = m_bot->getDeathState();
TC_LOG_FATAL("playerbot.death", " Bot {} BEFORE ResurrectPlayer: Health={}/{}, RestorePercent={}, DeathState={}, IsAlive={}, IsGhost={}",
m_bot->GetName(), healthBefore, maxHealth, restorePercent,
static_cast<int>(deathStateBefore), m_bot->IsAlive(), IsGhost());
// SAFE RESURRECTION (v9): Queue via BotSession::QueueSafeResurrection()
// ============================================================================
// CRITICAL FIX: DO NOT use CMSG_RECLAIM_CORPSE packet!
// HandleReclaimCorpse ? SpawnCorpseBones ? Map::RemoveWorldObject crashes
// due to corrupted i_worldObjects tree structure (infinite loop in _Erase).
//
// Instead, we use QueueSafeResurrection() which calls ResurrectPlayer() directly
// on the main thread WITHOUT SpawnCorpseBones. The corpse will decay naturally.
// ============================================================================
BotSession* botSession = BotSessionManager::GetBotSession(m_bot->GetSession());
if (!botSession)
{
TC_LOG_ERROR("playerbot.death", "Bot {} InteractWithCorpse FAILED: BotSession not found!",
m_bot->GetName());
return false;
}
// Queue safe resurrection (thread-safe atomic flag)
botSession->QueueSafeResurrection();
TC_LOG_WARN("playerbot.death", "Bot {} queued SAFE resurrection (distance: {:.1f}y, deathState: {}) - NO SpawnCorpseBones!",
m_bot->GetName(), m_corpseDistance, static_cast<int>(deathStateBefore));
return true;
}
bool DeathRecoveryManager::NavigateToSpiritHealer()
{
if (!m_bot)
return false;
// PHASE 5D: Thread-safe spatial grid validation
auto snapshot = SpatialGridQueryHelpers::FindCreatureByGuid(m_bot, m_spiritHealerGuid);
Creature* spiritHealer = nullptr;
if (snapshot)
{
// Get Creature* for navigation (validated via snapshot first)
spiritHealer = ObjectAccessor::GetCreature(*m_bot, m_spiritHealerGuid);
}
if (!spiritHealer)
return false;
// PHASE 3 MIGRATION: Use Movement Arbiter with DEATH_RECOVERY priority (255)
// Death recovery has the ABSOLUTE HIGHEST priority - must override ALL other movement
BotAI* botAI = dynamic_cast<BotAI*>(m_bot->GetAI());
if (botAI && botAI->GetUnifiedMovementCoordinator())
{
Position spiritHealerPos(spiritHealer->GetPositionX(),
spiritHealer->GetPositionY(),
spiritHealer->GetPositionZ(),
spiritHealer->GetOrientation());
// Use Movement Arbiter for priority-based arbitration
bool accepted = botAI->RequestPointMovement(
PlayerBotMovementPriority::DEATH_RECOVERY, // Priority 255 - HIGHEST
spiritHealerPos,
"Moving to spirit healer - death recovery",
"DeathRecoveryManager");
if (accepted)
{
TC_LOG_DEBUG("playerbot.movement.arbiter",
"DeathRecoveryManager: Bot {} requested spirit healer movement to ({:.2f}, {:.2f}, {:.2f}) with DEATH_RECOVERY priority (255)",
m_bot->GetName(),
spiritHealerPos.GetPositionX(),
spiritHealerPos.GetPositionY(),
spiritHealerPos.GetPositionZ());
m_navigationActive = true;
return true;
}
else
{
TC_LOG_WARN("playerbot.movement.arbiter",
"DeathRecoveryManager: Bot {} spirit healer movement request FILTERED (duplicate detected)",
m_bot->GetName());
// Still consider navigation active if filtered (likely duplicate)
m_navigationActive = true;
return true;
}
}
else
{
// FALLBACK: Direct MotionMaster call if arbiter not available
// This should only happen during transition period
TC_LOG_WARN("playerbot.movement.arbiter",
"DeathRecoveryManager: Bot {} has no MovementArbiter - using legacy MovePoint() for spirit healer",
m_bot->GetName());
// CRITICAL FIX: Clear MotionMaster before calling MovePoint (like mod-playerbot does)
// This prevents movement spam/cancellation that causes "teleporting" behavior
MotionMaster* mm = m_bot->GetMotionMaster();
mm->Clear();
mm->MovePoint(0,
spiritHealer->GetPositionX(),
spiritHealer->GetPositionY(),
spiritHealer->GetPositionZ(),
true); // generatePath = true for proper pathfinding
m_navigationActive = true;
return true;
}
}
bool DeathRecoveryManager::InteractWithSpiritHealer()
{
if (!m_bot || !CanInteractWithSpiritHealer())
return false;
// PHASE 5D: Thread-safe spatial grid validation
auto snapshot = SpatialGridQueryHelpers::FindCreatureByGuid(m_bot, m_spiritHealerGuid);
Creature* spiritHealer = nullptr;
if (snapshot)
{
// Get Creature* for interaction (validated via snapshot first)
spiritHealer = ObjectAccessor::GetCreature(*m_bot, m_spiritHealerGuid);
}
if (!spiritHealer)
return false;
// Use interaction manager if available
if (m_ai)
{
// This will trigger gossip and eventually SPIRIT_HEALER_CONFIRM event
// which will call ExecuteGraveyardResurrection()
TC_LOG_DEBUG("playerbot.death", "Bot {} initiating spirit healer gossip",
m_bot->GetName());
}
// Fallback: direct resurrection
return ExecuteGraveyardResurrection();
}
bool DeathRecoveryManager::ExecuteGraveyardResurrection()
{
if (!m_bot)
return false;
// THREAD SAFETY FIX (v10): DO NOT call SendSpiritResurrect from worker thread!
//
// SendSpiritResurrect() modifies player state (ResurrectPlayer, DurabilityLossAll,
// SpawnCorpseBones, TeleportTo) which causes Map.cpp:686 ACCESS_VIOLATION crash
// when called from ThreadPool worker during Map::Update iteration.
//
// Instead, we set a flag that the main thread will check and execute.
// The main thread path is BotWorldSessionMgr::ProcessPendingResurrections()
// which runs AFTER Map::Update completes.
//
// IMPORTANT FIX: The previous packet-based approach using CMSG_REPOP_REQUEST
// does NOT work for bots that are already ghosts. TrinityCore's HandleRepopRequest
// handler (MiscHandler.cpp:62) returns early if player has PLAYER_FLAGS_GHOST.
TC_LOG_INFO("playerbot.death", "Bot {} setting pending resurrection flag (main thread will execute)",
m_bot->GetName());
// Set flag for main thread to execute resurrection
_pendingMainThreadResurrection.store(true, ::std::memory_order_release);
return true;
}
bool DeathRecoveryManager::ExecutePendingMainThreadResurrection()
{
// Check and clear the flag atomically
bool expected = true;
if (!_pendingMainThreadResurrection.compare_exchange_strong(expected, false, ::std::memory_order_acq_rel))
{
return false; // No pending resurrection
}
if (!m_bot)
{
TC_LOG_ERROR("playerbot.death", "ExecutePendingMainThreadResurrection: Bot is null");
return false;
}
WorldSession* session = m_bot->GetSession();
if (!session)
{
TC_LOG_ERROR("playerbot.death", "Bot {} has no session, cannot perform spirit healer resurrection",
m_bot->GetName());
return false;
}
TC_LOG_INFO("playerbot.death", "Bot {} executing spirit healer resurrection via SendSpiritResurrect() [MAIN THREAD]",
m_bot->GetName());
// SAFE: Called from main thread - SendSpiritResurrect modifies player state
// This properly resurrects the ghost:
// 1. ResurrectPlayer(0.5f, true) - resurrects at 50% health with sickness
// 2. DurabilityLossAll(0.25f, true) - applies 25% durability loss
// 3. SpawnCorpseBones() - spawns bones at corpse location
// 4. TeleportTo() if needed - teleports to appropriate graveyard
session->SendSpiritResurrect();
return true;
}
// ========================================================================
// STATE QUERIES
// ========================================================================
uint64 DeathRecoveryManager::GetTimeSinceDeath() const
{
auto now = ::std::chrono::steady_clock::now();
return ::std::chrono::duration_cast<::std::chrono::milliseconds>(now - m_deathTime).count();
}
bool DeathRecoveryManager::IsGhost() const
{
// OPTION 3 REFACTOR: Let TrinityCore manage Ghost state entirely
// We only query the state, never modify it manually
// TrinityCore automatically sets/clears PLAYER_FLAGS_GHOST via Ghost aura (spell 8326)
return m_bot && m_bot->HasPlayerFlag(PLAYER_FLAGS_GHOST);
}
WorldLocation DeathRecoveryManager::GetCorpseLocation() const
{
if (!m_bot)
return WorldLocation();
Corpse* corpse = m_bot->GetCorpse();
if (!corpse)
return WorldLocation();
return WorldLocation(corpse->GetMapId(),
corpse->GetPositionX(),
corpse->GetPositionY(),
corpse->GetPositionZ(),
corpse->GetOrientation());
}
float DeathRecoveryManager::GetCorpseDistance() const
{
return m_corpseDistance;
}
bool DeathRecoveryManager::IsInCorpseRange() const
{
return m_corpseDistance >= 0.0f && m_corpseDistance <= CORPSE_RESURRECTION_RANGE;
}
Creature* DeathRecoveryManager::FindNearestSpiritHealer() const
{
if (!m_bot)
return nullptr;
::std::list<Creature*> spiritHealers;
Trinity::AllCreaturesOfEntryInRange checker(m_bot, 0, m_config.spiritHealerSearchRadius);
Trinity::CreatureListSearcher<Trinity::AllCreaturesOfEntryInRange> searcher(m_bot, spiritHealers, checker);
// DEADLOCK FIX: Use lock-free spatial grid instead of Cell::VisitGridObjects
Map* map = m_bot->GetMap();
if (!map)
return nullptr;
DoubleBufferedSpatialGrid* spatialGrid = sSpatialGridManager.GetGrid(map);
if (!spatialGrid)
{
sSpatialGridManager.CreateGrid(map);
spatialGrid = sSpatialGridManager.GetGrid(map);
if (!spatialGrid)
return nullptr;
}
// Query nearby GUIDs (lock-free!)
::std::vector<ObjectGuid> nearbyGuids = spatialGrid->QueryNearbyCreatureGuids(
m_bot->GetPosition(), m_config.spiritHealerSearchRadius);
// Process results (replace old loop)
for (ObjectGuid guid : nearbyGuids)
{
// PHASE 5D: Thread-safe spatial grid validation
auto snapshot = SpatialGridQueryHelpers::FindCreatureByGuid(m_bot, guid);
Creature* entity = nullptr;
if (snapshot)
{
// Get Creature* for spirit healer check (validated via snapshot first)
entity = ObjectAccessor::GetCreature(*m_bot, guid);
}
if (!entity || !entity->IsAlive())
continue;
// CRITICAL FIX: Filter for spirit healers and add to list
// BUG: Previous code never populated spiritHealers list, causing FindNearestSpiritHealer() to always return nullptr
// This is why bots got stuck in FINDING_SPIRIT_HEALER state (state 7) without resurrecting
uint64 npcFlags = entity->GetNpcFlags();
if ((npcFlags & UNIT_NPC_FLAG_SPIRIT_HEALER) || (npcFlags & UNIT_NPC_FLAG_AREA_SPIRIT_HEALER))
{
spiritHealers.push_back(entity);
}
}
// End of spatial grid fix
Creature* nearest = nullptr;
float nearestDist = m_config.spiritHealerSearchRadius;
for (Creature* creature : spiritHealers)
{
if (!creature || !creature->IsAlive())
continue;
uint64 npcFlags = creature->GetNpcFlags();
if (!(npcFlags & UNIT_NPC_FLAG_SPIRIT_HEALER) &&
!(npcFlags & UNIT_NPC_FLAG_AREA_SPIRIT_HEALER))
continue;
float dist = m_bot->GetDistance(creature);
if (dist < nearestDist)
{
nearest = creature;
nearestDist = dist;
}
}
return nearest;
}
bool DeathRecoveryManager::CanInteractWithSpiritHealer() const
{
if (!m_bot || m_spiritHealerGuid.IsEmpty())
return false;
// PHASE 5D: Thread-safe spatial grid validation
auto snapshot = SpatialGridQueryHelpers::FindCreatureByGuid(m_bot, m_spiritHealerGuid);
Creature* spiritHealer = nullptr;
if (snapshot && snapshot->IsAlive())
{
// Get Creature* for distance check (validated via snapshot first)
spiritHealer = ObjectAccessor::GetCreature(*m_bot, m_spiritHealerGuid);
}
if (!spiritHealer || !spiritHealer->IsAlive())
return false;
float distance = m_bot->GetDistance(spiritHealer);
return distance <= SPIRIT_HEALER_INTERACTION_RANGE;
}
// ========================================================================
// RESURRECTION CONTROL
// ========================================================================
bool DeathRecoveryManager::TriggerCorpseResurrection()
{
// No lock needed - death recovery state is per-bot instance data
if (m_state == DeathRecoveryState::NOT_DEAD)
return false;
m_method = ResurrectionMethod::CORPSE_RUN;
TransitionToState(DeathRecoveryState::RUNNING_TO_CORPSE, "Manual corpse resurrection");
return true;
}
bool DeathRecoveryManager::TriggerSpiritHealerResurrection()
{
// No lock needed - death recovery state is per-bot instance data
if (m_state == DeathRecoveryState::NOT_DEAD)
return false;
m_method = ResurrectionMethod::SPIRIT_HEALER;
TransitionToState(DeathRecoveryState::FINDING_SPIRIT_HEALER, "Manual spirit healer resurrection");
return true;
}
bool DeathRecoveryManager::AcceptBattleResurrection(ObjectGuid casterGuid, uint32 /*spellId*/)
{
if (!m_config.allowBattleResurrection)
return false;
if (m_state == DeathRecoveryState::NOT_DEAD)
return false;
m_method = ResurrectionMethod::BATTLE_RESURRECTION;
TransitionToState(DeathRecoveryState::RESURRECTING, "Accepting battle resurrection");
TC_LOG_INFO("playerbot.death", "Bot {} accepting battle resurrection from {}",
m_bot->GetName(), casterGuid.ToString());
return true;
}
bool DeathRecoveryManager::ForceResurrection(ResurrectionMethod method)
{
// GHOST AURA FIX: Mutex protection to prevent concurrent resurrection attempts
::std::unique_lock<::std::timed_mutex> lock(_resurrectionMutex, ::std::chrono::milliseconds(100));
if (!lock.owns_lock())
{
TC_LOG_WARN("playerbot.death", " Bot {} ForceResurrection: Resurrection already in progress, skipping concurrent attempt",
m_bot ? m_bot->GetName() : "nullptr");
return false;
}
// GHOST AURA FIX: Check atomic resurrection flag
bool expectedFalse = false;
if (!_resurrectionInProgress.compare_exchange_strong(expectedFalse, true))
{
TC_LOG_WARN("playerbot.death", " Bot {} ForceResurrection: Resurrection flag already set, rejecting concurrent attempt",
m_bot ? m_bot->GetName() : "nullptr");
return false;
}
// RAII guard to reset resurrection flag on exit
struct ResurrectionGuard {
::std::atomic<bool>& flag;
~ResurrectionGuard() { flag.store(false); }
} guard{_resurrectionInProgress};
if (!m_bot)
return false;
TC_LOG_WARN("playerbot.death", "Bot {} force resurrection via {}",
m_bot->GetName(),
method == ResurrectionMethod::CORPSE_RUN ? "corpse" : "spirit healer");
// SAFE RESURRECTION (v9): Force resurrect via BotSession::QueueSafeResurrection()
// ============================================================================
// CRITICAL FIX: DO NOT use CMSG_RECLAIM_CORPSE packet!
// HandleReclaimCorpse ? SpawnCorpseBones ? Map::RemoveWorldObject crashes.
// ============================================================================
BotSession* botSession = BotSessionManager::GetBotSession(m_bot->GetSession());
Corpse* corpse = m_bot->GetCorpse();
if (corpse && botSession)
{
// Use safe resurrection (bypasses SpawnCorpseBones crash)
botSession->QueueSafeResurrection();
TC_LOG_INFO("playerbot.death", "Bot {} queued SAFE force resurrection (method: {}) - NO SpawnCorpseBones!",
m_bot->GetName(), static_cast<int>(method));
}
else if (!corpse)
{
// No corpse - use CMSG_REPOP_REQUEST to resurrect at graveyard
// This path is safe as it doesn't involve SpawnCorpseBones
// TrinityCore 12.0: QueuePacket now takes WorldPacket&& instead of WorldPacket*
WorldPacket repopPacket(CMSG_REPOP_REQUEST, 1);
repopPacket << uint8(0);
m_bot->GetSession()->QueuePacket(std::move(repopPacket));
TC_LOG_INFO("playerbot.death", "Bot {} queued CMSG_REPOP_REQUEST for force resurrection (no corpse)",
m_bot->GetName());
}
else
{
TC_LOG_ERROR("playerbot.death", "Bot {} force resurrection FAILED: BotSession not found!",
m_bot->GetName());
return false;
}
OnResurrection();
return true;
}
// ========================================================================
// CONFIGURATION
// ========================================================================
void DeathRecoveryManager::SetConfig(DeathRecoveryConfig const& config)
{
// No lock needed - death recovery state is per-bot instance data
m_config = config;
}
void DeathRecoveryManager::ReloadConfig()
{
// No lock needed - death recovery state is per-bot instance data
m_config = DeathRecoveryConfig::LoadFromConfig();
TC_LOG_INFO("playerbot.death", "Death recovery configuration reloaded");
}
// ========================================================================
// STATISTICS
// ========================================================================
void DeathRecoveryManager::ResetStatistics()
{
// No lock needed - death recovery state is per-bot instance data
m_stats = DeathRecoveryStatistics();
}
void DeathRecoveryManager::LogStatistics() const
{
TC_LOG_INFO("playerbot.death", "Bot {} death recovery statistics:\n{}",
m_bot ? m_bot->GetName() : "nullptr",
m_stats.ToString());
}
// ========================================================================
// VALIDATION & ERROR HANDLING
// ========================================================================
bool DeathRecoveryManager::ValidateBotState() const
{
if (!m_bot->IsInWorld())
{
TC_LOG_ERROR("playerbot.death", "Bot {} is not in world", m_bot->GetName());
return false;
}
return true;
}
bool DeathRecoveryManager::IsResurrectionTimedOut() const
{
return GetTimeSinceDeath() > m_config.resurrectionTimeout;
}
void DeathRecoveryManager::HandleResurrectionFailure(::std::string const& reason)
{
TC_LOG_ERROR("playerbot.death", "Bot {} resurrection failed: {}",
m_bot ? m_bot->GetName() : "nullptr", reason);
TransitionToState(DeathRecoveryState::RESURRECTION_FAILED, reason);
}
void DeathRecoveryManager::TransitionToState(DeathRecoveryState newState, ::std::string const& reason)
{
DeathRecoveryState oldState = m_state.load();
m_state = newState;
m_lastStateTransition = ::std::chrono::steady_clock::now();
m_stateTimer = 0;
if (m_config.logDebugInfo)
{
TC_LOG_DEBUG("playerbot.death", "Bot {} death recovery: {} -> {} ({})",
m_bot ? m_bot->GetName() : "nullptr",
static_cast<int>(oldState),
static_cast<int>(newState),
reason);
}
}
// ========================================================================
// HELPER METHODS
// ========================================================================
void DeathRecoveryManager::UpdateCorpseDistance() const
{
WorldLocation corpseLocation = GetCorpseLocation();
if (corpseLocation.GetMapId() == MAPID_INVALID || !m_bot)
{
m_corpseDistance = -1.0f;
return;
}
if (corpseLocation.GetMapId() != m_bot->GetMapId())
{
m_corpseDistance = -1.0f; // Different map
return;
}
m_corpseDistance = m_bot->GetExactDist(
corpseLocation.GetPositionX(),
corpseLocation.GetPositionY(),
corpseLocation.GetPositionZ());
m_lastCorpseDistanceCheck = ::std::chrono::steady_clock::now();
}
bool DeathRecoveryManager::IsInSpecialZone() const
{
return m_bot && (m_bot->InBattleground() || m_bot->InArena() || (m_bot->GetMap() && m_bot->GetMap()->IsDungeon()));
}
bool DeathRecoveryManager::WillReceiveResurrectionSickness() const
{
if (!m_bot)
return false;
// Resurrection sickness only applies to players level 11+
return m_bot->GetLevel() > 10 && m_method == ResurrectionMethod::SPIRIT_HEALER;
}
void DeathRecoveryManager::LogDebug(::std::string const& message) const
{
if (m_config.logDebugInfo)
{
TC_LOG_DEBUG("playerbot.death", "Bot {}: {}",
m_bot ? m_bot->GetName() : "nullptr", message);
}
}
} // namespace Playerbot