15 KiB
ObjectAccessor → SpatialGrid Migration Plan
Comprehensive Replacement Strategy for 100+ Map/ObjectAccessor Calls
Created: 2025-10-21 Scope: Entire PlayerBot module (src/modules/Playerbot/) Goal: Eliminate ALL Map/ObjectAccessor calls from worker threads to prevent deadlocks
📊 AUDIT SUMMARY
Total Findings
- 100+ ObjectAccessor calls across PlayerBot module
- 1 commented Map::FindMap call (already disabled)
- 0 active direct Map access (excellent!)
Call Distribution by Type
| Method | Count | Risk Level |
|---|---|---|
ObjectAccessor::GetUnit() |
~70 | 🔴 HIGH (worker thread) |
ObjectAccessor::GetCreature() |
~25 | 🔴 HIGH (worker thread) |
ObjectAccessor::GetPlayer() |
~3 | 🟡 MEDIUM (mixed context) |
ObjectAccessor::GetGameObject() |
~3 | 🟡 MEDIUM (quest/loot) |
ObjectAccessor::GetDynamicObject() |
~8 | 🟡 MEDIUM (dungeon scripts) |
🎯 PRIORITY CLASSIFICATION
Priority 1: CRITICAL - Worker Thread Context (DEADLOCK SOURCES)
These files are called from BotAI::Update() worker thread loop:
| File | Calls | Pattern | Replacement Required |
|---|---|---|---|
AI/Combat/BotThreatManager.cpp |
8 | GetUnit from threat GUID | Query creature snapshots by GUID |
AI/Combat/TargetSelector.cpp |
4 | GetUnit from nearby GUIDs | Query creature snapshots in range |
AI/CombatBehaviors/InterruptRotationManager.cpp |
5 | GetUnit for cast validation | Query creature snapshots by GUID |
AI/CombatBehaviors/AoEDecisionManager.cpp |
3 | GetUnit for AoE target validation | Query creature snapshots in range |
AI/CombatBehaviors/DefensiveBehaviorManager.cpp |
3 | GetUnit for defensive target | Query player/creature snapshots |
AI/CombatBehaviors/DispelCoordinator.cpp |
3 | GetUnit for dispel target | Query creature snapshots by GUID |
AI/Combat/PositionManager.cpp |
1 | GetUnit for positioning | Query creature snapshots in range |
AI/Combat/ObstacleAvoidanceManager.cpp |
2 | GetUnit + GetDynamicObject | Query creature + dynamicObject snapshots |
Total Priority 1: ~29 calls
Priority 2: HIGH - Main Thread with Worker Thread Risk
These may be called from either thread context:
| File | Calls | Pattern | Replacement Required |
|---|---|---|---|
Advanced/AdvancedBehaviorManager.cpp |
7 | GetCreature for behavior logic | Query creature snapshots by GUID |
Advanced/GroupCoordinator.cpp |
2 | GetUnit for group target | Query player snapshots by GUID |
AI/EnhancedBotAI.cpp |
3 | GetPlayer for follow target | Query player snapshots by GUID |
Total Priority 2: ~12 calls
Priority 3: MEDIUM - Main Thread Context (Lower Risk)
These are typically called from main thread (NPC interaction, quest, loot):
| File | Calls | Pattern | Replacement Required |
|---|---|---|---|
Game/NPCInteractionManager.cpp |
14 | GetCreature for NPC interaction | Query creature snapshots, validate on main thread |
Game/QuestManager.cpp |
1 | GetCreature for quest NPC | Query creature snapshots |
Game/InventoryManager.cpp |
3 | GetCreature/GameObject for vendors | Query snapshots, validate on main thread |
AI/Strategy/QuestStrategy.cpp |
3 | GetGameObject/Unit for quest | Query snapshots |
AI/Strategy/LootStrategy.cpp |
2 | GetCreature/GameObject for loot | Query snapshots |
Lifecycle/DeathRecoveryManager.cpp |
6 | GetCreature for spirit healer | Query creature snapshots |
Interaction/Core/InteractionManager.cpp |
2 | GetCreature for interaction | Query creature snapshots |
Total Priority 3: ~31 calls
Priority 4: LOW - Dungeon Scripts (Instance-Specific)
| File | Calls | Pattern | Replacement Required |
|---|---|---|---|
Dungeon/Scripts/Vanilla/*.cpp |
8 | GetDynamicObject for mechanic avoidance | Query dynamicObject snapshots |
Dungeon/DungeonBehavior.cpp |
4 | GetCreature for boss mechanics | Query creature snapshots |
Dungeon/EncounterStrategy.cpp |
1 | GetDynamicObject for encounter | Query dynamicObject snapshots |
Total Priority 4: ~13 calls
Priority 5: DOCUMENTATION/TESTING (No Code Changes)
| File | Calls | Type | Action |
|---|---|---|---|
Events/EVENT_SYSTEM_DOCUMENTATION.md |
6 | Documentation examples | Update examples to use spatial grid |
PHASE4_EVENT_INTEGRATION_PLAN.md |
2 | Planning docs | Update migration plan |
AI/Combat/ThreatIntegrationExample.h |
1 | Example code | Update to spatial grid pattern |
Network/TYPED_PACKET_HOOKS_MIGRATION.md |
1 | Migration guide | Update to spatial grid pattern |
Total Priority 5: ~10 calls (documentation only)
Priority 6: CLASS-SPECIFIC AI (Medium Risk)
| File | Calls | Pattern | Replacement Required |
|---|---|---|---|
AI/ClassAI/Warriors/WarriorSpecialization.cpp |
3 | GetCreature for warrior mechanics | Query creature snapshots |
AI/ClassAI/Warriors/ProtectionSpecialization.cpp |
4 | GetCreature for tank targeting | Query creature snapshots |
AI/ClassAI/Warriors/FurySpecialization.cpp |
1 | GetCreature for DPS targeting | Query creature snapshots |
AI/ClassAI/Warriors/WarriorAI.cpp |
1 | GetCreature for AI logic | Query creature snapshots |
Total Priority 6: ~9 calls
🔧 REPLACEMENT PATTERNS
Pattern 1: GetUnit() for GUID Validation (Most Common - ~70 uses)
OLD CODE (DEADLOCK RISK):
Unit* target = ObjectAccessor::GetUnit(*_bot, guid);
if (!target || !target->IsAlive())
continue;
// Use target->GetHealth(), target->GetDistance(), etc.
NEW CODE (THREAD-SAFE):
// Step 1: Query spatial grid for creature snapshot
auto spatialGrid = sSpatialGridManager.GetGrid(_bot->GetMapId());
if (!spatialGrid)
continue;
auto creatureSnapshots = spatialGrid->QueryNearbyCreatures(_bot->GetPosition(), 100.0f);
// Step 2: Find snapshot matching GUID
const DoubleBufferedSpatialGrid::CreatureSnapshot* snapshot = nullptr;
for (auto const& snap : creatureSnapshots)
{
if (snap.guid == guid)
{
snapshot = &snap;
break;
}
}
if (!snapshot || !snapshot->IsAlive())
continue;
// Step 3: Use snapshot data (NO Map access)
uint64 health = snapshot->health;
float distance = _bot->GetDistance(snapshot->position);
// All data available in snapshot!
Pattern 2: GetCreature() for NPC Interaction (Main Thread OK)
OLD CODE:
Creature* creature = ObjectAccessor::GetCreature(*m_bot, guid);
if (!creature)
return nullptr;
return creature;
NEW CODE (HYBRID - Snapshot for validation, ObjectAccessor only on main thread):
// Step 1: Validate using spatial grid snapshot (thread-safe)
auto spatialGrid = sSpatialGridManager.GetGrid(m_bot->GetMapId());
if (!spatialGrid)
return nullptr;
auto creatureSnapshots = spatialGrid->QueryNearbyCreatures(m_bot->GetPosition(), 100.0f);
const DoubleBufferedSpatialGrid::CreatureSnapshot* snapshot = nullptr;
for (auto const& snap : creatureSnapshots)
{
if (snap.guid == guid)
{
snapshot = &snap;
break;
}
}
if (!snapshot || !snapshot->IsValid())
return nullptr;
// Step 2: If on main thread and need actual Creature*, use ObjectAccessor
// ONLY do this if you're certain you're on main thread (e.g., NPC interaction manager)
if (sScriptMgr->IsMainThread())
{
return ObjectAccessor::GetCreature(*m_bot, guid);
}
// Step 3: Otherwise, use GUID for SetTarget() or other GUID-based operations
return nullptr; // Can't return Creature* from worker thread
Pattern 3: GetPlayer() for Group/Follow Logic
OLD CODE:
if (Player* leader = ObjectAccessor::GetPlayer(*GetBot(), _followTarget))
{
float distance = GetBot()->GetDistance(leader);
// ...
}
NEW CODE (THREAD-SAFE):
auto spatialGrid = sSpatialGridManager.GetGrid(GetBot()->GetMapId());
if (!spatialGrid)
return;
auto playerSnapshots = spatialGrid->QueryNearbyPlayers(GetBot()->GetPosition(), 100.0f);
const DoubleBufferedSpatialGrid::PlayerSnapshot* leaderSnapshot = nullptr;
for (auto const& snap : playerSnapshots)
{
if (snap.guid == _followTarget)
{
leaderSnapshot = &snap;
break;
}
}
if (!leaderSnapshot || !leaderSnapshot->isAlive)
return;
float distance = GetBot()->GetDistance(leaderSnapshot->position);
// All data available in snapshot!
Pattern 4: GetGameObject() for Quest/Loot
OLD CODE:
GameObject* object = ObjectAccessor::GetGameObject(*bot, objectGuid);
if (!object || !object->isSpawned())
return false;
NEW CODE (THREAD-SAFE):
auto spatialGrid = sSpatialGridManager.GetGrid(bot->GetMapId());
if (!spatialGrid)
return false;
auto gameObjectSnapshots = spatialGrid->QueryNearbyGameObjects(bot->GetPosition(), 100.0f);
const DoubleBufferedSpatialGrid::GameObjectSnapshot* snapshot = nullptr;
for (auto const& snap : gameObjectSnapshots)
{
if (snap.guid == objectGuid)
{
snapshot = &snap;
break;
}
}
if (!snapshot || !snapshot->isSpawned)
return false;
// Use snapshot data
Pattern 5: GetDynamicObject() for Danger Avoidance
OLD CODE:
DynamicObject* dynObj = ObjectAccessor::GetDynamicObject(*player, guid);
if (!dynObj)
continue;
if (dynObj->GetRadius() > 0.0f)
{
// Avoid dangerous AoE
}
NEW CODE (THREAD-SAFE):
auto spatialGrid = sSpatialGridManager.GetGrid(player->GetMapId());
if (!spatialGrid)
continue;
auto dynamicObjectSnapshots = spatialGrid->QueryNearbyDynamicObjects(player->GetPosition(), 100.0f);
const DoubleBufferedSpatialGrid::DynamicObjectSnapshot* snapshot = nullptr;
for (auto const& snap : dynamicObjectSnapshots)
{
if (snap.guid == guid)
{
snapshot = &snap;
break;
}
}
if (!snapshot)
continue;
if (snapshot->radius > 0.0f && snapshot->isDangerous)
{
// Avoid dangerous AoE using snapshot data
}
🏗️ IMPLEMENTATION STRATEGY
Phase 5A: Create Helper Utility Class
Create src/modules/Playerbot/Spatial/SpatialGridQueryHelpers.h/cpp:
namespace Playerbot
{
class SpatialGridQueryHelpers
{
public:
/**
* @brief Find creature snapshot by GUID (thread-safe)
* @return Pointer to snapshot if found, nullptr otherwise
*/
static DoubleBufferedSpatialGrid::CreatureSnapshot const*
FindCreatureByGuid(Player* bot, ObjectGuid guid);
/**
* @brief Find player snapshot by GUID (thread-safe)
*/
static DoubleBufferedSpatialGrid::PlayerSnapshot const*
FindPlayerByGuid(Player* bot, ObjectGuid guid);
/**
* @brief Find GameObject snapshot by GUID (thread-safe)
*/
static DoubleBufferedSpatialGrid::GameObjectSnapshot const*
FindGameObjectByGuid(Player* bot, ObjectGuid guid);
/**
* @brief Find DynamicObject snapshot by GUID (thread-safe)
*/
static DoubleBufferedSpatialGrid::DynamicObjectSnapshot const*
FindDynamicObjectByGuid(Player* bot, ObjectGuid guid);
/**
* @brief Validate if creature exists and matches criteria (thread-safe)
*/
static bool ValidateCreature(Player* bot, ObjectGuid guid,
bool requireAlive = true, bool requireHostile = false);
/**
* @brief Get distance to entity by GUID (thread-safe)
*/
static float GetDistanceToEntity(Player* bot, ObjectGuid guid);
};
}
Phase 5B: Replace Priority 1 Files (CRITICAL - Worker Thread)
-
BotThreatManager.cpp (8 calls)
- Replace all
ObjectAccessor::GetUnit()in threat analysis loop - Use
FindCreatureByGuid()helper - Validate using snapshot data only
- Replace all
-
TargetSelector.cpp (4 calls)
- Replace GetUnit() in GetNearbyEnemies()
- Use spatial grid QueryNearbyCreatures() directly
- Filter using snapshot data
-
InterruptRotationManager.cpp (5 calls)
- Replace GetUnit() for cast validation
- Use snapshot spell/cast data
-
AoEDecisionManager.cpp (3 calls)
- Replace GetUnit() for AoE target validation
- Use snapshot position/health data
-
DefensiveBehaviorManager.cpp (3 calls)
- Replace GetUnit() for defensive target validation
- Use snapshot health/position data
-
DispelCoordinator.cpp (3 calls)
- Replace GetUnit() for dispel target validation
- Use snapshot aura/debuff data (if available)
-
PositionManager.cpp (1 call)
- Replace GetUnit() for positioning logic
- Use snapshot position data
-
ObstacleAvoidanceManager.cpp (2 calls)
- Replace GetUnit() and GetDynamicObject()
- Use creature + dynamicObject snapshots
Phase 5C: Replace Priority 2 Files (HIGH - Mixed Context)
- AdvancedBehaviorManager.cpp (7 calls)
- GroupCoordinator.cpp (2 calls)
- EnhancedBotAI.cpp (3 calls)
Phase 5D: Replace Priority 3 Files (MEDIUM - Main Thread)
12-18. NPCInteractionManager, QuestManager, InventoryManager, QuestStrategy, LootStrategy, DeathRecoveryManager, InteractionManager
Phase 5E: Replace Priority 4 Files (LOW - Dungeon Scripts)
19-21. Dungeon scripts and encounter strategies
Phase 5F: Replace Priority 6 Files (Class-Specific AI)
22-25. Warrior specializations and other class AIs
Phase 5G: Update Documentation (Priority 5)
- Update all documentation to reflect spatial grid patterns
✅ SUCCESS CRITERIA
Build Validation
- ✅ Zero compilation errors
- ✅ Zero warnings related to ObjectAccessor usage
- ✅ All tests pass
Runtime Validation
- ✅ Zero deadlocks with 100 concurrent bots
- ✅ Zero deadlocks with 1000 concurrent bots
- ✅ Zero deadlocks with 5000 concurrent bots
- ✅ No performance degradation (CPU/memory)
Code Quality
- ✅ All ObjectAccessor calls replaced (except main thread-only validated cases)
- ✅ No Map access from worker threads
- ✅ All spatial grid queries use lock-free snapshot pattern
- ✅ Documentation updated
📈 ESTIMATED EFFORT
| Phase | Files | Calls | Estimated Time |
|---|---|---|---|
| 5A: Helper Utilities | 1 | - | 2 hours |
| 5B: Priority 1 (Worker Thread) | 8 | 29 | 8 hours |
| 5C: Priority 2 (Mixed Context) | 3 | 12 | 3 hours |
| 5D: Priority 3 (Main Thread) | 7 | 31 | 6 hours |
| 5E: Priority 4 (Dungeon Scripts) | 3 | 13 | 3 hours |
| 5F: Priority 6 (Class AI) | 4 | 9 | 2 hours |
| 5G: Documentation Updates | 4 | 10 | 1 hour |
| TOTAL | 30 files | 104 calls | 25 hours |
🎯 NEXT STEPS
- ✅ Review and approve this plan (User approval required)
- ⏭️ Implement Phase 5A - Create SpatialGridQueryHelpers utility class
- ⏭️ Implement Phase 5B - Replace Priority 1 files (CRITICAL)
- ⏭️ Build and test after each file - Ensure no regressions
- ⏭️ Continue with Priorities 2-6 - Systematic replacement
- ⏭️ Final validation - 5000 bot deadlock test
- ⏭️ Commit and document - Comprehensive commit message
Ready to proceed with Phase 5A (Helper Utilities)?