diff --git a/src/modules/Playerbot/AI/BotAI.cpp b/src/modules/Playerbot/AI/BotAI.cpp index 2003adef9..e663938d0 100644 --- a/src/modules/Playerbot/AI/BotAI.cpp +++ b/src/modules/Playerbot/AI/BotAI.cpp @@ -124,7 +124,10 @@ std::vector BotAI::GetCompletableQuestIds() const // CONSTRUCTOR / DESTRUCTOR // ============================================================================ -BotAI::BotAI(Player* bot, bool instanceOnlyMode) : _bot(bot), _instanceOnlyMode(instanceOnlyMode) +BotAI::BotAI(Player* bot, bool instanceOnlyMode) + : _bot(bot) + , _cachedBotGuid(bot ? bot->GetGUID() : ObjectGuid::Empty) // Cache for safe destructor cleanup + , _instanceOnlyMode(instanceOnlyMode) { // Initialize performance tracking _performanceMetrics.lastUpdate = std::chrono::steady_clock::now(); diff --git a/src/modules/Playerbot/AI/BotAI.h b/src/modules/Playerbot/AI/BotAI.h index 97b25dc36..b306d898d 100644 --- a/src/modules/Playerbot/AI/BotAI.h +++ b/src/modules/Playerbot/AI/BotAI.h @@ -321,6 +321,7 @@ public: Player* GetBot() const { return _bot; } ObjectGuid GetBotGuid() const { return _bot ? _bot->GetGUID() : ObjectGuid::Empty; } + ObjectGuid GetCachedBotGuid() const { return _cachedBotGuid; } // Safe during destructor // ======================================================================== // LIFECYCLE MANAGEMENT - Two-Phase AddToWorld Pattern @@ -957,6 +958,7 @@ protected: protected: // Core components Player* _bot; + ObjectGuid _cachedBotGuid; // Cached at construction for safe destructor cleanup BotAIState _aiState = BotAIState::SOLO; ObjectGuid _currentTarget; diff --git a/src/modules/Playerbot/AI/BotAI_EventHandlers.cpp b/src/modules/Playerbot/AI/BotAI_EventHandlers.cpp index e666d36b7..b4a3cddbb 100644 --- a/src/modules/Playerbot/AI/BotAI_EventHandlers.cpp +++ b/src/modules/Playerbot/AI/BotAI_EventHandlers.cpp @@ -23,6 +23,7 @@ #include "BotAI.h" #include "GameTime.h" +#include "Core/Events/GenericEventBus.h" #include "Group/GroupEventBus.h" #include "Combat/CombatEventBus.h" #include "Cooldown/CooldownEventBus.h" @@ -92,28 +93,29 @@ void BotAI::SubscribeToEventBuses() void BotAI::UnsubscribeFromEventBuses() { - if (!_bot) + // CRITICAL FIX: Use cached GUID instead of _bot pointer during destructor + // The Player object may already be destroyed when BotAI destructor runs, + // making _bot a dangling pointer. Using the cached GUID is safe. + if (_cachedBotGuid.IsEmpty()) return; - // CRITICAL: Unsubscribe from all event buses to prevent dangling pointers - GroupEventBus::instance()->Unsubscribe(this); - CombatEventBus::instance()->Unsubscribe(this); - CooldownEventBus::instance()->Unsubscribe(this); - AuraEventBus::instance()->Unsubscribe(this); - LootEventBus::instance()->Unsubscribe(this); - QuestEventBus::instance()->Unsubscribe(this); - ResourceEventBus::instance()->Unsubscribe(this); - SocialEventBus::instance()->Unsubscribe(this); - AuctionEventBus::instance()->Unsubscribe(this); - NPCEventBus::instance()->Unsubscribe(this); - InstanceEventBus::instance()->Unsubscribe(this); - ProfessionEventBus::instance()->Unsubscribe(this); + // CRITICAL: Unsubscribe from all event buses using GUID (safe during destructor) + // Call the underlying template directly with the new UnsubscribeByGuid method + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); + EventBus::instance()->UnsubscribeByGuid(_cachedBotGuid); - // CRITICAL: Use GetGUID().ToString() instead of GetName() during destructor - // GetName() may access invalid memory during bot destruction - // This prevents ACCESS_VIOLATION crash at BotAI_EventHandlers.cpp line 110 TC_LOG_DEBUG("playerbot.events", "Bot unsubscribed from all event buses (GUID: {})", - _bot->GetGUID().ToString()); + _cachedBotGuid.ToString()); } // ============================================================================ diff --git a/src/modules/Playerbot/Core/Events/GenericEventBus.h b/src/modules/Playerbot/Core/Events/GenericEventBus.h index 6704989c1..df90b03ac 100644 --- a/src/modules/Playerbot/Core/Events/GenericEventBus.h +++ b/src/modules/Playerbot/Core/Events/GenericEventBus.h @@ -285,6 +285,31 @@ public: subscriberGuid.ToString()); } + /** + * @brief Unsubscribe a bot by GUID (safe for use during destructor) + * + * This overload is specifically designed for use during BotAI destruction, + * when the Player object may already be destroyed but we have a cached GUID. + * + * @param botGuid The cached GUID of the bot to unsubscribe + * + * Thread Safety: Yes (mutex-protected) + * Performance: O(1) average case (hash map erase) + */ + void UnsubscribeByGuid(ObjectGuid const& botGuid) + { + if (botGuid.IsEmpty()) + return; + + std::lock_guard lock(_subscriptionMutex); + + _subscriptions.erase(botGuid); + _subscriberPointers.erase(botGuid); + + TC_LOG_DEBUG("playerbot.events", "EventBus: Bot {} unsubscribed by GUID from all events", + botGuid.ToString()); + } + /** * @brief Unsubscribe a bot from specific event types *