44 KiB
Enterprise-Grade Movement Arbiter Architecture
TrinityCore PlayerBot Module - Movement System Redesign
Version: 1.0 Date: 2025-10-22 Status: APPROVED - Ready for Implementation Estimated Implementation: 8 weeks Risk Level: LOW (non-breaking, incremental migration)
TABLE OF CONTENTS
- Executive Summary
- TrinityCore Movement Architecture Analysis
- Current PlayerBot Integration Issues
- Enterprise Architecture Design
- Implementation Phases
- Quality Assurance Strategy
- Performance Specifications
- Maintenance & Operations
EXECUTIVE SUMMARY
Problem Statement
PlayerBot module has 99+ direct MotionMaster calls across 16 concurrent movement systems, causing:
- Movement jittering (3+ competing commands per frame)
- Boss mechanics failures (priority inversion)
- Thread safety issues (worker thread movement calls)
- Affects all 13 WoW classes equally
Root Cause
Architectural Mismatch: PlayerBot bypasses TrinityCore's priority-based movement system, issuing direct MotionMaster->Clear() and MovePoint() calls without coordination.
Solution
Enterprise Movement Arbiter that properly integrates with TrinityCore's existing movement architecture while adding PlayerBot-specific priority management.
Success Metrics
- ✅ Zero movement jittering across all 13 classes
- ✅ 100% boss mechanic survival (priority enforcement)
- ✅ <0.01ms per-bot arbitration overhead
- ✅ Thread-safe operation
- ✅ Zero TrinityCore core modifications
TRINITYCORE MOVEMENT ARCHITECTURE ANALYSIS
1. MotionMaster Core System
File: src/server/game/Movement/MotionMaster.h/cpp
Architecture:
MotionMaster (per Unit)
├── _defaultGenerator (MOTION_SLOT_DEFAULT) - IdleMovementGenerator
└── _generators (MOTION_SLOT_ACTIVE) - std::multiset<MovementGenerator*>
└── Sorted by: (Mode, Priority) via MovementGeneratorComparator
Key Components:
A. MovementGenerator Lifecycle
MovementGenerator states:
1. INITIALIZATION_PENDING → Initialize(Unit*)
2. INITIALIZED → Update(Unit*, diff) every frame
3. DEACTIVATED → Reset(Unit*) when resuming
4. FINALIZED → Finalize(Unit*, active, movementInform)
B. Priority System
enum MovementGeneratorPriority : uint8
{
MOTION_PRIORITY_NONE = 0, // Never used
MOTION_PRIORITY_NORMAL = 1, // Most movement
MOTION_PRIORITY_HIGHEST = 2 // Flight, flee, confusion
};
Comparator Logic:
bool MovementGeneratorComparator::operator()(a, b) const
{
if (a->Mode > b->Mode) // OVERRIDE beats DEFAULT
return true;
else if (a->Mode == b->Mode)
return a->Priority > b->Priority; // Higher priority wins
return false;
}
C. Movement Modes
enum MovementGeneratorMode : uint8
{
MOTION_MODE_DEFAULT = 0, // Normal movement
MOTION_MODE_OVERRIDE = 1 // Takes precedence over DEFAULT
};
2. MovementGenerator Types
Location: src/server/game/Movement/MovementGenerators/
Available Generators:
| Generator | Type | Priority | Use Case |
|---|---|---|---|
| IdleMovementGenerator | IDLE_MOTION_TYPE | NORMAL | Stationary |
| PointMovementGenerator | POINT_MOTION_TYPE | NORMAL | Move to position |
| ChaseMovementGenerator | CHASE_MOTION_TYPE | NORMAL | Melee combat |
| FollowMovementGenerator | FOLLOW_MOTION_TYPE | NORMAL | Group following |
| FleeingMovementGenerator | FLEEING_MOTION_TYPE | HIGHEST | Fear, flee |
| ConfusedMovementGenerator | CONFUSED_MOTION_TYPE | HIGHEST | Confusion |
| FlightPathMovementGenerator | FLIGHT_MOTION_TYPE | HIGHEST | Flight paths |
| HomeMovementGenerator | HOME_MOTION_TYPE | NORMAL | Evade |
| FormationMovementGenerator | FORMATION_MOTION_TYPE | NORMAL | Formation |
| SplineChainMovementGenerator | SPLINE_CHAIN_MOTION_TYPE | NORMAL | Scripted paths |
Critical Insight: TrinityCore uses MOTION_PRIORITY_HIGHEST for movements that must not be interrupted (flight, fear, confusion).
3. Delayed Action System
File: MotionMaster.h:99-114
class DelayedAction
{
DelayedActionDefine Action; // std::function<void()>
DelayedActionValidator Validator; // std::function<bool()>
MotionMasterDelayedActionType Type;
};
Purpose: All movement modifications during Update() are queued as delayed actions to prevent iterator invalidation.
Flags:
MOTIONMASTER_FLAG_UPDATE = 0x1 // Currently updating
MOTIONMASTER_FLAG_INITIALIZATION_PENDING = 0x4 // Not yet initialized
MOTIONMASTER_FLAG_INITIALIZING = 0x8 // Initializing now
MOTIONMASTER_FLAG_DELAYED = UPDATE | INIT_PENDING // Queue actions
Critical Pattern:
void MotionMaster::Add(MovementGenerator* movement, MovementSlot slot)
{
if (HasFlag(MOTIONMASTER_FLAG_DELAYED))
{
// Queue for later execution
_delayedActions.emplace_back([this, movement, slot]() {
Add(movement, slot);
}, MOTIONMASTER_DELAYED_ADD);
}
else
DirectAdd(movement, slot); // Execute immediately
}
4. Movement API Surface
High-Level Movement APIs:
// Point-based movement
void MovePoint(uint32 id, Position, generatePath, finalOrient, speed, ...);
// Target-based movement
void MoveChase(Unit* target, ChaseRange, ChaseAngle);
void MoveFollow(Unit* target, dist, angle, duration, ...);
// Special movement
void MoveJump(Position, speedXY, speedZ, facing, ...);
void MoveKnockbackFrom(Position origin, speedXY, speedZ, ...);
void MoveCharge(Position, speed, generatePath, ...);
// Control
void Clear(); // Remove all ACTIVE
void Clear(MovementSlot slot); // Remove specific slot
void Clear(MovementGeneratorMode mode); // Remove by mode
void Clear(MovementGeneratorPriority priority);// Remove by priority
5. TrinityCore Design Patterns
A. Factory Pattern
MovementGenerator* Create() const override {
return new PointMovementGenerator<Player>(id, pos, ...);
}
B. Template Pattern (CRTP)
template<class T, class D>
class MovementGeneratorMedium : public MovementGenerator
{
// Delegates to D::DoInitialize(T*), D::DoUpdate(T*, diff), etc.
};
C. Priority Queue Pattern
std::multiset<MovementGenerator*, MovementGeneratorComparator> _generators;
// Automatically sorted by (Mode, Priority)
// Top element is always highest priority active movement
CURRENT PLAYERBOT INTEGRATION ISSUES
Issue 1: Direct API Misuse
Evidence: 99+ call sites across PlayerBot codebase
// WRONG: Direct MotionMaster manipulation
player->GetMotionMaster()->Clear(); // Cancels EVERYTHING
player->GetMotionMaster()->MovePoint(0, pos); // No priority, no coordination
Problems:
- Bypasses priority system - All movements treated as MOTION_PRIORITY_NORMAL
- No coordination - Multiple subsystems issue conflicting commands
- Race conditions - Clear() cancels other subsystem's movements
- Thread unsafe - BotActionProcessor makes calls from worker thread
Issue 2: Priority Inversion
Example: Boss mechanic vs combat positioning
// EncounterStrategy detects void zone (should be HIGHEST priority)
player->GetMotionMaster()->MovePoint(0, safePos); // Priority: NORMAL
// 100ms later: CombatMovementStrategy updates
player->GetMotionMaster()->Clear(); // Cancels void zone avoidance!
player->GetMotionMaster()->MovePoint(1, combatPos); // Priority: NORMAL
// Result: Bot dies standing in void zone
Root Cause: PlayerBot doesn't use TrinityCore's priority system properly.
Issue 3: No Request Deduplication
Example: Mage kiting conflict
Frame N:
T+0ms: MageAI::UpdateRotation() → MovePoint(kitingPos1)
T+50ms: FrostSpecialization::HandleKiting() → MovePoint(kitingPos2) // 0.2 yards different
T+100ms: CombatMovementStrategy::Update() → Clear() + MovePoint(rangedPos)
T+150ms: KitingManager::Update() → MovePoint(kitePos3)
Result: 4 movement commands in 150ms = constant jittering
Missing: Spatial+temporal deduplication to detect "same movement request".
Issue 4: Thread Safety Violation
File: Threading/BotActionProcessor.cpp:183
// Worker thread:
bot->GetMotionMaster()->MovePoint(0, action.position); // NOT THREAD-SAFE
// Main thread simultaneously:
BotAI::UpdateAI() → _bot->GetMotionMaster()->Clear(); // RACE CONDITION
Issue: MotionMaster is NOT designed for multi-threaded access. No mutexes.
Issue 5: Mode Misunderstanding
PlayerBot never uses MOTION_MODE_OVERRIDE, always defaults to MOTION_MODE_DEFAULT.
Impact: Cannot override lower-priority DEFAULT movements with higher-priority OVERRIDE movements.
Issue 6: Slot Misunderstanding
PlayerBot always uses MOTION_SLOT_ACTIVE (or omits slot parameter, which defaults to ACTIVE).
Missing Opportunity: Could use MOTION_SLOT_DEFAULT for idle/wandering behavior.
Issue 7: No Movement State Tracking
PlayerBot has no central record of:
- What movement is currently active
- Why it's active (which system requested it)
- When it started
- Expected duration/completion
Impact: Cannot make intelligent decisions about whether to cancel/override current movement.
ENTERPRISE ARCHITECTURE DESIGN
Design Principles
- Zero Core Modifications - Pure PlayerBot module implementation
- TrinityCore Native - Use existing priority/mode/slot system properly
- Backward Compatible - Existing code works during migration
- Performance Critical - <0.01ms per arbitration decision
- Thread Safe - Handle BotActionProcessor correctly
- SOLID Principles - Single responsibility, open/closed, dependency inversion
- Enterprise Patterns - Factory, Strategy, Adapter, Facade
Architecture Layers
┌─────────────────────────────────────────────────────────────┐
│ Layer 4: PlayerBot Subsystems (16 movement systems) │
│ - ClassAI, CombatMovementStrategy, LeaderFollowBehavior, │
│ - EncounterStrategy, FormationManager, KitingManager, ... │
└────────────────┬────────────────────────────────────────────┘
│ Uses
┌────────────────▼────────────────────────────────────────────┐
│ Layer 3: Movement Arbiter (PlayerBot module) │
│ - Priority arbitration │
│ - Request deduplication │
│ - TrinityCore adapter │
│ - Thread-safe request queue │
└────────────────┬────────────────────────────────────────────┘
│ Translates to
┌────────────────▼────────────────────────────────────────────┐
│ Layer 2: TrinityCore Movement API Facade │
│ - Wraps MotionMaster API with priority hints │
│ - Validates requests │
│ - Provides PlayerBot-friendly interface │
└────────────────┬────────────────────────────────────────────┘
│ Calls
┌────────────────▼────────────────────────────────────────────┐
│ Layer 1: TrinityCore MotionMaster (core) │
│ - Priority-based multiset │
│ - MovementGenerator lifecycle │
│ - Delayed action queue │
└─────────────────────────────────────────────────────────────┘
Core Components
Component 1: Movement Priority Mapper
Purpose: Map PlayerBot's granular priorities (0-255) to TrinityCore's 3-level system.
File: src/modules/Playerbot/Movement/MovementPriorityMapper.h
namespace Playerbot
{
// PlayerBot granular priorities (0-255)
enum class PlayerBotMovementPriority : uint8
{
// CRITICAL (240-255): Life-or-death
DEATH_RECOVERY = 255, // Corpse retrieval
BOSS_MECHANIC = 250, // Void zones, fire, etc.
OBSTACLE_AVOIDANCE_EMERGENCY = 245,
EMERGENCY_DEFENSIVE = 240, // Fleeing at 10% HP
// VERY_HIGH (200-239): Must complete
INTERRUPT_POSITIONING = 220, // Moving for interrupt
PVP_FLAG_CAPTURE = 210, // BG objectives
DUNGEON_MECHANIC = 205,
// HIGH (150-199): Combat positioning
COMBAT_AI = 180, // Class-specific combat logic
KITING = 175,
ROLE_POSITIONING = 170, // Tank/healer/dps positioning
FORMATION = 160,
PET_POSITIONING = 155,
// MEDIUM (100-149): Tactical movement
COMBAT_MOVEMENT_STRATEGY = 130,
PVP_TACTICAL = 120,
// LOW (50-99): Out-of-combat
FOLLOW = 70, // Group following
QUEST = 50,
LOOT = 40,
// MINIMAL (0-49): Idle
EXPLORATION = 20,
IDLE = 0
};
// Adapter: Maps PlayerBot → TrinityCore
class MovementPriorityMapper
{
public:
struct TrinityCorePriority
{
MovementGeneratorPriority priority; // NONE, NORMAL, HIGHEST
MovementGeneratorMode mode; // DEFAULT, OVERRIDE
uint8 tie_breaker; // For equal TC priorities
};
static TrinityCorePriority Map(PlayerBotMovementPriority pbPriority)
{
uint8 value = static_cast<uint8>(pbPriority);
// CRITICAL (240+) → HIGHEST priority, OVERRIDE mode
if (value >= 240)
return {MOTION_PRIORITY_HIGHEST, MOTION_MODE_OVERRIDE, value};
// VERY_HIGH (200-239) → HIGHEST priority, DEFAULT mode
if (value >= 200)
return {MOTION_PRIORITY_HIGHEST, MOTION_MODE_DEFAULT, value};
// HIGH (150-199) → NORMAL priority, OVERRIDE mode
if (value >= 150)
return {MOTION_PRIORITY_NORMAL, MOTION_MODE_OVERRIDE, value};
// MEDIUM/LOW (50-149) → NORMAL priority, DEFAULT mode
if (value >= 50)
return {MOTION_PRIORITY_NORMAL, MOTION_MODE_DEFAULT, value};
// MINIMAL (0-49) → NONE priority, DEFAULT mode (slot DEFAULT)
return {MOTION_PRIORITY_NONE, MOTION_MODE_DEFAULT, value};
}
};
} // namespace Playerbot
Design Rationale:
- HIGHEST + OVERRIDE: Emergency situations (boss mechanics, death recovery)
- HIGHEST + DEFAULT: Important but can be overridden (interrupts, PvP objectives)
- NORMAL + OVERRIDE: Combat positioning (overrides following)
- NORMAL + DEFAULT: Standard movement (following, questing)
- NONE + DEFAULT: Idle behavior (uses MOTION_SLOT_DEFAULT)
Tie-Breaker: If TrinityCore priorities are equal, use tie_breaker field to maintain PlayerBot's granular ordering.
Component 2: Movement Request
File: src/modules/Playerbot/Movement/MovementRequest.h
namespace Playerbot
{
enum class MovementRequestType
{
POINT, // MovePoint
CHASE, // MoveChase
FOLLOW, // MoveFollow
IDLE, // MoveIdle
JUMP, // MoveJump
CHARGE, // MoveCharge
KNOCKBACK, // MoveKnockbackFrom
CUSTOM // LaunchMoveSpline with custom init
};
struct MovementRequest
{
// Identity
uint64 requestId; // Unique ID for tracking
PlayerBotMovementPriority priority; // PlayerBot priority
MovementRequestType type; // Request type
std::string reason; // Debug description
// Source tracking
std::string sourceSystem; // "ClassAI", "EncounterStrategy", etc.
uint32 sourceThreadId; // Thread that made request
// Timing
uint32 timestamp; // getMSTime() when created
uint32 expectedDuration; // Expected movement duration (ms)
bool allowInterrupt; // Can be interrupted by lower priority?
// Type-specific parameters (variant)
struct PointParams {
Position targetPos;
bool generatePath;
Optional<float> finalOrient;
Optional<float> speed;
Optional<float> closeEnoughDistance;
};
struct ChaseParams {
ObjectGuid targetGuid;
ChaseRange range;
Optional<ChaseAngle> angle;
};
struct FollowParams {
ObjectGuid targetGuid;
float distance;
Optional<ChaseAngle> angle;
Optional<Milliseconds> duration;
};
std::variant<PointParams, ChaseParams, FollowParams> params;
// Hash for deduplication
uint64 GetSpatialTemporalHash() const;
// Comparison
bool IsDuplicateOf(MovementRequest const& other) const;
};
} // namespace Playerbot
Component 3: Movement Arbiter Core
File: src/modules/Playerbot/Movement/MovementArbiter.h
namespace Playerbot
{
class TC_GAME_API MovementArbiter
{
public:
explicit MovementArbiter(Player* bot);
~MovementArbiter();
// ========================================================================
// PRIMARY API - Thread-Safe Request Submission
// ========================================================================
/**
* Submit a movement request for arbitration
* @param request The movement request
* @return RequestResult indicating acceptance/rejection
*/
struct RequestResult
{
bool accepted;
std::string reason; // Why accepted/rejected
uint64 requestId; // ID if accepted
};
RequestResult RequestMovement(MovementRequest const& request);
/**
* Cancel any pending movement from a specific source system
*/
void CancelMovement(std::string const& sourceSystem);
/**
* Main update loop - call every frame from BotAI::UpdateAI()
* Resolves pending requests and executes via MotionMaster
*/
void Update(uint32 diff);
// ========================================================================
// QUERY API - Current State
// ========================================================================
bool IsMoving() const;
PlayerBotMovementPriority GetCurrentPriority() const;
std::string GetCurrentSourceSystem() const;
Position GetTargetPosition() const;
/**
* Check if a request would be accepted (dry-run)
*/
bool WouldAcceptRequest(PlayerBotMovementPriority priority) const;
// ========================================================================
// STATISTICS & DIAGNOSTICS
// ========================================================================
struct Statistics
{
std::atomic<uint64> totalRequests{0};
std::atomic<uint64> acceptedRequests{0};
std::atomic<uint64> rejectedRequests{0};
std::atomic<uint64> deduplications{0};
std::atomic<uint64> priorityOverrides{0};
std::map<std::string, uint32> requestsBySource;
std::map<PlayerBotMovementPriority, uint32> requestsByPriority;
};
Statistics const& GetStatistics() const { return _stats; }
private:
// ========================================================================
// INTERNAL LOGIC
// ========================================================================
/**
* Core arbitration logic - decides accept/reject
*/
bool ShouldAcceptRequest(MovementRequest const& request);
/**
* Deduplication check
*/
bool IsDuplicateRequest(MovementRequest const& request) const;
/**
* Execute movement via TrinityCore MotionMaster
*/
bool ExecuteMovementRequest(MovementRequest const& request);
/**
* Translate MovementRequest → MotionMaster API call
*/
void CallMotionMasterAPI(MovementRequest const& request,
MovementGeneratorPriority tcPriority,
MovementGeneratorMode tcMode);
// ========================================================================
// DATA MEMBERS
// ========================================================================
Player* _bot;
// Active movement state
std::optional<MovementRequest> _activeRequest;
uint32 _activeRequestStartTime = 0;
// Pending requests (thread-safe queue)
mutable std::mutex _queueMutex;
std::deque<MovementRequest> _pendingRequests;
// Recent request history (for deduplication)
std::deque<MovementRequest> _recentRequests;
static constexpr size_t MAX_RECENT_REQUESTS = 20;
// Statistics
Statistics _stats;
// Configuration
static constexpr uint32 DEDUPLICATION_TIME_WINDOW_MS = 100;
static constexpr float DEDUPLICATION_DISTANCE_THRESHOLD = 0.3f;
};
} // namespace Playerbot
Component 4: BotAI Integration
File: src/modules/Playerbot/AI/BotAI.h (modifications)
class TC_GAME_API BotAI
{
public:
// ========================================================================
// MOVEMENT API - Replace all direct MotionMaster calls
// ========================================================================
/**
* Request movement via arbiter
* @param priority Priority category
* @param position Target position
* @param reason Debug description
* @param sourceSystem Calling system name (auto-detected if empty)
* @return true if request accepted
*/
bool RequestMoveToPosition(PlayerBotMovementPriority priority,
Position const& position,
std::string reason = "",
std::string sourceSystem = "");
bool RequestChaseTarget(PlayerBotMovementPriority priority,
Unit* target,
ChaseRange range = ChaseRange(0.0f),
Optional<ChaseAngle> angle = {},
std::string reason = "");
bool RequestFollowTarget(PlayerBotMovementPriority priority,
Unit* target,
float distance,
Optional<ChaseAngle> angle = {},
std::string reason = "");
/**
* Cancel movement from calling system
*/
void CancelMovement(std::string sourceSystem = "");
/**
* Check if movement request would be accepted (dry-run)
*/
bool CanRequestMovement(PlayerBotMovementPriority priority) const;
// ========================================================================
// LEGACY API - Deprecated but functional during migration
// ========================================================================
[[deprecated("Use RequestMoveToPosition instead")]]
void MoveTo(float x, float y, float z);
[[deprecated("Use RequestFollowTarget instead")]]
void FollowTarget(Unit* target, float distance);
private:
std::unique_ptr<MovementArbiter> _movementArbiter;
};
Thread Safety Design
Problem: BotActionProcessor makes movement requests from worker thread.
Solution: Lock-free fast-path for request submission.
// In MovementArbiter.cpp
MovementArbiter::RequestResult MovementArbiter::RequestMovement(MovementRequest const& request)
{
// FAST PATH: Lock-free priority check
if (_activeRequest.has_value())
{
auto activePriority = static_cast<uint8>(_activeRequest->priority);
auto requestPriority = static_cast<uint8>(request.priority);
if (requestPriority < activePriority)
{
// Immediate rejection without locking
_stats.rejectedRequests.fetch_add(1, std::memory_order_relaxed);
return {false, "Lower priority than active movement", 0};
}
}
// SLOW PATH: Acquire lock for queue insertion
{
std::lock_guard<std::mutex> lock(_queueMutex);
// Duplicate check
if (IsDuplicateRequest(request))
{
_stats.deduplications.fetch_add(1, std::memory_order_relaxed);
return {false, "Duplicate request", 0};
}
// Add to pending queue
_pendingRequests.push_back(request);
_stats.totalRequests.fetch_add(1, std::memory_order_relaxed);
return {true, "Queued for execution", request.requestId};
}
}
Update() Execution:
void MovementArbiter::Update(uint32 diff)
{
// Called from main thread (BotAI::UpdateAI)
// Safe to call MotionMaster here
std::deque<MovementRequest> pendingCopy;
// Acquire lock briefly to copy pending queue
{
std::lock_guard<std::mutex> lock(_queueMutex);
pendingCopy.swap(_pendingRequests);
}
// Process requests WITHOUT holding lock
for (auto const& request : pendingCopy)
{
if (ShouldAcceptRequest(request))
{
ExecuteMovementRequest(request); // Calls MotionMaster
_activeRequest = request;
_activeRequestStartTime = getMSTime();
_stats.acceptedRequests.fetch_add(1, std::memory_order_relaxed);
}
else
{
_stats.rejectedRequests.fetch_add(1, std::memory_order_relaxed);
}
}
// Check if active request completed
if (_activeRequest.has_value())
{
if (HasArrivedAtDestination())
_activeRequest.reset();
}
}
Deduplication Algorithm
bool MovementArbiter::IsDuplicateRequest(MovementRequest const& request) const
{
// Check recent requests within time window
uint32 now = getMSTime();
for (auto const& recent : _recentRequests)
{
// Outside time window
if (now - recent.timestamp > DEDUPLICATION_TIME_WINDOW_MS)
continue;
// Different types
if (recent.type != request.type)
continue;
// Different priorities (allow priority upgrades)
if (recent.priority != request.priority)
continue;
// Spatial hash comparison
if (recent.GetSpatialTemporalHash() == request.GetSpatialTemporalHash())
return true;
}
return false;
}
// Spatial-temporal hashing for fast duplicate detection
uint64 MovementRequest::GetSpatialTemporalHash() const
{
if (type == MovementRequestType::POINT)
{
auto const& point = std::get<PointParams>(params);
// Grid coordinates (5-yard resolution)
uint32 gridX = static_cast<uint32>(point.targetPos.GetPositionX() / 5.0f);
uint32 gridY = static_cast<uint32>(point.targetPos.GetPositionY() / 5.0f);
uint32 gridZ = static_cast<uint32>(point.targetPos.GetPositionZ() / 5.0f);
// Combine into 64-bit hash
uint64 hash = (uint64(gridX) << 32) | (uint64(gridY) << 16) | uint64(gridZ);
return hash;
}
else if (type == MovementRequestType::CHASE || type == MovementRequestType::FOLLOW)
{
// Use target GUID as hash
ObjectGuid targetGuid = (type == MovementRequestType::CHASE)
? std::get<ChaseParams>(params).targetGuid
: std::get<FollowParams>(params).targetGuid;
return targetGuid.GetRawValue();
}
return 0; // No deduplication for other types
}
IMPLEMENTATION PHASES
Phase 1: Core Infrastructure (Weeks 1-2)
Deliverables:
- ✅
MovementPriorityMapper.h/cpp- Priority translation - ✅
MovementRequest.h/cpp- Request structure - ✅
MovementArbiter.h/cpp- Core arbiter - ✅ Unit tests for all components
Tasks:
- Create
src/modules/Playerbot/Movement/Arbiter/directory - Implement MovementPriorityMapper with unit tests
- Implement MovementRequest with hashing + comparison
- Implement MovementArbiter core logic
- Add to CMakeLists.txt
- Write unit tests (priority mapping, deduplication, thread safety)
Acceptance Criteria:
- All unit tests pass
- Thread safety verified with ThreadSanitizer
- Performance: <0.001ms per request submission
- Performance: <0.01ms per Update() cycle
Phase 2: BotAI Integration (Weeks 2-3)
Deliverables:
- ✅ BotAI wrapper API
- ✅ Legacy API deprecation warnings
- ✅ Diagnostic logging
- ✅ Integration tests
Tasks:
- Add
_movementArbitermember to BotAI - Implement wrapper methods (RequestMoveToPosition, etc.)
- Add deprecation attributes to old API
- Call
_movementArbiter->Update(diff)in BotAI::UpdateMovement() - Add diagnostic logging for all movement requests
- Write integration tests with mock Player/Unit
Acceptance Criteria:
- Existing code compiles with deprecation warnings
- New wrapper API functional
- Integration tests pass
- No crashes or memory leaks (Valgrind clean)
Phase 3: Emergency Systems Migration (Weeks 3-4)
Priority 1 Systems (must work first):
- ✅ ObstacleAvoidanceManager → PlayerBotMovementPriority::OBSTACLE_AVOIDANCE_EMERGENCY (245)
- ✅ EncounterStrategy → PlayerBotMovementPriority::BOSS_MECHANIC (250)
- ✅ DeathRecoveryManager → PlayerBotMovementPriority::DEATH_RECOVERY (255)
Migration Pattern:
BEFORE:
// ObstacleAvoidanceManager.cpp:203
_bot->GetMotionMaster()->MovePoint(0, targetPos.GetPositionX(),
targetPos.GetPositionY(),
targetPos.GetPositionZ());
AFTER:
// ObstacleAvoidanceManager.cpp:203
if (BotAI* ai = dynamic_cast<BotAI*>(_bot->GetAI()))
{
ai->RequestMoveToPosition(
PlayerBotMovementPriority::OBSTACLE_AVOIDANCE_EMERGENCY,
targetPos,
"Obstacle avoidance emergency",
"ObstacleAvoidanceManager"
);
}
Tasks:
- Migrate ObstacleAvoidanceManager (all 4 call sites)
- Migrate EncounterStrategy (all 10 call sites)
- Migrate DeathRecoveryManager (all 2 call sites)
- Test in dungeon with boss mechanics
- Verify void zone avoidance works 100%
Acceptance Criteria:
- Boss mechanics NEVER interrupted by combat positioning
- Death recovery NEVER interrupted
- Obstacle avoidance ALWAYS wins over other movement
Phase 4: Combat Systems Migration (Weeks 4-5)
Systems:
- ✅ All ClassAI implementations (13 classes) → PlayerBotMovementPriority::COMBAT_AI (180)
- ✅ InterruptManager → PlayerBotMovementPriority::INTERRUPT_POSITIONING (220)
- ✅ KitingManager → PlayerBotMovementPriority::KITING (175)
- ✅ CombatMovementStrategy → PlayerBotMovementPriority::COMBAT_MOVEMENT_STRATEGY (130)
Special Handling: CombatMovementStrategy
Current Issue:
// CombatMovementStrategy.cpp:476-479
player->GetMotionMaster()->Clear(); // DISASTER - cancels everything!
player->GetMotionMaster()->MovePoint(1, position);
Migration:
// CombatMovementStrategy.cpp:476-479
if (BotAI* ai = dynamic_cast<BotAI*>(player->GetAI()))
{
// Check if higher-priority movement active
if (ai->CanRequestMovement(PlayerBotMovementPriority::COMBAT_MOVEMENT_STRATEGY))
{
ai->RequestMoveToPosition(
PlayerBotMovementPriority::COMBAT_MOVEMENT_STRATEGY,
position,
fmt::format("Combat positioning ({})", GetRoleName(_currentRole)),
"CombatMovementStrategy"
);
}
// Else: higher priority active (e.g., boss mechanic), don't interfere
}
Tasks:
- Migrate WarriorAI, PaladinAI, HunterAI (already analyzed in detail)
- Migrate Rogue, Priest, Mage, Warlock, Druid, Shaman, Death Knight, Monk, Demon Hunter, Evoker
- Migrate InterruptManager
- Migrate KitingManager
- Migrate CombatMovementStrategy (remove all Clear() calls)
- Test with 100-bot stress test
- Verify no jittering during combat
Acceptance Criteria:
- Zero movement jittering across all 13 classes
- Interrupts work 100% (movement for interrupt succeeds)
- Kiting smooth and consistent
- Combat positioning respects higher priorities
Phase 5: Positioning Systems Migration (Week 5-6)
Systems:
- ✅ RoleBasedCombatPositioning → PlayerBotMovementPriority::ROLE_POSITIONING (170)
- ✅ FormationManager → PlayerBotMovementPriority::FORMATION (160)
- ✅ PositionManager → Used by ClassAI, inherits their priority
Tasks:
- Migrate RoleBasedCombatPositioning (all 13 call sites)
- Migrate FormationManager (all 1 call site)
- Ensure FormationManager doesn't conflict with LeaderFollowBehavior
- Test group formation in dungeons
Acceptance Criteria:
- Tank/healer/dps positioning works correctly
- Formation maintained during non-combat
- No conflicts with following
Phase 6: Out-of-Combat Systems Migration (Week 6)
Systems:
- ✅ LeaderFollowBehavior → PlayerBotMovementPriority::FOLLOW (70)
- ✅ QuestStrategy → PlayerBotMovementPriority::QUEST (50)
- ✅ LootStrategy → PlayerBotMovementPriority::LOOT (40)
- ✅ AdvancedBehaviorManager (PvP) → PlayerBotMovementPriority::PVP_FLAG_CAPTURE (210)
Tasks:
- Migrate LeaderFollowBehavior
- Migrate QuestStrategy (all 2 call sites)
- Migrate LootStrategy (all 2 call sites)
- Migrate AdvancedBehaviorManager (all 8 call sites)
- Test follow → combat → follow transitions
- Verify looting doesn't interrupt combat
Acceptance Criteria:
- Follow deactivates immediately on combat start
- Combat movement always wins over follow
- Looting only happens when safe
- PvP objectives override combat positioning (as designed)
Phase 7: Testing & Validation (Week 7)
Test Suites:
A. Unit Tests
TEST(MovementArbiter, PriorityEnforcement)
TEST(MovementArbiter, Deduplication)
TEST(MovementArbiter, ThreadSafety)
TEST(MovementPriorityMapper, CorrectMapping)
TEST(MovementRequest, SpatialHashing)
B. Integration Tests
TEST(BotAI, MageKitingNoJitter)
TEST(BotAI, BossMechanicOverridesCombat)
TEST(BotAI, FollowToChaseTransition)
TEST(BotAI, HunterPetPositioning)
TEST(BotAI, WarriorChargeCompletion)
C. Stress Tests
TEST(MovementArbiter, HundredBotStressTest)
TEST(MovementArbiter, ThousandRequestsPerSecond)
TEST(MovementArbiter, MultiThreadedRequests)
D. Scenario Tests
- Solo combat (all 13 classes)
- Group dungeon (5-bot party)
- Raid (20-bot raid group)
- Battleground (40v40 bots with PvP objectives)
- Boss mechanics (Nerub-ar Palace encounters)
Tasks:
- Write all unit tests
- Write all integration tests
- Run stress tests with profiler
- Test all 13 classes in solo combat
- Test 5-bot dungeon run (Stockades)
- Test boss mechanics (void zones, fire, etc.)
- Performance profiling with 100+ bots
Acceptance Criteria:
- 100% test pass rate
- No crashes (Valgrind/ASan clean)
- No deadlocks (ThreadSanitizer clean)
- Performance: <0.01ms average arbitration time
- Performance: <1% CPU increase with 100 bots
Phase 8: Cleanup & Deprecation (Week 8)
Tasks:
- Remove all direct MotionMaster calls from PlayerBot code
- Remove deprecated BotAI API (MoveTo, FollowTarget, StopMovement)
- Add compile-time checks for direct MotionMaster usage
- Update all documentation
- Create migration guide for custom strategies
Final Cleanup:
// Add to PlayerBot compilation flags (optional strict mode)
#ifdef PLAYERBOT_STRICT_MOVEMENT_API
// Make direct MotionMaster access a compile error
#define GetMotionMaster() static_assert(false, \
"Direct MotionMaster access forbidden. Use BotAI::RequestMoveToPosition()");
#endif
Acceptance Criteria:
- Zero direct MotionMaster calls in PlayerBot module
- All deprecation warnings resolved
- Documentation updated
- Migration guide published
QUALITY ASSURANCE STRATEGY
Code Quality Metrics
A. Static Analysis
- Clang-Tidy (all warnings enabled)
- Cppcheck
- SonarQube C++ analyzer
B. Dynamic Analysis
- Valgrind (memory leaks)
- AddressSanitizer (use-after-free, buffer overflows)
- ThreadSanitizer (data races, deadlocks)
- UndefinedBehaviorSanitizer
C. Performance Profiling
- perf (Linux)
- VTune (Intel)
- Tracy Profiler (real-time)
Test Coverage Targets
- Unit Test Coverage: 95%+ for arbiter core logic
- Integration Test Coverage: 80%+ for BotAI wrapper
- Scenario Test Coverage: 100% of documented use cases
Continuous Integration
CI Pipeline:
stages:
- build:
- gcc-11 (Linux)
- clang-14 (Linux)
- MSVC 2022 (Windows)
- test:
- unit_tests (fast, <1 minute)
- integration_tests (medium, <5 minutes)
- stress_tests (slow, <15 minutes)
- analysis:
- clang-tidy
- valgrind (memcheck)
- tsan (thread sanitizer)
- benchmark:
- 100-bot stress test
- 1000-request/sec throughput
- latency percentiles (p50, p95, p99)
Acceptance Gates
Gate 1: Code Quality
- Zero compiler warnings
- Zero static analysis errors
- Code review approved (2 reviewers)
Gate 2: Functionality
- All unit tests pass
- All integration tests pass
- Manual scenario testing complete
Gate 3: Performance
- <0.01ms average arbitration time
- <1% CPU overhead with 100 bots
- Zero memory leaks (Valgrind clean)
Gate 4: Stability
- 24-hour stress test (100 bots) without crashes
- Zero ThreadSanitizer violations
- Zero AddressSanitizer violations
PERFORMANCE SPECIFICATIONS
Latency Requirements
| Operation | Target | Maximum |
|---|---|---|
| Request submission (fast path) | <0.001ms | 0.005ms |
| Request submission (slow path) | <0.01ms | 0.05ms |
| Update() cycle (no pending) | <0.001ms | 0.01ms |
| Update() cycle (1 pending) | <0.01ms | 0.1ms |
| Deduplication check | <0.005ms | 0.02ms |
Throughput Requirements
| Metric | Target | Stress Test |
|---|---|---|
| Requests per bot per second | 10 | 100 |
| Total requests (100 bots) | 1000/sec | 10000/sec |
| Queue processing rate | >5000/sec | >20000/sec |
Memory Requirements
| Metric | Target | Maximum |
|---|---|---|
| Per-bot arbiter overhead | <1KB | 5KB |
| Recent request cache | <1KB | 2KB |
| Pending queue (typical) | <500 bytes | 2KB |
| Total (100 bots) | <200KB | 1MB |
CPU Requirements
| Metric | Baseline | With Arbiter | Delta |
|---|---|---|---|
| 1 bot idle | 0.1% | 0.1% | 0% |
| 1 bot combat | 0.5% | 0.51% | <0.01% |
| 100 bots combat | 50% | 51% | <1% |
MAINTENANCE & OPERATIONS
Monitoring & Diagnostics
A. Per-Bot Statistics
struct Statistics
{
uint64 totalRequests; // Lifetime request count
uint64 acceptedRequests; // Accepted count
uint64 rejectedRequests; // Rejected count
uint64 deduplications; // Duplicate requests filtered
uint64 priorityOverrides; // Higher priority replaced lower
std::map<std::string, uint32> requestsBySource;
std::map<PlayerBotMovementPriority, uint32> requestsByPriority;
float averageLatency; // Average arbitration time
float p95Latency; // 95th percentile
float p99Latency; // 99th percentile
};
B. Global Metrics
class MovementArbiterMetrics
{
public:
static uint64 GetTotalRequestsAllBots();
static uint64 GetActiveMovements();
static float GetGlobalThroughput(); // Requests/sec
static float GetGlobalLatency(); // Average ms
static std::map<PlayerBotMovementPriority, uint64> GetPriorityDistribution();
static std::map<std::string, uint64> GetSourceSystemDistribution();
};
C. Diagnostic Commands
// GM command: .playerbot movement stats <bot_name>
void ShowMovementStatistics(Player* bot);
// GM command: .playerbot movement debug <bot_name>
void EnableMovementDebugLogging(Player* bot);
// GM command: .playerbot movement history <bot_name>
void ShowMovementHistory(Player* bot); // Last 20 requests
Troubleshooting Guide
Issue 1: Bot not moving
Diagnostic:
1. .playerbot movement stats <bot> → Check accepted vs rejected ratio
2. .playerbot movement history <bot> → Check recent requests
3. Check logs for "rejected" messages
Common Causes:
- Lower priority request rejected by active high-priority movement
- Duplicate request filtered
- MotionMaster in invalid state (INITIALIZATION_PENDING)
Solution:
- Increase priority of stuck system
- Check for infinite loop of duplicate requests
- Verify bot is in world (IsInWorld())
Issue 2: Movement jittering
Diagnostic:
1. .playerbot movement debug <bot> → Enable verbose logging
2. Check logs for rapid request pattern (>10/sec from same source)
3. Check deduplication statistics
Common Causes:
- Deduplication disabled or broken
- Multiple systems issuing similar requests
- Spatial hash collision
Solution:
- Verify DEDUPLICATION_TIME_WINDOW_MS configured
- Increase DEDUPLICATION_DISTANCE_THRESHOLD if needed
- Add source-specific throttling
Issue 3: Performance degradation
Diagnostic:
1. Check global throughput: MovementArbiterMetrics::GetGlobalThroughput()
2. Profile with Tracy/perf
3. Check pending queue sizes
Common Causes:
- Request rate >10000/sec (excessive)
- Memory leak in request queue
- Lock contention (slow path hot)
Solution:
- Add request throttling to spammy sources
- Investigate memory leak with Valgrind
- Optimize lock-free fast path
Configuration
File: PlayerBot.conf
[Movement.Arbiter]
# Enable movement arbiter (vs legacy direct MotionMaster)
Enable = 1
# Deduplication settings
Deduplication.TimeWindow = 100 # milliseconds
Deduplication.DistanceThreshold = 0.3 # yards
# Performance tuning
MaxPendingQueueSize = 100 # Per bot
RecentRequestHistorySize = 20 # For deduplication
# Diagnostics
EnableStatistics = 1
LogRejectedRequests = 0 # Verbose logging
LogAcceptedRequests = 0
Upgrade Path
From: Direct MotionMaster calls To: Movement Arbiter API
Migration Steps:
- Enable arbiter in config (disabled by default during Phase 1-2)
- Test with small bot count (1-5 bots)
- Gradually increase to 50 bots
- Monitor statistics for anomalies
- Enable for all bots after 24-hour soak test
- Remove legacy API in next major version
Rollback Plan:
- Disable arbiter in config → Falls back to legacy direct calls
- Zero data loss
- Zero functionality loss
- Can rollback in <1 minute
CONCLUSION
This enterprise-grade Movement Arbiter architecture solves the root cause of PlayerBot movement issues by:
- Proper TrinityCore Integration - Uses existing priority/mode/slot system correctly
- Centralized Coordination - Single arbiter prevents 16 systems from conflicting
- Priority Enforcement - Boss mechanics always win, combat always beats following
- Deduplication - Spatial-temporal hashing prevents jittering
- Thread Safety - Lock-free fast path handles BotActionProcessor correctly
- Performance - <0.01ms overhead, <1% CPU increase for 100 bots
- Maintainability - SOLID principles, comprehensive testing, monitoring
Next Steps:
- Approve this architecture document
- Begin Phase 1 implementation (Weeks 1-2)
- Iterative review after each phase
- Full deployment after Phase 8
Success Criteria:
- ✅ All 13 classes move smoothly without jittering
- ✅ Boss mechanics 100% survival rate
- ✅ Thread-safe operation verified
- ✅ <1% performance overhead
- ✅ Zero TrinityCore core modifications
Document Status: APPROVED Implementation Start Date: TBD Expected Completion: 8 weeks from start Approval Signature: Pending