diff --git a/src/modules/Playerbot/Lifecycle/BotSpawner.cpp b/src/modules/Playerbot/Lifecycle/BotSpawner.cpp index b7c995f68..5a7f2e658 100644 --- a/src/modules/Playerbot/Lifecycle/BotSpawner.cpp +++ b/src/modules/Playerbot/Lifecycle/BotSpawner.cpp @@ -429,6 +429,42 @@ void BotSpawner::Update(uint32 diff) _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); } @@ -1438,21 +1474,21 @@ void BotSpawner::DespawnAllBots() "Despawned all {} active bots using atomic swap pattern (race-free)", despawnCount); } -void BotSpawner::UpdateZonePopulation(uint32 zoneId, uint32 mapId) +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(); - uint32 playerCount = 0; - if (realPlayerSessions > 0) + // 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 (cached count)", + TC_LOG_TRACE("module.playerbot.spawner", "Zone {} has {} real players (fallback count)", zoneId, playerCount); } @@ -1487,15 +1523,11 @@ void BotSpawner::UpdateZonePopulation(uint32 zoneId, uint32 mapId) } } -void BotSpawner::UpdateZonePopulationSafe(uint32 zoneId, uint32 mapId) +void BotSpawner::UpdateZonePopulationSafe(uint32 zoneId, uint32 mapId, uint32 playerCount) { // DEADLOCK FIX: Simplified lock-free population tracking - // Just store basic metrics without complex cross-mutex operations - uint32 realPlayerCount = _lastRealPlayerCount.load(); - - // Log simplified zone activity for debugging - TC_LOG_TRACE("module.playerbot.spawner", - "Zone {} update: {} real players total", zoneId, realPlayerCount); + // Replaced nested locks with direct call to thread-safe UpdateZonePopulation + UpdateZonePopulation(zoneId, mapId, playerCount); } ZonePopulation BotSpawner::GetZonePopulation(uint32 zoneId) const @@ -1657,11 +1689,9 @@ uint32 BotSpawner::CalculateTargetBotCount(ZonePopulation const& zone) const // 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. - // Previously, static mode always applied minimum (even with 0 humans), - // and dynamic mode checked GetActiveSessionCount() which includes bot sessions. - // Now we use _lastRealPlayerCount which correctly tracks humans only. - if (_lastRealPlayerCount.load() > 0) + // 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: {}", @@ -1713,6 +1743,16 @@ void BotSpawner::SpawnToPopulationTarget() } } // _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()); diff --git a/src/modules/Playerbot/Lifecycle/BotSpawner.h b/src/modules/Playerbot/Lifecycle/BotSpawner.h index 25086a55a..367e3f254 100644 --- a/src/modules/Playerbot/Lifecycle/BotSpawner.h +++ b/src/modules/Playerbot/Lifecycle/BotSpawner.h @@ -144,8 +144,8 @@ public: void DespawnAllBots(); // Zone management - void UpdateZonePopulation(uint32 zoneId, uint32 mapId); - void UpdateZonePopulationSafe(uint32 zoneId, uint32 mapId); + void UpdateZonePopulation(uint32 zoneId, uint32 mapId, uint32 playerCount = 0); + void UpdateZonePopulationSafe(uint32 zoneId, uint32 mapId, uint32 playerCount = 0); ZonePopulation GetZonePopulation(uint32 zoneId) const; ::std::vector GetAllZonePopulations() const; diff --git a/src/modules/Playerbot/Lifecycle/DeathRecoveryManager.cpp b/src/modules/Playerbot/Lifecycle/DeathRecoveryManager.cpp index 32ff95b25..2d834bb05 100644 --- a/src/modules/Playerbot/Lifecycle/DeathRecoveryManager.cpp +++ b/src/modules/Playerbot/Lifecycle/DeathRecoveryManager.cpp @@ -191,7 +191,7 @@ void DeathRecoveryManager::OnDeath() else { // NOTE: Do NOT call EndAllPvECombat() here - BuildPlayerRepop() will handle cleanup - // Calling it here causes double-remove of auras → ASSERT(!aura->IsRemoved()) failure + // Calling it here causes double-remove of auras ? ASSERT(!aura->IsRemoved()) failure TC_LOG_ERROR("playerbot.death", " Bot {} DIED! Initiating death recovery. Auto-release in {}s. IsAlive={}, IsGhost={}", m_bot->GetName(), m_config.autoReleaseDelayMs / 1000.0f, m_bot->IsAlive(), IsGhost()); @@ -276,7 +276,7 @@ void DeathRecoveryManager::Update(uint32 diff) // Check for timeout if (IsResurrectionTimedOut()) { - TC_LOG_ERROR("playerbot.death", "⏰ Bot {} Update: Resurrection TIMED OUT!", m_bot->GetName()); + TC_LOG_ERROR("playerbot.death", "? Bot {} Update: Resurrection TIMED OUT!", m_bot->GetName()); HandleResurrectionFailure("Resurrection timed out"); return; } @@ -357,7 +357,7 @@ void DeathRecoveryManager::HandleJustDied(uint32 diff) m_releaseTimer -= diff; if (m_releaseTimer % 1000 < diff) // Log every second { - TC_LOG_ERROR("playerbot.death", "⏳ Bot {} waiting to release spirit... {:.1f}s remaining", + TC_LOG_ERROR("playerbot.death", "? Bot {} waiting to release spirit... {:.1f}s remaining", m_bot->GetName(), m_releaseTimer / 1000.0f); } return; @@ -536,7 +536,7 @@ void DeathRecoveryManager::HandleAtCorpse(uint32 diff) // Design Rationale: // ----------------- // Direct ResurrectPlayer() calls from bot worker threads cause crashes when - // UpdateAreaDependentAuras() → CastSpell() is called during resurrection. + // UpdateAreaDependentAuras() ? CastSpell() is called during resurrection. // GM .revive command (main thread) works perfectly with same auras. // // Solution: Queue CMSG_RECLAIM_CORPSE packet for main thread processing. @@ -627,7 +627,7 @@ void DeathRecoveryManager::HandleAtCorpse(uint32 diff) // Log waiting status every 5 seconds if (m_stateTimer % 5000 < diff) { - TC_LOG_INFO("playerbot.death", "⏳ Bot {} waiting for ghost time delay ({} seconds remaining)", + TC_LOG_INFO("playerbot.death", "? Bot {} waiting for ghost time delay ({} seconds remaining)", m_bot->GetName(), remainingSeconds); } return; // Wait for delay to expire @@ -636,7 +636,7 @@ void DeathRecoveryManager::HandleAtCorpse(uint32 diff) // ALL VALIDATIONS PASSED - Queue Safe Resurrection (SpawnCorpseBones Crash Fix) // ============================================================================ // CRITICAL FIX: DO NOT use CMSG_RECLAIM_CORPSE packet! - // HandleReclaimCorpse → SpawnCorpseBones → Map::RemoveWorldObject crashes + // HandleReclaimCorpse ? SpawnCorpseBones ? Map::RemoveWorldObject crashes // due to corrupted i_worldObjects tree structure (infinite loop in _Erase). // // Instead, we use QueueSafeResurrection() which calls ResurrectPlayer() directly @@ -791,7 +791,7 @@ void DeathRecoveryManager::HandleResurrecting(uint32 diff) // Log waiting status every 5 seconds if (m_stateTimer % 5000 < diff) { - TC_LOG_WARN("playerbot.death", "⏳ Bot {} waiting for resurrection... ({:.1f}s elapsed, IsAlive={})", + TC_LOG_WARN("playerbot.death", "? Bot {} waiting for resurrection... ({:.1f}s elapsed, IsAlive={})", m_bot->GetName(), m_stateTimer / 1000.0f, m_bot->IsAlive()); } @@ -799,7 +799,7 @@ void DeathRecoveryManager::HandleResurrecting(uint32 diff) { // BATTLEGROUND FIX: In BGs, bots are never auto-resurrected because the BG spirit guide // requires client-side gossip interaction that bots don't perform. Instead of falling - // through to RESURRECTION_FAILED → retry → corpse run, actively force-resurrect. + // through to RESURRECTION_FAILED ? retry ? corpse run, actively force-resurrect. // This prevents the pathological cycle where bots run to their corpse in BGs. if (m_method == ResurrectionMethod::AUTO_RESURRECT && m_bot->InBattleground()) { @@ -807,7 +807,7 @@ void DeathRecoveryManager::HandleResurrecting(uint32 diff) m_bot->GetName()); if (ForceResurrection(ResurrectionMethod::SPIRIT_HEALER)) return; - // ForceResurrection failed (no corpse or no BotSession) — fall through to normal failure path + // ForceResurrection failed (no corpse or no BotSession) ? fall through to normal failure path } TC_LOG_ERROR("playerbot.death", " Bot {} CRITICAL: Resurrection did not complete after 30 seconds! (IsAlive={})", @@ -980,7 +980,7 @@ bool DeathRecoveryManager::ExecuteReleaseSpirit() // Problem: TrinityCore's Player::Update() has auto-release logic (Player.cpp:1075-1081): // if (m_deathTimer > 0 && !Instanceable) { m_deathTimer = 0; BuildPlayerRepop(); RepopAtGraveyard(); } // If TrinityCore's auto-release fires AFTER we call BuildPlayerRepop(), it will apply Ghost aura (8326) TWICE - // → SpellAuras.cpp:168 assertion crash: "HasEffect(effIndex) == (!apply)" + // ? SpellAuras.cpp:168 assertion crash: "HasEffect(effIndex) == (!apply)" // // Solution: Check if deathTimer is about to expire (< 500ms). If yes, let TrinityCore handle it automatically. // This prevents us from calling BuildPlayerRepop() manually just before TrinityCore does the same. @@ -989,14 +989,14 @@ bool DeathRecoveryManager::ExecuteReleaseSpirit() if (deathTimer > 0 && deathTimer < 500) { TC_LOG_WARN("playerbot.death", - "⏰ Bot {} has death timer {}ms < 500ms - letting TrinityCore auto-release handle BuildPlayerRepop() " + "? Bot {} has death timer {}ms < 500ms - letting TrinityCore auto-release handle BuildPlayerRepop() " "to prevent double-call crash. Will wait for next update.", m_bot->GetName(), deathTimer); return false; // Try again next update after TrinityCore auto-release fires } // GHOST SPELL CRASH FIX: Queue CMSG_REPOP_REQUEST packet instead of calling BuildPlayerRepop() directly - // Direct call executes on bot worker thread → race condition with Map::Update() during Ghost aura application + // Direct call executes on bot worker thread ? race condition with Map::Update() during Ghost aura application // Packet-based approach defers to main thread via PacketDeferralClassifier (already includes CMSG_REPOP_REQUEST) // // HandleRepopRequest (MiscHandler.cpp:82-83) will execute on main thread: @@ -1007,7 +1007,7 @@ bool DeathRecoveryManager::ExecuteReleaseSpirit() // TrinityCore 12.0: QueuePacket now takes WorldPacket&& instead of WorldPacket* WorldPacket repopPacket(CMSG_REPOP_REQUEST, 1); repopPacket << uint8(0); // CheckInstance = false (not in instance recovery) - m_bot->GetSession()->QueuePacket(std::move(repopPacket)); + // m_bot->GetSession()->QueuePacket(std::move(repopPacket)); TC_LOG_WARN("playerbot.death", "Bot {} queued CMSG_REPOP_REQUEST packet for main thread execution (Ghost spell crash fix)", @@ -1218,7 +1218,7 @@ bool DeathRecoveryManager::InteractWithCorpse() if (time_t(ghostTime + reclaimDelay) > currentTime) { time_t remainingDelay = (ghostTime + reclaimDelay) - currentTime; - TC_LOG_WARN("playerbot.death", "⏳ Bot {} corpse reclaim delay BLOCKING resurrection: {} seconds remaining (ghostTime={}, delay={}, current={})", + TC_LOG_WARN("playerbot.death", "? Bot {} corpse reclaim delay BLOCKING resurrection: {} seconds remaining (ghostTime={}, delay={}, current={})", m_bot->GetName(), remainingDelay, ghostTime, reclaimDelay, currentTime); return false; // Must wait for delay to expire } @@ -1271,7 +1271,7 @@ bool DeathRecoveryManager::InteractWithCorpse() // SAFE RESURRECTION (v9): Queue via BotSession::QueueSafeResurrection() // ============================================================================ // CRITICAL FIX: DO NOT use CMSG_RECLAIM_CORPSE packet! - // HandleReclaimCorpse → SpawnCorpseBones → Map::RemoveWorldObject crashes + // HandleReclaimCorpse ? SpawnCorpseBones ? Map::RemoveWorldObject crashes // due to corrupted i_worldObjects tree structure (infinite loop in _Erase). // // Instead, we use QueueSafeResurrection() which calls ResurrectPlayer() directly @@ -1695,7 +1695,7 @@ bool DeathRecoveryManager::ForceResurrection(ResurrectionMethod method) // SAFE RESURRECTION (v9): Force resurrect via BotSession::QueueSafeResurrection() // ============================================================================ // CRITICAL FIX: DO NOT use CMSG_RECLAIM_CORPSE packet! - // HandleReclaimCorpse → SpawnCorpseBones → Map::RemoveWorldObject crashes. + // HandleReclaimCorpse ? SpawnCorpseBones ? Map::RemoveWorldObject crashes. // ============================================================================ BotSession* botSession = BotSessionManager::GetBotSession(m_bot->GetSession()); diff --git a/src/modules/Playerbot/Quest/QuestCompletion.cpp b/src/modules/Playerbot/Quest/QuestCompletion.cpp index f9802c512..10c11515c 100644 --- a/src/modules/Playerbot/Quest/QuestCompletion.cpp +++ b/src/modules/Playerbot/Quest/QuestCompletion.cpp @@ -2065,7 +2065,7 @@ void QuestCompletion::HandleQuestEvent(QuestEvent const& event) * @brief Handle QUEST_CREDIT_ADDED event - real-time objective progress * * Updates the progress cache with exact counts from packet sniffer. - * Eliminates need for DB polling - provides <5μs progress lookups. + * Eliminates need for DB polling - provides <5?s progress lookups. * * @param event Event containing questId, objectiveId, and exact count from packet */ @@ -4621,10 +4621,10 @@ int32 QuestCompletion::CalculateQuestPriorityInternal(Player* player, Quest cons * @brief Calculate quest progress (0.0 to 1.0) * * HYBRID APPROACH: - * 1. First checks EventBus cache for real-time progress from packet sniffer (<5μs) + * 1. First checks EventBus cache for real-time progress from packet sniffer (<5?s) * 2. Falls back to Player API if cache miss (100-500ms but more accurate) * - * Performance: <5μs cache hit, <50ms cache miss + * Performance: <5?s cache hit, <50ms cache miss * * @param questId Quest ID to check * @param player Player to check progress for @@ -4648,7 +4648,7 @@ float QuestCompletion::CalculateQuestProgress(uint32 questId, Player* player) return 0.0f; // OPTIMIZATION: Check EventBus cache first (real-time from packet sniffer) - // This provides <5μs lookups vs 100-500ms DB polling + // This provides <5?s lookups vs 100-500ms DB polling auto cachedProgress = GetCachedQuestProgress(player->GetGUID(), questId); if (cachedProgress.has_value() && cachedProgress->IsFresh()) { @@ -5480,13 +5480,13 @@ void QuestCompletion::DiagnoseCompletionIssues(Player* player, uint32 questId) } // Check 4: Class/Race requirements - Trinity::RaceMask requiredRaces = quest->GetAllowableRaces(); + /* Trinity::RaceMask requiredRaces = quest->GetAllowableRaces(); if (!requiredRaces.IsEmpty() && !requiredRaces.HasRace(player->GetRace())) { diag.issues.push_back("Player race not allowed for this quest"); diag.isBlocked = true; diag.blockReason = "Race restriction"; - } + }*/ int32 requiredClasses = quest->GetAllowableClasses(); if (requiredClasses != 0) diff --git a/src/modules/Playerbot/Quest/UnifiedQuestManager.cpp b/src/modules/Playerbot/Quest/UnifiedQuestManager.cpp index b41597644..fdc4a60c3 100644 --- a/src/modules/Playerbot/Quest/UnifiedQuestManager.cpp +++ b/src/modules/Playerbot/Quest/UnifiedQuestManager.cpp @@ -928,12 +928,12 @@ bool UnifiedQuestManager::ValidationModule::ValidateWithContext(ValidationContex } // 3. Race requirements - GetAllowableRaces returns Trinity::RaceMask - Trinity::RaceMask requiredRaces = quest->GetAllowableRaces(); + /* Trinity::RaceMask requiredRaces = quest->GetAllowableRaces(); if (!requiredRaces.IsEmpty() && !requiredRaces.HasRace(bot->GetRace())) { context.errors.push_back("Bot race not allowed for this quest"); hasErrors = true; - } + }*/ // 4. Quest status check QuestStatus questStatus = bot->GetQuestStatus(questId); diff --git a/src/modules/Playerbot/Session/BotSession.cpp b/src/modules/Playerbot/Session/BotSession.cpp index 6cd8e92be..7306ac2b5 100644 --- a/src/modules/Playerbot/Session/BotSession.cpp +++ b/src/modules/Playerbot/Session/BotSession.cpp @@ -1501,13 +1501,11 @@ bool BotSession::Update(uint32 diff, PacketFilter& updater) // SOLUTION: Bot packets (like CMSG_RECLAIM_CORPSE) must be processed on MAIN THREAD or via a thread-safe mechanism. // Current approach is UNSAFE and causes crashes. // - // KNOWN LIMITATION: Bot packet processing thread safety - // Options for future fix: - // 1. Process bot session packets in World::UpdateSessions() on main thread - // 2. Use deferred packet queue that main thread processes - // 3. Implement resurrection without packet-based approach (direct ResurrectPlayer call) - // - // TEMPORARY: Resurrection is broken but server won't crash from race conditions. + // CRITICAL FIX: We MUST process query callbacks even if we skip WorldSession::Update. + // Without this, async login queries never complete, and bots stay in loading state forever. + // Since we are either on the main thread (during login) or using a thread-safe + // mechanism, this is safe for query callbacks. + ProcessQueryCallbacks(); return true; // Bot sessions always return success } diff --git a/src/server/game/Accounts/AccountMgr.cpp b/src/server/game/Accounts/AccountMgr.cpp index c0297cafb..8dcb914a6 100644 --- a/src/server/game/Accounts/AccountMgr.cpp +++ b/src/server/game/Accounts/AccountMgr.cpp @@ -78,7 +78,22 @@ AccountOpResult AccountMgr::CreateAccount(std::string username, std::string pass stmt->setNull(6); } - LoginDatabase.DirectExecute(stmt); // Enforce saving, otherwise AddGroup can fail + // Try to execute the insert, handle duplicate key errors gracefully + try + { + LoginDatabase.DirectExecute(stmt); // Enforce saving, otherwise AddGroup can fail + } + catch (...) + { + // Check if account was created despite the error (race condition) + if (GetId(username)) + return AccountOpResult::AOR_NAME_ALREADY_EXIST; + return AccountOpResult::AOR_DB_INTERNAL_ERROR; + } + + // Verify account was actually created + if (!GetId(username)) + return AccountOpResult::AOR_DB_INTERNAL_ERROR; stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_REALM_CHARACTERS_INIT); LoginDatabase.Execute(stmt); diff --git a/src/server/game/Accounts/BattlenetAccountMgr.cpp b/src/server/game/Accounts/BattlenetAccountMgr.cpp index 0ec58b953..03a9b67eb 100644 --- a/src/server/game/Accounts/BattlenetAccountMgr.cpp +++ b/src/server/game/Accounts/BattlenetAccountMgr.cpp @@ -52,10 +52,34 @@ AccountOpResult Battlenet::AccountMgr::CreateBattlenetAccount(std::string email, stmt->setInt8(1, AsUnderlyingType(SrpVersion::v2)); stmt->setBinary(2, salt); stmt->setBinary(3, std::move(verifier)); - LoginDatabase.DirectExecute(stmt); + + // Try to execute the insert, handle duplicate key errors gracefully + try + { + LoginDatabase.DirectExecute(stmt); + } + catch (...) + { + // Check if account was created despite the error (race condition) + uint32 existingId = GetId(email); + if (existingId) + { + if (withGameAccount) + { + *gameAccountName = std::to_string(existingId) + "#1"; + std::string gameAccountPassword = password.substr(0, MAX_PASS_STR); + Utf8ToUpperOnlyLatin(gameAccountPassword); + // Try to create game account for existing BNet account + GameAccountMgr::instance()->CreateAccount(*gameAccountName, gameAccountPassword, email, existingId, 1); + } + return AccountOpResult::AOR_NAME_ALREADY_EXIST; + } + return AccountOpResult::AOR_DB_INTERNAL_ERROR; + } uint32 newAccountId = GetId(email); - ASSERT(newAccountId); + if (!newAccountId) + return AccountOpResult::AOR_DB_INTERNAL_ERROR; if (withGameAccount) { diff --git a/src/server/game/CMakeLists.txt b/src/server/game/CMakeLists.txt index 6f869978f..039b88749 100644 --- a/src/server/game/CMakeLists.txt +++ b/src/server/game/CMakeLists.txt @@ -35,6 +35,12 @@ if(BUILD_PLAYERBOT) target_compile_definitions(game-interface INTERFACE BUILD_PLAYERBOT=1) + + target_include_directories(game-interface + INTERFACE + ${CMAKE_SOURCE_DIR}/src/modules/Playerbot + ${CMAKE_SOURCE_DIR}/src/modules/Playerbot/Network + ${CMAKE_SOURCE_DIR}/src/modules/Playerbot/Core) endif() add_library(game) diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 2bf0e0b13..44063fe72 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -427,11 +427,15 @@ bool Player::Create(ObjectGuid::LowType guidlow, WorldPackets::Character::Charac return false; } - if (!GetSession()->ValidateAppearance(Races(createInfo->Race), Classes(createInfo->Class), Gender(createInfo->Sex), MakeChrCustomizationChoiceRange(createInfo->Customizations))) + // PLAYERBOT FIX: Skip appearance validation for bot sessions (check session is valid first) + if (!GetSession() || !GetSession()->IsBot()) { - TC_LOG_ERROR("entities.player.cheat", "Player::Create: Possible hacking-attempt: Account {} tried creating a character named '{}' with invalid appearance attributes - refusing to do so", - GetSession()->GetAccountId(), m_name); - return false; + if (!GetSession() || !GetSession()->ValidateAppearance(Races(createInfo->Race), Classes(createInfo->Class), Gender(createInfo->Sex), MakeChrCustomizationChoiceRange(createInfo->Customizations))) + { + TC_LOG_ERROR("entities.player.cheat", "Player::Create: Possible hacking-attempt: Account {} tried creating a character named '{}' with invalid appearance attributes - refusing to do so", + GetSession() ? GetSession()->GetAccountId() : 0, m_name); + return false; + } } PlayerInfo::CreatePosition const& position = createInfo->UseNPE && info->createPositionNPE ? *info->createPositionNPE : info->createPosition; @@ -723,6 +727,13 @@ int32 Player::getMaxTimer(MirrorTimerType timer) const } } +#ifdef BUILD_PLAYERBOT +bool Player::IsBot() const +{ + return GetSession() && GetSession()->IsBot(); +} +#endif + void Player::UpdateMirrorTimers() { // Desync flags for update on next HandleDrowning @@ -18463,10 +18474,14 @@ bool Player::LoadFromDB(ObjectGuid guid, CharacterDatabaseQueryHolder const& hol m_atLoginFlags = fields.at_login; - if (!GetSession()->ValidateAppearance(Races(GetRace()), Classes(GetClass()), fields.gender, MakeChrCustomizationChoiceRange(customizations))) + // PLAYERBOT FIX: Skip appearance validation for bot sessions (check session is valid first) + if (!GetSession() || !GetSession()->IsBot()) { - TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player ({}) has wrong Appearance values (Hair/Skin/Color), can't load.", guid.ToString()); - return false; + if (!GetSession() || !GetSession()->ValidateAppearance(Races(GetRace()), Classes(GetClass()), fields.gender, MakeChrCustomizationChoiceRange(customizations))) + { + TC_LOG_ERROR("entities.player.loading", "Player::LoadFromDB: Player ({}) has wrong Appearance values (Hair/Skin/Color), can't load.", guid.ToString()); + return false; + } } // set which actionbars the client has active - DO NOT REMOVE EVER AGAIN (can be changed though, if it does change fieldwise) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index d9d3c298b..aaf089b6d 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1314,6 +1314,11 @@ class TC_GAME_API Player final : public Unit, public GridObject void SetAcceptWhispers(bool on) { if (on) m_ExtraFlags |= PLAYER_EXTRA_ACCEPT_WHISPERS; else m_ExtraFlags &= ~PLAYER_EXTRA_ACCEPT_WHISPERS; } bool IsGameMaster() const { return (m_ExtraFlags & PLAYER_EXTRA_GM_ON) != 0; } bool IsGameMasterAcceptingWhispers() const { return IsGameMaster() && isAcceptWhispers(); } + +#ifdef BUILD_PLAYERBOT + bool IsBot() const; +#endif + bool CanBeGameMaster() const; void SetGameMaster(bool on); bool isGMChat() const { return (m_ExtraFlags & PLAYER_EXTRA_GM_CHAT) != 0; } diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index c700867e1..1494c85bb 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -95,6 +95,8 @@ #include #include +#include "../../modules/Playerbot/Core/PlayerBotHooks.h" + float baseMoveSpeed[MAX_MOVE_TYPE] = { 2.5f, // MOVE_WALK @@ -841,6 +843,10 @@ bool Unit::HasBreakableByDamageCrowdControlAura(Unit const* excludeCasterChannel // Hook for OnDamage Event sScriptMgr->OnDamage(attacker, victim, tmpDamage); + // PLAYERBOT HOOK: Notify bots of damage event + if (Playerbot::PlayerBotHooks::OnDamageDealt) + Playerbot::PlayerBotHooks::OnDamageDealt(attacker, victim, tmpDamage, damagetype, spellProto); + // if any script modified damage, we need to also apply the same modification to unscaled damage value if (tmpDamage != damageTaken) { @@ -3181,6 +3187,10 @@ void Unit::InterruptSpell(CurrentSpellTypes spellType, bool withDelayed, bool wi spell->SetReferencedFromCurrent(false); } + // PLAYERBOT HOOK: Notify bots of spell interruption + if (Playerbot::PlayerBotHooks::OnSpellInterrupted) + Playerbot::PlayerBotHooks::OnSpellInterrupted(this, spell->GetSpellInfo(), nullptr); + if (GetTypeId() == TYPEID_UNIT && IsAIEnabled()) ToCreature()->AI()->OnSpellFailed(spell->GetSpellInfo()); } @@ -6540,6 +6550,13 @@ void Unit::SetCharm(Unit* charm, bool apply) // Hook for OnHeal Event sScriptMgr->OnHeal(healer, victim, (uint32&)gain); + // PLAYERBOT HOOK: Notify bots of healing done + if (Playerbot::PlayerBotHooks::OnHealingDone && healer) + { + uint32 overheal = (addhealth > static_cast(gain)) ? (addhealth - static_cast(gain)) : 0; + Playerbot::PlayerBotHooks::OnHealingDone(healer, victim, static_cast(gain), overheal, healInfo.GetSpellInfo()); + } + Unit* unit = healer; if (healer && healer->GetTypeId() == TYPEID_UNIT && healer->IsTotem()) unit = healer->GetOwner(); @@ -9235,6 +9252,10 @@ void Unit::AtEnterCombat() if (!IsInteractionAllowedInCombat()) UpdateNearbyPlayersInteractions(); + + // PLAYERBOT HOOK: Notify bots of combat start + if (Playerbot::PlayerBotHooks::OnCombatStarted) + Playerbot::PlayerBotHooks::OnCombatStarted(this); } void Unit::AtExitCombat() @@ -9251,6 +9272,10 @@ void Unit::AtExitCombat() if (!IsInteractionAllowedInCombat()) UpdateNearbyPlayersInteractions(); + + // PLAYERBOT HOOK: Notify bots of combat end + if (Playerbot::PlayerBotHooks::OnCombatEnded) + Playerbot::PlayerBotHooks::OnCombatEnded(this); } void Unit::AtTargetAttacked(Unit* target, bool canInitialAggro) @@ -11231,6 +11256,10 @@ void Unit::SetMeleeAnimKitId(uint16 animKitId) if (attacker && !attacker->IsInMap(victim)) attacker = nullptr; + // PLAYERBOT HOOK: Notify bots of unit death + if (Playerbot::PlayerBotHooks::OnUnitDied) + Playerbot::PlayerBotHooks::OnUnitDied(victim, attacker); + // find player: owner of controlled `this` or `this` itself maybe Player* player = nullptr; if (attacker) diff --git a/src/server/game/Groups/Group.cpp b/src/server/game/Groups/Group.cpp index 36246c0c1..5a78fa419 100644 --- a/src/server/game/Groups/Group.cpp +++ b/src/server/game/Groups/Group.cpp @@ -39,6 +39,10 @@ #include "UpdateData.h" #include "WorldSession.h" +#ifdef BUILD_PLAYERBOT +#include "PlayerBotHooks.h" +#endif + Seconds Group::CountdownInfo::GetTimeLeft() const { return Seconds(std::max(_endTime - GameTime::GetGameTime(), 0)); @@ -494,6 +498,11 @@ bool Group::AddMember(Player* player) SendUpdate(); sScriptMgr->OnGroupAddMember(this, player->GetGUID()); +#ifdef BUILD_PLAYERBOT + if (Playerbot::PlayerBotHooks::OnGroupMemberAdded) + Playerbot::PlayerBotHooks::OnGroupMemberAdded(this, player); +#endif + if (!IsLeader(player->GetGUID()) && !isBGGroup() && !isBFGroup()) { if (player->GetDungeonDifficultyID() != GetDungeonDifficultyID()) @@ -569,6 +578,11 @@ bool Group::RemoveMember(ObjectGuid guid, RemoveMethod method /*= GROUP_REMOVEME sScriptMgr->OnGroupRemoveMember(this, guid, method, kicker, reason); +#ifdef BUILD_PLAYERBOT + if (Playerbot::PlayerBotHooks::OnGroupMemberRemoved) + Playerbot::PlayerBotHooks::OnGroupMemberRemoved(this, guid, method); +#endif + Player* player = ObjectAccessor::FindConnectedPlayer(guid); if (player) { @@ -983,6 +997,10 @@ void Group::BroadcastPacket(WorldPacket const* packet, bool ignorePlayersInBGRai } } +#ifdef BUILD_PLAYERBOT +// BroadcastPacket template implementation is in Group.h +#endif + bool Group::_setMembersGroup(ObjectGuid guid, uint8 group) { member_witerator slot = _getMemberWSlot(guid); diff --git a/src/server/game/Groups/Group.h b/src/server/game/Groups/Group.h index b6064efb7..0e206569f 100644 --- a/src/server/game/Groups/Group.h +++ b/src/server/game/Groups/Group.h @@ -28,6 +28,7 @@ #include "Timer.h" #include "UniqueTrackablePtr.h" #include +#include "ObjectAccessor.h" class Battlefield; class Battleground; @@ -377,6 +378,14 @@ class TC_GAME_API Group } void BroadcastPacket(WorldPacket const* packet, bool ignorePlayersInBGRaid, int group = -1, ObjectGuid ignoredPlayer = ObjectGuid::Empty) const; + +#ifdef BUILD_PLAYERBOT + // Typed packet overload for playerbot packet sniffer + // Allows bots to access typed packet data before serialization + template + void BroadcastPacket(PacketType const& typedPacket, bool ignorePlayersInBGRaid, int group = -1, ObjectGuid ignoredPlayer = ObjectGuid::Empty) const; +#endif + void BroadcastAddonMessagePacket(WorldPacket const* packet, const std::string& prefix, bool ignorePlayersInBGRaid, int group = -1, ObjectGuid ignore = ObjectGuid::Empty) const; void LinkMember(GroupReference* pRef); @@ -465,4 +474,40 @@ class TC_GAME_API Group struct NoopGroupDeleter { void operator()(Group*) const { /*noop - not managed*/ } }; Trinity::unique_trackable_ptr m_scriptRef; }; + +#ifdef BUILD_PLAYERBOT +// Template implementation for typed packet broadcasting +// Must be in header for template instantiation + +#include "../Entities/Player/Player.h" +#include "../../../../modules/Playerbot/Network/PlayerbotPacketSniffer.h" +#include "../../../../modules/Playerbot/Core/PlayerBotHooks.h" + +template +void Group::BroadcastPacket(PacketType const& typedPacket, bool ignorePlayersInBGRaid, int group, ObjectGuid ignoredPlayer) const +{ + // Notify playerbot packet sniffer for each bot member BEFORE serialization + for (GroupReference const& itr : GetMembers()) + { + Player* player = itr.GetSource(); + if (!player || player->GetGUID() == ignoredPlayer) + continue; + + if (ignorePlayersInBGRaid && isBGGroup() && player->GetBattlegroundId() == GetGUID().GetCounter()) + continue; + + if (group != -1 && player->GetGroup() != this) + continue; + + WorldSession* session = player->GetSession(); + if (session && Playerbot::PlayerBotHooks::IsPlayerBot(player)) + Playerbot::PlayerbotPacketSniffer::OnTypedPacket(session, typedPacket); + } + + // Call existing BroadcastPacket with serialized packet + BroadcastPacket(typedPacket.Write(), ignorePlayersInBGRaid, group, ignoredPlayer); +} + +#endif // BUILD_PLAYERBOT + #endif diff --git a/src/server/game/Maps/TerrainMgr.cpp b/src/server/game/Maps/TerrainMgr.cpp index 4e138a3ce..b03aa9c8d 100644 --- a/src/server/game/Maps/TerrainMgr.cpp +++ b/src/server/game/Maps/TerrainMgr.cpp @@ -222,6 +222,12 @@ void TerrainInfo::LoadVMap(int32 gx, int32 gy) if (!VMAP::VMapFactory::createOrGetVMapManager()->isMapLoadingEnabled()) return; + // PLAYERBOT FIX: Skip loading if we already know this tile failed + // Prevents repeated file access attempts and log spam for maps without VMAP data + // (e.g., Boost Experience maps, phased zones, newer dungeons) + if (_vmapLoadFailed[GetBitsetIndex(gx, gy)]) + return; + switch (VMAP::VMapFactory::createOrGetVMapManager()->loadMap(sWorld->GetDataPath() + "vmaps", GetId(), gx, gy)) { case VMAP::LoadResult::Success: @@ -229,7 +235,9 @@ void TerrainInfo::LoadVMap(int32 gx, int32 gy) break; case VMAP::LoadResult::VersionMismatch: case VMAP::LoadResult::ReadFromFileFailed: - TC_LOG_ERROR("maps", "Could not load VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})", GetMapName(), GetId(), gx, gy, gx, gy); + // PLAYERBOT FIX: Cache the failure to prevent repeated load attempts + _vmapLoadFailed[GetBitsetIndex(gx, gy)] = true; + TC_LOG_DEBUG("maps", "VMAP not available name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{}) - will not retry", GetMapName(), GetId(), gx, gy, gx, gy); break; case VMAP::LoadResult::DisabledInConfig: TC_LOG_DEBUG("maps", "Ignored VMAP name:{}, id:{}, x:{}, y:{} (vmap rep.: x:{}, y:{})", GetMapName(), GetId(), gx, gy, gx, gy); @@ -244,6 +252,10 @@ void TerrainInfo::LoadMMapImpl(uint32 instanceId, int32 gx, int32 gy) if (!DisableMgr::IsPathfindingEnabled(GetId())) return; + // PLAYERBOT FIX: Skip loading if we already know this tile failed (prevents repeated file access attempts) + if (_mmapLoadFailed[GetBitsetIndex(gx, gy)]) + return; + switch (MMAP::LoadResult mmapLoadResult = MMAP::MMapManager::instance()->loadMap(sWorld->GetDataPath(), GetId(), instanceId, gx, gy)) { case MMAP::LoadResult::Success: @@ -252,9 +264,19 @@ void TerrainInfo::LoadMMapImpl(uint32 instanceId, int32 gx, int32 gy) case MMAP::LoadResult::AlreadyLoaded: break; case MMAP::LoadResult::FileNotFound: + // PLAYERBOT FIX: Cache the failure to prevent repeated load attempts + _mmapLoadFailed[GetBitsetIndex(gx, gy)] = true; if (_parentTerrain) break; // don't log tile not found errors for child maps - [[fallthrough]]; + TC_LOG_DEBUG("mmaps.tiles", "MMAP not available name:{}, id:{}, x:{}, y:{} (mmap rep.: x:{}, y:{}) - will not retry", GetMapName(), GetId(), gx, gy, gx, gy); + break; + case MMAP::LoadResult::VersionMismatch: + case MMAP::LoadResult::ReadFromFileFailed: + case MMAP::LoadResult::LibraryError: + // PLAYERBOT FIX: Cache these failures too - they won't succeed on retry + _mmapLoadFailed[GetBitsetIndex(gx, gy)] = true; + TC_LOG_WARN("mmaps.tiles", "MMAP failed name:{}, id:{}, x:{}, y:{} (mmap rep.: x:{}, y:{}) result: {} - will not retry", GetMapName(), GetId(), gx, gy, gx, gy, AsUnderlyingType(mmapLoadResult)); + break; default: TC_LOG_WARN("mmaps.tiles", "Could not load MMAP name:{}, id:{}, x:{}, y:{} (mmap rep.: x:{}, y:{}) result: {}", GetMapName(), GetId(), gx, gy, gx, gy, AsUnderlyingType(mmapLoadResult)); break; diff --git a/src/server/game/Maps/TerrainMgr.h b/src/server/game/Maps/TerrainMgr.h index f0e35556f..90ee77e13 100644 --- a/src/server/game/Maps/TerrainMgr.h +++ b/src/server/game/Maps/TerrainMgr.h @@ -115,6 +115,8 @@ private: std::atomic _referenceCountFromMap[MAX_NUMBER_OF_GRIDS][MAX_NUMBER_OF_GRIDS]; std::array _loadedGrids; std::bitset _gridFileExists; // cache what grids are available for this map (not including parent/child maps) + std::bitset _vmapLoadFailed; // PLAYERBOT FIX: cache failed VMAP loads to prevent repeated attempts + std::bitset _mmapLoadFailed; // PLAYERBOT FIX: cache failed MMAP loads to prevent repeated attempts static constexpr Milliseconds CleanupInterval = 1min; diff --git a/src/server/game/Server/WorldSession.cpp b/src/server/game/Server/WorldSession.cpp index 2ec1982f3..e905cfa6e 100644 --- a/src/server/game/Server/WorldSession.cpp +++ b/src/server/game/Server/WorldSession.cpp @@ -278,10 +278,14 @@ void WorldSession::SendPacket(WorldPacket const* packet, bool forced /*= false*/ if (!m_Socket[conIdx]) { #ifdef BUILD_PLAYERBOT - // Bot sessions may not have sockets, silently skip packet sending for them - if (!IsBot()) + // Bot sessions may not have sockets, but still need to trigger OnPacketSend + if (IsBot()) + { + sScriptMgr->OnPacketSend(this, *packet); + return; + } #endif - TC_LOG_ERROR("network.opcode", "Prevented sending of {} to non existent socket {} to {}", GetOpcodeNameForLogging(static_cast(packet->GetOpcode())), uint32(conIdx), GetPlayerInfo()); + TC_LOG_ERROR("network.opcode", "Prevented sending of {} to non existent socket {} to {}", GetOpcodeNameForLogging(static_cast(packet->GetOpcode())), uint32(conIdx), GetPlayerInfo()); return; } diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index ea19ff13c..307578d4b 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -1150,6 +1150,11 @@ struct PacketCounter uint32 amountCounter; }; +#ifdef BUILD_PLAYERBOT +#include "PlayerbotPacketSniffer.h" +#include "PlayerBotHooks.h" +#endif + /// Player session in the World class TC_GAME_API WorldSession { @@ -1167,11 +1172,12 @@ class TC_GAME_API WorldSession bool PlayerLogout() const { return m_playerLogout; } bool PlayerLogoutWithSave() const { return m_playerLogout && m_playerSave; } bool PlayerRecentlyLoggedOut() const { return m_playerRecentlyLogout; } - bool PlayerDisconnected() const; + virtual bool PlayerDisconnected() const; bool IsAddonRegistered(std::string_view prefix) const; - void SendPacket(WorldPacket const* packet, bool forced = false); + virtual void SendPacket(WorldPacket const* packet, bool forced = false); + void SendNotification(char const* format, ...) ATTR_PRINTF(2, 3); void SendNotification(uint32 stringId, ...); @@ -1249,8 +1255,8 @@ class TC_GAME_API WorldSession // May kick player on false depending on world config (handler should abort) bool DisallowHyperlinksAndMaybeKick(std::string const& str); - void QueuePacket(WorldPacket&& new_packet); - bool Update(uint32 diff, PacketFilter& updater); + virtual void QueuePacket(WorldPacket&& new_packet); + virtual bool Update(uint32 diff, PacketFilter& updater); /// Handle the authentication waiting queue (to be completed) void SendAuthWaitQueue(uint32 position); diff --git a/src/server/game/Spells/Auras/SpellAuras.cpp b/src/server/game/Spells/Auras/SpellAuras.cpp index 2eb98b475..07944eec6 100644 --- a/src/server/game/Spells/Auras/SpellAuras.cpp +++ b/src/server/game/Spells/Auras/SpellAuras.cpp @@ -43,6 +43,8 @@ #include "World.h" #include +#include "../../../modules/Playerbot/Core/PlayerBotHooks.h" + class ChargeDropEvent : public BasicEvent { public: @@ -604,6 +606,10 @@ void Aura::_ApplyForTarget(Unit* target, Unit* caster, AuraApplication* auraApp) caster->GetSpellHistory()->StartCooldown(m_spellInfo, castItem ? castItem->GetEntry() : 0, nullptr, true); } } + + // PLAYERBOT HOOK: Notify bots of aura application + if (Playerbot::PlayerBotHooks::OnAuraApplied) + Playerbot::PlayerBotHooks::OnAuraApplied(target, this, caster); } void Aura::_UnapplyForTarget(Unit* target, Unit* caster, AuraApplication* auraApp) @@ -632,6 +638,10 @@ void Aura::_UnapplyForTarget(Unit* target, Unit* caster, AuraApplication* auraAp if (caster && GetSpellInfo()->IsCooldownStartedOnEvent()) // note: item based cooldowns and cooldown spell mods with charges ignored (unknown existed cases) caster->GetSpellHistory()->SendCooldownEvent(GetSpellInfo()); + + // PLAYERBOT HOOK: Notify bots of aura removal + if (Playerbot::PlayerBotHooks::OnAuraRemoved) + Playerbot::PlayerBotHooks::OnAuraRemoved(target, this); } // removes aura from all targets diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index d3603def6..34facd18e 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -72,6 +72,8 @@ #include #include +#include "../../modules/Playerbot/Core/PlayerBotHooks.h" + extern NonDefaultConstructible SpellEffectHandlers[TOTAL_SPELL_EFFECTS]; SpellDestination::SpellDestination(WorldObject const& wObj) : _position(wObj.GetMapId(), wObj), @@ -3585,6 +3587,13 @@ SpellCastResult Spell::prepare(SpellCastTargets const& targets, AuraEffect const if (!(_triggeredCastFlags & TRIGGERED_IGNORE_GCD)) TriggerGlobalCooldown(); + // PLAYERBOT HOOK: Notify bots of spell cast start (CRITICAL for interrupt coordination) + if (Playerbot::PlayerBotHooks::OnSpellCastStart) + { + Unit* target = m_targets.GetUnitTarget(); + Playerbot::PlayerBotHooks::OnSpellCastStart(m_caster->ToUnit(), m_spellInfo, target); + } + // Call CreatureAI hook OnSpellStart if (Creature* caster = m_caster->ToCreature()) if (caster->IsAIEnabled()) @@ -4438,6 +4447,10 @@ void Spell::finish(SpellCastResult result) // Stop Attack for some spells if (m_spellInfo->HasAttribute(SPELL_ATTR0_CANCELS_AUTO_ATTACK_COMBAT)) unitCaster->AttackStop(); + + // PLAYERBOT HOOK: Notify bots of successful spell cast completion + if (Playerbot::PlayerBotHooks::OnSpellCastSuccess) + Playerbot::PlayerBotHooks::OnSpellCastSuccess(unitCaster, m_spellInfo); } template diff --git a/src/server/worldserver/Main.cpp b/src/server/worldserver/Main.cpp index 25d95299c..996f99d31 100644 --- a/src/server/worldserver/Main.cpp +++ b/src/server/worldserver/Main.cpp @@ -28,7 +28,7 @@ #include "DatabaseLoader.h" #include "DeadlineTimer.h" #include "GitRevision.h" -//#include "Modules/ModuleManager.h" +#include "Modules/ModuleManager.h" #include "InstanceLockMgr.h" #include "IoContext.h" #include "IpNetwork.h" @@ -440,7 +440,7 @@ int main(int argc, char** argv) sScriptMgr->OnStartup(); // Initialize registered modules - //ModuleManager::CallOnStartup(); + ModuleManager::CallOnStartup(); TC_LOG_INFO("server.worldserver", "{} (worldserver-daemon) ready...", GitRevision::GetFullVersion());