From 63c0420f719cdd0500298c6355324935964c36f9 Mon Sep 17 00:00:00 2001 From: luis Date: Tue, 31 Mar 2026 11:34:42 -0300 Subject: [PATCH] =?UTF-8?q?Fix=20rest=20strategy=20using=20stale=20cached?= =?UTF-8?q?=20health=20=E2=80=94=20read=20bot=20stats=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main thread SnapshotBotState runs in OnWorldUpdate BEFORE Map::Update/Player::Update, so it captures pre-regen health values. The cache showed 49.4% while actual health was 100%. Worker thread reads of bot->GetHealthPct() return correct values because Player::Update has already run by the time the worker executes. Reverted to direct reads which are proven correct by the RestStrategy::UpdateBehavior logs showing health=100%. --- .../Playerbot/AI/Strategy/RestStrategy.cpp | 1104 ++++++++--------- 1 file changed, 548 insertions(+), 556 deletions(-) diff --git a/src/modules/Playerbot/AI/Strategy/RestStrategy.cpp b/src/modules/Playerbot/AI/Strategy/RestStrategy.cpp index 606495ccd..728406671 100644 --- a/src/modules/Playerbot/AI/Strategy/RestStrategy.cpp +++ b/src/modules/Playerbot/AI/Strategy/RestStrategy.cpp @@ -22,649 +22,641 @@ namespace Playerbot { -RestStrategy::RestStrategy() - : Strategy("rest") - , _criticalRestThreshold(15.0f + frand(0.0f, 10.0f)) -{ - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Initialized (critical threshold={:.0f}%)", _criticalRestThreshold); -} - -void RestStrategy::InitializeActions() -{ - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: No actions (direct rest control)"); -} - -void RestStrategy::InitializeTriggers() -{ - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: No triggers (using relevance system)"); -} - -void RestStrategy::InitializeValues() -{ - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: No values"); -} - -void RestStrategy::OnActivate(BotAI* ai) -{ - if (!ai || !ai->GetBot()) - return; - - Player* bot = ai->GetBot(); - if (!bot->IsInWorld()) - return; - - TC_LOG_DEBUG("module.playerbot.strategy", "Rest strategy activated for bot {}", bot->GetName()); - SetActive(true); -} - -void RestStrategy::OnDeactivate(BotAI* ai) -{ - if (!ai || !ai->GetBot()) - return; - - Player* bot = ai->GetBot(); - if (!bot->IsInWorld()) - return; - - TC_LOG_DEBUG("module.playerbot.strategy", "Rest strategy deactivated for bot {}", bot->GetName()); - SetActive(false); - - _isEating = false; - _isDrinking = false; - _restStartTime = 0; -} - -bool RestStrategy::IsActive(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) - return false; - - if (!_active) - return false; - - Player* bot = ai->GetBot(); - - // CRITICAL FIX: Safety check for worker thread access during bot destruction - if (!bot->IsInWorld()) - return false; - - // NOT active during combat (can't eat/drink in combat) - if (bot->IsInCombat()) - return false; - - // NOT active in a group - if (bot->GetGroup()) - return false; - - // Only active when bot actually needs rest - bool needsFood = NeedsFood(ai); - bool needsDrink = NeedsDrink(ai); - - if (!needsFood && !needsDrink) - return false; - - // If we have consumables, always rest (most efficient) - bool hasConsumables = (needsFood && FindFood(ai) != nullptr) || - (needsDrink && FindDrink(ai) != nullptr); - if (hasConsumables) - return true; - - // No consumables: only rest if critically low ? must wait for passive regen - // Use cached values from BotAI (populated on main thread, worker-thread-safe) - float healthPct = ai->GetCachedHealthPct(); - float manaPct = ai->GetCachedManaPct(); - - return (needsFood && healthPct < _criticalRestThreshold) || (needsDrink && manaPct < _criticalRestThreshold); -} - -float RestStrategy::GetRelevance(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) - return 0.0f; - - Player* bot = ai->GetBot(); - - // CRITICAL FIX: Safety check for worker thread access during bot destruction - // Bot may be destroyed/logging out between null check and method calls - // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash - if (!bot->IsInWorld()) - return 0.0f; - - // Can't rest in combat - if (bot->IsInCombat()) - return 0.0f; - - // Check if bot needs to rest - bool needsFood = NeedsFood(ai); - bool needsDrink = NeedsDrink(ai); - - if (!needsFood && !needsDrink) - return 0.0f; - - // CRITICAL FIX: Only claim high priority if we CAN actually rest - // If no consumables available, return 0 to let other strategies run - // Bot will passively regenerate while questing/grinding - bool canRestForFood = needsFood && (FindFood(ai) != nullptr); - bool canRestForDrink = needsDrink && (FindDrink(ai) != nullptr); - - if (!canRestForFood && !canRestForDrink) + RestStrategy::RestStrategy() + : Strategy("rest") + , _criticalRestThreshold(15.0f + frand(0.0f, 10.0f)) { - // Can't fulfill any rest needs - let other strategies run, bot regens naturally - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} needs rest but has no consumables, yielding priority", - bot->GetName()); - return 0.0f; + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Initialized (critical threshold={:.0f}%)", _criticalRestThreshold); } - // High priority when we can actually rest (have consumables for at least one need) - return 90.0f; -} - -void RestStrategy::UpdateBehavior(BotAI* ai, uint32 diff) -{ - if (!ai || !ai->GetBot()) - return; - Player* bot = ai->GetBot(); - - // CRITICAL FIX: Safety check for worker thread access during bot destruction - // Bot may be destroyed/logging out between null check and method calls - // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash - // This prevents RCX=0 null pointer dereference when bot's internal state is destroyed - if (!bot->IsInWorld()) - return; - - // Can't rest in combat - if (bot->IsInCombat()) + void RestStrategy::InitializeActions() { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: No actions (direct rest control)"); + } + + void RestStrategy::InitializeTriggers() + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: No triggers (using relevance system)"); + } + + void RestStrategy::InitializeValues() + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: No values"); + } + + void RestStrategy::OnActivate(BotAI* ai) + { + if (!ai || !ai->GetBot()) + return; + + Player* bot = ai->GetBot(); + if (!bot->IsInWorld()) + return; + + TC_LOG_INFO("module.playerbot.strategy", "Rest strategy activated for bot {}", bot->GetName()); + SetActive(true); + } + + void RestStrategy::OnDeactivate(BotAI* ai) + { + if (!ai || !ai->GetBot()) + return; + + Player* bot = ai->GetBot(); + if (!bot->IsInWorld()) + return; + + TC_LOG_INFO("module.playerbot.strategy", "Rest strategy deactivated for bot {}", bot->GetName()); + SetActive(false); + _isEating = false; _isDrinking = false; _restStartTime = 0; - return; } - // Log current status - float healthPct = bot->GetHealthPct(); - float manaPct = bot->GetMaxPower(POWER_MANA) > 0 - ? (bot->GetPower(POWER_MANA) * 100.0f / bot->GetMaxPower(POWER_MANA)) - : 100.0f; - - TC_LOG_TRACE("module.playerbot.strategy", "RestStrategy::UpdateBehavior: Bot {} health={:.1f}%, mana={:.1f}%, needsFood={}, needsDrink={}", - bot->GetName(), healthPct, manaPct, NeedsFood(ai), NeedsDrink(ai)); - - uint32 currentTime = GameTime::GetGameTimeMS(); - - // Check for rest timeout (prevent infinite resting) - if (_restStartTime > 0 && (currentTime - _restStartTime) > _maxRestTime) + bool RestStrategy::IsActive(BotAI* ai) const { - TC_LOG_WARN("module.playerbot.strategy", "RestStrategy: Bot {} rest timeout after 30s", + if (!ai || !ai->GetBot()) + return false; + + if (!_active) + return false; + + Player* bot = ai->GetBot(); + + // CRITICAL FIX: Safety check for worker thread access during bot destruction + if (!bot->IsInWorld()) + return false; + + // NOT active during combat (can't eat/drink in combat) + if (bot->IsInCombat()) + return false; + + // NOT active in a group + if (bot->GetGroup()) + return false; + + // Only active when bot actually needs rest + bool needsFood = NeedsFood(ai); + bool needsDrink = NeedsDrink(ai); + + if (!needsFood && !needsDrink) + return false; + + // If we have consumables, always rest (most efficient) + bool hasConsumables = (needsFood && FindFood(ai) != nullptr) || + (needsDrink && FindDrink(ai) != nullptr); + if (hasConsumables) + return true; + + // No consumables: only rest if critically low ? must wait for passive regen + float healthPct = bot->GetHealthPct(); + float manaPct = bot->GetMaxPower(POWER_MANA) > 0 + ? (bot->GetPower(POWER_MANA) * 100.0f / bot->GetMaxPower(POWER_MANA)) + : 100.0f; + + return (needsFood && healthPct < _criticalRestThreshold) || (needsDrink && manaPct < _criticalRestThreshold); + } + + float RestStrategy::GetRelevance(BotAI* ai) const + { + if (!ai || !ai->GetBot()) + return 0.0f; + + Player* bot = ai->GetBot(); + + // CRITICAL FIX: Safety check for worker thread access during bot destruction + // Bot may be destroyed/logging out between null check and method calls + // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash + if (!bot->IsInWorld()) + return 0.0f; + + // Can't rest in combat + if (bot->IsInCombat()) + return 0.0f; + + // Check if bot needs to rest + bool needsFood = NeedsFood(ai); + bool needsDrink = NeedsDrink(ai); + + if (!needsFood && !needsDrink) + return 0.0f; + + // CRITICAL FIX: Only claim high priority if we CAN actually rest + // If no consumables available, return 0 to let other strategies run + // Bot will passively regenerate while questing/grinding + bool canRestForFood = needsFood && (FindFood(ai) != nullptr); + bool canRestForDrink = needsDrink && (FindDrink(ai) != nullptr); + + if (!canRestForFood && !canRestForDrink) + { + // Can't fulfill any rest needs - let other strategies run, bot regens naturally + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} needs rest but has no consumables, yielding priority", + bot->GetName()); + return 0.0f; + } + + // High priority when we can actually rest (have consumables for at least one need) + return 90.0f; + } + + void RestStrategy::UpdateBehavior(BotAI* ai, uint32 diff) + { + if (!ai || !ai->GetBot()) + return; + Player* bot = ai->GetBot(); + + // CRITICAL FIX: Safety check for worker thread access during bot destruction + // Bot may be destroyed/logging out between null check and method calls + // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash + // This prevents RCX=0 null pointer dereference when bot's internal state is destroyed + if (!bot->IsInWorld()) + return; + + // Can't rest in combat + if (bot->IsInCombat()) + { + _isEating = false; + _isDrinking = false; + _restStartTime = 0; + return; + } + + // Log current status + float healthPct = bot->GetHealthPct(); + float manaPct = bot->GetMaxPower(POWER_MANA) > 0 + ? (bot->GetPower(POWER_MANA) * 100.0f / bot->GetMaxPower(POWER_MANA)) + : 100.0f; + + TC_LOG_TRACE("module.playerbot.strategy", "RestStrategy::UpdateBehavior: Bot {} health={:.1f}%, mana={:.1f}%, needsFood={}, needsDrink={}", + bot->GetName(), healthPct, manaPct, NeedsFood(ai), NeedsDrink(ai)); + + uint32 currentTime = GameTime::GetGameTimeMS(); + + // Check for rest timeout (prevent infinite resting) + if (_restStartTime > 0 && (currentTime - _restStartTime) > _maxRestTime) + { + TC_LOG_WARN("module.playerbot.strategy", "RestStrategy: Bot {} rest timeout after 30s", + bot->GetName()); + _isEating = false; + _isDrinking = false; + _restStartTime = 0; + return; + } + + // Check if rest is complete (healthPct and manaPct already declared above) + if (_isEating && healthPct >= _restCompleteHealth) + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} finished eating ({:.1f}% health)", + bot->GetName(), healthPct); + _isEating = false; + } + + if (_isDrinking && manaPct >= _restCompleteMana) + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} finished drinking ({:.1f}% mana)", + bot->GetName(), manaPct); + _isDrinking = false; + } + // If both complete, stop resting + if (!_isEating && !_isDrinking && _restStartTime > 0) + { + _restStartTime = 0; + return; + } + + // Start eating if needed + // CRITICAL FIX: Check for existing food aura to prevent assertion failure in AuraApplication::_HandleEffect + // Food auras can use EITHER SPELL_AURA_OBS_MOD_HEALTH OR SPELL_AURA_MOD_REGEN (see SpellInfo.cpp:2788) + // The assertion "!(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_HEALTH) || bot->HasAuraType(SPELL_AURA_MOD_REGEN); + if (NeedsFood(ai) && !_isEating && !bot->IsSitState() && !hasFoodAura) + { + Item* food = FindFood(ai); + if (food) + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} found food item {}, attempting to eat", + bot->GetName(), food->GetTemplate()->GetName(DEFAULT_LOCALE)); + if (EatFood(ai, food)) + { + _isEating = true; + if (_restStartTime == 0) + _restStartTime = currentTime; + + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} started eating ({:.1f}% health)", + bot->GetName(), healthPct); + } + else + { + TC_LOG_WARN("module.playerbot.strategy", "RestStrategy: Bot {} failed to eat food {}", + bot->GetName(), food->GetTemplate()->GetName(DEFAULT_LOCALE)); + } + } + else + { + TC_LOG_ERROR("module.playerbot.strategy", "RestStrategy: Bot {} needs food but none found in inventory", bot->GetName()); - _isEating = false; - _isDrinking = false; - _restStartTime = 0; - return; - } - - // Check if rest is complete (healthPct and manaPct already declared above) - if (_isEating && healthPct >= _restCompleteHealth) - { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} finished eating ({:.1f}% health)", - bot->GetName(), healthPct); - _isEating = false; - } - - if (_isDrinking && manaPct >= _restCompleteMana) - { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} finished drinking ({:.1f}% mana)", - bot->GetName(), manaPct); - _isDrinking = false; - } - // If both complete, stop resting - if (!_isEating && !_isDrinking && _restStartTime > 0) - { - _restStartTime = 0; - return; - } - - // Start eating if needed - // CRITICAL FIX: Check for existing food aura to prevent assertion failure in AuraApplication::_HandleEffect - // Food auras can use EITHER SPELL_AURA_OBS_MOD_HEALTH OR SPELL_AURA_MOD_REGEN (see SpellInfo.cpp:2788) - // The assertion "!(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_HEALTH) || bot->HasAuraType(SPELL_AURA_MOD_REGEN); - if (NeedsFood(ai) && !_isEating && !bot->IsSitState() && !hasFoodAura) - { - Item* food = FindFood(ai); - if (food) - { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} found food item {}, attempting to eat", - bot->GetName(), food->GetTemplate()->GetName(DEFAULT_LOCALE)); - if (EatFood(ai, food)) - { - _isEating = true; - if (_restStartTime == 0) - _restStartTime = currentTime; - - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} started eating ({:.1f}% health)", - bot->GetName(), healthPct); - } - else - { - TC_LOG_WARN("module.playerbot.strategy", "RestStrategy: Bot {} failed to eat food {}", - bot->GetName(), food->GetTemplate()->GetName(DEFAULT_LOCALE)); } } - else + // If already has food aura but _isEating is false, sync the flag + else if ((bot->IsSitState() || hasFoodAura) && !_isEating && NeedsFood(ai)) { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} needs food but none found in inventory", - bot->GetName()); + _isEating = true; + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} already eating (has aura), syncing eat flag", bot->GetName()); } - } - // If already has food aura but _isEating is false, sync the flag - else if ((bot->IsSitState() || hasFoodAura) && !_isEating && NeedsFood(ai)) - { - _isEating = true; - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} already eating (has aura), syncing eat flag", bot->GetName()); - } - // Start drinking if needed - // CRITICAL FIX: Check for existing drink aura to prevent assertion failure in AuraApplication::_HandleEffect - // Drink auras can use EITHER SPELL_AURA_OBS_MOD_POWER OR SPELL_AURA_MOD_POWER_REGEN (see SpellInfo.cpp:2793) - // The assertion "!(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_POWER) || bot->HasAuraType(SPELL_AURA_MOD_POWER_REGEN); - if (NeedsDrink(ai) && !_isDrinking && !bot->IsSitState() && !hasDrinkAura) - { - Item* drink = FindDrink(ai); - if (drink) + // Start drinking if needed + // CRITICAL FIX: Check for existing drink aura to prevent assertion failure in AuraApplication::_HandleEffect + // Drink auras can use EITHER SPELL_AURA_OBS_MOD_POWER OR SPELL_AURA_MOD_POWER_REGEN (see SpellInfo.cpp:2793) + // The assertion "!(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_POWER) || bot->HasAuraType(SPELL_AURA_MOD_POWER_REGEN); + if (NeedsDrink(ai) && !_isDrinking && !bot->IsSitState() && !hasDrinkAura) { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} found drink item {}, attempting to drink", - bot->GetName(), drink->GetTemplate()->GetName(DEFAULT_LOCALE)); - - if (DrinkWater(ai, drink)) + Item* drink = FindDrink(ai); + if (drink) { - _isDrinking = true; - if (_restStartTime == 0) - _restStartTime = currentTime; + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} found drink item {}, attempting to drink", + bot->GetName(), drink->GetTemplate()->GetName(DEFAULT_LOCALE)); - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} started drinking ({:.1f}% mana)", - bot->GetName(), manaPct); - } - else - { - TC_LOG_WARN("module.playerbot.strategy", "RestStrategy: Bot {} failed to drink {}", - bot->GetName(), drink->GetTemplate()->GetName(DEFAULT_LOCALE)); - } - } - else - { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} needs drink but none found in inventory", - bot->GetName()); - } - } - // If already has drink aura but _isDrinking is false, sync the flag - else if ((bot->IsSitState() || hasDrinkAura) && !_isDrinking && NeedsDrink(ai)) - { - _isDrinking = true; - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} already drinking (has aura), syncing drink flag", bot->GetName()); - } - - // Use bandage if health critical and no food - if (healthPct < 30.0f && !_isEating) - { - Item* bandage = FindBandage(ai); - if (bandage && UseBandage(ai, bandage)) - { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} used bandage ({:.1f}% health)", - bot->GetName(), healthPct); - } - } - - // Stop movement while resting - if (IsResting(ai) && bot->isMoving()) - { - bot->StopMoving(); - } -} - -bool RestStrategy::NeedsFood(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) - return false; - - Player* bot = ai->GetBot(); - - // CRITICAL FIX: Safety check for worker thread access during bot destruction - // Bot may be destroyed/logging out between null check and GetHealthPct() call - // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash - if (!bot->IsInWorld()) - return false; - - // Use cached health from BotAI ? bot->GetHealthPct() is stale on worker threads - float cachedHp = ai->GetCachedHealthPct(); - bool result = cachedHp < _eatHealthThreshold; - if (result) - { - TC_LOG_INFO("module.playerbot.strategy", - "NeedsFood: Bot {} cachedHp={:.1f} threshold={:.1f} -> TRUE", - bot->GetName(), cachedHp, _eatHealthThreshold); - } - return result; -} - -bool RestStrategy::NeedsDrink(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) - return false; - - Player* bot = ai->GetBot(); - - // CRITICAL FIX: Safety check for worker thread access during bot destruction - // Bot may be destroyed/logging out between null check and GetMaxPower() call - // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash - if (!bot->IsInWorld()) - return false; - - // Only mana users need to drink - if (bot->GetMaxPower(POWER_MANA) == 0) - return false; - - // Use cached mana from BotAI ? bot->GetPower() is stale on worker threads - return ai->GetCachedManaPct() < _drinkManaThreshold; -} - -Item* RestStrategy::FindFood(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) - return nullptr; - - Player* bot = ai->GetBot(); - - // Scan inventory for food - for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) - { - Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i); - if (!item) - continue; - - ItemTemplate const* proto = item->GetTemplate(); - if (!proto) - continue; - - // Check if item is food - if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && proto->GetSubClass() == ITEM_SUBCLASS_FOOD_DRINK) - { - // Verify it's actually food by checking spell specific type - for (ItemEffectEntry const* effect : proto->Effects) - { - if (effect && effect->SpellID > 0) + if (DrinkWater(ai, drink)) { - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); - if (spellInfo) + _isDrinking = true; + if (_restStartTime == 0) + _restStartTime = currentTime; + + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} started drinking ({:.1f}% mana)", + bot->GetName(), manaPct); + } + else + { + TC_LOG_WARN("module.playerbot.strategy", "RestStrategy: Bot {} failed to drink {}", + bot->GetName(), drink->GetTemplate()->GetName(DEFAULT_LOCALE)); + } + } + else + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} needs drink but none found in inventory", + bot->GetName()); + } + } + // If already has drink aura but _isDrinking is false, sync the flag + else if ((bot->IsSitState() || hasDrinkAura) && !_isDrinking && NeedsDrink(ai)) + { + _isDrinking = true; + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} already drinking (has aura), syncing drink flag", bot->GetName()); + } + + // Use bandage if health critical and no food + if (healthPct < 30.0f && !_isEating) + { + Item* bandage = FindBandage(ai); + if (bandage && UseBandage(ai, bandage)) + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy: Bot {} used bandage ({:.1f}% health)", + bot->GetName(), healthPct); + } + } + + // Stop movement while resting + if (IsResting(ai) && bot->isMoving()) + { + bot->StopMoving(); + } + } + + bool RestStrategy::NeedsFood(BotAI* ai) const + { + if (!ai || !ai->GetBot()) + return false; + + Player* bot = ai->GetBot(); + + // CRITICAL FIX: Safety check for worker thread access during bot destruction + // Bot may be destroyed/logging out between null check and GetHealthPct() call + // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash + if (!bot->IsInWorld()) + return false; + + return bot->GetHealthPct() < _eatHealthThreshold; + } + + bool RestStrategy::NeedsDrink(BotAI* ai) const + { + if (!ai || !ai->GetBot()) + return false; + + Player* bot = ai->GetBot(); + + // CRITICAL FIX: Safety check for worker thread access during bot destruction + // Bot may be destroyed/logging out between null check and GetMaxPower() call + // IsInWorld() returns false during destruction, preventing ACCESS_VIOLATION crash + if (!bot->IsInWorld()) + return false; + + // Only mana users need to drink + if (bot->GetMaxPower(POWER_MANA) == 0) + return false; + + float manaPct = (bot->GetPower(POWER_MANA) * 100.0f) / bot->GetMaxPower(POWER_MANA); + return manaPct < _drinkManaThreshold; + } + + Item* RestStrategy::FindFood(BotAI* ai) const + { + if (!ai || !ai->GetBot()) + return nullptr; + + Player* bot = ai->GetBot(); + + // Scan inventory for food + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) + { + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + // Check if item is food + if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && proto->GetSubClass() == ITEM_SUBCLASS_FOOD_DRINK) + { + // Verify it's actually food by checking spell specific type + for (ItemEffectEntry const* effect : proto->Effects) + { + if (effect && effect->SpellID > 0) { - SpellSpecificType specificType = spellInfo->GetSpellSpecific(); - if (specificType == SPELL_SPECIFIC_FOOD || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) - return item; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); + if (spellInfo) + { + SpellSpecificType specificType = spellInfo->GetSpellSpecific(); + if (specificType == SPELL_SPECIFIC_FOOD || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) + return item; + } } } } } - } - // Check bags - for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) - { - if (Bag* pBag = bot->GetBagByPos(bag)) + // Check bags + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) { - for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot) + if (Bag* pBag = bot->GetBagByPos(bag)) { - Item* item = pBag->GetItemByPos(slot); - if (!item) - continue; - - ItemTemplate const* proto = item->GetTemplate(); - if (!proto) - continue; - - if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && proto->GetSubClass() == ITEM_SUBCLASS_FOOD_DRINK) + for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot) { - // Verify it's actually food by checking spell specific type - for (ItemEffectEntry const* effect : proto->Effects) + Item* item = pBag->GetItemByPos(slot); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && proto->GetSubClass() == ITEM_SUBCLASS_FOOD_DRINK) { - if (effect && effect->SpellID > 0) + // Verify it's actually food by checking spell specific type + for (ItemEffectEntry const* effect : proto->Effects) { - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); - if (spellInfo) + if (effect && effect->SpellID > 0) { - SpellSpecificType specificType = spellInfo->GetSpellSpecific(); - if (specificType == SPELL_SPECIFIC_FOOD || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) - return item; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); + if (spellInfo) + { + SpellSpecificType specificType = spellInfo->GetSpellSpecific(); + if (specificType == SPELL_SPECIFIC_FOOD || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) + return item; + } } } } } } } + + return nullptr; } - return nullptr; -} - -Item* RestStrategy::FindDrink(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) - return nullptr; - - Player* bot = ai->GetBot(); - - // Scan inventory for drink - for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) + Item* RestStrategy::FindDrink(BotAI* ai) const { - Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i); - if (!item) - continue; + if (!ai || !ai->GetBot()) + return nullptr; - ItemTemplate const* proto = item->GetTemplate(); - if (!proto) - continue; + Player* bot = ai->GetBot(); - // Check if item is drink (restores mana) - if (proto->GetClass() == ITEM_CLASS_CONSUMABLE) + // Scan inventory for drink + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { - // Check if spell is drink-specific - for (ItemEffectEntry const* effect : proto->Effects) + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + // Check if item is drink (restores mana) + if (proto->GetClass() == ITEM_CLASS_CONSUMABLE) { - if (effect && effect->SpellID > 0) + // Check if spell is drink-specific + for (ItemEffectEntry const* effect : proto->Effects) { - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); - if (spellInfo) + if (effect && effect->SpellID > 0) { - SpellSpecificType specificType = spellInfo->GetSpellSpecific(); - if (specificType == SPELL_SPECIFIC_DRINK || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) - return item; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); + if (spellInfo) + { + SpellSpecificType specificType = spellInfo->GetSpellSpecific(); + if (specificType == SPELL_SPECIFIC_DRINK || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) + return item; + } } } } } - } - // Check bags - for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) - { - if (Bag* pBag = bot->GetBagByPos(bag)) + // Check bags + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) { - for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot) + if (Bag* pBag = bot->GetBagByPos(bag)) { - Item* item = pBag->GetItemByPos(slot); - if (!item) - continue; - - ItemTemplate const* proto = item->GetTemplate(); - if (!proto) - continue; - - if (proto->GetClass() == ITEM_CLASS_CONSUMABLE) + for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot) { - for (ItemEffectEntry const* effect : proto->Effects) + Item* item = pBag->GetItemByPos(slot); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + if (proto->GetClass() == ITEM_CLASS_CONSUMABLE) { - if (effect && effect->SpellID > 0) + for (ItemEffectEntry const* effect : proto->Effects) { - SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); - if (spellInfo) + if (effect && effect->SpellID > 0) { - SpellSpecificType specificType = spellInfo->GetSpellSpecific(); - if (specificType == SPELL_SPECIFIC_DRINK || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) - return item; + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, DIFFICULTY_NONE); + if (spellInfo) + { + SpellSpecificType specificType = spellInfo->GetSpellSpecific(); + if (specificType == SPELL_SPECIFIC_DRINK || specificType == SPELL_SPECIFIC_FOOD_AND_DRINK) + return item; + } } } } } } } - } - return nullptr; -} - -Item* RestStrategy::FindBandage(BotAI* ai) const -{ - if (!ai || !ai->GetBot()) return nullptr; - - Player* bot = ai->GetBot(); - - // Bandages are usually ITEM_CLASS_CONSUMABLE, ITEM_SUBCLASS_BANDAGE - // Scan inventory - for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) - { - Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i); - if (!item) - continue; - - ItemTemplate const* proto = item->GetTemplate(); - if (!proto) - continue; - - // Check item name contains "Bandage" - ::std::string itemName = proto->GetName(DEFAULT_LOCALE); - if (itemName.find("Bandage") != ::std::string::npos) - return item; } - // Check bags - for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + Item* RestStrategy::FindBandage(BotAI* ai) const { - if (Bag* pBag = bot->GetBagByPos(bag)) + if (!ai || !ai->GetBot()) + return nullptr; + + Player* bot = ai->GetBot(); + + // Bandages are usually ITEM_CLASS_CONSUMABLE, ITEM_SUBCLASS_BANDAGE + // Scan inventory + for (uint8 i = INVENTORY_SLOT_ITEM_START; i < INVENTORY_SLOT_ITEM_END; ++i) { - for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot) + Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, i); + if (!item) + continue; + + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; + + // Check item name contains "Bandage" + ::std::string itemName = proto->GetName(DEFAULT_LOCALE); + if (itemName.find("Bandage") != ::std::string::npos) + return item; + } + + // Check bags + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + if (Bag* pBag = bot->GetBagByPos(bag)) { - Item* item = pBag->GetItemByPos(slot); - if (!item) - continue; + for (uint32 slot = 0; slot < pBag->GetBagSize(); ++slot) + { + Item* item = pBag->GetItemByPos(slot); + if (!item) + continue; - ItemTemplate const* proto = item->GetTemplate(); - if (!proto) - continue; + ItemTemplate const* proto = item->GetTemplate(); + if (!proto) + continue; - ::std::string itemName = proto->GetName(DEFAULT_LOCALE); - if (itemName.find("Bandage") != ::std::string::npos) - return item; + ::std::string itemName = proto->GetName(DEFAULT_LOCALE); + if (itemName.find("Bandage") != ::std::string::npos) + return item; + } } } + + return nullptr; } - return nullptr; -} - -bool RestStrategy::EatFood(BotAI* ai, Item* food) -{ - if (!ai || !ai->GetBot() || !food) - return false; - - Player* bot = ai->GetBot(); - - // ======================================================================== - // CRITICAL FIX (2026-01-15): Prevent duplicate aura assertion crash - // Check for existing food aura INSIDE this function (not just in caller) - // to prevent race conditions where another thread/process applies food - // between the caller's check and this function's execution. - // - // Crash: SpellAuras.cpp:174 ASSERTION FAILED: !(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_HEALTH) || bot->HasAuraType(SPELL_AURA_MOD_REGEN)) + bool RestStrategy::EatFood(BotAI* ai, Item* food) { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy::EatFood: Bot {} already has food aura, skipping", - bot->GetName()); - return false; + if (!ai || !ai->GetBot() || !food) + return false; + + Player* bot = ai->GetBot(); + + // ======================================================================== + // CRITICAL FIX (2026-01-15): Prevent duplicate aura assertion crash + // Check for existing food aura INSIDE this function (not just in caller) + // to prevent race conditions where another thread/process applies food + // between the caller's check and this function's execution. + // + // Crash: SpellAuras.cpp:174 ASSERTION FAILED: !(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_HEALTH) || bot->HasAuraType(SPELL_AURA_MOD_REGEN)) + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy::EatFood: Bot {} already has food aura, skipping", + bot->GetName()); + return false; + } + + // Use the food item + SpellCastTargets targets; + targets.SetUnitTarget(bot); + + // WoW 12.0: CastItemUseSpell signature changed to std::array + std::array misc = { 0, 0, 0 }; + bot->CastItemUseSpell(food, targets, ObjectGuid::Empty, misc); + + _foodConsumed++; + return true; } - // Use the food item - SpellCastTargets targets; - targets.SetUnitTarget(bot); - - // WoW 12.0: CastItemUseSpell signature changed to std::array - std::array misc = { 0, 0, 0 }; - bot->CastItemUseSpell(food, targets, ObjectGuid::Empty, misc); - - _foodConsumed++; - return true; -} - -bool RestStrategy::DrinkWater(BotAI* ai, Item* drink) -{ - if (!ai || !ai->GetBot() || !drink) - return false; - - Player* bot = ai->GetBot(); - - // ======================================================================== - // CRITICAL FIX (2026-01-15): Prevent duplicate aura assertion crash - // Check for existing drink aura INSIDE this function to prevent race - // conditions. Drink auras use SPELL_AURA_OBS_MOD_POWER (mana regen) - // or SPELL_AURA_MOD_POWER_REGEN. - // - // Crash: SpellAuras.cpp:174 ASSERTION FAILED: !(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_POWER) || bot->HasAuraType(SPELL_AURA_MOD_POWER_REGEN)) + bool RestStrategy::DrinkWater(BotAI* ai, Item* drink) { - TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy::DrinkWater: Bot {} already has drink aura, skipping", - bot->GetName()); - return false; + if (!ai || !ai->GetBot() || !drink) + return false; + + Player* bot = ai->GetBot(); + + // ======================================================================== + // CRITICAL FIX (2026-01-15): Prevent duplicate aura assertion crash + // Check for existing drink aura INSIDE this function to prevent race + // conditions. Drink auras use SPELL_AURA_OBS_MOD_POWER (mana regen) + // or SPELL_AURA_MOD_POWER_REGEN. + // + // Crash: SpellAuras.cpp:174 ASSERTION FAILED: !(_effectMask & (1<HasAuraType(SPELL_AURA_OBS_MOD_POWER) || bot->HasAuraType(SPELL_AURA_MOD_POWER_REGEN)) + { + TC_LOG_DEBUG("module.playerbot.strategy", "RestStrategy::DrinkWater: Bot {} already has drink aura, skipping", + bot->GetName()); + return false; + } + + // Use the drink item + SpellCastTargets targets; + targets.SetUnitTarget(bot); + + // WoW 12.0: CastItemUseSpell signature changed to std::array + std::array misc = { 0, 0, 0 }; + bot->CastItemUseSpell(drink, targets, ObjectGuid::Empty, misc); + + _drinkConsumed++; + return true; } - // Use the drink item - SpellCastTargets targets; - targets.SetUnitTarget(bot); + bool RestStrategy::UseBandage(BotAI* ai, Item* bandage) + { + if (!ai || !ai->GetBot() || !bandage) + return false; - // WoW 12.0: CastItemUseSpell signature changed to std::array - std::array misc = { 0, 0, 0 }; - bot->CastItemUseSpell(drink, targets, ObjectGuid::Empty, misc); + Player* bot = ai->GetBot(); - _drinkConsumed++; - return true; -} + // Use bandage on self + SpellCastTargets targets; + targets.SetUnitTarget(bot); -bool RestStrategy::UseBandage(BotAI* ai, Item* bandage) -{ - if (!ai || !ai->GetBot() || !bandage) - return false; + // WoW 12.0: CastItemUseSpell signature changed to std::array + std::array misc = { 0, 0, 0 }; + bot->CastItemUseSpell(bandage, targets, ObjectGuid::Empty, misc); - Player* bot = ai->GetBot(); + _bandagesUsed++; + return true; + } - // Use bandage on self - SpellCastTargets targets; - targets.SetUnitTarget(bot); - - // WoW 12.0: CastItemUseSpell signature changed to std::array - std::array misc = { 0, 0, 0 }; - bot->CastItemUseSpell(bandage, targets, ObjectGuid::Empty, misc); - - _bandagesUsed++; - return true; -} - -bool RestStrategy::IsResting(BotAI* ai) const -{ - return _isEating || _isDrinking; -} + bool RestStrategy::IsResting(BotAI* ai) const + { + return _isEating || _isDrinking; + } } // namespace Playerbot