16 KiB
Phase 2.5: Update IdleStrategy - Observer Pattern
Duration: 1 week (2025-02-17 to 2025-02-24) Status: ⏳ PENDING Owner: Development Team
Objectives
Transform IdleStrategy from heavyweight delegator to lightweight observer:
- Remove all Automation::instance() calls
- Replace with fast manager state queries
- Eliminate throttling timers (no longer needed)
- Achieve <0.1ms per update (from 100ms+)
- Implement true observer pattern
Background
Current Problem: Strategy Does Heavyweight Work
IdleStrategy.cpp (Current):
void IdleStrategy::UpdateBehavior(BotAI* ai, uint32 diff)
{
Player* bot = ai->GetBot();
uint32 currentTime = getMSTime();
// ❌ Calls expensive singleton operations
if (currentTime - _lastQuestUpdate > 2000)
{
QuestAutomation::instance()->UpdateBotAutomation(bot, diff); // 50-100ms!
_lastQuestUpdate = currentTime;
}
if (currentTime - _lastGatheringUpdate > 1000)
{
GatheringAutomation::instance()->Update(bot, diff); // 50-100ms!
_lastGatheringUpdate = currentTime;
}
// ... etc for Trade, Auction
}
Problems:
- Performance: Even throttled, takes 0-144ms per update
- Architecture: Strategy shouldn't do heavyweight work
- Complexity: Strategy manages timers for external systems
- Redundancy: Managers already have their own throttling in Phase 2.4
- Scalability: Doesn't scale to 5000+ bots
Solution: Lightweight Observer Pattern
IdleStrategy.cpp (New):
void IdleStrategy::UpdateBehavior(BotAI* ai, uint32 diff)
{
// ✅ Fast state queries only (atomic reads, <0.001ms each)
QuestManager* questMgr = ai->GetQuestManager();
GatheringManager* gatherMgr = ai->GetGatheringManager();
TradeManager* tradeMgr = ai->GetTradeManager();
// Priority 1: Quest behavior (observe state only)
if (questMgr && questMgr->IsQuestingActive())
{
// Quest movement/behavior handled by managers
// Strategy just observes and coordinates
return; // Questing has priority
}
// Priority 2: Gathering behavior
if (gatherMgr && gatherMgr->HasNearbyResources())
{
// Gathering handled by manager
return;
}
// Priority 3: Trading behavior
if (tradeMgr && tradeMgr->NeedsRepair())
{
// Trading handled by manager
return;
}
// Priority 4: Fallback - Simple wander
DoWander(ai, diff);
}
Benefits:
- ✅ <0.1ms per update (vs 100ms+)
- ✅ No throttling needed (queries are instant)
- ✅ True observer pattern (strategies observe, managers work)
- ✅ Clean separation of concerns
- ✅ Scalable to 5000+ bots
Technical Requirements
Performance Constraints
- IdleStrategy::UpdateBehavior(): <0.1ms per call
- Manager state query: <0.001ms per call (atomic read)
- Total idle strategy overhead: <0.1ms per bot per frame
- Must call every frame (no throttling)
Observer Pattern Principles
- Strategies Observe: Read state from managers (fast queries)
- Managers Work: Do heavyweight operations (throttled)
- No Cross-Calling: Strategies never call manager Update() methods
- Atomic State: All state queries are atomic (thread-safe, lock-free)
Integration Points
- Requires all 4 managers from Phase 2.4 (Quest, Trade, Gathering, Auction)
- Managers already updating via BotAI::UpdateManagers()
- IdleStrategy just observes results
Deliverables
1. Refactored IdleStrategy.h
Location: src/modules/Playerbot/AI/Strategy/IdleStrategy.h
Before (with throttling timers):
class IdleStrategy : public Strategy
{
private:
uint32 _lastWanderTime = 0;
uint32 _wanderInterval = 30000;
// ❌ Throttling timers - not needed anymore
uint32 _lastQuestUpdate = 0;
uint32 _questUpdateInterval = 2000;
uint32 _lastGatheringUpdate = 0;
uint32 _gatheringUpdateInterval = 1000;
uint32 _lastTradeUpdate = 0;
uint32 _tradeUpdateInterval = 5000;
uint32 _lastAuctionUpdate = 0;
uint32 _auctionUpdateInterval = 10000;
};
After (clean, no timers):
class IdleStrategy : public Strategy
{
public:
IdleStrategy();
~IdleStrategy() override = default;
void InitializeActions() override;
void InitializeTriggers() override;
void InitializeValues() override;
void OnActivate(BotAI* ai) override;
void OnDeactivate(BotAI* ai) override;
bool IsActive(BotAI* ai) const override;
void UpdateBehavior(BotAI* ai, uint32 diff) override;
private:
// Wander behavior
void DoWander(BotAI* ai, uint32 diff);
// State
uint32 _lastWanderTime = 0;
uint32 _wanderInterval = 30000;
};
2. Refactored IdleStrategy.cpp
Location: src/modules/Playerbot/AI/Strategy/IdleStrategy.cpp
Complete New Implementation:
#include "IdleStrategy.h"
#include "BotAI.h"
#include "Player.h"
#include "Log.h"
#include "Game/QuestManager.h"
#include "Social/TradeManager.h"
#include "Professions/GatheringManager.h"
#include "Economy/AuctionManager.h"
namespace Playerbot
{
IdleStrategy::IdleStrategy() : Strategy("idle")
{
SetPriority(50); // Lower than group strategies
}
void IdleStrategy::InitializeActions()
{
// TODO: Add idle actions if needed
}
void IdleStrategy::InitializeTriggers()
{
// TODO: Add idle triggers if needed
}
void IdleStrategy::InitializeValues()
{
// TODO: Add idle values if needed
}
void IdleStrategy::OnActivate(BotAI* ai)
{
if (!ai || !ai->GetBot())
return;
TC_LOG_INFO("module.playerbot", "Idle strategy activated for bot {}", ai->GetBot()->GetName());
SetActive(true);
}
void IdleStrategy::OnDeactivate(BotAI* ai)
{
if (!ai || !ai->GetBot())
return;
TC_LOG_INFO("module.playerbot", "Idle strategy deactivated for bot {}", ai->GetBot()->GetName());
SetActive(false);
}
bool IdleStrategy::IsActive(BotAI* ai) const
{
if (!ai || !ai->GetBot())
return false;
// Active when not in a group
return _active && !ai->GetBot()->GetGroup();
}
void IdleStrategy::UpdateBehavior(BotAI* ai, uint32 diff)
{
if (!ai || !ai->GetBot())
return;
// OBSERVER PATTERN: Query manager state (fast atomic reads)
// Managers already updating via BotAI::UpdateManagers()
// Strategy just observes and coordinates
// Get manager references
QuestManager* questMgr = ai->GetQuestManager();
GatheringManager* gatherMgr = ai->GetGatheringManager();
TradeManager* tradeMgr = ai->GetTradeManager();
AuctionManager* auctionMgr = ai->GetAuctionManager();
// Priority 1: Active questing (highest priority)
if (questMgr && questMgr->IsQuestingActive())
{
// Quest manager is handling quest logic
// Bot is moving to objectives, killing mobs, etc.
// Strategy just needs to observe - don't interfere
static uint32 questCounter = 0;
if (++questCounter % 100 == 0)
{
TC_LOG_DEBUG("module.playerbot", "🎯 IdleStrategy: Bot {} actively questing",
ai->GetBot()->GetName());
}
return;
}
// Priority 2: Gathering resources (if available)
if (gatherMgr && gatherMgr->HasNearbyResources())
{
// Gathering manager is handling resource collection
// Strategy just observes
static uint32 gatherCounter = 0;
if (++gatherCounter % 100 == 0)
{
TC_LOG_DEBUG("module.playerbot", "⛏️ IdleStrategy: Bot {} gathering resources",
ai->GetBot()->GetName());
}
return;
}
// Priority 3: Trading/repair (if needed)
if (tradeMgr && tradeMgr->NeedsRepair())
{
// Trade manager is handling vendor interaction
// Strategy just observes
static uint32 tradeCounter = 0;
if (++tradeCounter % 100 == 0)
{
TC_LOG_DEBUG("module.playerbot", "🔧 IdleStrategy: Bot {} needs repair",
ai->GetBot()->GetName());
}
return;
}
// Priority 4: Auction house (if near and has items to sell)
if (auctionMgr && auctionMgr->HasActiveAuctions())
{
// Auction manager is handling AH interaction
static uint32 auctionCounter = 0;
if (++auctionCounter % 100 == 0)
{
TC_LOG_DEBUG("module.playerbot", "💰 IdleStrategy: Bot {} using auction house",
ai->GetBot()->GetName());
}
return;
}
// Priority 5: Fallback - Simple wandering behavior
DoWander(ai, diff);
}
void IdleStrategy::DoWander(BotAI* ai, uint32 diff)
{
if (!ai || !ai->GetBot())
return;
uint32 currentTime = getMSTime();
if (currentTime - _lastWanderTime < _wanderInterval)
return;
_lastWanderTime = currentTime;
// TODO: Implement proper wandering with pathfinding
// For now, just log that bot is idle
static uint32 idleCounter = 0;
if (++idleCounter % 100 == 0)
{
TC_LOG_DEBUG("module.playerbot", "💤 IdleStrategy: Bot {} is wandering (truly idle)",
ai->GetBot()->GetName());
}
}
} // namespace Playerbot
3. Remove Automation Includes
Location: src/modules/Playerbot/AI/Strategy/IdleStrategy.cpp
Before:
#include "Quest/QuestAutomation.h"
#include "Professions/GatheringAutomation.h"
#include "Social/TradeAutomation.h"
#include "Social/AuctionAutomation.h"
After:
#include "Game/QuestManager.h"
#include "Social/TradeManager.h"
#include "Professions/GatheringManager.h"
#include "Economy/AuctionManager.h"
4. Performance Tests
Location: tests/performance/IdleStrategyPerformanceTest.cpp
Test Benchmark:
TEST(IdleStrategyPerformance, UpdateBehaviorUnder100Microseconds)
{
// Setup: 100 bots with idle strategy
std::vector<Bot*> bots;
for (int i = 0; i < 100; ++i)
{
Bot* bot = CreateTestBot(CLASS_WARRIOR, 30);
bot->GetAI()->ActivateStrategy("idle");
bots.push_back(bot);
}
// Benchmark: 1000 update cycles
auto start = std::chrono::high_resolution_clock::now();
for (int cycle = 0; cycle < 1000; ++cycle)
{
for (Bot* bot : bots)
{
bot->GetAI()->GetStrategy("idle")->UpdateBehavior(bot->GetAI(), 100);
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
// Calculate average per-bot update time
double avgPerUpdate = duration.count() / (100.0 * 1000.0);
TC_LOG_INFO("test", "IdleStrategy avg update time: {} microseconds", avgPerUpdate);
// Assert: Must be under 100 microseconds (0.1ms)
ASSERT_LT(avgPerUpdate, 100.0) << "IdleStrategy update took " << avgPerUpdate << "us, exceeds 100us limit";
}
5. Integration Tests
Location: tests/integration/IdleStrategyIntegrationTest.cpp
Test Scenarios:
TEST(IdleStrategyIntegration, PrioritizesQuesting)
{
// Setup: Bot with active quest
Bot* bot = CreateTestBot(CLASS_WARRIOR, 20);
bot->GetAI()->GetQuestManager()->AddQuest(12345);
bot->GetAI()->ActivateStrategy("idle");
// Execute 100 updates
for (int i = 0; i < 100; ++i)
bot->GetAI()->UpdateAI(100);
// Verify: Bot is following quest logic
ASSERT_TRUE(bot->GetAI()->GetQuestManager()->IsQuestingActive());
}
TEST(IdleStrategyIntegration, FallsBackToWander)
{
// Setup: Bot with no active tasks
Bot* bot = CreateTestBot(CLASS_WARRIOR, 20);
bot->GetAI()->ActivateStrategy("idle");
// Verify: No active systems
ASSERT_FALSE(bot->GetAI()->GetQuestManager()->IsQuestingActive());
ASSERT_FALSE(bot->GetAI()->GetGatheringManager()->HasNearbyResources());
// Execute updates
for (int i = 0; i < 100; ++i)
bot->GetAI()->UpdateAI(100);
// Verify: Bot executes wander behavior (logged)
// (Check logs for wander messages)
}
6. Documentation
Location: docs/OBSERVER_PATTERN_GUIDE.md
Content:
- Observer pattern explanation
- Manager/Strategy separation principles
- State query best practices
- Performance benefits explanation
- Migration from delegation to observation
- Troubleshooting guide
Implementation Steps
Day 1: Remove Automation Calls
- Open IdleStrategy.cpp
- Delete all Automation::instance() calls
- Delete all throttling timer logic
- Add manager state queries
- Verify compilation
Day 2: Implement Observer Pattern
- Rewrite UpdateBehavior() with priority logic
- Use manager state queries only
- Add debug logging (temporary)
- Test with 10 bots
- Verify <0.1ms per update
Day 3: Test Priority System
- Test quest priority (highest)
- Test gathering priority
- Test trade priority
- Test auction priority
- Test wander fallback
Day 4: Performance Testing
- Benchmark with 100 bots
- Benchmark with 500 bots
- Benchmark with 1000 bots
- Profile UpdateBehavior()
- Verify <0.1ms per update
Day 5: Integration Testing
- Test with all managers active
- Test priority transitions (quest → wander)
- Test with mixed bot activities
- Verify no regressions
- Test 5000 bots (stress test)
Day 6: Cleanup and Optimization
- Remove temporary debug logging
- Optimize state query order
- Clean up code comments
- Final code review
- Performance validation
Day 7: Documentation
- Write observer pattern guide
- Document priority system
- Create migration examples
- Update architecture docs
- Final review
Success Criteria
Performance Requirements
- ✅ IdleStrategy::UpdateBehavior() <0.1ms per call
- ✅ Manager state queries <0.001ms each
- ✅ No throttling needed (called every frame)
- ✅ 100 bots: No performance degradation
- ✅ 500 bots: <1% CPU increase for idle strategy
- ✅ 1000 bots: <2% CPU increase
- ✅ 5000 bots: <10% CPU increase
Functional Requirements
- ✅ All Automation::instance() calls removed
- ✅ All throttling timers removed
- ✅ Priority system works correctly
- ✅ Quest priority is highest
- ✅ Wander is fallback
- ✅ State queries are atomic
- ✅ No blocking operations
Architecture Quality
- ✅ True observer pattern implemented
- ✅ Clean separation: strategies observe, managers work
- ✅ No cross-calling between systems
- ✅ Follows Phase 2.1 architecture design
- ✅ All managers from Phase 2.4 integrated
Code Quality
- ✅ Clean, readable code
- ✅ Full documentation
- ✅ No compiler warnings
- ✅ Comprehensive tests
- ✅ No memory leaks
Dependencies
Requires
- Phase 2.1 complete (BehaviorManager base class)
- Phase 2.4 complete (All 4 managers refactored)
- Manager state query methods implemented
- BotAI::UpdateManagers() working
Blocks
- Phase 2.6 (Integration testing needs observer pattern working)
- Full idle bot functionality
- Performance scalability to 5000+ bots
Risk Mitigation
Risk: Breaking idle bot behavior
Mitigation:
- Implement new code alongside old code temporarily
- Test extensively before removing old code
- Create rollback plan
Risk: Performance regression
Mitigation:
- Benchmark before/after
- Profile every step
- Test with 5000 bots
Risk: Priority system too complex
Mitigation:
- Keep priority logic simple (if/return pattern)
- Document priority order clearly
- Test each priority level independently
Risk: State queries not thread-safe
Mitigation:
- Use atomic state flags in managers (from Phase 2.4)
- No locks needed (single-threaded access)
- Test with high bot counts
Before/After Comparison
Performance
| Metric | Before | After | Improvement |
|---|---|---|---|
| Avg update time | 100-144ms | <0.1ms | 1000x faster |
| Min update time | 0ms (throttled) | <0.1ms | Consistent |
| Max update time | 600ms | <0.1ms | 6000x faster |
| CPU usage (100 bots) | 15-20% | <1% | 95% reduction |
Code Complexity
| Metric | Before | After | Improvement |
|---|---|---|---|
| LOC in UpdateBehavior() | 83 lines | 60 lines | Simpler |
| Timer variables | 8 timers | 1 timer | Cleaner |
| External dependencies | 4 singletons | 4 managers | Better |
| Blocking calls | 4 calls | 0 calls | No blocking |
Next Phase
After completion, proceed to Phase 2.6: Integration Testing
Last Updated: 2025-01-13 Next Review: 2025-02-24