/* * Copyright (C) 2024 TrinityCore * * 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 #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 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 #include #include #include 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(_resourceMonitor.GetPressureLevel()), static_cast(_circuitBreaker.GetState()), static_cast(_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 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(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 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(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(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(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(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(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(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 const& requests) { uint32 successCount = 0; // Collect valid requests first ::std::vector 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(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 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 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 callback) { TC_LOG_TRACE("module.playerbot.spawner", "Selecting character for spawn request asynchronously"); // Get available accounts if not specified ::std::vector 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 BotSpawner::GetAvailableCharacters(uint32 accountId, SpawnRequest const& request) { ::std::vector 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(request.minLevel) << " AND " << static_cast(request.maxLevel); else if (request.minLevel > 0) query << " AND level >= " << static_cast(request.minLevel); else if (request.maxLevel > 0) query << " AND level <= " << static_cast(request.maxLevel); } if (request.raceFilter > 0) query << " AND race = " << static_cast(request.raceFilter); if (request.classFilter > 0) query << " AND class = " << static_cast(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)> 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()); 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 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(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(CHAR_SEL_CHARS_BY_ACCOUNT_ID)); sPlayerbotCharDB->ExecuteAsync(stmt, ::std::move(queryCallback)); } void BotSpawner::SelectCharacterAsyncRecursive(::std::vector accounts, size_t index, SpawnRequest const& request, ::std::function 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 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(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::accessor acc; _activeBots.insert(acc, characterGuid); acc->second = zoneId; } { tbb::concurrent_hash_map>::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::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>::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 oldBots; _activeBots.swap(oldBots); // Atomic operation - now we own the old map tbb::concurrent_hash_map> 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>::const_accessor it; botCount = _botsByZone.find(it, zoneId) ? it->second.size() : 0; } // Phase 2: Update zone data with separate lock { tbb::concurrent_hash_map::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::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>::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::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::const_accessor it; return _activeBots.find(it, guid); } ::std::vector BotSpawner::GetActiveBotsInZone(uint32 zoneId) const { tbb::concurrent_hash_map>::const_accessor it; if (_botsByZone.find(it, zoneId)) return it->second; return {}; } ::std::vector BotSpawner::GetAllZonePopulations() const { ::std::vector result; result.reserve(_zonePopulations.size()); tbb::concurrent_hash_map::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(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> zonesCopy; ::std::vector<::std::pair> 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::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(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(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 spawnRequests; ::std::vector<::std::pair> 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::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(zonePlacement->minLevel); newZone.maxLevel = static_cast(zonePlacement->maxLevel); newZone.botDensity = 0.5f; newZone.lastUpdate = ::std::chrono::system_clock::now(); tbb::concurrent_hash_map::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::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(zonePlacement->minLevel); newZone.maxLevel = static_cast(zonePlacement->maxLevel); newZone.botDensity = 0.5f; newZone.lastUpdate = ::std::chrono::system_clock::now(); tbb::concurrent_hash_map::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::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().Generate(); ObjectGuid characterGuid = ObjectGuid::Create(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(); 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 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 newChar = ::std::make_unique(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().Generate(); ObjectGuid characterGuid = ObjectGuid::Create(guidLow); // Create character info structure auto createInfo = ::std::make_shared(); 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 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 newChar = ::std::make_unique(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& 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(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