Files
ThordekkCore/PLAYERBOT_DEVELOPER_GUIDE.md
T
2026-01-20 21:33:16 -03:00

22 KiB

PlayerBot Developer Guide

Table of Contents

  1. Introduction
  2. Development Setup
  3. Adding a New Manager
  4. Adding a New Strategy
  5. Writing Tests
  6. Debugging Techniques
  7. Performance Profiling
  8. Common Pitfalls
  9. Best Practices
  10. Code Examples

Introduction

This guide provides comprehensive instructions for developers working on the PlayerBot module for TrinityCore 11.2. The PlayerBot system is a sophisticated AI framework that enables bots to behave like real players, supporting up to 5000 concurrent bots with minimal performance impact.

Prerequisites

  • C++20 compatible compiler (MSVC 2022, GCC 11+, Clang 14+)
  • CMake 3.24+
  • Boost 1.74+
  • MySQL 9.4
  • Understanding of TrinityCore architecture
  • Basic knowledge of game mechanics

Development Setup

Building the Module

# Clone the repository
git clone https://github.com/TrinityCore/TrinityCore.git
cd TrinityCore
git checkout playerbot-dev

# Create build directory
mkdir build
cd build

# Configure with PlayerBot enabled
cmake .. -DWITH_PLAYERBOT=1 -DCMAKE_BUILD_TYPE=RelWithDebInfo

# Build (adjust -j based on your CPU cores)
make -j8

# Or on Windows with Visual Studio
msbuild TrinityCore.sln /p:Configuration=RelWithDebInfo /m

Configuration

Create playerbots.conf in your server directory:

# Enable PlayerBot system
Playerbot.Enable = 1

# Performance settings
Playerbot.MaxBots = 100
Playerbot.UpdateInterval = 100

# Manager intervals (milliseconds)
Playerbot.QuestManager.UpdateInterval = 2000
Playerbot.TradeManager.UpdateInterval = 5000
Playerbot.GatheringManager.UpdateInterval = 1000
Playerbot.AuctionManager.UpdateInterval = 10000

Adding a New Manager

Step 1: Create Manager Header

// src/modules/Playerbot/CustomManager.h
#pragma once

#include "AI/Strategy/BehaviorManager.h"
#include <atomic>

namespace Playerbot
{
    class CustomManager : public BehaviorManager
    {
    public:
        explicit CustomManager(BotAI* ai);
        ~CustomManager() override = default;

        // BehaviorManager interface
        std::string GetName() const override { return "CustomManager"; }

        // Custom public interface
        bool HasWork() const { return m_hasWork.load(); }
        void QueueTask(uint32 taskId);

    protected:
        // Required virtual method from BehaviorManager
        void UpdateBehavior(uint32 timeDelta) override;

    private:
        // State management (atomic for thread safety)
        std::atomic<bool> m_hasWork{false};
        std::atomic<uint32> m_taskCount{0};

        // Internal methods
        void ProcessTasks();
        void ValidateState();
    };
}

Step 2: Implement Manager Logic

// src/modules/Playerbot/CustomManager.cpp
#include "CustomManager.h"
#include "AI/BotAI.h"
#include "Player.h"
#include "Log.h"

namespace Playerbot
{
    CustomManager::CustomManager(BotAI* ai)
        : BehaviorManager(ai, 3000) // 3 second update interval
    {
        // Initialize manager state
        LOG_DEBUG("bot.ai", "CustomManager initialized for bot {}",
                  m_ai->GetBot()->GetName());
    }

    void CustomManager::UpdateBehavior(uint32 timeDelta)
    {
        // This method is called by BehaviorManager template method
        // It's already throttled based on update interval

        if (!m_ai || !m_ai->GetBot())
            return;

        Player* bot = m_ai->GetBot();

        // Validate bot state
        if (bot->isDead() || bot->IsBeingTeleported())
            return;

        // Process manager logic
        ProcessTasks();
        ValidateState();

        // Update state flags
        m_hasWork.store(m_taskCount.load() > 0);
    }

    void CustomManager::ProcessTasks()
    {
        uint32 tasks = m_taskCount.load();
        if (tasks == 0)
            return;

        // Process up to 5 tasks per update
        uint32 processed = 0;
        while (processed < 5 && m_taskCount.load() > 0)
        {
            // Task processing logic here
            m_taskCount.fetch_sub(1);
            processed++;
        }

        LOG_DEBUG("bot.ai", "CustomManager processed {} tasks", processed);
    }

    void CustomManager::QueueTask(uint32 taskId)
    {
        m_taskCount.fetch_add(1);
        m_hasWork.store(true);
    }
}

Step 3: Register Manager in BotAI

// In BotAI.h - Add to manager list
class BotAI
{
private:
    std::unique_ptr<CustomManager> m_customManager;
    // ... other managers
};

// In BotAI.cpp - Initialize in constructor
BotAI::BotAI(Player* bot) : m_bot(bot)
{
    // ... existing initialization
    m_customManager = std::make_unique<CustomManager>(this);
    m_customManager->Enable(); // Enable by default or based on config
}

// In UpdateManagers method
void BotAI::UpdateManagers(uint32 diff)
{
    // ... existing managers
    if (m_customManager)
        m_customManager->Update(diff);
}

Adding a New Strategy

Step 1: Define Strategy Interface

// src/modules/Playerbot/AI/Strategy/CustomStrategy.h
#pragma once

#include "Strategy.h"
#include <vector>

namespace Playerbot
{
    class CustomStrategy : public Strategy
    {
    public:
        explicit CustomStrategy(BotAI* ai);
        ~CustomStrategy() override = default;

        // Strategy interface
        std::string getName() const override { return "custom"; }
        int getPriority() const override { return 50; }

        // Triggers this strategy handles
        void InitTriggers(std::list<std::shared_ptr<Trigger>>& triggers) override;

        // Actions this strategy can perform
        void InitActions(ActionList& actions) override;

    private:
        bool ShouldActivate() const;
        void ExecuteRotation();
    };
}

Step 2: Implement Strategy Logic

// src/modules/Playerbot/AI/Strategy/CustomStrategy.cpp
#include "CustomStrategy.h"
#include "AI/BotAI.h"
#include "Actions/CustomAction.h"
#include "Triggers/CustomTrigger.h"

namespace Playerbot
{
    CustomStrategy::CustomStrategy(BotAI* ai) : Strategy(ai)
    {
        // Initialize strategy-specific data
    }

    void CustomStrategy::InitTriggers(std::list<std::shared_ptr<Trigger>>& triggers)
    {
        // Add triggers that activate this strategy
        triggers.push_back(std::make_shared<CustomTrigger>(m_ai));
        triggers.push_back(std::make_shared<HealthLowTrigger>(m_ai));
    }

    void CustomStrategy::InitActions(ActionList& actions)
    {
        // Define action priority mappings
        actions.push_back(ActionNode("custom action", 100));
        actions.push_back(ActionNode("heal", 90));
        actions.push_back(ActionNode("flee", 80));
    }

    bool CustomStrategy::ShouldActivate() const
    {
        if (!m_ai || !m_ai->GetBot())
            return false;

        Player* bot = m_ai->GetBot();

        // Custom activation conditions
        return bot->GetHealthPct() < 50.0f ||
               bot->GetPowerPct(bot->getPowerType()) < 20.0f;
    }
}

Writing Tests

Unit Test Template

// src/modules/Playerbot/Tests/CustomManagerTest.cpp
#include "gtest/gtest.h"
#include "CustomManager.h"
#include "TestUtilities.h"

using namespace Playerbot;

class CustomManagerTest : public ::testing::Test
{
protected:
    void SetUp() override
    {
        // Create test bot and AI
        m_testBot = TestUtilities::CreateTestBot();
        m_botAI = std::make_unique<BotAI>(m_testBot);
        m_manager = std::make_unique<CustomManager>(m_botAI.get());
    }

    void TearDown() override
    {
        m_manager.reset();
        m_botAI.reset();
        TestUtilities::CleanupTestBot(m_testBot);
    }

    Player* m_testBot = nullptr;
    std::unique_ptr<BotAI> m_botAI;
    std::unique_ptr<CustomManager> m_manager;
};

TEST_F(CustomManagerTest, InitializationTest)
{
    EXPECT_NE(m_manager, nullptr);
    EXPECT_FALSE(m_manager->IsEnabled());
    EXPECT_EQ(m_manager->GetName(), "CustomManager");
}

TEST_F(CustomManagerTest, EnableDisableTest)
{
    m_manager->Enable();
    EXPECT_TRUE(m_manager->IsEnabled());

    m_manager->Disable();
    EXPECT_FALSE(m_manager->IsEnabled());
}

TEST_F(CustomManagerTest, UpdateThrottlingTest)
{
    m_manager->Enable();
    m_manager->SetUpdateInterval(1000); // 1 second

    // First update should execute
    m_manager->Update(500);
    EXPECT_EQ(m_manager->GetUpdateCount(), 0); // Not enough time

    // Second update should trigger
    m_manager->Update(600);
    EXPECT_EQ(m_manager->GetUpdateCount(), 1); // Should update now
}

TEST_F(CustomManagerTest, PerformanceTest)
{
    m_manager->Enable();

    auto start = std::chrono::high_resolution_clock::now();

    // Simulate 1000 updates
    for (int i = 0; i < 1000; ++i)
    {
        m_manager->Update(33); // ~30 FPS
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    // Should complete in under 100ms
    EXPECT_LT(duration.count(), 100);
}

Integration Test Example

// src/modules/Playerbot/Tests/IntegrationTest.cpp
TEST(BotIntegration, FullUpdateCycle)
{
    // Create bot environment
    auto bot = TestUtilities::CreateTestBot();
    auto ai = std::make_unique<BotAI>(bot);

    // Enable all managers
    ai->EnableManager(ManagerType::Quest);
    ai->EnableManager(ManagerType::Trade);
    ai->EnableManager(ManagerType::Gathering);

    // Simulate game loop
    for (int frame = 0; frame < 100; ++frame)
    {
        ai->UpdateAI(33); // 33ms per frame (~30 FPS)
    }

    // Verify no crashes and proper state
    EXPECT_TRUE(bot->IsAlive());
    EXPECT_GE(ai->GetPerformanceMetrics().totalUpdates, 100);
}

Debugging Techniques

1. Enable Verbose Logging

// In your code
LOG_DEBUG("bot.ai", "Manager {} updating, state: {}, tasks: {}",
          GetName(), m_enabled.load(), m_taskCount.load());

// Set log level in config
LogLevel = 3  # Debug level
Logger.bot.ai = 3,Console Server

2. Use Performance Profiling Macros

#define PROFILE_SCOPE(name) \
    ProfileTimer timer##__LINE__(name)

class ProfileTimer
{
public:
    explicit ProfileTimer(const char* name) : m_name(name)
    {
        m_start = std::chrono::high_resolution_clock::now();
    }

    ~ProfileTimer()
    {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - m_start);
        LOG_DEBUG("bot.performance", "{}: {}μs", m_name, duration.count());
    }

private:
    const char* m_name;
    std::chrono::time_point<std::chrono::high_resolution_clock> m_start;
};

// Usage
void UpdateBehavior(uint32 timeDelta) override
{
    PROFILE_SCOPE("CustomManager::UpdateBehavior");
    // Your code here
}

3. Visual Studio Debugging

// Add conditional breakpoints
if (m_taskCount.load() > 100)
{
    __debugbreak(); // Windows-specific breakpoint
}

// Use debug visualizers in .natvis file
<?xml version="1.0" encoding="utf-8"?>
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
  <Type Name="Playerbot::BehaviorManager">
    <DisplayString>{{Manager: {m_name}, Enabled: {m_enabled}, Interval: {m_updateInterval}ms}}</DisplayString>
  </Type>
</AutoVisualizer>

4. Memory Leak Detection

// Windows-specific memory debugging
#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>

// In main() or test setup
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif

// Linux with Valgrind
// valgrind --leak-check=full --track-origins=yes ./worldserver

Performance Profiling

1. Built-in Performance Metrics

class PerformanceMonitor
{
public:
    void StartFrame()
    {
        m_frameStart = std::chrono::high_resolution_clock::now();
    }

    void EndFrame()
    {
        auto frameEnd = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>
                       (frameEnd - m_frameStart);

        m_frameTimes.push_back(duration.count());

        if (m_frameTimes.size() > 100)
            m_frameTimes.erase(m_frameTimes.begin());
    }

    double GetAverageFrameTime() const
    {
        if (m_frameTimes.empty())
            return 0.0;

        double sum = 0.0;
        for (auto time : m_frameTimes)
            sum += time;

        return sum / m_frameTimes.size();
    }

private:
    std::chrono::time_point<std::chrono::high_resolution_clock> m_frameStart;
    std::vector<uint64_t> m_frameTimes;
};

2. CPU Profiling with perf (Linux)

# Record performance data
perf record -g ./worldserver

# Analyze results
perf report

# Generate flame graph
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg

3. Visual Studio Performance Profiler

  1. Open project in Visual Studio 2022
  2. Debug → Performance Profiler
  3. Select "CPU Usage" and "Memory Usage"
  4. Start profiling with debugging
  5. Analyze hot paths and memory allocations

Common Pitfalls

1. Forgetting Atomic Operations

// WRONG - Race condition
class BadManager
{
    bool m_enabled = false; // Not atomic!

    void Enable() { m_enabled = true; }
    bool IsEnabled() { return m_enabled; }
};

// CORRECT - Thread-safe
class GoodManager
{
    std::atomic<bool> m_enabled{false};

    void Enable() { m_enabled.store(true); }
    bool IsEnabled() { return m_enabled.load(); }
};

2. Blocking Operations in Update Loop

// WRONG - Blocks entire update
void UpdateBehavior(uint32 timeDelta) override
{
    // This can block for seconds!
    auto result = DatabaseQuery("SELECT * FROM huge_table");
    ProcessResult(result);
}

// CORRECT - Async or cached
void UpdateBehavior(uint32 timeDelta) override
{
    if (m_cacheExpired)
    {
        // Queue async query
        QueryDatabase(callback);
        return;
    }

    ProcessCachedData();
}

3. Memory Leaks with Shared Pointers

// WRONG - Circular reference
class A
{
    std::shared_ptr<B> b;
};

class B
{
    std::shared_ptr<A> a; // Circular!
};

// CORRECT - Use weak_ptr
class A
{
    std::shared_ptr<B> b;
};

class B
{
    std::weak_ptr<A> a; // Breaks cycle
};

Best Practices

1. Follow RAII Principles

class ResourceManager
{
public:
    ResourceManager()
    {
        m_resource = AcquireResource();
    }

    ~ResourceManager()
    {
        if (m_resource)
            ReleaseResource(m_resource);
    }

    // Delete copy operations
    ResourceManager(const ResourceManager&) = delete;
    ResourceManager& operator=(const ResourceManager&) = delete;

    // Allow move operations
    ResourceManager(ResourceManager&& other) noexcept
        : m_resource(std::exchange(other.m_resource, nullptr))
    {
    }

private:
    Resource* m_resource = nullptr;
};

2. Use Modern C++20 Features

// Concepts for template constraints
template<typename T>
concept Updatable = requires(T t, uint32 diff)
{
    { t.Update(diff) } -> std::same_as<void>;
    { t.IsEnabled() } -> std::convertible_to<bool>;
};

template<Updatable T>
class UpdateQueue
{
    // Only accepts types that can be updated
};

// Designated initializers
struct ManagerConfig
{
    uint32 updateInterval = 1000;
    bool enabled = false;
    std::string name;
};

auto config = ManagerConfig{
    .updateInterval = 2000,
    .enabled = true,
    .name = "QuestManager"
};

// Ranges and views
auto activeManagers = m_managers
    | std::views::filter([](auto& mgr) { return mgr->IsEnabled(); })
    | std::views::take(5);

3. Document Intent with Comments

class BehaviorManager
{
protected:
    // Template method pattern - defines the update algorithm
    // Derived classes override UpdateBehavior() to customize
    void Update(uint32 diff)
    {
        // Early exit for disabled managers (atomic check)
        if (!m_enabled.load(std::memory_order_acquire))
            return;

        // Throttling mechanism - reduces CPU by 80-95%
        m_timeSinceLastUpdate += diff;
        if (m_timeSinceLastUpdate < m_updateInterval)
            return;

        // Performance tracking for profiling
        auto start = std::chrono::high_resolution_clock::now();

        // Call derived implementation
        UpdateBehavior(m_timeSinceLastUpdate);

        // Calculate and store performance metrics
        auto end = std::chrono::high_resolution_clock::now();
        m_lastUpdateDuration = std::chrono::duration_cast<std::chrono::microseconds>
                              (end - start).count();

        // Reset throttle timer for next cycle
        m_timeSinceLastUpdate = 0;
    }
};

4. Error Handling Strategy

enum class ErrorAction
{
    IGNORE,      // Log and continue
    RETRY,       // Retry operation
    DISABLE,     // Disable component
    FATAL        // Terminate bot
};

template<typename Func>
void ExecuteWithErrorHandling(Func&& func, ErrorAction onError = ErrorAction::IGNORE)
{
    try
    {
        func();
    }
    catch (const std::exception& e)
    {
        LOG_ERROR("bot.ai", "Exception in bot update: {}", e.what());

        switch (onError)
        {
            case ErrorAction::RETRY:
                // Retry once
                try { func(); }
                catch (...) { /* Give up */ }
                break;

            case ErrorAction::DISABLE:
                Disable();
                break;

            case ErrorAction::FATAL:
                m_ai->DisableBot();
                break;

            default:
                break;
        }
    }
}

Code Examples

Complete Manager Implementation

// FishingManager.h
#pragma once

#include "AI/Strategy/BehaviorManager.h"
#include <atomic>
#include <chrono>

namespace Playerbot
{
    class FishingManager : public BehaviorManager
    {
    public:
        explicit FishingManager(BotAI* ai);
        ~FishingManager() override = default;

        std::string GetName() const override { return "FishingManager"; }

        bool IsFishing() const { return m_isFishing.load(); }
        bool HasFishingPole() const;
        uint32 GetFishCaught() const { return m_fishCaught.load(); }

    protected:
        void UpdateBehavior(uint32 timeDelta) override;

    private:
        void FindFishingSpot();
        void CastLine();
        void CheckBobber();
        void LootFish();

        std::atomic<bool> m_isFishing{false};
        std::atomic<uint32> m_fishCaught{0};
        std::chrono::steady_clock::time_point m_castTime;
        ObjectGuid m_bobberGuid;
    };
}

// FishingManager.cpp
#include "FishingManager.h"
#include "AI/BotAI.h"
#include "GameObject.h"
#include "SpellInfo.h"

namespace Playerbot
{
    FishingManager::FishingManager(BotAI* ai)
        : BehaviorManager(ai, 2000) // Check every 2 seconds
    {
    }

    void FishingManager::UpdateBehavior(uint32 timeDelta)
    {
        if (!m_ai || !m_ai->GetBot())
            return;

        Player* bot = m_ai->GetBot();

        // Can't fish while in combat or dead
        if (bot->IsInCombat() || bot->isDead())
        {
            m_isFishing.store(false);
            return;
        }

        // Check if we have a fishing pole equipped
        if (!HasFishingPole())
            return;

        if (!m_isFishing.load())
        {
            FindFishingSpot();
            if (bot->IsNearWater(5.0f))
            {
                CastLine();
            }
        }
        else
        {
            CheckBobber();
        }
    }

    bool FishingManager::HasFishingPole() const
    {
        Player* bot = m_ai->GetBot();
        Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);

        if (!mainHand)
            return false;

        ItemTemplate const* proto = mainHand->GetTemplate();
        return proto && proto->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE;
    }

    void FishingManager::CastLine()
    {
        Player* bot = m_ai->GetBot();

        // Cast fishing spell (id: 131474 for current expansion)
        if (bot->HasSpell(131474))
        {
            bot->CastSpell(bot, 131474, false);
            m_isFishing.store(true);
            m_castTime = std::chrono::steady_clock::now();

            LOG_DEBUG("bot.ai.fishing", "Bot {} started fishing", bot->GetName());
        }
    }

    void FishingManager::CheckBobber()
    {
        auto now = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - m_castTime);

        // Fishing cast lasts ~20 seconds
        if (elapsed.count() > 20)
        {
            m_isFishing.store(false);
            return;
        }

        Player* bot = m_ai->GetBot();

        // Find fishing bobber game object
        GameObject* bobber = bot->GetGameObjectIfCanInteractWith(m_bobberGuid, GAMEOBJECT_TYPE_FISHINGNODE);
        if (bobber && bobber->IsReady())
        {
            LootFish();
        }
    }

    void FishingManager::LootFish()
    {
        Player* bot = m_ai->GetBot();

        GameObject* bobber = bot->GetGameObjectIfCanInteractWith(m_bobberGuid, GAMEOBJECT_TYPE_FISHINGNODE);
        if (!bobber)
            return;

        bot->SendLoot(bobber->GetGUID(), LOOT_FISHING);
        m_fishCaught.fetch_add(1);
        m_isFishing.store(false);

        LOG_DEBUG("bot.ai.fishing", "Bot {} caught fish! Total: {}",
                  bot->GetName(), m_fishCaught.load());
    }

    void FishingManager::FindFishingSpot()
    {
        // Simple logic - could be enhanced with pathfinding to water
        Player* bot = m_ai->GetBot();

        if (!bot->IsNearWater(10.0f))
        {
            // Move towards nearest water
            // This would integrate with movement system
        }
    }
}

Conclusion

This developer guide provides the foundation for extending and maintaining the PlayerBot system. Key takeaways:

  1. Always use BehaviorManager as base for new managers
  2. Implement strategies for combat and behavior logic
  3. Write comprehensive tests for all new features
  4. Profile performance regularly
  5. Follow modern C++20 best practices
  6. Maintain thread safety with atomic operations
  7. Document your code thoroughly

For additional help, consult the existing code examples in src/modules/Playerbot/ and the integration tests in the Tests directory.


Developer Guide Version 2.0 TrinityCore 11.2 - The War Within Last Updated: October 2024