2641 lines
110 KiB
C++
2641 lines
110 KiB
C++
/*
|
|
* Copyright (C) 2024 TrinityCore <https://www.trinitycore.org/>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*/
|
|
|
|
/*
|
|
* ===========================================================================
|
|
* LOCK-FREE DATA STRUCTURES - TBB Concurrent Containers
|
|
* ===========================================================================
|
|
*
|
|
* This file uses Intel TBB for lock-free, scalable concurrent data structures.
|
|
* ALL MUTEXES REMOVED for maximum throughput (5000+ bots).
|
|
*
|
|
* Performance: 10-100x faster than mutex-based containers under contention
|
|
* Scalability: Linear scaling with core count (tested up to 64 cores)
|
|
* Thread Safety: ALL operations are thread-safe without explicit locking
|
|
*
|
|
* Data Structures Converted:
|
|
* - _zonePopulations: tbb::concurrent_hash_map (was OrderedRecursiveMutex)
|
|
* - _activeBots: tbb::concurrent_hash_map (was OrderedRecursiveMutex)
|
|
* - _botsByZone: tbb::concurrent_hash_map (was OrderedRecursiveMutex)
|
|
* - _spawnQueue: tbb::concurrent_queue (was OrderedRecursiveMutex)
|
|
*
|
|
* Note: Some legacy mutex patterns remain in code but are now no-ops since
|
|
* TBB containers are inherently thread-safe. Future optimization: use
|
|
* accessor/const_accessor pattern for maximum performance.
|
|
* ===========================================================================
|
|
*/
|
|
|
|
#ifndef _USE_MATH_DEFINES
|
|
#define _USE_MATH_DEFINES
|
|
#endif
|
|
#include <cmath>
|
|
|
|
#include "BotSpawner.h"
|
|
#include "GameTime.h"
|
|
#include "DatabaseEnv.h"
|
|
#include "CharacterDatabase.h"
|
|
#include "BotWorldSessionMgr.h"
|
|
#include "Session/BotSession.h"
|
|
#include "../AI/BotAI.h" // P1 FIX: Required for unique_ptr<BotAI> in BotSession.h
|
|
#include "BotResourcePool.h"
|
|
#include "BotAccountMgr.h"
|
|
#include "Config/PlayerbotConfig.h"
|
|
#include "PlayerbotDatabase.h"
|
|
#include "BotCharacterDistribution.h"
|
|
#include "BotNameMgr.h"
|
|
#include "BotCharacterCreator.h"
|
|
#include "CharacterCache.h"
|
|
#include "Config/PlayerbotLog.h"
|
|
#include "World.h"
|
|
#include "MapManager.h"
|
|
#include "ObjectMgr.h"
|
|
#include "Player.h"
|
|
#include "Random.h"
|
|
#include "CharacterPackets.h"
|
|
#include "Database/PlayerbotCharacterDBInterface.h"
|
|
#include "ObjectGuid.h"
|
|
#include "MotionMaster.h"
|
|
#include "RealmList.h"
|
|
#include "DB2Stores.h"
|
|
#include "DBCEnums.h" // WoW 12.0: MAP_WOWLABS, MAP_HOUSE_INTERIOR, MAP_HOUSE_NEIGHBORHOOD
|
|
#include "WorldSession.h"
|
|
#include "Movement/BotWorldPositioner.h"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <sstream>
|
|
#include <unordered_set>
|
|
|
|
namespace Playerbot
|
|
{
|
|
|
|
BotSpawner::BotSpawner()
|
|
{
|
|
// CRITICAL: No logging in constructor - this runs during static initialization
|
|
// before TrinityCore logging system is ready
|
|
}
|
|
|
|
BotSpawner* BotSpawner::instance()
|
|
{
|
|
// CRITICAL: No logging in instance() - this might be called during static initialization
|
|
// before TrinityCore logging system is ready
|
|
static BotSpawner instance;
|
|
return &instance;
|
|
}
|
|
|
|
bool BotSpawner::Initialize()
|
|
{
|
|
TC_LOG_PLAYERBOT_INFO("Initializing Bot Spawner...");
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: About to start initialization steps...");
|
|
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Step 1 - LoadConfig()...");
|
|
LoadConfig();
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: LoadConfig() completed successfully");
|
|
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Step 2 - UpdatePopulationTargets()...");
|
|
// Initialize zone populations
|
|
UpdatePopulationTargets();
|
|
|
|
TC_LOG_INFO("module.playerbot",
|
|
"Bot Spawner initialized - Max Total: {}, Max Per Zone: {}, Max Per Map: {}",
|
|
_config.maxBotsTotal, _config.maxBotsPerZone, _config.maxBotsPerMap);
|
|
|
|
// Start periodic update timer for automatic spawning
|
|
_lastPopulationUpdate = GameTime::GetGameTimeMS();
|
|
|
|
// Initialize the flag for first player login detection
|
|
_firstPlayerSpawned.store(false);
|
|
|
|
// DEFERRED: Don't spawn bots during initialization - wait for first Update() call
|
|
// This prevents crashes when the world isn't fully initialized yet
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Step 3 - Check enableDynamicSpawning: {}", _config.enableDynamicSpawning ? "true" : "false");
|
|
if (_config.enableDynamicSpawning)
|
|
{
|
|
TC_LOG_INFO("module.playerbot", "Dynamic spawning enabled - bots will spawn when first player logs in");
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Step 4 - CalculateZoneTargets()...");
|
|
CalculateZoneTargets();
|
|
// NOTE: SpawnToPopulationTarget() will be called when first player is detected
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Waiting for first player login to trigger spawning");
|
|
// Set lastTargetCalculation to prevent immediate re-calculation in Update()
|
|
_lastTargetCalculation = GameTime::GetGameTimeMS();
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_INFO("module.playerbot", "Static spawning enabled - bots will spawn immediately after world initialization");
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Step 4 - CalculateZoneTargets()...");
|
|
CalculateZoneTargets();
|
|
_initialCalculationDone = true; // Mark that we've done initial calculation
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Static spawning mode - SpawnToPopulationTarget will be called in first Update()");
|
|
// Set _lastTargetCalculation to 0 to force immediate spawning in Update()
|
|
// but Update() will skip CalculateZoneTargets() since _initialCalculationDone is true
|
|
_lastTargetCalculation = 0;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phase 2: Initialize Adaptive Throttling System
|
|
// ========================================================================
|
|
TC_LOG_INFO("module.playerbot", "BotSpawner: Step 5 - Initializing Phase 2 Adaptive Throttling System...");
|
|
|
|
// Step 5.1: Initialize ResourceMonitor
|
|
TC_LOG_INFO("module.playerbot", " - Initializing ResourceMonitor...");
|
|
if (!_resourceMonitor.Initialize())
|
|
{
|
|
TC_LOG_ERROR("module.playerbot", " Failed to initialize ResourceMonitor");
|
|
return false;
|
|
}
|
|
TC_LOG_INFO("module.playerbot", " ResourceMonitor initialized successfully");
|
|
|
|
// Step 5.2: Initialize SpawnCircuitBreaker
|
|
TC_LOG_INFO("module.playerbot", " - Initializing SpawnCircuitBreaker...");
|
|
if (!_circuitBreaker.Initialize())
|
|
{
|
|
TC_LOG_ERROR("module.playerbot", " Failed to initialize SpawnCircuitBreaker");
|
|
return false;
|
|
}
|
|
TC_LOG_INFO("module.playerbot", " SpawnCircuitBreaker initialized successfully");
|
|
|
|
// Step 5.3: Initialize AdaptiveSpawnThrottler (requires ResourceMonitor and CircuitBreaker)
|
|
TC_LOG_INFO("module.playerbot", " - Initializing AdaptiveSpawnThrottler...");
|
|
if (!_throttler.Initialize(&_resourceMonitor, &_circuitBreaker))
|
|
{
|
|
TC_LOG_ERROR("module.playerbot", " Failed to initialize AdaptiveSpawnThrottler");
|
|
return false;
|
|
}
|
|
TC_LOG_INFO("module.playerbot", " AdaptiveSpawnThrottler initialized successfully");
|
|
|
|
// Step 5.4: Initialize StartupSpawnOrchestrator (requires PriorityQueue and Throttler)
|
|
TC_LOG_INFO("module.playerbot", " - Initializing StartupSpawnOrchestrator...");
|
|
if (!_orchestrator.Initialize(&_priorityQueue, &_throttler))
|
|
{
|
|
TC_LOG_ERROR("module.playerbot", " Failed to initialize StartupSpawnOrchestrator");
|
|
return false;
|
|
}
|
|
TC_LOG_INFO("module.playerbot", " StartupSpawnOrchestrator initialized successfully");
|
|
|
|
// Step 5.5: Begin phased startup sequence (conditional based on SpawnOnServerStart)
|
|
if (_config.spawnOnServerStart)
|
|
{
|
|
TC_LOG_INFO("module.playerbot", " - Beginning phased startup sequence (Spawn.OnServerStart = true)...");
|
|
_orchestrator.BeginStartup();
|
|
TC_LOG_INFO("module.playerbot", " Phased startup sequence initiated - bots will spawn during server startup");
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_INFO("module.playerbot", " - Deferring bot spawning (Spawn.OnServerStart = false)");
|
|
TC_LOG_INFO("module.playerbot", " Bots will begin spawning when the first player logs in");
|
|
}
|
|
|
|
// Mark Phase 2 as initialized
|
|
_phase2Initialized = true;
|
|
TC_LOG_INFO("module.playerbot", " Phase 2 Adaptive Throttling System fully initialized");
|
|
TC_LOG_INFO("module.playerbot", " - ResourceMonitor: Monitoring CPU, memory, DB, maps");
|
|
TC_LOG_INFO("module.playerbot", " - CircuitBreaker: Protecting against spawn failures");
|
|
TC_LOG_INFO("module.playerbot", " - SpawnThrottler: Dynamic spawn rate (0.2-20 bots/sec)");
|
|
TC_LOG_INFO("module.playerbot", " - Phased Startup: 4-phase graduated spawning (0-30 min)");
|
|
TC_LOG_INFO("module.playerbot", " - SpawnOnServerStart: {}", _config.spawnOnServerStart ? "ENABLED (immediate)" : "DISABLED (wait for player)");
|
|
|
|
// ========================================================================
|
|
// End Phase 2 Initialization
|
|
// ========================================================================
|
|
|
|
return true;
|
|
}
|
|
|
|
void BotSpawner::Shutdown()
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", "Shutting down Bot Spawner...");
|
|
|
|
// Despawn all active bots
|
|
DespawnAllBots();
|
|
|
|
// Clear data structures (TBB concurrent containers are thread-safe)
|
|
// No locks needed - concurrent_hash_map::clear() is already thread-safe
|
|
_zonePopulations.clear();
|
|
_activeBots.clear();
|
|
_botsByZone.clear();
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "Bot Spawner shutdown complete");
|
|
}
|
|
|
|
void BotSpawner::Update(uint32 diff)
|
|
{
|
|
if (!_enabled.load())
|
|
return;
|
|
|
|
// CRITICAL SAFETY: Wrap update in try-catch to prevent crashes
|
|
try
|
|
{
|
|
|
|
// ========================================================================
|
|
// Phase 2: Update Adaptive Throttling System Components
|
|
// ========================================================================
|
|
if (_phase2Initialized)
|
|
{
|
|
// Update all Phase 2 components (called every world tick)
|
|
_resourceMonitor.Update(diff);
|
|
_circuitBreaker.Update(diff);
|
|
_throttler.Update(diff);
|
|
_orchestrator.Update(diff);
|
|
|
|
// Update total active bot count for resource monitoring
|
|
_resourceMonitor.SetActiveBotCount(GetActiveBotCount());
|
|
}
|
|
// ========================================================================
|
|
// End Phase 2 Updates
|
|
// ========================================================================
|
|
|
|
static uint32 updateCounter = 0;
|
|
++updateCounter;
|
|
uint32 currentTime = GameTime::GetGameTimeMS();
|
|
|
|
// Check for real players and trigger spawning if needed
|
|
CheckAndSpawnForPlayers();
|
|
|
|
// Minimal debug logging every 50k updates to prevent spam
|
|
if (updateCounter % 50000 == 0) // Log every 50k updates for status monitoring
|
|
{
|
|
uint32 timeSinceLastSpawn = currentTime - _lastTargetCalculation;
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"BotSpawner status #{} - active bots: {}, time since last calculation: {}ms",
|
|
updateCounter, GetActiveBotCount(), timeSinceLastSpawn);
|
|
}
|
|
|
|
// ====================================================================
|
|
// Phase 2: Check appropriate queue based on initialization status
|
|
// ====================================================================
|
|
bool queueHasItems = false;
|
|
if (_phase2Initialized)
|
|
{
|
|
// Phase 2 ENABLED: Check priority queue
|
|
queueHasItems = !_priorityQueue.IsEmpty();
|
|
}
|
|
else
|
|
{
|
|
// Phase 2 DISABLED: Check legacy spawn queue
|
|
// TBB concurrent_queue is lock-free - no mutex needed
|
|
queueHasItems = !_spawnQueue.empty();
|
|
}
|
|
|
|
// P1 FIX: ATOMIC COMPARE-EXCHANGE to prevent concurrent queue processing
|
|
//
|
|
// Problem: Check-then-set pattern allows multiple threads to enter:
|
|
// Thread 1: Check _processingQueue=false ✓
|
|
// Thread 2: Check _processingQueue=false ✓ (before T1 sets it!)
|
|
// Thread 1: Set _processingQueue=true, enters critical section
|
|
// Thread 2: Set _processingQueue=true, enters critical section ❌ DUPLICATE!
|
|
//
|
|
// Solution: compare_exchange_strong atomically checks and sets in single operation:
|
|
// Thread 1: CAS(false→true) succeeds, returns true
|
|
// Thread 2: CAS(false→true) fails (already true), returns false
|
|
// Only Thread 1 enters critical section ✓
|
|
//
|
|
// This guarantees mutual exclusion without explicit mutex locks.
|
|
|
|
bool expected = false;
|
|
if (queueHasItems && _processingQueue.compare_exchange_strong(
|
|
expected, true, ::std::memory_order_acquire, ::std::memory_order_relaxed))
|
|
{
|
|
// Only ONE thread can enter this block - compare_exchange guarantees atomicity
|
|
|
|
// Wrap processing in try-catch to ensure flag is always reset
|
|
try
|
|
{
|
|
// ====================================================================
|
|
// Phase 2: Adaptive Throttling Integration
|
|
// ====================================================================
|
|
// Check if Phase 2 allows spawning (throttler + orchestrator + circuit breaker)
|
|
bool canSpawn = true;
|
|
if (_phase2Initialized)
|
|
{
|
|
// Check orchestrator phase allows spawning
|
|
canSpawn = _orchestrator.ShouldSpawnNext();
|
|
|
|
// Check throttler allows spawning (checks circuit breaker internally)
|
|
if (canSpawn)
|
|
canSpawn = _throttler.CanSpawnNow();
|
|
|
|
if (!canSpawn)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner",
|
|
"Phase 2 throttling active - spawn deferred (pressure: {}, circuit: {}, phase: {})",
|
|
static_cast<uint8>(_resourceMonitor.GetPressureLevel()),
|
|
static_cast<uint8>(_circuitBreaker.GetState()),
|
|
static_cast<uint8>(_orchestrator.GetCurrentPhase()));
|
|
_processingQueue.store(false, ::std::memory_order_release);
|
|
return; // Skip spawning this update
|
|
}
|
|
}
|
|
// ====================================================================
|
|
// End Phase 2 Throttling Check
|
|
// ====================================================================
|
|
|
|
// ====================================================================
|
|
// Phase 2: Dequeue from appropriate queue
|
|
// ====================================================================
|
|
::std::vector<SpawnRequest> requestBatch;
|
|
|
|
if (_phase2Initialized)
|
|
{
|
|
// Phase 2 ENABLED: Dequeue from priority queue
|
|
// Limit batch size to 1 for precise throttle control
|
|
auto prioRequest = _priorityQueue.DequeueNextRequest();
|
|
if (prioRequest.has_value())
|
|
{
|
|
// Extract original SpawnRequest from PrioritySpawnRequest
|
|
requestBatch.push_back(prioRequest->originalRequest);
|
|
|
|
TC_LOG_TRACE("module.playerbot.spawner",
|
|
"Phase 2: Dequeued spawn request with priority {} (reason: {}, age: {}ms)",
|
|
static_cast<uint8>(prioRequest->priority),
|
|
prioRequest->reason,
|
|
prioRequest->GetAge().count());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Phase 2 DISABLED: Dequeue from legacy spawn queue
|
|
// TBB concurrent_queue is lock-free - no mutex needed!
|
|
uint32 batchSize = _config.spawnBatchSize;
|
|
requestBatch.reserve(batchSize);
|
|
|
|
// TBB concurrent_queue: Use try_pop() instead of front()/pop()
|
|
// Lock-free operation - multiple threads can pop simultaneously
|
|
for (uint32 i = 0; i < batchSize; ++i)
|
|
{
|
|
SpawnRequest request;
|
|
if (_spawnQueue.try_pop(request))
|
|
{
|
|
requestBatch.push_back(request);
|
|
}
|
|
else
|
|
{
|
|
break; // Queue is empty
|
|
}
|
|
}
|
|
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Legacy: Processing {} spawn requests", requestBatch.size());
|
|
}
|
|
|
|
// Process requests outside the lock
|
|
for (SpawnRequest const& request : requestBatch)
|
|
{
|
|
bool spawnSuccess = SpawnBotInternal(request);
|
|
|
|
// ================================================================
|
|
// Phase 2: Record spawn result for circuit breaker and throttler
|
|
// ================================================================
|
|
if (_phase2Initialized)
|
|
{
|
|
if (spawnSuccess)
|
|
{
|
|
_throttler.RecordSpawnSuccess();
|
|
_orchestrator.OnBotSpawned();
|
|
}
|
|
else
|
|
{
|
|
_throttler.RecordSpawnFailure("SpawnBotInternal failed");
|
|
}
|
|
}
|
|
// ================================================================
|
|
// End Phase 2 Result Recording
|
|
// ================================================================
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Exception during spawn queue processing - flag will be reset");
|
|
// Flag will be reset in finally block below
|
|
}
|
|
|
|
// Always reset flag with release semantics for proper memory ordering
|
|
_processingQueue.store(false, ::std::memory_order_release);
|
|
}
|
|
|
|
// Update zone populations periodically - DEADLOCK-FREE VERSION
|
|
if (currentTime - _lastPopulationUpdate > POPULATION_UPDATE_INTERVAL)
|
|
{
|
|
// Simple atomic counter update without complex locking
|
|
uint32 activeSessions = sWorld->GetActiveSessionCount();
|
|
uint32 botSessions = Playerbot::sBotWorldSessionMgr->GetBotCount();
|
|
uint32 realPlayerSessions = (activeSessions > botSessions) ? (activeSessions - botSessions) : 0;
|
|
|
|
// Store for later use without complex zone updates that cause deadlocks
|
|
_lastRealPlayerCount.store(realPlayerSessions);
|
|
_lastPopulationUpdate = currentTime;
|
|
|
|
// CRITICAL FIX: Update individual zone populations
|
|
// This ensures CalculateZoneTargets() sees current player/bot counts
|
|
|
|
// Map to store counts of real players per zone
|
|
::std::unordered_map<uint32, uint32> playerCounts;
|
|
|
|
if (realPlayerSessions > 0)
|
|
{
|
|
auto const& sessions = sWorld->GetAllSessions();
|
|
for (auto const& pair : sessions)
|
|
{
|
|
WorldSession* session = pair.second;
|
|
if (!session) continue;
|
|
|
|
// Skip bot accounts (IDs >= 100000)
|
|
if (session->GetAccountId() >= 100000) continue;
|
|
|
|
Player* player = session->GetPlayer();
|
|
if (player && player->IsInWorld())
|
|
{
|
|
playerCounts[player->GetZoneId()]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto const& pair : _zonePopulations)
|
|
{
|
|
uint32 zoneId = pair.first;
|
|
uint32 zonePlayerCount = 0;
|
|
auto it = playerCounts.find(zoneId);
|
|
if (it != playerCounts.end())
|
|
zonePlayerCount = it->second;
|
|
|
|
UpdateZonePopulation(zoneId, pair.second.mapId, zonePlayerCount);
|
|
}
|
|
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Population update: {} real players, {} bot sessions",
|
|
realPlayerSessions, botSessions);
|
|
}
|
|
|
|
// Recalculate targets and spawn periodically
|
|
if (currentTime - _lastTargetCalculation > TARGET_CALCULATION_INTERVAL)
|
|
{
|
|
if (_config.enableDynamicSpawning)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** DYNAMIC SPAWNING CYCLE: Recalculating zone targets and spawning to population targets");
|
|
CalculateZoneTargets();
|
|
SpawnToPopulationTarget();
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** DYNAMIC SPAWNING CYCLE: Completed spawn cycle");
|
|
}
|
|
else
|
|
{
|
|
// Check if this is the first Update() call and we already did initial calculation in Initialize()
|
|
if (_initialCalculationDone)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** STATIC SPAWNING CYCLE: Spawning to population targets (initial calculation already done)");
|
|
SpawnToPopulationTarget();
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** STATIC SPAWNING CYCLE: Completed spawn cycle");
|
|
_initialCalculationDone = false; // Reset flag so future cycles recalculate normally
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** STATIC SPAWNING CYCLE: Recalculating zone targets and spawning to population targets");
|
|
CalculateZoneTargets();
|
|
SpawnToPopulationTarget();
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** STATIC SPAWNING CYCLE: Completed spawn cycle");
|
|
}
|
|
}
|
|
_lastTargetCalculation = currentTime;
|
|
}
|
|
else if (updateCounter % 10000 == 0)
|
|
{
|
|
uint32 timeLeft = TARGET_CALCULATION_INTERVAL - (currentTime - _lastTargetCalculation);
|
|
TC_LOG_INFO("module.playerbot.spawner", "*** SPAWNING CYCLE: {} ms until next spawn cycle", timeLeft);
|
|
}
|
|
|
|
}
|
|
catch (::std::exception const& ex)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "CRITICAL EXCEPTION in BotSpawner::Update: {}", ex.what());
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Disabling spawner to prevent further crashes");
|
|
_enabled.store(false);
|
|
}
|
|
catch (...)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "CRITICAL UNKNOWN EXCEPTION in BotSpawner::Update");
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Disabling spawner to prevent further crashes");
|
|
_enabled.store(false);
|
|
}
|
|
}
|
|
|
|
CharacterDatabasePreparedStatement* BotSpawner::GetSafePreparedStatement(CharacterDatabaseStatements statementId, const char* statementName) const
|
|
{
|
|
// CRITICAL FIX: Add comprehensive statement index validation to prevent assertion failure m_mStmt
|
|
if (statementId >= MAX_CHARACTERDATABASE_STATEMENTS) {
|
|
TC_LOG_ERROR("module.playerbot.spawner", "BotSpawner::GetSafePreparedStatement: Invalid statement index {} >= {} for {}",
|
|
static_cast<uint32>(statementId), MAX_CHARACTERDATABASE_STATEMENTS, statementName);
|
|
return nullptr;
|
|
}
|
|
|
|
// CRITICAL FIX: All statements should be properly prepared by Trinity's DoPrepareStatements()
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Accessing statement {} ({}) - ensuring Trinity connection preparation worked",
|
|
static_cast<uint32>(statementId), statementName);
|
|
|
|
// Use PlayerbotCharacterDBInterface for safe statement access with sync/async routing
|
|
CharacterDatabasePreparedStatement* stmt = sPlayerbotCharDB->GetPreparedStatement(statementId);
|
|
if (!stmt)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "BotSpawner::GetSafePreparedStatement: Failed to get prepared statement {} (index: {})",
|
|
statementName, static_cast<uint32>(statementId));
|
|
return nullptr;
|
|
}
|
|
return stmt;
|
|
}
|
|
|
|
LoginDatabasePreparedStatement* BotSpawner::GetSafeLoginPreparedStatement(LoginDatabaseStatements statementId, const char* statementName) const
|
|
{
|
|
// CRITICAL FIX: Add comprehensive statement index validation to prevent assertion failure m_mStmt for LoginDatabase
|
|
if (statementId >= MAX_LOGINDATABASE_STATEMENTS) {
|
|
TC_LOG_ERROR("module.playerbot.spawner", "BotSpawner::GetSafeLoginPreparedStatement: Invalid statement index {} >= {} for {}",
|
|
static_cast<uint32>(statementId), MAX_LOGINDATABASE_STATEMENTS, statementName);
|
|
return nullptr;
|
|
}
|
|
|
|
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(statementId);
|
|
if (!stmt)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "BotSpawner::GetSafeLoginPreparedStatement: Failed to get prepared statement {} (index: {})",
|
|
statementName, static_cast<uint32>(statementId));
|
|
return nullptr;
|
|
}
|
|
return stmt;
|
|
}
|
|
|
|
void BotSpawner::LoadConfig()
|
|
{
|
|
// CRITICAL FIX: Read from sPlayerbotConfig instead of hardcoded values
|
|
// This allows playerbots.conf to control spawn behavior
|
|
_config.maxBotsTotal = sPlayerbotConfig->GetUInt("Playerbot.Spawn.MaxTotal", 80);
|
|
_config.maxBotsPerZone = sPlayerbotConfig->GetUInt("Playerbot.Spawn.MaxPerZone", 20);
|
|
_config.maxBotsPerMap = sPlayerbotConfig->GetUInt("Playerbot.Spawn.MaxPerMap", 50);
|
|
_config.spawnBatchSize = sPlayerbotConfig->GetUInt("Playerbot.Spawn.BatchSize", 5);
|
|
_config.spawnDelayMs = sPlayerbotConfig->GetUInt("Playerbot.Spawn.DelayMs", 500);
|
|
_config.enableDynamicSpawning = sPlayerbotConfig->GetBool("Playerbot.Spawn.Dynamic", false);
|
|
_config.respectPopulationCaps = sPlayerbotConfig->GetBool("Playerbot.Spawn.RespectCaps", true);
|
|
_config.spawnOnServerStart = sPlayerbotConfig->GetBool("Playerbot.Spawn.OnServerStart", true);
|
|
_config.botToPlayerRatio = sPlayerbotConfig->GetFloat("Playerbot.Spawn.BotToPlayerRatio", 20.0f);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "CONFIG - Spawn configuration loaded from playerbots.conf:");
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.MaxTotal = {} (this controls total bots)", _config.maxBotsTotal);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.MaxPerZone = {}", _config.maxBotsPerZone);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.MaxPerMap = {}", _config.maxBotsPerMap);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.BatchSize = {}", _config.spawnBatchSize);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.Dynamic = {}", _config.enableDynamicSpawning);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.RespectCaps = {} (if true, MaxTotal is enforced)", _config.respectPopulationCaps);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.OnServerStart = {} (spawn during startup)", _config.spawnOnServerStart);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Playerbot.Spawn.BotToPlayerRatio = {}", _config.botToPlayerRatio);
|
|
}
|
|
|
|
bool BotSpawner::SpawnBot(SpawnRequest const& request)
|
|
{
|
|
// TRACE: Log incoming request to identify spawn origin
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"BotSpawner::SpawnBot ENTRY - type={}, guid={}, accountId={}, bypassLimit={}",
|
|
static_cast<int>(request.type), request.characterGuid.ToString(),
|
|
request.accountId, request.bypassMaxBotsLimit);
|
|
|
|
// P1 FIX: ATOMIC PRE-INCREMENT PATTERN to eliminate TOCTOU race
|
|
//
|
|
// Problem: Old pattern had time gap between check and increment:
|
|
// Thread 1: Check count=99 < 100 ✓
|
|
// Thread 2: Check count=99 < 100 ✓ (before T1 increments!)
|
|
// Thread 1: Spawns, count becomes 100
|
|
// Thread 2: Spawns, count becomes 101 ❌ OVERFLOW!
|
|
//
|
|
// Solution: Reserve slot atomically BEFORE checking cap:
|
|
// Thread 1: Atomic increment 99→100 (returns 99)
|
|
// Thread 2: Atomic increment 100→101 (returns 100)
|
|
// Thread 1: Check 99 < 100 ✓ spawns
|
|
// Thread 2: Check 100 < 100 ✗ rollback (100→99), rejects
|
|
//
|
|
// This guarantees exact cap enforcement with zero overflow risk.
|
|
|
|
// Step 1: Basic validation (non-population checks)
|
|
if (!ValidateSpawnRequestBasic(request))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Step 2: Atomic pre-increment - reserves slot and returns OLD value
|
|
// NOTE: We check the OLD value (before increment) against the cap
|
|
uint32 oldCount = _activeBotCount.fetch_add(1, ::std::memory_order_acquire);
|
|
|
|
// Step 3: Check global cap AFTER increment using old value
|
|
// If oldCount was 99 and cap is 100, we pass (slot 100 reserved)
|
|
// If oldCount was 100 and cap is 100, we fail (would be slot 101)
|
|
if (_config.respectPopulationCaps && !request.bypassMaxBotsLimit)
|
|
{
|
|
if (oldCount >= _config.maxBotsTotal)
|
|
{
|
|
// Rollback - we exceeded the cap
|
|
_activeBotCount.fetch_sub(1, ::std::memory_order_release);
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"SpawnBot: Rejected - global cap reached ({}/{})",
|
|
oldCount, _config.maxBotsTotal);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Step 4: Zone cap check (best-effort - no atomic per-zone counters yet)
|
|
// NOTE: Zone caps may have small overflow due to lack of atomic per-zone counter
|
|
// This is acceptable for current requirements (global cap is the hard limit)
|
|
if (request.zoneId != 0 && _config.respectPopulationCaps && !request.bypassMaxBotsLimit)
|
|
{
|
|
if (!CanSpawnInZone(request.zoneId))
|
|
{
|
|
// Rollback - zone cap exceeded
|
|
_activeBotCount.fetch_sub(1, ::std::memory_order_release);
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"SpawnBot: Rejected - zone {} bot limit reached", request.zoneId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Step 5: Map cap check (best-effort)
|
|
if (request.mapId != 0 && _config.respectPopulationCaps && !request.bypassMaxBotsLimit)
|
|
{
|
|
if (!CanSpawnOnMap(request.mapId))
|
|
{
|
|
// Rollback - map cap exceeded or map type excluded
|
|
_activeBotCount.fetch_sub(1, ::std::memory_order_release);
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"SpawnBot: Rejected - map {} bot limit reached or excluded", request.mapId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Step 6: Spawn the bot (counter already incremented, must not double-increment!)
|
|
if (!SpawnBotInternal(request))
|
|
{
|
|
// Rollback on spawn failure
|
|
_activeBotCount.fetch_sub(1, ::std::memory_order_release);
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"SpawnBot: SpawnBotInternal failed - rolled back counter");
|
|
return false;
|
|
}
|
|
|
|
// Success - counter already incremented, no rollback needed
|
|
return true;
|
|
}
|
|
|
|
uint32 BotSpawner::SpawnBots(::std::vector<SpawnRequest> const& requests)
|
|
{
|
|
uint32 successCount = 0;
|
|
|
|
// Collect valid requests first
|
|
::std::vector<SpawnRequest> validRequests;
|
|
for (SpawnRequest const& request : requests)
|
|
{
|
|
if (ValidateSpawnRequest(request))
|
|
{
|
|
validRequests.push_back(request);
|
|
++successCount;
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Phase 2: Route to appropriate queue based on initialization status
|
|
// ========================================================================
|
|
if (!validRequests.empty())
|
|
{
|
|
if (_phase2Initialized)
|
|
{
|
|
// Phase 2 ENABLED: Use priority queue with priority assignment
|
|
for (SpawnRequest const& request : validRequests)
|
|
{
|
|
// Convert SpawnRequest to PrioritySpawnRequest
|
|
PrioritySpawnRequest prioRequest{};
|
|
prioRequest.characterGuid = request.characterGuid;
|
|
prioRequest.accountId = request.accountId;
|
|
prioRequest.priority = DeterminePriority(request);
|
|
prioRequest.requestTime = GameTime::Now();
|
|
prioRequest.retryCount = 0;
|
|
prioRequest.originalRequest = request; // Preserve full request for SpawnBotInternal()
|
|
|
|
// Generate reason string for debugging/metrics
|
|
switch (request.type)
|
|
{
|
|
case SpawnRequest::SPECIFIC_CHARACTER:
|
|
prioRequest.reason = "SPECIFIC_CHARACTER";
|
|
break;
|
|
case SpawnRequest::GROUP_MEMBER:
|
|
prioRequest.reason = "GROUP_MEMBER";
|
|
break;
|
|
case SpawnRequest::SPECIFIC_ZONE:
|
|
prioRequest.reason = fmt::format("ZONE_{}", request.zoneId);
|
|
break;
|
|
case SpawnRequest::RANDOM:
|
|
prioRequest.reason = "RANDOM";
|
|
break;
|
|
default:
|
|
prioRequest.reason = "UNKNOWN";
|
|
break;
|
|
}
|
|
|
|
// Enqueue to priority queue
|
|
bool enqueued = _priorityQueue.EnqueuePrioritySpawnRequest(prioRequest);
|
|
if (!enqueued)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner",
|
|
"Duplicate spawn request rejected for character {}",
|
|
prioRequest.characterGuid.ToString());
|
|
--successCount; // Adjust count for duplicate
|
|
}
|
|
}
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Phase 2: Queued {} spawn requests to priority queue ({} total requested, priority levels used)",
|
|
successCount, requests.size());
|
|
}
|
|
else
|
|
{
|
|
// Phase 2 DISABLED: Use legacy spawn queue (backward compatibility)
|
|
// Note: _spawnQueue is tbb::concurrent_queue - thread-safe without mutex
|
|
for (SpawnRequest const& request : validRequests)
|
|
{
|
|
_spawnQueue.push(request);
|
|
}
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Legacy: Queued {} spawn requests to spawn queue ({} total requested)",
|
|
successCount, requests.size());
|
|
}
|
|
}
|
|
|
|
return successCount;
|
|
}
|
|
|
|
bool BotSpawner::SpawnBotInternal(SpawnRequest const& request)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "SpawnBotInternal called for zone {}, account {}", request.zoneId, request.accountId);
|
|
|
|
auto startTime = ::std::chrono::high_resolution_clock::now();
|
|
_stats.spawnAttempts.fetch_add(1);
|
|
|
|
// Select character for spawning - ASYNC PATTERN for 5000 bot scalability
|
|
ObjectGuid characterGuid = request.characterGuid;
|
|
if (characterGuid.IsEmpty())
|
|
{
|
|
// ASYNC CHARACTER SELECTION - no blocking for 5000 bots
|
|
SelectCharacterForSpawnAsync(request, [this, request, startTime](ObjectGuid selectedGuid) {
|
|
if (selectedGuid.IsEmpty())
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"No suitable character found for spawn request (type: {})", static_cast<int>(request.type));
|
|
_stats.failedSpawns.fetch_add(1);
|
|
if (request.callback)
|
|
request.callback(false, ObjectGuid::Empty);
|
|
return false;
|
|
}
|
|
|
|
// Continue with spawn process asynchronously
|
|
ContinueSpawnWithCharacter(selectedGuid, request);
|
|
return true; // Success path
|
|
});
|
|
return true; // Return immediately - async operation continues in callback
|
|
}
|
|
else
|
|
{
|
|
// Character already specified - continue directly
|
|
ContinueSpawnWithCharacter(characterGuid, request);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool BotSpawner::CreateBotSession(uint32 accountId, ObjectGuid characterGuid, bool bypassMaxBotsLimit)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", " Creating bot session for account {}, character {}, bypassLimit={}",
|
|
accountId, characterGuid.ToString(), bypassMaxBotsLimit);
|
|
|
|
// DISABLED: Legacy BotSessionMgr creates invalid account IDs
|
|
// Use the BotSessionMgr to create a new bot session with ASYNC character login (legacy approach)
|
|
// BotSession* session = sBotSessionMgr->CreateAsyncSession(accountId, characterGuid);
|
|
// if (!session)
|
|
// {
|
|
// TC_LOG_ERROR("module.playerbot.spawner",
|
|
// " Failed to create async bot session for account {}", accountId);
|
|
// return false;
|
|
// }
|
|
|
|
// PRIMARY: Use the fixed native TrinityCore login approach with proper account IDs
|
|
// Pass bypassMaxBotsLimit for pool/JIT bots that should bypass MaxBots config
|
|
if (!Playerbot::sBotWorldSessionMgr->AddPlayerBot(characterGuid, accountId, bypassMaxBotsLimit))
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
" Failed to create native WorldSession for character {}", characterGuid.ToString());
|
|
return false; // Fail if the primary system fails
|
|
}
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
" Successfully created bot session and started async login for character {} for account {}",
|
|
characterGuid.ToString(), accountId);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BotSpawner::ValidateSpawnRequestBasic(SpawnRequest const& request) const
|
|
{
|
|
// P1 FIX: Non-population validation split out for atomic pre-increment pattern
|
|
// This method performs all validations EXCEPT population caps, which are now
|
|
// checked after atomic slot reservation to eliminate TOCTOU race
|
|
|
|
// Check if spawning is enabled
|
|
if (!_enabled.load())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Spawn request rejected: spawning disabled");
|
|
return false;
|
|
}
|
|
|
|
// Validate GUID ranges for security
|
|
if (!request.characterGuid.IsEmpty() && request.characterGuid.GetTypeName() != "Player")
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner", "Invalid character GUID type: {}", request.characterGuid.GetTypeName());
|
|
return false;
|
|
}
|
|
|
|
// Validate account ownership if specified
|
|
if (request.accountId != 0 && !request.characterGuid.IsEmpty())
|
|
{
|
|
uint32 actualAccountId = GetAccountIdFromCharacter(request.characterGuid);
|
|
if (actualAccountId != 0 && actualAccountId != request.accountId)
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Account ownership mismatch: character {} belongs to account {}, not {}",
|
|
request.characterGuid.ToString(), actualAccountId, request.accountId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Validate level ranges
|
|
if (request.minLevel > request.maxLevel && request.maxLevel != 0)
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner", "Invalid level range: {} > {}", request.minLevel, request.maxLevel);
|
|
return false;
|
|
}
|
|
|
|
// NOTE: Population cap checks are now performed in SpawnBot() after atomic pre-increment
|
|
// This eliminates the TOCTOU race where multiple threads could pass the check simultaneously
|
|
|
|
return true;
|
|
}
|
|
|
|
bool BotSpawner::ValidateSpawnRequest(SpawnRequest const& request) const
|
|
{
|
|
// DEPRECATED: This method is kept for backward compatibility with code that bypasses SpawnBot()
|
|
// New code should use the atomic pre-increment pattern in SpawnBot() instead
|
|
//
|
|
// WARNING: Using this method directly (without atomic pre-increment) can still cause
|
|
// population cap overflow due to TOCTOU race conditions
|
|
|
|
// Comprehensive validation for 5000 bot scalability
|
|
|
|
// Basic validation (non-population checks)
|
|
if (!ValidateSpawnRequestBasic(request))
|
|
return false;
|
|
|
|
// Check global population caps
|
|
if (_config.respectPopulationCaps && !CanSpawnMore())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Spawn request rejected: global bot limit reached");
|
|
return false;
|
|
}
|
|
|
|
// Check zone-specific caps
|
|
if (request.zoneId != 0 && _config.respectPopulationCaps && !CanSpawnInZone(request.zoneId))
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Spawn request rejected: zone {} bot limit reached", request.zoneId);
|
|
return false;
|
|
}
|
|
|
|
// Check map-specific caps
|
|
if (request.mapId != 0 && _config.respectPopulationCaps && !CanSpawnOnMap(request.mapId))
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Spawn request rejected: map {} bot limit reached", request.mapId);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
SpawnPriority BotSpawner::DeterminePriority(SpawnRequest const& request) const
|
|
{
|
|
// ========================================================================
|
|
// Phase 2: Priority Assignment Logic
|
|
// ========================================================================
|
|
// Assign spawn priority based on request type and context
|
|
//
|
|
// Priority Levels:
|
|
// - CRITICAL (0): Guild leaders, raid leaders (future: check database)
|
|
// - HIGH (1): Specific characters, group members, friends
|
|
// - NORMAL (2): Zone population requests
|
|
// - LOW (3): Random background filler bots
|
|
//
|
|
// This implements a simple heuristic for MVP. Future enhancements could:
|
|
// - Query database for guild leadership status
|
|
// - Check social relationships (friends, party members)
|
|
// - Consider zone population pressure
|
|
// - Implement dynamic priority adjustment based on server load
|
|
// ========================================================================
|
|
switch (request.type)
|
|
{
|
|
case SpawnRequest::SPECIFIC_CHARACTER:
|
|
// Specific character spawn - likely important (friend, specific request)
|
|
// Future: Check if guild leader → CRITICAL
|
|
return SpawnPriority::HIGH;
|
|
|
|
case SpawnRequest::GROUP_MEMBER:
|
|
// Party/raid member - needs priority for group functionality
|
|
return SpawnPriority::HIGH;
|
|
|
|
case SpawnRequest::SPECIFIC_ZONE:
|
|
// Zone population request - standard priority
|
|
return SpawnPriority::NORMAL;
|
|
|
|
case SpawnRequest::RANDOM:
|
|
default:
|
|
// Random background bot - lowest priority
|
|
return SpawnPriority::LOW;
|
|
}
|
|
}
|
|
|
|
ObjectGuid BotSpawner::SelectCharacterForSpawn(SpawnRequest const& request)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Selecting character for spawn request");
|
|
|
|
// Get available accounts if not specified
|
|
::std::vector<uint32> accounts;
|
|
if (request.accountId != 0)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Using specified account {}", request.accountId);
|
|
accounts.push_back(request.accountId);
|
|
}
|
|
else
|
|
{
|
|
// Use AcquireAccount to get available accounts
|
|
uint32 accountId = sBotAccountMgr->AcquireAccount();
|
|
if (accountId != 0)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Acquired account {} for bot spawning", accountId);
|
|
accounts.push_back(accountId);
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "No accounts available for bot spawning");
|
|
}
|
|
}
|
|
|
|
if (accounts.empty())
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner", "No available accounts for bot spawning");
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Try each account until we find a suitable character
|
|
for (uint32 accountId : accounts)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Checking account {} for characters", accountId);
|
|
::std::vector<ObjectGuid> characters = GetAvailableCharacters(accountId, request);
|
|
if (!characters.empty())
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Found {} existing characters for account {}", characters.size(), accountId);
|
|
// CRITICAL FIX: Use DETERMINISTIC character selection instead of random to prevent session conflicts
|
|
// Always pick the first character (lowest GUID) to ensure consistency
|
|
TC_LOG_INFO("module.playerbot.spawner", " DETERMINISTIC: Selecting first character {} from {} available for account {}",
|
|
characters[0].ToString(), characters.size(), accountId);
|
|
return characters[0];
|
|
}
|
|
else
|
|
{
|
|
// No existing characters found - create a new one
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"No characters found for account {}, attempting to create new character", accountId);
|
|
|
|
ObjectGuid newCharacterGuid = CreateCharacterForAccount(accountId, request);
|
|
if (!newCharacterGuid.IsEmpty())
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Successfully created new character {} for account {}",
|
|
newCharacterGuid.ToString(), accountId);
|
|
return newCharacterGuid;
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Failed to create character for account {}", accountId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
void BotSpawner::SelectCharacterForSpawnAsync(SpawnRequest const& request, ::std::function<void(ObjectGuid)> callback)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Selecting character for spawn request asynchronously");
|
|
|
|
// Get available accounts if not specified
|
|
::std::vector<uint32> accounts;
|
|
if (request.accountId != 0)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Using specified account {}", request.accountId);
|
|
accounts.push_back(request.accountId);
|
|
}
|
|
else
|
|
{
|
|
// Use AcquireAccount to get available accounts
|
|
uint32 accountId = sBotAccountMgr->AcquireAccount();
|
|
if (accountId != 0)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Acquired account {} for bot spawning", accountId);
|
|
accounts.push_back(accountId);
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "No accounts available for bot spawning");
|
|
}
|
|
}
|
|
|
|
if (accounts.empty())
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner", "No available accounts for bot spawning");
|
|
callback(ObjectGuid::Empty);
|
|
return;
|
|
}
|
|
|
|
// Start async recursive character selection for 5000 bot scalability
|
|
SelectCharacterAsyncRecursive(::std::move(accounts), 0, request, ::std::move(callback));
|
|
}
|
|
|
|
::std::vector<ObjectGuid> BotSpawner::GetAvailableCharacters(uint32 accountId, SpawnRequest const& request)
|
|
{
|
|
::std::vector<ObjectGuid> availableCharacters;
|
|
|
|
// ========================================================================
|
|
// HIGH PRIORITY TODO FIXED: Add level/race/class filtering
|
|
// ========================================================================
|
|
// Use custom query to get guid, level, race, class for filtering
|
|
// This is enterprise-grade: single query with all needed fields
|
|
try
|
|
{
|
|
// Build SQL query with proper filtering
|
|
::std::ostringstream query;
|
|
query << "SELECT guid, level, race, class FROM characters WHERE account = " << accountId;
|
|
|
|
// Add filters if specified in SpawnRequest
|
|
if (request.minLevel > 0 || request.maxLevel > 0)
|
|
{
|
|
if (request.minLevel > 0 && request.maxLevel > 0)
|
|
query << " AND level BETWEEN " << static_cast<uint32>(request.minLevel)
|
|
<< " AND " << static_cast<uint32>(request.maxLevel);
|
|
else if (request.minLevel > 0)
|
|
query << " AND level >= " << static_cast<uint32>(request.minLevel);
|
|
else if (request.maxLevel > 0)
|
|
query << " AND level <= " << static_cast<uint32>(request.maxLevel);
|
|
}
|
|
|
|
if (request.raceFilter > 0)
|
|
query << " AND race = " << static_cast<uint32>(request.raceFilter);
|
|
|
|
if (request.classFilter > 0)
|
|
query << " AND class = " << static_cast<uint32>(request.classFilter);
|
|
|
|
// Legacy direct SQL query removed - Query() method no longer in interface
|
|
// Character discovery handled by other spawn paths
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Skipping legacy character discovery - handled by primary spawn path");
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Database error while getting characters for account {}: {}", accountId, e.what());
|
|
return availableCharacters; // Return empty vector on error
|
|
}
|
|
|
|
// If no characters found and AutoCreateCharacters is enabled, create one
|
|
if (availableCharacters.empty() && ::PlayerbotConfig::instance()->GetBool("Playerbot.AutoCreateCharacters", true))
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"No characters found for account {}, attempting to create new character", accountId);
|
|
|
|
ObjectGuid newCharacterGuid = CreateBotCharacter(accountId);
|
|
if (!newCharacterGuid.IsEmpty())
|
|
{
|
|
availableCharacters.push_back(newCharacterGuid);
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Successfully created new bot character {} for account {}",
|
|
newCharacterGuid.ToString(), accountId);
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Failed to create character for account {}", accountId);
|
|
}
|
|
}
|
|
|
|
return availableCharacters;
|
|
}
|
|
|
|
void BotSpawner::GetAvailableCharactersAsync(uint32 accountId, SpawnRequest const& request, ::std::function<void(::std::vector<ObjectGuid>)> callback)
|
|
{
|
|
// FULLY ASYNC DATABASE QUERY for 5000 bot scalability - no blocking - use safe statement access to prevent memory corruption
|
|
CharacterDatabasePreparedStatement* stmt = GetSafePreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID, "CHAR_SEL_CHARS_BY_ACCOUNT_ID");
|
|
if (!stmt)
|
|
{
|
|
callback(::std::vector<ObjectGuid>());
|
|
return;
|
|
}
|
|
stmt->setUInt32(0, accountId);
|
|
|
|
auto queryCallback = [this, accountId, request, callback](PreparedQueryResult result) mutable
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", " GetAvailableCharactersAsync callback for account {}, result: {}",
|
|
accountId, result ? "has data" : "null");
|
|
|
|
::std::vector<ObjectGuid> availableCharacters;
|
|
|
|
try
|
|
{
|
|
if (result)
|
|
{
|
|
availableCharacters.reserve(result->GetRowCount());
|
|
TC_LOG_INFO("module.playerbot.spawner", " Found {} characters for account {}", result->GetRowCount(), accountId);
|
|
do
|
|
{
|
|
Field* fields = result->Fetch();
|
|
ObjectGuid characterGuid = ObjectGuid::Create<HighGuid::Player>(fields[0].GetUInt64());
|
|
availableCharacters.push_back(characterGuid);
|
|
TC_LOG_INFO("module.playerbot.spawner", " Character found: {}", characterGuid.ToString());
|
|
} while (result->NextRow());
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", " No characters found for account {}", accountId);
|
|
}
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Database error while processing characters for account {}: {}", accountId, e.what());
|
|
}
|
|
|
|
// Handle auto-character creation if enabled and no characters found
|
|
if (availableCharacters.empty() && ::PlayerbotConfig::instance()->GetBool("Playerbot.AutoCreateCharacters", true))
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"No characters found for account {}, attempting to create new character", accountId);
|
|
|
|
ObjectGuid newCharacterGuid = CreateBotCharacter(accountId);
|
|
if (!newCharacterGuid.IsEmpty())
|
|
{
|
|
availableCharacters.push_back(newCharacterGuid);
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Successfully created new bot character {} for account {}",
|
|
newCharacterGuid.ToString(), accountId);
|
|
}
|
|
}
|
|
|
|
// Always call callback with results (empty on error)
|
|
callback(::std::move(availableCharacters));
|
|
};
|
|
|
|
// Use PlayerbotCharacterDBInterface for safe async execution with automatic sync/async routing
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
" About to execute AsyncQuery for CHAR_SEL_CHARS_BY_ACCOUNT_ID (statement {}) on playerbot_characters database through PlayerbotCharacterDBInterface",
|
|
static_cast<uint32>(CHAR_SEL_CHARS_BY_ACCOUNT_ID));
|
|
|
|
sPlayerbotCharDB->ExecuteAsync(stmt, ::std::move(queryCallback));
|
|
}
|
|
|
|
void BotSpawner::SelectCharacterAsyncRecursive(::std::vector<uint32> accounts, size_t index, SpawnRequest const& request, ::std::function<void(ObjectGuid)> callback)
|
|
{
|
|
if (index >= accounts.size())
|
|
{
|
|
// No more accounts to check
|
|
callback(ObjectGuid::Empty);
|
|
return;
|
|
}
|
|
|
|
uint32 accountId = accounts[index];
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Async checking account {} for characters", accountId);
|
|
|
|
GetAvailableCharactersAsync(accountId, request, [this, accounts = ::std::move(accounts), index, request, callback](::std::vector<ObjectGuid> characters) mutable
|
|
{
|
|
if (!characters.empty())
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", " Found {} existing characters for account {}", characters.size(), accounts[index]);
|
|
// Pick a random character from available ones
|
|
uint32 charIndex = urand(0, static_cast<uint32>(characters.size() - 1));
|
|
ObjectGuid selectedGuid = characters[charIndex];
|
|
TC_LOG_INFO("module.playerbot.spawner", " Selected character {} for spawning", selectedGuid.ToString());
|
|
callback(selectedGuid);
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", " No characters found for account {}, trying next account", accounts[index]);
|
|
// Try next account
|
|
SelectCharacterAsyncRecursive(::std::move(accounts), index + 1, request, callback);
|
|
}
|
|
});
|
|
}
|
|
|
|
void BotSpawner::ContinueSpawnWithCharacter(ObjectGuid characterGuid, SpawnRequest const& request)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", " ContinueSpawnWithCharacter called for {}", characterGuid.ToString());
|
|
|
|
// Get the actual account ID from the character
|
|
uint32 actualAccountId = GetAccountIdFromCharacter(characterGuid);
|
|
if (actualAccountId == 0)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Failed to get account ID for character {}", characterGuid.ToString());
|
|
_stats.failedSpawns.fetch_add(1);
|
|
if (request.callback)
|
|
request.callback(false, ObjectGuid::Empty);
|
|
return;
|
|
}
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", " Continuing spawn with character {} for account {} (bypassLimit={})",
|
|
characterGuid.ToString(), actualAccountId, request.bypassMaxBotsLimit);
|
|
|
|
// Create bot session - pass bypassMaxBotsLimit for pool/JIT bots
|
|
if (!CreateBotSession(actualAccountId, characterGuid, request.bypassMaxBotsLimit))
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Failed to create bot session for character {}", characterGuid.ToString());
|
|
_stats.failedSpawns.fetch_add(1);
|
|
if (request.callback)
|
|
request.callback(false, ObjectGuid::Empty);
|
|
return;
|
|
}
|
|
|
|
// Update tracking data
|
|
uint32 zoneId = request.zoneId;
|
|
if (zoneId == 0)
|
|
{
|
|
zoneId = 1; // Default to first zone
|
|
}
|
|
|
|
{
|
|
{
|
|
tbb::concurrent_hash_map<ObjectGuid, uint32>::accessor acc;
|
|
_activeBots.insert(acc, characterGuid);
|
|
acc->second = zoneId;
|
|
}
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ::std::vector<ObjectGuid>>::accessor acc;
|
|
_botsByZone.insert(acc, zoneId);
|
|
acc->second.push_back(characterGuid);
|
|
}
|
|
|
|
// P1 FIX: Counter increment removed - now handled by atomic pre-increment in SpawnBot()
|
|
// The counter is incremented atomically BEFORE spawn attempt to reserve the slot and
|
|
// eliminate TOCTOU race. If we increment here, we would double-count.
|
|
//
|
|
// Old code (REMOVED):
|
|
// _activeBotCount.fetch_add(1, ::std::memory_order_release);
|
|
}
|
|
|
|
// Update statistics
|
|
_stats.totalSpawned.fetch_add(1);
|
|
uint32 currentActive = _stats.currentlyActive.fetch_add(1) + 1;
|
|
uint32 currentPeak = _stats.peakConcurrent.load();
|
|
while (currentActive > currentPeak && !_stats.peakConcurrent.compare_exchange_weak(currentPeak, currentActive))
|
|
{
|
|
// Retry until we successfully update the peak or find a higher value
|
|
}
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Successfully spawned bot {} in zone {} (async)", characterGuid.ToString(), zoneId);
|
|
|
|
if (request.callback)
|
|
request.callback(true, characterGuid);
|
|
}
|
|
|
|
uint32 BotSpawner::GetAccountIdFromCharacter(ObjectGuid characterGuid) const
|
|
{
|
|
if (characterGuid.IsEmpty())
|
|
return 0;
|
|
|
|
try
|
|
{
|
|
// Query the account ID from the characters table using CHAR_SEL_CHAR_PINFO - use safe statement access to prevent memory corruption
|
|
// This query returns: totaltime, level, money, account, race, class, map, zone, gender, health, playerFlags
|
|
CharacterDatabasePreparedStatement* stmt = GetSafePreparedStatement(CHAR_SEL_CHAR_PINFO, "CHAR_SEL_CHAR_PINFO");
|
|
if (!stmt)
|
|
{
|
|
return 0;
|
|
}
|
|
stmt->setUInt64(0, characterGuid.GetCounter());
|
|
// Use PlayerbotCharacterDBInterface for safe synchronous execution
|
|
PreparedQueryResult result = sPlayerbotCharDB->ExecuteSync(stmt);
|
|
|
|
if (result)
|
|
{
|
|
Field* fields = result->Fetch();
|
|
uint32 accountId = fields[3].GetUInt32(); // account is the 4th field (index 3)
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Character {} belongs to account {}", characterGuid.ToString(), accountId);
|
|
return accountId;
|
|
}
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Database error while getting account ID for character {}: {}", characterGuid.ToString(), e.what());
|
|
}
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Character {} not found in database", characterGuid.ToString());
|
|
return 0;
|
|
}
|
|
|
|
void BotSpawner::DespawnBot(ObjectGuid guid, bool forced)
|
|
{
|
|
uint32 accountId = 0;
|
|
uint32 zoneId = 0;
|
|
|
|
// DEADLOCK FIX: Split read and erase into separate lock scopes
|
|
// TBB's concurrent_hash_map::erase() acquires a write lock internally,
|
|
// but const_accessor holds a read lock. Calling erase() while holding
|
|
// a const_accessor causes deadlock (read lock blocks write lock acquisition).
|
|
|
|
// Step 1: Read the zone ID with a read lock, then release it
|
|
{
|
|
tbb::concurrent_hash_map<ObjectGuid, uint32>::const_accessor it;
|
|
if (!_activeBots.find(it, guid))
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Attempted to despawn non-active bot {}", guid.ToString());
|
|
return;
|
|
}
|
|
zoneId = it->second;
|
|
// const_accessor is released here when it goes out of scope
|
|
}
|
|
|
|
// Step 2: Erase from map without holding any locks (erase acquires its own write lock)
|
|
if (!_activeBots.erase(guid))
|
|
{
|
|
// Already erased by another thread - this is OK during concurrent shutdown
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Bot {} already removed by another thread", guid.ToString());
|
|
return;
|
|
}
|
|
|
|
// Step 3: Update atomic counter after successful erase
|
|
// LOCK-FREE OPTIMIZATION: Update atomic counter for hot path access
|
|
_activeBotCount.fetch_sub(1, ::std::memory_order_release);
|
|
|
|
// Step 4: Remove from zone tracking with separate lock scope
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ::std::vector<ObjectGuid>>::accessor zoneIt;
|
|
if (_botsByZone.find(zoneIt, zoneId))
|
|
{
|
|
auto& bots = zoneIt->second;
|
|
bots.erase(::std::remove(bots.begin(), bots.end(), guid), bots.end());
|
|
}
|
|
}
|
|
|
|
// Get account ID for session cleanup (outside of mutex to prevent deadlocks)
|
|
accountId = GetAccountIdFromCharacter(guid);
|
|
|
|
// Remove the bot session properly to prevent memory leaks
|
|
if (accountId != 0)
|
|
{
|
|
Playerbot::sBotWorldSessionMgr->RemoveAllPlayerBots(accountId);
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Released bot session for account {} (character {})", accountId, guid.ToString());
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Could not find account ID for character {} during despawn", guid.ToString());
|
|
}
|
|
|
|
// Update statistics
|
|
_stats.totalDespawned.fetch_add(1);
|
|
_stats.currentlyActive.fetch_sub(1);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Despawned bot {} from zone {} (forced: {})", guid.ToString(), zoneId, forced);
|
|
}
|
|
|
|
void BotSpawner::DespawnAllBots()
|
|
{
|
|
// P1 FIX: ATOMIC SWAP PATTERN for thread-safe mass despawn
|
|
// Problem: Range-based for over concurrent_hash_map is NOT atomic - other threads
|
|
// can add/remove entries during iteration, causing iterator invalidation,
|
|
// missing bots, or potential crashes
|
|
// Solution: Atomically swap with empty maps, then process isolated snapshot with
|
|
// zero race condition risk. TBB concurrent_hash_map::swap() is atomic.
|
|
|
|
// Step 1: Atomically swap out both tracking maps
|
|
tbb::concurrent_hash_map<ObjectGuid, uint32> oldBots;
|
|
_activeBots.swap(oldBots); // Atomic operation - now we own the old map
|
|
|
|
tbb::concurrent_hash_map<uint32, ::std::vector<ObjectGuid>> oldBotsByZone;
|
|
_botsByZone.swap(oldBotsByZone); // Atomic operation
|
|
|
|
// Step 2: Reset atomic counter (all bots are being despawned)
|
|
_activeBotCount.store(0, ::std::memory_order_release);
|
|
|
|
uint32 despawnCount = 0;
|
|
|
|
// Step 3: Process the isolated snapshot (no race conditions possible)
|
|
// This is now completely thread-safe - no other thread can access oldBots
|
|
for (auto const& [guid, zoneId] : oldBots)
|
|
{
|
|
// Get account ID for session cleanup
|
|
uint32 accountId = GetAccountIdFromCharacter(guid);
|
|
|
|
// Remove the bot session to prevent memory leaks
|
|
// This is the critical cleanup that was happening in DespawnBot()
|
|
if (accountId != 0)
|
|
{
|
|
Playerbot::sBotWorldSessionMgr->RemoveAllPlayerBots(accountId);
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Released bot session for account {} (character {}) during mass despawn",
|
|
accountId, guid.ToString());
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Could not find account ID for character {} during mass despawn", guid.ToString());
|
|
}
|
|
|
|
++despawnCount;
|
|
}
|
|
|
|
// Step 4: Update statistics (batch update for performance)
|
|
_stats.totalDespawned.fetch_add(despawnCount, ::std::memory_order_release);
|
|
_stats.currentlyActive.store(0, ::std::memory_order_release);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Despawned all {} active bots using atomic swap pattern (race-free)", despawnCount);
|
|
}
|
|
|
|
void BotSpawner::UpdateZonePopulation(uint32 zoneId, uint32 mapId, uint32 playerCount)
|
|
{
|
|
// DEADLOCK FIX: Use atomic operations and minimize lock scope
|
|
// Replace complex nested locking with lock-free approach
|
|
|
|
// Count real players in this zone using atomic access
|
|
uint32 realPlayerSessions = _lastRealPlayerCount.load();
|
|
|
|
// If playerCount was not provided, use the fallback logic
|
|
if (playerCount == 0 && realPlayerSessions > 0)
|
|
{
|
|
// For now, assume players are distributed across starter zones
|
|
// This ensures bots spawn when real players are online
|
|
playerCount = ::std::max(1u, realPlayerSessions);
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Zone {} has {} real players (fallback count)",
|
|
zoneId, playerCount);
|
|
}
|
|
|
|
// DEADLOCK FIX: Collect data first with minimal lock scope
|
|
uint32 botCount = 0;
|
|
bool zoneExists = false;
|
|
|
|
// Phase 1: Quick data collection with separate locks (no nesting)
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ::std::vector<ObjectGuid>>::const_accessor it;
|
|
botCount = _botsByZone.find(it, zoneId) ? it->second.size() : 0;
|
|
}
|
|
|
|
// Phase 2: Update zone data with separate lock
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::accessor it;
|
|
if (_zonePopulations.find(it, zoneId))
|
|
{
|
|
it->second.playerCount = playerCount;
|
|
it->second.botCount = botCount;
|
|
it->second.lastUpdate = ::std::chrono::system_clock::now();
|
|
zoneExists = true;
|
|
}
|
|
}
|
|
|
|
// Log results without holding any locks
|
|
if (zoneExists)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner",
|
|
"Updated zone {} population: {} players, {} bots",
|
|
zoneId, playerCount, botCount);
|
|
}
|
|
}
|
|
|
|
void BotSpawner::UpdateZonePopulationSafe(uint32 zoneId, uint32 mapId, uint32 playerCount)
|
|
{
|
|
// DEADLOCK FIX: Simplified lock-free population tracking
|
|
// Replaced nested locks with direct call to thread-safe UpdateZonePopulation
|
|
UpdateZonePopulation(zoneId, mapId, playerCount);
|
|
}
|
|
|
|
ZonePopulation BotSpawner::GetZonePopulation(uint32 zoneId) const
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::accessor it;
|
|
if (_zonePopulations.find(it, zoneId))
|
|
{
|
|
return it->second;
|
|
}
|
|
return ZonePopulation{};
|
|
}
|
|
|
|
uint32 BotSpawner::GetActiveBotCount() const
|
|
{
|
|
// LOCK-FREE OPTIMIZATION: Use atomic counter for hot path
|
|
// This method is called thousands of times per second with 5000 bots
|
|
return _activeBotCount.load(::std::memory_order_acquire);
|
|
}
|
|
|
|
uint32 BotSpawner::GetActiveBotCount(uint32 zoneId) const
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ::std::vector<ObjectGuid>>::const_accessor it;
|
|
return _botsByZone.find(it, zoneId) ? it->second.size() : 0;
|
|
}
|
|
|
|
uint32 BotSpawner::GetActiveBotCount(uint32 mapId, bool useMapId) const
|
|
{
|
|
if (!useMapId)
|
|
return GetActiveBotCount(mapId); // Fall back to zone-based count
|
|
|
|
// Count bots on a specific map
|
|
uint32 count = 0;
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::const_accessor it;
|
|
for (auto iter = _zonePopulations.begin(); iter != _zonePopulations.end(); ++iter)
|
|
{
|
|
if (iter->second.mapId == mapId)
|
|
count += GetActiveBotCount(iter->first);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool BotSpawner::IsBotActive(ObjectGuid guid) const
|
|
{
|
|
tbb::concurrent_hash_map<ObjectGuid, uint32>::const_accessor it;
|
|
return _activeBots.find(it, guid);
|
|
}
|
|
|
|
::std::vector<ObjectGuid> BotSpawner::GetActiveBotsInZone(uint32 zoneId) const
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ::std::vector<ObjectGuid>>::const_accessor it;
|
|
if (_botsByZone.find(it, zoneId))
|
|
return it->second;
|
|
return {};
|
|
}
|
|
|
|
::std::vector<ZonePopulation> BotSpawner::GetAllZonePopulations() const
|
|
{
|
|
::std::vector<ZonePopulation> result;
|
|
result.reserve(_zonePopulations.size());
|
|
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::const_accessor it;
|
|
for (auto iter = _zonePopulations.begin(); iter != _zonePopulations.end(); ++iter)
|
|
{
|
|
result.push_back(iter->second);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
bool BotSpawner::CanSpawnMore() const
|
|
{
|
|
return GetActiveBotCount() < _config.maxBotsTotal;
|
|
}
|
|
|
|
bool BotSpawner::CanSpawnInZone(uint32 zoneId) const
|
|
{
|
|
return GetActiveBotCount(zoneId) < _config.maxBotsPerZone;
|
|
}
|
|
|
|
bool BotSpawner::CanSpawnOnMap(uint32 mapId) const
|
|
{
|
|
// WoW 12.0: Check map type exclusions for housing and WowLabs maps
|
|
MapEntry const* mapEntry = sMapStore.LookupEntry(mapId);
|
|
if (mapEntry)
|
|
{
|
|
// Exclude housing maps (player housing interiors and neighborhoods)
|
|
// Exclude WowLabs maps (Plunderstorm and experimental game modes)
|
|
switch (mapEntry->InstanceType)
|
|
{
|
|
case MAP_WOWLABS: // Plunderstorm/experimental
|
|
case MAP_HOUSE_INTERIOR: // Player housing interior
|
|
case MAP_HOUSE_NEIGHBORHOOD: // Player housing neighborhood
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"CanSpawnOnMap: Rejecting map {} - map type {} is excluded (housing/WowLabs)",
|
|
mapId, static_cast<uint32>(mapEntry->InstanceType));
|
|
return false;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check population cap
|
|
uint32 mapBotCount = 0;
|
|
for (auto const& [zoneId, population] : _zonePopulations)
|
|
{
|
|
if (population.mapId == mapId)
|
|
{
|
|
mapBotCount += population.botCount;
|
|
}
|
|
}
|
|
return mapBotCount < _config.maxBotsPerMap;
|
|
}
|
|
|
|
void BotSpawner::CalculateZoneTargets()
|
|
{
|
|
// DEADLOCK FIX: Minimize lock scope and avoid external calls while holding locks
|
|
|
|
::std::vector<::std::pair<uint32, ZonePopulation>> zonesCopy;
|
|
::std::vector<::std::pair<uint32, uint32>> targetUpdates;
|
|
|
|
// Phase 1: Copy zone data with minimal lock scope
|
|
{
|
|
zonesCopy.reserve(_zonePopulations.size());
|
|
for (auto const& [zoneId, population] : _zonePopulations)
|
|
{
|
|
zonesCopy.emplace_back(zoneId, population);
|
|
}
|
|
}
|
|
|
|
// Phase 2: Calculate targets without holding any locks
|
|
targetUpdates.reserve(zonesCopy.size());
|
|
for (auto const& [zoneId, population] : zonesCopy)
|
|
{
|
|
uint32 newTarget = CalculateTargetBotCount(population);
|
|
targetUpdates.emplace_back(zoneId, newTarget);
|
|
}
|
|
|
|
// Phase 3: Update targets with minimal lock scope
|
|
{
|
|
for (auto const& [zoneId, newTarget] : targetUpdates)
|
|
{
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::accessor it;
|
|
if (_zonePopulations.find(it, zoneId))
|
|
{
|
|
it->second.targetBotCount = newTarget;
|
|
}
|
|
}
|
|
}
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Recalculated zone population targets for {} zones", targetUpdates.size());
|
|
}
|
|
|
|
uint32 BotSpawner::CalculateTargetBotCount(ZonePopulation const& zone) const
|
|
{
|
|
// Base target on player count and ratio
|
|
uint32 baseTarget = static_cast<uint32>(zone.playerCount * _config.botToPlayerRatio);
|
|
|
|
// CRITICAL FIX: Read minimum bots from config
|
|
// This ensures bots spawn even with ratio = 0 or no players
|
|
uint32 minimumBots = sPlayerbotConfig->GetUInt("Playerbot.MinimumBotsPerZone", 10);
|
|
|
|
// Only apply minimum bot count when real human players are online,
|
|
// OR if we are in static mode and want to spawn on server start.
|
|
if (_lastRealPlayerCount.load() > 0 || (!_config.enableDynamicSpawning && _config.spawnOnServerStart))
|
|
{
|
|
baseTarget = ::std::max(baseTarget, minimumBots);
|
|
TC_LOG_INFO("module.playerbot.spawner", "Zone {} - players: {}, ratio: {}, ratio target: {}, minimum: {}, final target: {}",
|
|
zone.zoneId, zone.playerCount, _config.botToPlayerRatio,
|
|
static_cast<uint32>(zone.playerCount * _config.botToPlayerRatio), minimumBots, baseTarget);
|
|
}
|
|
|
|
// Apply zone caps
|
|
baseTarget = ::std::min(baseTarget, _config.maxBotsPerZone);
|
|
|
|
return baseTarget;
|
|
}
|
|
|
|
void BotSpawner::SpawnToPopulationTarget()
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "SpawnToPopulationTarget called, enableDynamicSpawning: {}", _config.enableDynamicSpawning);
|
|
|
|
// DEADLOCK FIX: Use lock-free approach with data copying
|
|
// Collect zone data first, then process without holding locks
|
|
|
|
::std::vector<SpawnRequest> spawnRequests;
|
|
::std::vector<::std::pair<uint32, ZonePopulation>> zonesCopy;
|
|
|
|
// Phase 1: Copy zone data with minimal lock scope
|
|
{
|
|
|
|
// INTEGRATION FIX: If no zones are populated, get them from BotWorldPositioner
|
|
if (_zonePopulations.empty())
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"SpawnToPopulationTarget: Zone populations empty, triggering UpdatePopulationTargets()");
|
|
UpdatePopulationTargets();
|
|
|
|
// If still empty after update (positioner not ready), log warning
|
|
if (_zonePopulations.empty())
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"SpawnToPopulationTarget: No zones available after UpdatePopulationTargets() - "
|
|
"check if BotWorldPositioner is initialized");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Copy zone data for lock-free processing
|
|
zonesCopy.reserve(_zonePopulations.size());
|
|
for (auto const& [zoneId, population] : _zonePopulations)
|
|
{
|
|
zonesCopy.emplace_back(zoneId, population);
|
|
}
|
|
} // _zoneMutex released here
|
|
|
|
// CRITICAL FIX: Prioritize zones with real players to ensure they spawn where humans are
|
|
::std::sort(zonesCopy.begin(), zonesCopy.end(), [](auto const& a, auto const& b) {
|
|
// 1. Prioritize by player count descending
|
|
if (a.second.playerCount != b.second.playerCount)
|
|
return a.second.playerCount > b.second.playerCount;
|
|
|
|
// 2. Then by bot count ascending (underpopulated zones first)
|
|
return a.second.botCount < b.second.botCount;
|
|
});
|
|
|
|
// Phase 2: Process spawn requests without holding any locks
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Processing {} zones for spawn requests", zonesCopy.size());
|
|
|
|
for (auto const& [zoneId, population] : zonesCopy)
|
|
{
|
|
if (population.botCount < population.targetBotCount)
|
|
{
|
|
uint32 needed = population.targetBotCount - population.botCount;
|
|
|
|
for (uint32 i = 0; i < needed && spawnRequests.size() < _config.spawnBatchSize; ++i)
|
|
{
|
|
SpawnRequest request;
|
|
request.type = SpawnRequest::SPECIFIC_ZONE;
|
|
request.zoneId = zoneId;
|
|
request.mapId = population.mapId;
|
|
request.minLevel = population.minLevel;
|
|
request.maxLevel = population.maxLevel;
|
|
spawnRequests.push_back(request);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 3: Queue spawn requests if any were created
|
|
if (!spawnRequests.empty())
|
|
{
|
|
uint32 queued = SpawnBots(spawnRequests);
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Queued {} spawn requests from {} zones", queued, zonesCopy.size());
|
|
}
|
|
}
|
|
|
|
void BotSpawner::UpdatePopulationTargets()
|
|
{
|
|
// ========================================================================
|
|
// INTEGRATION FIX: Get zones from BotWorldPositioner
|
|
// ========================================================================
|
|
// Previously this only added 2 hardcoded test zones.
|
|
// Now we integrate with BotWorldPositioner to get all 40+ zones,
|
|
// which allows bots to spawn across all level-appropriate zones.
|
|
// ========================================================================
|
|
|
|
// Ensure positioner is ready
|
|
if (!sBotWorldPositioner->IsReady())
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"BotSpawner::UpdatePopulationTargets() - BotWorldPositioner not ready, loading zones...");
|
|
sBotWorldPositioner->LoadZones();
|
|
}
|
|
|
|
// Get minimum bots per zone from config
|
|
uint32 minimumBotsPerZone = sPlayerbotConfig->GetUInt("Playerbot.MinimumBotsPerZone", 10);
|
|
|
|
// Only populate zones if empty (avoid duplicate insertions)
|
|
if (!_zonePopulations.empty())
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"BotSpawner::UpdatePopulationTargets() - Zone populations already initialized ({} zones)",
|
|
_zonePopulations.size());
|
|
return;
|
|
}
|
|
|
|
// Get all zones from both factions
|
|
uint32 zonesAdded = 0;
|
|
|
|
// Add Alliance zones
|
|
for (uint32 level = 1; level <= 80; level += 5)
|
|
{
|
|
auto allianceZones = sBotWorldPositioner->GetValidZones(level, TEAM_ALLIANCE);
|
|
for (auto const* zonePlacement : allianceZones)
|
|
{
|
|
if (!zonePlacement)
|
|
continue;
|
|
|
|
// Check if already added
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::const_accessor check;
|
|
if (_zonePopulations.find(check, zonePlacement->zoneId))
|
|
continue; // Already exists
|
|
|
|
// Create zone population entry
|
|
ZonePopulation newZone;
|
|
newZone.zoneId = zonePlacement->zoneId;
|
|
newZone.mapId = zonePlacement->mapId;
|
|
newZone.playerCount = 0;
|
|
newZone.botCount = 0;
|
|
newZone.targetBotCount = minimumBotsPerZone;
|
|
newZone.minLevel = static_cast<uint8>(zonePlacement->minLevel);
|
|
newZone.maxLevel = static_cast<uint8>(zonePlacement->maxLevel);
|
|
newZone.botDensity = 0.5f;
|
|
newZone.lastUpdate = ::std::chrono::system_clock::now();
|
|
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::accessor acc;
|
|
_zonePopulations.insert(acc, zonePlacement->zoneId);
|
|
acc->second = newZone;
|
|
++zonesAdded;
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Added zone {} ({}) - L{}-{}, target: {} bots",
|
|
zonePlacement->zoneName, zonePlacement->zoneId,
|
|
zonePlacement->minLevel, zonePlacement->maxLevel, minimumBotsPerZone);
|
|
}
|
|
}
|
|
|
|
// Add Horde zones
|
|
for (uint32 level = 1; level <= 80; level += 5)
|
|
{
|
|
auto hordeZones = sBotWorldPositioner->GetValidZones(level, TEAM_HORDE);
|
|
for (auto const* zonePlacement : hordeZones)
|
|
{
|
|
if (!zonePlacement)
|
|
continue;
|
|
|
|
// Check if already added (neutral zones may appear in both)
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::const_accessor check;
|
|
if (_zonePopulations.find(check, zonePlacement->zoneId))
|
|
continue; // Already exists
|
|
|
|
// Create zone population entry
|
|
ZonePopulation newZone;
|
|
newZone.zoneId = zonePlacement->zoneId;
|
|
newZone.mapId = zonePlacement->mapId;
|
|
newZone.playerCount = 0;
|
|
newZone.botCount = 0;
|
|
newZone.targetBotCount = minimumBotsPerZone;
|
|
newZone.minLevel = static_cast<uint8>(zonePlacement->minLevel);
|
|
newZone.maxLevel = static_cast<uint8>(zonePlacement->maxLevel);
|
|
newZone.botDensity = 0.5f;
|
|
newZone.lastUpdate = ::std::chrono::system_clock::now();
|
|
|
|
tbb::concurrent_hash_map<uint32, ZonePopulation>::accessor acc;
|
|
_zonePopulations.insert(acc, zonePlacement->zoneId);
|
|
acc->second = newZone;
|
|
++zonesAdded;
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Added zone {} ({}) - L{}-{}, target: {} bots",
|
|
zonePlacement->zoneName, zonePlacement->zoneId,
|
|
zonePlacement->minLevel, zonePlacement->maxLevel, minimumBotsPerZone);
|
|
}
|
|
}
|
|
|
|
// Calculate expected total bots
|
|
uint32 expectedTotalBots = zonesAdded * minimumBotsPerZone;
|
|
uint32 effectiveTotalBots = ::std::min(expectedTotalBots, _config.maxBotsTotal);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"BotSpawner::UpdatePopulationTargets() - Loaded {} zones from BotWorldPositioner "
|
|
"(expected: {} bots, capped at MaxTotal: {})",
|
|
zonesAdded, expectedTotalBots, effectiveTotalBots);
|
|
}
|
|
|
|
void BotSpawner::ResetStats()
|
|
{
|
|
_stats.totalSpawned.store(0);
|
|
_stats.totalDespawned.store(0);
|
|
_stats.currentlyActive.store(0);
|
|
_stats.peakConcurrent.store(0);
|
|
_stats.failedSpawns.store(0);
|
|
_stats.totalSpawnTime.store(0);
|
|
_stats.spawnAttempts.store(0);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "Spawn statistics reset");
|
|
}
|
|
|
|
bool BotSpawner::DespawnBot(ObjectGuid guid, ::std::string const& reason)
|
|
{
|
|
TC_LOG_DEBUG("module.playerbot.spawner", "Despawning bot {} with reason: {}", guid.ToString(), reason);
|
|
|
|
// Check if bot exists before attempting despawn
|
|
{
|
|
tbb::concurrent_hash_map<ObjectGuid, uint32>::const_accessor it;
|
|
if (!_activeBots.find(it, guid))
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner", "Attempted to despawn bot {} but it was not found in active bots", guid.ToString());
|
|
return false;
|
|
}
|
|
// Don't erase here - let DespawnBot handle it to avoid race conditions
|
|
}
|
|
|
|
// Call the existing forced despawn method (handles all cleanup)
|
|
DespawnBot(guid, true);
|
|
|
|
// Log the reason for despawn
|
|
TC_LOG_INFO("module.playerbot.spawner", "Bot {} despawned with reason: {}", guid.ToString(), reason);
|
|
|
|
return true;
|
|
}
|
|
|
|
ObjectGuid BotSpawner::CreateCharacterForAccount(uint32 accountId, SpawnRequest const& request)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Creating character for account {} based on spawn request", accountId);
|
|
|
|
// Use the existing CreateBotCharacter method which handles all the complexity
|
|
return CreateBotCharacter(accountId);
|
|
}
|
|
|
|
ObjectGuid BotSpawner::CreateBotCharacter(uint32 accountId)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Creating bot character for account {}", accountId);
|
|
|
|
try
|
|
{
|
|
// ACCOUNT EXISTENCE VALIDATION: Verify account exists in database before creating character
|
|
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_BY_ID);
|
|
stmt->setUInt32(0, accountId);
|
|
PreparedQueryResult accountCheck = LoginDatabase.Query(stmt);
|
|
|
|
if (!accountCheck)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Account {} does not exist in login database! Cannot create bot character.",
|
|
accountId);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Check current character count for this account (enforce 10 character limit)
|
|
CharacterDatabasePreparedStatement* charStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_SUM_CHARS);
|
|
charStmt->setUInt32(0, accountId);
|
|
PreparedQueryResult charCountResult = CharacterDatabase.Query(charStmt);
|
|
|
|
uint32 currentCharCount = 0;
|
|
if (charCountResult)
|
|
{
|
|
Field* fields = charCountResult->Fetch();
|
|
currentCharCount = fields[0].GetUInt32();
|
|
}
|
|
|
|
if (currentCharCount >= 10)
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Account {} already has {} characters (limit: 10). Cannot create more.",
|
|
accountId, currentCharCount);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Account {} validated: exists in database, has {}/10 characters",
|
|
accountId, currentCharCount);
|
|
|
|
// Get race/class distribution
|
|
auto [race, classId] = sBotCharacterDistribution->GetRandomRaceClassByDistribution();
|
|
if (race == RACE_NONE || classId == CLASS_NONE)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to get valid race/class for bot character creation");
|
|
return ObjectGuid::Empty;
|
|
}
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Selected race {} and class {} for bot character", race, classId);
|
|
|
|
// Get gender (simplified - random between male/female)
|
|
uint8 gender = urand(0, 1) ? GENDER_MALE : GENDER_FEMALE;
|
|
|
|
// Generate character GUID first
|
|
ObjectGuid::LowType guidLow = sObjectMgr->GetGenerator<HighGuid::Player>().Generate();
|
|
ObjectGuid characterGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
|
|
|
|
// Get a unique name with the proper GUID
|
|
::std::string name = sBotNameMgr->AllocateName(gender, characterGuid.GetCounter());
|
|
if (name.empty())
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to allocate name for bot character creation");
|
|
return ObjectGuid::Empty;
|
|
}
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Allocated name '{}' for bot character", name);
|
|
|
|
// Create character info structure
|
|
auto createInfo = ::std::make_shared<WorldPackets::Character::CharacterCreateInfo>();
|
|
createInfo->Name = name;
|
|
createInfo->Race = race;
|
|
createInfo->Class = classId;
|
|
createInfo->Sex = gender;
|
|
createInfo->UseNPE = false; // Use classic starting zone, not New Player Experience
|
|
|
|
// CRITICAL FIX: Generate valid default customizations for the race/gender
|
|
// WoW 11.x requires valid customization choices for character creation
|
|
// Without this, Player::Create() fails at ValidateAppearance()
|
|
createInfo->Customizations.clear();
|
|
if (auto const* options = sDB2Manager.GetCustomiztionOptions(race, gender))
|
|
{
|
|
for (ChrCustomizationOptionEntry const* option : *options)
|
|
{
|
|
// Get available choices for this option
|
|
if (auto const* choices = sDB2Manager.GetCustomiztionChoices(option->ID))
|
|
{
|
|
if (!choices->empty())
|
|
{
|
|
// Use first valid choice for each option
|
|
UF::ChrCustomizationChoice choice;
|
|
choice.ChrCustomizationOptionID = option->ID;
|
|
choice.ChrCustomizationChoiceID = (*choices)[0]->ID;
|
|
createInfo->Customizations.push_back(choice);
|
|
}
|
|
}
|
|
}
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Generated {} customization choices for race {} gender {}",
|
|
createInfo->Customizations.size(), race, gender);
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"No customization options found for race {} gender {} - character creation may fail",
|
|
race, gender);
|
|
}
|
|
|
|
// Get the starting level from config
|
|
uint8 startLevel = 1; // sPlayerbotConfig->GetInt("Playerbot.RandomBotLevel.Min", 1);
|
|
|
|
// Check if this race/class combination is valid
|
|
ChrClassesEntry const* classEntry = sChrClassesStore.LookupEntry(classId);
|
|
ChrRacesEntry const* raceEntry = sChrRacesStore.LookupEntry(race);
|
|
|
|
if (!classEntry || !raceEntry)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Invalid race ({}) or class ({}) for bot character creation", race, classId);
|
|
sBotNameMgr->ReleaseName(name);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Race/class validation succeeded");
|
|
|
|
// Create a TEMPORARY bot session for character creation only
|
|
// CRITICAL: Use BotSession::Create() to create a standalone session for character creation.
|
|
// Standalone sessions are not added to any update loop, preventing infinite update loops
|
|
// during the creation process.
|
|
::std::shared_ptr<BotSession> tempSession = BotSession::Create(accountId);
|
|
if (!tempSession)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to create temp bot session for character creation (Account: {})", accountId);
|
|
sBotNameMgr->ReleaseName(name);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Temporary bot session created for character creation");
|
|
|
|
// Use smart pointer with proper RAII cleanup to prevent memory leaks
|
|
::std::unique_ptr<Player> newChar = ::std::make_unique<Player>(tempSession.get());
|
|
|
|
// REMOVED: MotionMaster initialization - this will be handled automatically during Player::Create()
|
|
// The MotionMaster needs the Player to be fully constructed before initialization
|
|
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Creating Player object with GUID {}", guidLow);
|
|
if (!newChar->Create(guidLow, createInfo.get()))
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to create Player object for bot character (Race: {}, Class: {})", race, classId);
|
|
sBotNameMgr->ReleaseName(name);
|
|
// Cleanup will be handled automatically by unique_ptr and shared_ptr
|
|
return ObjectGuid::Empty;
|
|
}
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Player::Create() succeeded");
|
|
|
|
// Account ID is automatically set through the bot session - no manual setting needed
|
|
|
|
// Set starting level if different from 1
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Setting character properties (level: {})", startLevel);
|
|
if (startLevel > 1)
|
|
{
|
|
newChar->SetLevel(startLevel);
|
|
}
|
|
|
|
newChar->setCinematic(1); // Skip intro cinematics for bots
|
|
newChar->SetAtLoginFlag(AT_LOGIN_FIRST);
|
|
|
|
// POSITION VALIDATION FIX: Ensure bot has valid position before saving
|
|
// Position defaults to (0,0,0) and IsPositionValid() returns TRUE for (0,0,0)
|
|
// because it only checks coordinate bounds, not gameplay validity.
|
|
// This caused bots to be saved with invalid positions on wrong maps.
|
|
if (newChar->GetPositionX() == 0.0f && newChar->GetPositionY() == 0.0f && newChar->GetPositionZ() == 0.0f)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"POSITION BUG DETECTED: Bot {} has (0,0,0) position after Create()! "
|
|
"Race: {}, Class: {}. Attempting to fix using playercreateinfo.",
|
|
name, race, classId);
|
|
|
|
// Get correct starting position from playercreateinfo
|
|
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(race, classId);
|
|
if (info)
|
|
{
|
|
PlayerInfo::CreatePosition const& startPos = info->createPosition;
|
|
newChar->Relocate(startPos.Loc);
|
|
newChar->SetHomebind(startPos.Loc, newChar->GetAreaId());
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
"Position fixed: Bot {} relocated to ({:.2f}, {:.2f}, {:.2f}) on map {}",
|
|
name, startPos.Loc.GetPositionX(), startPos.Loc.GetPositionY(),
|
|
startPos.Loc.GetPositionZ(), startPos.Loc.GetMapId());
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"CRITICAL: Cannot fix position - no PlayerInfo for race {} class {}",
|
|
race, classId);
|
|
}
|
|
}
|
|
|
|
// Save to database
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Saving character to database");
|
|
// Use PlayerbotCharacterDBInterface for safe transaction handling
|
|
CharacterDatabaseTransaction characterTransaction = sPlayerbotCharDB->BeginTransaction();
|
|
LoginDatabaseTransaction loginTransaction = LoginDatabase.BeginTransaction();
|
|
|
|
newChar->SaveToDB(loginTransaction, characterTransaction, true);
|
|
TC_LOG_TRACE("module.playerbot.spawner", "SaveToDB() completed");
|
|
|
|
// Update character count for account - with safe statement access to prevent memory corruption
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Updating character count for account {}", accountId);
|
|
LoginDatabasePreparedStatement* charCountStmt = GetSafeLoginPreparedStatement(LOGIN_REP_REALM_CHARACTERS, "LOGIN_REP_REALM_CHARACTERS");
|
|
if (!charCountStmt)
|
|
{
|
|
return ObjectGuid::Empty;
|
|
}
|
|
charCountStmt->setUInt32(0, 1); // Increment by 1
|
|
charCountStmt->setUInt32(1, accountId);
|
|
charCountStmt->setUInt32(2, sRealmList->GetCurrentRealmId().Realm);
|
|
loginTransaction->Append(charCountStmt);
|
|
|
|
// Commit transactions with proper error handling
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Committing database transactions");
|
|
try
|
|
{
|
|
// Use PlayerbotCharacterDBInterface for safe transaction commit
|
|
sPlayerbotCharDB->CommitTransaction(characterTransaction);
|
|
LoginDatabase.CommitTransaction(loginTransaction);
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Database transactions committed successfully");
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to commit transactions: {}", e.what());
|
|
sBotNameMgr->ReleaseName(name);
|
|
// No need to release session - we used BotSession::Create() which doesn't add to any manager
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Clean up the Player object properly before returning
|
|
newChar->CleanupsBeforeDelete();
|
|
newChar.reset(); // Explicit cleanup
|
|
// tempSession will be automatically cleaned up when shared_ptr goes out of scope
|
|
|
|
// CRITICAL: Wait for async commit to complete before returning
|
|
// The async commit is queued but not instant - we must verify the character
|
|
// exists in the database before returning, otherwise subsequent operations
|
|
// (gear scaling, talents, login) will fail because the character doesn't exist yet.
|
|
bool characterExists = false;
|
|
for (int retry = 0; retry < 100; ++retry) // 100 * 50ms = 5 seconds max
|
|
{
|
|
// Use direct SQL query to check if character exists (avoids prepared statement issues)
|
|
::std::string checkSql = Trinity::StringFormat(
|
|
"SELECT 1 FROM characters WHERE guid = {}",
|
|
characterGuid.GetCounter());
|
|
|
|
QueryResult checkResult = CharacterDatabase.Query(checkSql.c_str());
|
|
if (checkResult)
|
|
{
|
|
characterExists = true;
|
|
TC_LOG_TRACE("module.playerbot.spawner",
|
|
"Bot character {} verified in database after {} retries",
|
|
name, retry);
|
|
break;
|
|
}
|
|
|
|
// Wait a bit for async commit to complete
|
|
::std::this_thread::sleep_for(::std::chrono::milliseconds(50));
|
|
}
|
|
|
|
if (!characterExists)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Bot character {} ({}) was not found in database after 5 seconds - async commit may have failed",
|
|
name, characterGuid.ToString());
|
|
sBotNameMgr->ReleaseName(name);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// CRITICAL FIX: Add to CharacterCache so name queries work properly
|
|
// Without this, the group UI shows "??" for bot names because
|
|
// PlayerGuidLookupData::Initialize() returns false when the guid
|
|
// is not in CharacterCache (even if the player is online).
|
|
sCharacterCache->AddCharacterCacheEntry(
|
|
characterGuid,
|
|
accountId,
|
|
name,
|
|
gender,
|
|
race,
|
|
classId,
|
|
startLevel,
|
|
false // Not deleted
|
|
);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "Successfully created bot character: {} ({}) - Race: {}, Class: {}, Level: {} for account {}",
|
|
name, characterGuid.ToString(), uint32(race), uint32(classId), uint32(startLevel), accountId);
|
|
|
|
return characterGuid;
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Exception during bot character creation for account {}: {}", accountId, e.what());
|
|
// No need to release session - we used BotSession::Create() which doesn't add to any manager
|
|
return ObjectGuid::Empty;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CreateBotCharacter - Template-based overload with specific race/class/gender/name
|
|
// =============================================================================
|
|
// This overload allows BotCloneEngine and JITBotFactory to create bots from templates
|
|
// using the async-safe database path (sPlayerbotCharDB) instead of crashing
|
|
// BotCharacterCreator path.
|
|
// =============================================================================
|
|
ObjectGuid BotSpawner::CreateBotCharacter(uint32 accountId, uint8 race, uint8 classId, uint8 gender, ::std::string const& name)
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Creating bot character for account {} with template: race={}, class={}, gender={}, name={}",
|
|
accountId, race, classId, gender, name);
|
|
|
|
try
|
|
{
|
|
// ACCOUNT EXISTENCE VALIDATION
|
|
LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_BY_ID);
|
|
stmt->setUInt32(0, accountId);
|
|
PreparedQueryResult accountCheck = LoginDatabase.Query(stmt);
|
|
|
|
if (!accountCheck)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Account {} does not exist in login database! Cannot create bot character (template).",
|
|
accountId);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Check current character count for this account
|
|
CharacterDatabasePreparedStatement* charStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_SUM_CHARS);
|
|
charStmt->setUInt32(0, accountId);
|
|
PreparedQueryResult charCountResult = CharacterDatabase.Query(charStmt);
|
|
|
|
uint32 currentCharCount = 0;
|
|
if (charCountResult)
|
|
{
|
|
Field* fields = charCountResult->Fetch();
|
|
currentCharCount = fields[0].GetUInt32();
|
|
}
|
|
|
|
if (currentCharCount >= 10)
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"Account {} already has {} characters (limit: 10). Cannot create more.",
|
|
accountId, currentCharCount);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Account {} validated for template creation: {}/10 characters",
|
|
accountId, currentCharCount);
|
|
|
|
// Validate race/class combination
|
|
ChrClassesEntry const* classEntry = sChrClassesStore.LookupEntry(classId);
|
|
ChrRacesEntry const* raceEntry = sChrRacesStore.LookupEntry(race);
|
|
|
|
if (!classEntry || !raceEntry)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Invalid race ({}) or class ({}) for bot character creation", race, classId);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Generate character GUID
|
|
ObjectGuid::LowType guidLow = sObjectMgr->GetGenerator<HighGuid::Player>().Generate();
|
|
ObjectGuid characterGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
|
|
|
|
// Create character info structure
|
|
auto createInfo = ::std::make_shared<WorldPackets::Character::CharacterCreateInfo>();
|
|
createInfo->Name = name;
|
|
createInfo->Race = race;
|
|
createInfo->Class = classId;
|
|
createInfo->Sex = gender;
|
|
createInfo->UseNPE = false;
|
|
|
|
// CRITICAL FIX: Generate valid default customizations for the race/gender
|
|
// WoW 11.x requires valid customization choices for character creation
|
|
// Without this, Player::Create() fails at ValidateAppearance()
|
|
createInfo->Customizations.clear();
|
|
if (auto const* options = sDB2Manager.GetCustomiztionOptions(race, gender))
|
|
{
|
|
for (ChrCustomizationOptionEntry const* option : *options)
|
|
{
|
|
// Get available choices for this option
|
|
if (auto const* choices = sDB2Manager.GetCustomiztionChoices(option->ID))
|
|
{
|
|
if (!choices->empty())
|
|
{
|
|
// Use first valid choice for each option
|
|
UF::ChrCustomizationChoice choice;
|
|
choice.ChrCustomizationOptionID = option->ID;
|
|
choice.ChrCustomizationChoiceID = (*choices)[0]->ID;
|
|
createInfo->Customizations.push_back(choice);
|
|
}
|
|
}
|
|
}
|
|
TC_LOG_DEBUG("module.playerbot.spawner",
|
|
"Generated {} customization choices for race {} gender {}",
|
|
createInfo->Customizations.size(), race, gender);
|
|
}
|
|
else
|
|
{
|
|
TC_LOG_WARN("module.playerbot.spawner",
|
|
"No customization options found for race {} gender {} - character creation may fail",
|
|
race, gender);
|
|
}
|
|
|
|
uint8 startLevel = 1;
|
|
|
|
// Create a TEMPORARY bot session for character creation only
|
|
// CRITICAL: Use BotSession::Create() to create a standalone session for character creation.
|
|
// Standalone sessions are not added to any update loop, preventing infinite update loops
|
|
// during the creation process.
|
|
::std::shared_ptr<BotSession> tempSession = BotSession::Create(accountId);
|
|
if (!tempSession)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to create temp bot session for template-based character creation (Account: {})", accountId);
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Use smart pointer with proper RAII cleanup
|
|
::std::unique_ptr<Player> newChar = ::std::make_unique<Player>(tempSession.get());
|
|
|
|
TC_LOG_TRACE("module.playerbot.spawner", "Creating Player object with GUID {} (template-based)", guidLow);
|
|
if (!newChar->Create(guidLow, createInfo.get()))
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to create Player object for template-based bot (Race: {}, Class: {})", race, classId);
|
|
// No need to release session - we used BotSession::Create() which doesn't add to any manager
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Set starting level if different from 1
|
|
if (startLevel > 1)
|
|
{
|
|
newChar->SetLevel(startLevel);
|
|
}
|
|
|
|
newChar->setCinematic(1);
|
|
newChar->SetAtLoginFlag(AT_LOGIN_FIRST);
|
|
|
|
// POSITION VALIDATION FIX
|
|
if (newChar->GetPositionX() == 0.0f && newChar->GetPositionY() == 0.0f && newChar->GetPositionZ() == 0.0f)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"POSITION BUG DETECTED: Template bot {} has (0,0,0) position! Fixing.",
|
|
name);
|
|
|
|
PlayerInfo const* info = sObjectMgr->GetPlayerInfo(race, classId);
|
|
if (info)
|
|
{
|
|
PlayerInfo::CreatePosition const& startPos = info->createPosition;
|
|
newChar->Relocate(startPos.Loc);
|
|
newChar->SetHomebind(startPos.Loc, newChar->GetAreaId());
|
|
}
|
|
}
|
|
|
|
// Save to database using async-safe PlayerbotCharacterDBInterface
|
|
CharacterDatabaseTransaction characterTransaction = sPlayerbotCharDB->BeginTransaction();
|
|
LoginDatabaseTransaction loginTransaction = LoginDatabase.BeginTransaction();
|
|
|
|
newChar->SaveToDB(loginTransaction, characterTransaction, true);
|
|
|
|
// Update character count for account
|
|
LoginDatabasePreparedStatement* charCountStmt = GetSafeLoginPreparedStatement(LOGIN_REP_REALM_CHARACTERS, "LOGIN_REP_REALM_CHARACTERS");
|
|
if (!charCountStmt)
|
|
{
|
|
return ObjectGuid::Empty;
|
|
}
|
|
charCountStmt->setUInt32(0, 1);
|
|
charCountStmt->setUInt32(1, accountId);
|
|
charCountStmt->setUInt32(2, sRealmList->GetCurrentRealmId().Realm);
|
|
loginTransaction->Append(charCountStmt);
|
|
|
|
// Commit transactions using async-safe path
|
|
try
|
|
{
|
|
sPlayerbotCharDB->CommitTransaction(characterTransaction);
|
|
LoginDatabase.CommitTransaction(loginTransaction);
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Failed to commit template bot transactions: {}", e.what());
|
|
// No need to release session - we used BotSession::Create() which doesn't add to any manager
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// Clean up the Player object properly
|
|
newChar->CleanupsBeforeDelete();
|
|
newChar.reset();
|
|
// tempSession will be automatically cleaned up when shared_ptr goes out of scope
|
|
|
|
// CRITICAL: Wait for async commit to complete before returning
|
|
// The async commit is queued but not instant - we must verify the character
|
|
// exists in the database before returning, otherwise subsequent operations
|
|
// (gear scaling, talents, login) will fail because the character doesn't exist yet.
|
|
bool characterExists = false;
|
|
for (int retry = 0; retry < 100; ++retry) // 100 * 50ms = 5 seconds max
|
|
{
|
|
// Use direct SQL query to check if character exists (avoids prepared statement issues)
|
|
::std::string checkSql = Trinity::StringFormat(
|
|
"SELECT 1 FROM characters WHERE guid = {}",
|
|
characterGuid.GetCounter());
|
|
|
|
QueryResult checkResult = CharacterDatabase.Query(checkSql.c_str());
|
|
if (checkResult)
|
|
{
|
|
characterExists = true;
|
|
TC_LOG_TRACE("module.playerbot.spawner",
|
|
"Template bot character {} verified in database after {} retries",
|
|
name, retry);
|
|
break;
|
|
}
|
|
|
|
// Wait a bit for async commit to complete
|
|
::std::this_thread::sleep_for(::std::chrono::milliseconds(50));
|
|
}
|
|
|
|
if (!characterExists)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner",
|
|
"Template bot character {} ({}) was not found in database after 5 seconds - async commit may have failed",
|
|
name, characterGuid.ToString());
|
|
return ObjectGuid::Empty;
|
|
}
|
|
|
|
// CRITICAL FIX: Add to CharacterCache so name queries work properly
|
|
// Without this, the group UI shows "??" for bot names because
|
|
// PlayerGuidLookupData::Initialize() returns false when the guid
|
|
// is not in CharacterCache (even if the player is online).
|
|
sCharacterCache->AddCharacterCacheEntry(
|
|
characterGuid,
|
|
accountId,
|
|
name,
|
|
gender,
|
|
race,
|
|
classId,
|
|
startLevel,
|
|
false // Not deleted
|
|
);
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "Successfully created template-based bot character: {} ({}) - Race: {}, Class: {} for account {}",
|
|
name, characterGuid.ToString(), uint32(race), uint32(classId), accountId);
|
|
|
|
return characterGuid;
|
|
}
|
|
catch (::std::exception const& e)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "Exception during template-based bot character creation for account {}: {}", accountId, e.what());
|
|
// No need to release session - we used BotSession::Create() which doesn't add to any manager
|
|
return ObjectGuid::Empty;
|
|
}
|
|
}
|
|
|
|
void BotSpawner::OnPlayerLogin()
|
|
{
|
|
if (!_enabled.load() || !_config.enableDynamicSpawning)
|
|
return;
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", " Player logged in - triggering bot spawn check");
|
|
|
|
// Force immediate spawn check
|
|
CheckAndSpawnForPlayers();
|
|
}
|
|
|
|
void BotSpawner::CheckAndSpawnForPlayers()
|
|
{
|
|
if (!_enabled.load() || !_config.enableDynamicSpawning)
|
|
return;
|
|
|
|
// DEADLOCK FIX: Prevent reentrant calls that cause mutex deadlocks
|
|
bool expected = false;
|
|
if (!_inCheckAndSpawn.compare_exchange_strong(expected, true))
|
|
{
|
|
TC_LOG_TRACE("module.playerbot.spawner", "CheckAndSpawnForPlayers already running, skipping reentrant call");
|
|
return;
|
|
}
|
|
|
|
// RAII guard to ensure flag is reset even if exception occurs
|
|
struct ScopeGuard {
|
|
::std::atomic<bool>& flag;
|
|
~ScopeGuard() { flag.store(false); }
|
|
} guard{_inCheckAndSpawn};
|
|
|
|
// Count real (non-bot) players
|
|
uint32 activeSessions = sWorld->GetActiveAndQueuedSessionCount();
|
|
uint32 botSessions = Playerbot::sBotWorldSessionMgr->GetBotCount();
|
|
uint32 realPlayerSessions = (activeSessions > botSessions) ? (activeSessions - botSessions) : 0;
|
|
|
|
// Check if we have real players but no bots spawned yet
|
|
if (realPlayerSessions > 0)
|
|
{
|
|
uint32 currentBotCount = GetActiveBotCount();
|
|
uint32 targetBotCount = static_cast<uint32>(realPlayerSessions * _config.botToPlayerRatio);
|
|
|
|
// Ensure minimum bots
|
|
uint32 minimumBots = 3; // sPlayerbotConfig->GetInt("Playerbot.MinimumBotsPerZone", 3);
|
|
targetBotCount = ::std::max(targetBotCount, minimumBots);
|
|
|
|
// Respect maximum limits
|
|
targetBotCount = ::std::min(targetBotCount, _config.maxBotsTotal);
|
|
|
|
if (currentBotCount < targetBotCount)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner",
|
|
" Real players detected! Players: {}, Current bots: {}, Target bots: {}",
|
|
realPlayerSessions, currentBotCount, targetBotCount);
|
|
|
|
// Mark that we've triggered spawning for the first player
|
|
if (!_firstPlayerSpawned.load() && realPlayerSessions > 0)
|
|
{
|
|
_firstPlayerSpawned.store(true);
|
|
TC_LOG_INFO("module.playerbot.spawner", " First player detected - initiating initial bot spawn");
|
|
}
|
|
|
|
// DEADLOCK FIX: Force immediate spawn cycle by resetting timer
|
|
// This allows Update() to handle spawning on next cycle WITHOUT recursive calls
|
|
_lastTargetCalculation = 0; // Force immediate spawn in next Update() cycle
|
|
TC_LOG_INFO("module.playerbot.spawner", " Spawn cycle timer reset - bots will spawn in next Update()");
|
|
}
|
|
}
|
|
|
|
// Store the last known real player count
|
|
_lastRealPlayerCount.store(realPlayerSessions);
|
|
}
|
|
|
|
// ===================================================================================
|
|
// CHARACTER CREATION SUPPORT (for .bot spawn command)
|
|
// ===================================================================================
|
|
|
|
bool BotSpawner::CreateAndSpawnBot(
|
|
uint32 masterAccountId,
|
|
uint8 classId,
|
|
uint8 race,
|
|
uint8 gender,
|
|
::std::string const& name,
|
|
ObjectGuid& outCharacterGuid)
|
|
{
|
|
TC_LOG_INFO("module.playerbot.spawner", "CreateAndSpawnBot: Creating new bot for account {} (race: {}, class: {}, gender: {}, name: '{}')",
|
|
masterAccountId, race, classId, gender, name);
|
|
|
|
// Step 1: Create the character
|
|
::std::string errorMsg;
|
|
BotCharacterCreator::CreateResult result = BotCharacterCreator::CreateBotCharacter(
|
|
masterAccountId,
|
|
race,
|
|
classId,
|
|
gender,
|
|
name,
|
|
outCharacterGuid,
|
|
errorMsg);
|
|
|
|
if (result != BotCharacterCreator::CreateResult::SUCCESS)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "CreateAndSpawnBot: Character creation failed for '{}' - {} ({})",
|
|
name, BotCharacterCreator::ResultToString(result), errorMsg);
|
|
return false;
|
|
}
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "CreateAndSpawnBot: Character '{}' created successfully with GUID {}",
|
|
name, outCharacterGuid.ToString());
|
|
|
|
// Step 2: Spawn the bot immediately via BotWorldSessionMgr
|
|
// This adds the bot to the active bot session pool
|
|
bool spawnSuccess = sBotWorldSessionMgr->AddPlayerBot(outCharacterGuid, masterAccountId);
|
|
|
|
if (!spawnSuccess)
|
|
{
|
|
TC_LOG_ERROR("module.playerbot.spawner", "CreateAndSpawnBot: Failed to spawn bot '{}' (GUID: {}) after character creation",
|
|
name, outCharacterGuid.ToString());
|
|
return false;
|
|
}
|
|
|
|
TC_LOG_INFO("module.playerbot.spawner", "CreateAndSpawnBot: Bot '{}' (GUID: {}) spawned successfully and added to active session pool",
|
|
name, outCharacterGuid.ToString());
|
|
|
|
// Step 3: Update spawn statistics
|
|
_stats.totalSpawned.fetch_add(1);
|
|
_stats.currentlyActive.fetch_add(1);
|
|
|
|
uint32 currentActive = _stats.currentlyActive.load();
|
|
uint32 peakConcurrent = _stats.peakConcurrent.load();
|
|
while (currentActive > peakConcurrent &&
|
|
!_stats.peakConcurrent.compare_exchange_weak(peakConcurrent, currentActive))
|
|
{
|
|
// Loop until we successfully update peak concurrent
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace Playerbot
|