fix(playerbot): Fix bot spec detection, gear application, and group combat assist
Three critical fixes for bot functionality:
1. BaselineRotationManager: Fix specialization detection
- GetActiveTalentGroup() returns talent GROUP index (0 or 1), not spec
- Changed to GetPrimarySpecialization() == ChrSpecialization::None
- Bots with specs now use proper spec-based rotations instead of BASELINE
2. BotPostLoginConfigurator: Fix gear never being applied
- shouldApplyGear was always false due to broken condition
- Changed to always apply gear, using BotGearFactory as fallback
- Added WasRecentlyConfigured() to prevent LevelManager re-leveling race
- Fixed ApplyClassSpells() to properly learn specialization spells
3. GroupCombatStrategy: Fix group combat assist detection
- ObjectAccessor::FindPlayer() returned NULL for some bot group members
- Added FindGroupMember() helper with fallback lookups:
* ObjectAccessor::FindPlayer() (in-world)
* ObjectAccessor::FindConnectedPlayer() (any connected player)
* BotWorldSessionMgr::GetPlayerBot() (module registry)
- Bots now properly detect when group members are in combat and assist
Co-Authored-By: Claude Opus 4.5 <[email protected]>
Signed-off-by: luis <[email protected]>
This commit is contained in:
committed by
luis
co-authored by
Claude Opus 4.5
parent
788a2dd08f
commit
9f7447ff83
@@ -118,9 +118,10 @@ bool BaselineRotationManager::ShouldUseBaselineRotation(Player* bot)
|
||||
|
||||
// Also use if bot is level 10+ but hasn't chosen a specialization
|
||||
// (This is an edge case that should trigger auto-specialization)
|
||||
// Check if bot has an active talent group (specialization)
|
||||
uint8 activeGroup = bot->GetActiveTalentGroup();
|
||||
return (level >= 10 && activeGroup == 0);
|
||||
// FIXED: GetActiveTalentGroup() returns talent GROUP index (0 or 1), not specialization!
|
||||
// Use GetPrimarySpecialization() to correctly detect if bot has a spec assigned.
|
||||
ChrSpecialization spec = bot->GetPrimarySpecialization();
|
||||
return (level >= 10 && spec == ChrSpecialization::None);
|
||||
}
|
||||
|
||||
bool BaselineRotationManager::ExecuteBaselineRotation(Player* bot, ::Unit* target)
|
||||
|
||||
@@ -23,10 +23,33 @@
|
||||
#include "GridNotifiers.h"
|
||||
#include "CellImpl.h"
|
||||
#include "../../Group/GroupRoleEnums.h" // For IsPlayerHealer
|
||||
#include "../../Session/BotWorldSessionMgr.h" // For GetPlayerBot fallback
|
||||
|
||||
namespace Playerbot
|
||||
{
|
||||
|
||||
// Helper function to find a player with fallback lookups
|
||||
// Uses: FindPlayer (in-world) -> FindConnectedPlayer (connected) -> GetPlayerBot (module registry)
|
||||
static Player* FindGroupMember(ObjectGuid memberGuid)
|
||||
{
|
||||
if (memberGuid.IsEmpty())
|
||||
return nullptr;
|
||||
|
||||
// Method 1: Standard ObjectAccessor::FindPlayer (fast, in same map)
|
||||
if (Player* player = ObjectAccessor::FindPlayer(memberGuid))
|
||||
return player;
|
||||
|
||||
// Method 2: FindConnectedPlayer (finds any connected player, even on different maps)
|
||||
if (Player* player = ObjectAccessor::FindConnectedPlayer(memberGuid))
|
||||
return player;
|
||||
|
||||
// Method 3: Check our bot registry (for bots not properly registered with ObjectAccessor)
|
||||
if (Player* bot = sBotWorldSessionMgr->GetPlayerBot(memberGuid))
|
||||
return bot;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GroupCombatStrategy::GroupCombatStrategy()
|
||||
: Strategy("group_combat")
|
||||
{
|
||||
@@ -129,7 +152,8 @@ void GroupCombatStrategy::UpdateBehavior(BotAI* ai, uint32 diff)
|
||||
Player* tankToFollow = nullptr;
|
||||
for (auto const& slot : group->GetMemberSlots())
|
||||
{
|
||||
Player* member = ObjectAccessor::FindPlayer(slot.guid);
|
||||
// Use FindGroupMember() with fallback lookups
|
||||
Player* member = FindGroupMember(slot.guid);
|
||||
if (!member || member == bot || !member->IsAlive())
|
||||
continue;
|
||||
|
||||
@@ -145,7 +169,8 @@ void GroupCombatStrategy::UpdateBehavior(BotAI* ai, uint32 diff)
|
||||
{
|
||||
for (auto const& slot : group->GetMemberSlots())
|
||||
{
|
||||
Player* member = ObjectAccessor::FindPlayer(slot.guid);
|
||||
// Use FindGroupMember() with fallback lookups
|
||||
Player* member = FindGroupMember(slot.guid);
|
||||
if (member && member != bot && member->IsAlive() && member->IsInCombat())
|
||||
{
|
||||
tankToFollow = member;
|
||||
@@ -300,7 +325,7 @@ bool GroupCombatStrategy::IsGroupInCombat(BotAI* ai) const
|
||||
|
||||
// CRITICAL FIX: Use GetMemberSlots() instead of GetMembers()
|
||||
// GetMembers()/GetSource() only works for players in the same map
|
||||
// GetMemberSlots() gives us GUIDs which we can use with ObjectAccessor
|
||||
// GetMemberSlots() gives us GUIDs which we can use with FindGroupMember()
|
||||
for (auto const& slot : group->GetMemberSlots())
|
||||
{
|
||||
ObjectGuid memberGuid = slot.guid;
|
||||
@@ -312,19 +337,22 @@ bool GroupCombatStrategy::IsGroupInCombat(BotAI* ai) const
|
||||
continue;
|
||||
}
|
||||
|
||||
Player* member = ObjectAccessor::FindPlayer(memberGuid);
|
||||
// CRITICAL FIX: Use FindGroupMember() with fallback lookups
|
||||
// ObjectAccessor::FindPlayer() alone fails for bots not properly registered
|
||||
Player* member = FindGroupMember(memberGuid);
|
||||
|
||||
if (shouldLog)
|
||||
{
|
||||
if (!member)
|
||||
TC_LOG_ERROR("module.playerbot.strategy", " - NULL member for GUID {}", memberGuid.ToString());
|
||||
TC_LOG_ERROR("module.playerbot.strategy", " - NULL member for GUID {} (all lookups failed)", memberGuid.ToString());
|
||||
else if (member == bot)
|
||||
TC_LOG_ERROR("module.playerbot.strategy", " - {} (is bot - SKIPPING)", member->GetName());
|
||||
else
|
||||
TC_LOG_ERROR("module.playerbot.strategy", " - {} InCombat={}, HasTarget={}",
|
||||
TC_LOG_ERROR("module.playerbot.strategy", " - {} InCombat={}, HasTarget={}, IsBot={}",
|
||||
member->GetName(),
|
||||
member->IsInCombat(),
|
||||
member->GetSelectedUnit() != nullptr);
|
||||
member->GetSelectedUnit() != nullptr,
|
||||
sBotWorldSessionMgr->GetPlayerBot(memberGuid) != nullptr);
|
||||
}
|
||||
|
||||
if (!member || member == bot)
|
||||
@@ -359,7 +387,8 @@ Unit* GroupCombatStrategy::FindGroupCombatTarget(BotAI* ai) const
|
||||
// 3. What group members have selected (GetSelectedUnit)
|
||||
for (auto const& slot : group->GetMemberSlots())
|
||||
{
|
||||
Player* member = ObjectAccessor::FindPlayer(slot.guid);
|
||||
// Use FindGroupMember() with fallback lookups
|
||||
Player* member = FindGroupMember(slot.guid);
|
||||
if (!member || member == bot || !member->IsInCombat())
|
||||
continue;
|
||||
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
|
||||
#include "BotPostLoginConfigurator.h"
|
||||
#include "BotTemplateRepository.h"
|
||||
#include "Equipment/BotGearFactory.h"
|
||||
#include "LFG/LFGBotManager.h"
|
||||
#include "Player.h"
|
||||
#include "Item.h"
|
||||
#include "Log.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "DB2Stores.h"
|
||||
#include "World.h"
|
||||
@@ -61,6 +63,7 @@ void BotPostLoginConfigurator::Shutdown()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_configMutex);
|
||||
_pendingConfigs.clear();
|
||||
_recentlyConfiguredBots.clear();
|
||||
}
|
||||
|
||||
_initialized.store(false);
|
||||
@@ -129,6 +132,34 @@ void BotPostLoginConfigurator::RemovePendingConfiguration(ObjectGuid botGuid)
|
||||
}
|
||||
}
|
||||
|
||||
bool BotPostLoginConfigurator::WasRecentlyConfigured(ObjectGuid botGuid) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_configMutex);
|
||||
bool found = _recentlyConfiguredBots.find(botGuid) != _recentlyConfiguredBots.end();
|
||||
|
||||
if (found)
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"WasRecentlyConfigured: GUID={} -> YES (in recently configured set)",
|
||||
botGuid.ToString());
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
void BotPostLoginConfigurator::ClearRecentlyConfigured(ObjectGuid botGuid)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_configMutex);
|
||||
auto erased = _recentlyConfiguredBots.erase(botGuid);
|
||||
|
||||
if (erased > 0)
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"ClearRecentlyConfigured: Removed GUID={} from recently configured set (remaining: {})",
|
||||
botGuid.ToString(), _recentlyConfiguredBots.size());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION APPLICATION
|
||||
// ============================================================================
|
||||
@@ -174,20 +205,26 @@ bool BotPostLoginConfigurator::ApplyPendingConfiguration(Player* player)
|
||||
bool success = true;
|
||||
|
||||
// Get template if not cached
|
||||
// NOTE: Template is OPTIONAL - warm pool bots may not have a template
|
||||
// In that case, we still apply level, spec, and use BotGearFactory for equipment
|
||||
BotTemplate const* tmpl = config.templatePtr;
|
||||
if (!tmpl && config.templateId > 0)
|
||||
{
|
||||
tmpl = sBotTemplateRepository->GetTemplateById(config.templateId);
|
||||
if (!tmpl)
|
||||
{
|
||||
TC_LOG_WARN("module.playerbot.configurator",
|
||||
"Template {} not found for bot {} - will use BotGearFactory fallback",
|
||||
config.templateId, player->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
// If no template is available, log info and continue with fallback behavior
|
||||
if (!tmpl)
|
||||
{
|
||||
TC_LOG_ERROR("module.playerbot.configurator",
|
||||
"Failed to get template {} for bot {}",
|
||||
config.templateId, player->GetName());
|
||||
_stats.failedConfigs.fetch_add(1);
|
||||
RemovePendingConfiguration(playerGuid);
|
||||
return false;
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"No template for bot {} (templateId={}) - using BotGearFactory for equipment",
|
||||
player->GetName(), config.templateId);
|
||||
}
|
||||
|
||||
// Step 1: Apply Level
|
||||
@@ -207,7 +244,8 @@ bool BotPostLoginConfigurator::ApplyPendingConfiguration(Player* player)
|
||||
}
|
||||
|
||||
// Step 2: Apply Specialization (must be before talents)
|
||||
uint32 specId = config.specId > 0 ? config.specId : tmpl->specId;
|
||||
// Use config.specId if set, otherwise fallback to template spec (if available)
|
||||
uint32 specId = config.specId > 0 ? config.specId : (tmpl ? tmpl->specId : 0);
|
||||
if (specId > 0)
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
@@ -230,8 +268,8 @@ bool BotPostLoginConfigurator::ApplyPendingConfiguration(Player* player)
|
||||
|
||||
ApplyClassSpells(player);
|
||||
|
||||
// Step 4: Apply Talents
|
||||
if (!tmpl->talents.talentIds.empty())
|
||||
// Step 4: Apply Talents (only if template exists with talents)
|
||||
if (tmpl && !tmpl->talents.talentIds.empty())
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Applying {} talents to bot {}",
|
||||
@@ -247,11 +285,22 @@ bool BotPostLoginConfigurator::ApplyPendingConfiguration(Player* player)
|
||||
}
|
||||
|
||||
// Step 5: Apply Gear
|
||||
if (config.targetGearScore > 0 || !tmpl->gearSets.empty())
|
||||
// CRITICAL FIX: Always apply gear for instance bots
|
||||
// BotGearFactory will handle gear generation when template is null/empty/has no gear
|
||||
// Previous condition was broken because:
|
||||
// - targetGearScore=0 (templates set this to 0)
|
||||
// - hasTemplateGear=false (no gear sets in database)
|
||||
// - tmpl != nullptr (templates exist)
|
||||
// Result: shouldApplyGear was always false, bots had no gear!
|
||||
bool hasTemplateGear = tmpl && !tmpl->gearSets.empty();
|
||||
// ALWAYS apply gear - if no template gear exists, BotGearFactory will generate appropriate gear
|
||||
bool shouldApplyGear = true; // Always equip gear for instance bots
|
||||
|
||||
if (shouldApplyGear)
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Applying gear (target GS: {}) to bot {}",
|
||||
config.targetGearScore, player->GetName());
|
||||
"Applying gear to bot {} (targetGS={}, hasTemplate={}, hasTemplateGear={})",
|
||||
player->GetName(), config.targetGearScore, tmpl != nullptr, hasTemplateGear);
|
||||
|
||||
if (!ApplyGear(player, tmpl, config.targetGearScore))
|
||||
{
|
||||
@@ -262,8 +311,8 @@ bool BotPostLoginConfigurator::ApplyPendingConfiguration(Player* player)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Apply Action Bars
|
||||
if (!tmpl->actionBars.buttons.empty())
|
||||
// Step 6: Apply Action Bars (only if template exists with action bars)
|
||||
if (tmpl && !tmpl->actionBars.buttons.empty())
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Applying {} action buttons to bot {}",
|
||||
@@ -336,6 +385,19 @@ bool BotPostLoginConfigurator::ApplyPendingConfiguration(Player* player)
|
||||
player->GetName(), durationMs);
|
||||
}
|
||||
|
||||
// CRITICAL: Add to recently configured set BEFORE removing pending config
|
||||
// This prevents the race condition where:
|
||||
// 1. We remove pending config
|
||||
// 2. BotWorldSessionMgr checks HasPendingConfiguration() - returns FALSE
|
||||
// 3. Bot gets submitted to BotLevelManager which re-levels it
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_configMutex);
|
||||
_recentlyConfiguredBots.insert(playerGuid);
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Added bot {} to recently configured set (size: {})",
|
||||
player->GetName(), _recentlyConfiguredBots.size());
|
||||
}
|
||||
|
||||
// Remove pending configuration
|
||||
RemovePendingConfiguration(playerGuid);
|
||||
|
||||
@@ -553,47 +615,105 @@ bool BotPostLoginConfigurator::LearnTalent(Player* player, uint32 talentId)
|
||||
|
||||
bool BotPostLoginConfigurator::ApplyGear(Player* player, BotTemplate const* tmpl, uint32 targetGearScore)
|
||||
{
|
||||
if (!player || !tmpl)
|
||||
if (!player)
|
||||
return false;
|
||||
|
||||
// Select best gear set for target
|
||||
GearSetTemplate const* gearSet = SelectGearSet(tmpl, targetGearScore);
|
||||
if (!gearSet)
|
||||
// First, try to use template gear set if available and has valid items
|
||||
bool useTemplateGear = false;
|
||||
GearSetTemplate const* gearSet = nullptr;
|
||||
|
||||
if (tmpl)
|
||||
{
|
||||
gearSet = SelectGearSet(tmpl, targetGearScore);
|
||||
if (gearSet)
|
||||
{
|
||||
// Check if template has any valid item IDs (non-zero)
|
||||
for (auto const& slotData : gearSet->slots)
|
||||
{
|
||||
if (slotData.itemId != 0)
|
||||
{
|
||||
useTemplateGear = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If template has valid items, use them
|
||||
if (useTemplateGear && gearSet)
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"No gear set found for bot {} (target GS: {})",
|
||||
player->GetName(), targetGearScore);
|
||||
"Using template gear set iLvl {} (actual GS: {}) for bot {}",
|
||||
gearSet->targetItemLevel, gearSet->actualGearScore, player->GetName());
|
||||
|
||||
uint32 itemsEquipped = 0;
|
||||
uint32 itemsFailed = 0;
|
||||
|
||||
for (uint8 slot = 0; slot < EQUIPMENT_SLOT_END; ++slot)
|
||||
{
|
||||
if (slot >= gearSet->slots.size())
|
||||
break;
|
||||
|
||||
GearSlotTemplate const& slotData = gearSet->slots[slot];
|
||||
if (slotData.itemId == 0)
|
||||
continue;
|
||||
|
||||
if (EquipItem(player, slot, slotData.itemId))
|
||||
++itemsEquipped;
|
||||
else
|
||||
++itemsFailed;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Applied template gear to bot {}: {} equipped, {} failed",
|
||||
player->GetName(), itemsEquipped, itemsFailed);
|
||||
|
||||
return itemsFailed == 0;
|
||||
}
|
||||
|
||||
// FALLBACK: Use BotGearFactory to generate and apply gear dynamically
|
||||
// This handles cases where:
|
||||
// 1. Template has no gear sets
|
||||
// 2. Template gear sets have placeholder items (itemId = 0)
|
||||
// 3. No template was provided
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Template has no valid gear - using BotGearFactory for bot {} (level {}, class {}, spec {})",
|
||||
player->GetName(), player->GetLevel(), player->GetClass(),
|
||||
static_cast<uint32>(player->GetPrimarySpecialization()));
|
||||
|
||||
if (!sBotGearFactory->IsReady())
|
||||
{
|
||||
TC_LOG_WARN("module.playerbot.configurator",
|
||||
"BotGearFactory not ready - cannot generate gear for bot {}",
|
||||
player->GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine faction
|
||||
TeamId faction = player->GetTeamId();
|
||||
|
||||
// Build gear set using BotGearFactory
|
||||
GearSet generatedGear = sBotGearFactory->BuildGearSet(
|
||||
player->GetClass(),
|
||||
static_cast<uint32>(player->GetPrimarySpecialization()),
|
||||
player->GetLevel(),
|
||||
faction
|
||||
);
|
||||
|
||||
// Apply the generated gear set
|
||||
if (!sBotGearFactory->ApplyGearSet(player, generatedGear))
|
||||
{
|
||||
TC_LOG_WARN("module.playerbot.configurator",
|
||||
"BotGearFactory failed to apply gear to bot {}",
|
||||
player->GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Selected gear set iLvl {} (actual GS: {}) for bot {}",
|
||||
gearSet->targetItemLevel, gearSet->actualGearScore, player->GetName());
|
||||
"BotGearFactory successfully equipped bot {} with generated gear",
|
||||
player->GetName());
|
||||
|
||||
uint32 itemsEquipped = 0;
|
||||
uint32 itemsFailed = 0;
|
||||
|
||||
// Equipment slots (0-18)
|
||||
for (uint8 slot = 0; slot < EQUIPMENT_SLOT_END; ++slot)
|
||||
{
|
||||
if (slot >= gearSet->slots.size())
|
||||
break;
|
||||
|
||||
GearSlotTemplate const& slotData = gearSet->slots[slot];
|
||||
if (slotData.itemId == 0)
|
||||
continue;
|
||||
|
||||
if (EquipItem(player, slot, slotData.itemId))
|
||||
++itemsEquipped;
|
||||
else
|
||||
++itemsFailed;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"Applied gear to bot {}: {} equipped, {} failed",
|
||||
player->GetName(), itemsEquipped, itemsFailed);
|
||||
|
||||
return itemsFailed == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BotPostLoginConfigurator::EquipItem(Player* player, uint8 slot, uint32 itemId)
|
||||
@@ -669,16 +789,78 @@ bool BotPostLoginConfigurator::ApplyClassSpells(Player* player)
|
||||
player->LearnDefaultSkills();
|
||||
player->UpdateSkillsForLevel();
|
||||
|
||||
// Learn specialization spells if spec is set
|
||||
// NOTE: SKIPPING LearnSpecializationSpells() - it calls LearnSpell() which
|
||||
// sends packets via SendDirectMessage() and crashes for bots without proper session.
|
||||
// The bot AI will learn necessary spells when it initializes.
|
||||
// ========================================================================
|
||||
// SPECIALIZATION SPELL LEARNING
|
||||
// ========================================================================
|
||||
// Use standard TrinityCore method when possible:
|
||||
// - If NOT in world: LearnSpecializationSpells() won't send packets (IsInWorld check)
|
||||
// - If IN world: Use AddSpell directly to avoid log spam from failed packet sends
|
||||
// (Bot sessions have no socket, so SendDirectMessage logs errors but doesn't crash)
|
||||
// ========================================================================
|
||||
if (player->GetPrimarySpecialization() != ChrSpecialization::None)
|
||||
{
|
||||
if (!player->IsInWorld())
|
||||
{
|
||||
// Standard TrinityCore path - safe because packets won't be sent
|
||||
player->LearnSpecializationSpells();
|
||||
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"ApplyClassSpells: Bot {} learned specialization spells via standard method (not in world)",
|
||||
player->GetName());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bot is already in world - use AddSpell directly to avoid log spam
|
||||
uint32 specId = static_cast<uint32>(player->GetPrimarySpecialization());
|
||||
uint32 spellsLearned = 0;
|
||||
|
||||
if (std::vector<SpecializationSpellsEntry const*> const* specSpells = sDB2Manager.GetSpecializationSpells(specId))
|
||||
{
|
||||
for (SpecializationSpellsEntry const* specSpell : *specSpells)
|
||||
{
|
||||
if (!specSpell)
|
||||
continue;
|
||||
|
||||
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(specSpell->SpellID, DIFFICULTY_NONE);
|
||||
if (!spellInfo || spellInfo->SpellLevel > player->GetLevel())
|
||||
continue;
|
||||
|
||||
if (player->AddSpell(specSpell->SpellID, true, true, false, false, false, 0, false, {}))
|
||||
{
|
||||
++spellsLearned;
|
||||
if (specSpell->OverridesSpellID)
|
||||
player->AddOverrideSpell(specSpell->OverridesSpellID, specSpell->SpellID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Learn mastery spells
|
||||
if (player->CanUseMastery())
|
||||
{
|
||||
ChrSpecializationEntry const* spec = sChrSpecializationStore.LookupEntry(specId);
|
||||
if (spec)
|
||||
{
|
||||
for (uint32 i = 0; i < MAX_MASTERY_SPELLS; ++i)
|
||||
{
|
||||
if (uint32 masterySpellId = spec->MasterySpellID[i])
|
||||
{
|
||||
if (player->AddSpell(masterySpellId, true, true, false, false, false, 0, false, {}))
|
||||
++spellsLearned;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"ApplyClassSpells: Bot {} (spec={}) learned {} spells via AddSpell (already in world)",
|
||||
player->GetName(), specId, spellsLearned);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
"ApplyClassSpells: SKIPPING LearnSpecializationSpells() for bot {} (spec={}) - crashes with SendDirectMessage",
|
||||
player->GetName(), static_cast<uint32>(player->GetPrimarySpecialization()));
|
||||
// player->LearnSpecializationSpells(); // DISABLED - causes crash
|
||||
"ApplyClassSpells: Bot {} has no specialization set - skipping spec spells",
|
||||
player->GetName());
|
||||
}
|
||||
|
||||
TC_LOG_INFO("module.playerbot.configurator",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "BotTemplateRepository.h"
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -117,6 +118,15 @@ public:
|
||||
/// Remove pending configuration (called after successful application)
|
||||
void RemovePendingConfiguration(ObjectGuid botGuid);
|
||||
|
||||
/// Check if a bot was recently JIT-configured (prevents LevelManager re-leveling)
|
||||
/// This returns true for bots that have been configured but not yet processed by
|
||||
/// the BotWorldSessionMgr session update loop. This prevents the race condition where
|
||||
/// pending config is removed before the skip check runs.
|
||||
bool WasRecentlyConfigured(ObjectGuid botGuid) const;
|
||||
|
||||
/// Clear a bot from the recently configured list (called after bot is fully processed)
|
||||
void ClearRecentlyConfigured(ObjectGuid botGuid);
|
||||
|
||||
// ========================================================================
|
||||
// CONFIGURATION APPLICATION (Main thread only, called from BotSession)
|
||||
// ========================================================================
|
||||
@@ -201,6 +211,15 @@ private:
|
||||
mutable std::mutex _configMutex;
|
||||
std::unordered_map<ObjectGuid, BotPendingConfiguration> _pendingConfigs;
|
||||
|
||||
// Recently configured bots - prevents BotLevelManager from re-leveling JIT bots
|
||||
// This tracks bots that have been configured but not yet processed by the
|
||||
// BotWorldSessionMgr session update loop. The race condition is:
|
||||
// 1. ApplyPendingConfiguration() applies level, removes pending config
|
||||
// 2. BotWorldSessionMgr.cpp checks HasPendingConfiguration() - returns FALSE
|
||||
// 3. Bot gets submitted to BotLevelManager which re-levels it
|
||||
// Solution: Track in _recentlyConfiguredBots until cleared by the session manager
|
||||
std::unordered_set<ObjectGuid> _recentlyConfiguredBots;
|
||||
|
||||
// Statistics
|
||||
ConfiguratorStats _stats;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user