bot fixes

This commit is contained in:
luis
2026-05-01 07:18:39 -03:00
parent 19deb2f50e
commit b2a6d321f7
21 changed files with 322 additions and 70 deletions
+56 -16
View File
@@ -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<uint32, uint32> playerCounts;
if (realPlayerSessions > 0)
{
auto const& sessions = sWorld->GetAllSessions();
for (auto const& pair : sessions)
{
WorldSession* session = pair.second;
if (!session) continue;
// Skip bot accounts (IDs >= 100000)
if (session->GetAccountId() >= 100000) continue;
Player* player = session->GetPlayer();
if (player && player->IsInWorld())
{
playerCounts[player->GetZoneId()]++;
}
}
}
for (auto const& pair : _zonePopulations)
{
uint32 zoneId = pair.first;
uint32 zonePlayerCount = 0;
auto it = playerCounts.find(zoneId);
if (it != playerCounts.end())
zonePlayerCount = it->second;
UpdateZonePopulation(zoneId, pair.second.mapId, zonePlayerCount);
}
TC_LOG_TRACE("module.playerbot.spawner", "Population update: {} real players, {} bot sessions",
realPlayerSessions, botSessions);
}
@@ -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());
+2 -2
View File
@@ -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<ZonePopulation> GetAllZonePopulations() const;
@@ -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());
@@ -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<uint64> requiredRaces = quest->GetAllowableRaces();
/* Trinity::RaceMask<uint64> 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)
@@ -928,12 +928,12 @@ bool UnifiedQuestManager::ValidationModule::ValidateWithContext(ValidationContex
}
// 3. Race requirements - GetAllowableRaces returns Trinity::RaceMask<uint64>
Trinity::RaceMask<uint64> requiredRaces = quest->GetAllowableRaces();
/* Trinity::RaceMask<uint64> 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);
+5 -7
View File
@@ -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
}
+16 -1
View File
@@ -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);
@@ -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)
{
+6
View File
@@ -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)
+22 -7
View File
@@ -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)
+5
View File
@@ -1314,6 +1314,11 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player>
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; }
+29
View File
@@ -95,6 +95,8 @@
#include <sstream>
#include <cmath>
#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<uint32>(gain)) ? (addhealth - static_cast<uint32>(gain)) : 0;
Playerbot::PlayerBotHooks::OnHealingDone(healer, victim, static_cast<uint32>(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)
+18
View File
@@ -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<time_t>(_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);
+45
View File
@@ -28,6 +28,7 @@
#include "Timer.h"
#include "UniqueTrackablePtr.h"
#include <map>
#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<typename PacketType>
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<Group> 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<typename PacketType>
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
+24 -2
View File
@@ -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;
+2
View File
@@ -115,6 +115,8 @@ private:
std::atomic<uint16> _referenceCountFromMap[MAX_NUMBER_OF_GRIDS][MAX_NUMBER_OF_GRIDS];
std::array<uint64, MAX_NUMBER_OF_GRIDS> _loadedGrids;
std::bitset<MAX_NUMBER_OF_GRIDS * MAX_NUMBER_OF_GRIDS> _gridFileExists; // cache what grids are available for this map (not including parent/child maps)
std::bitset<MAX_NUMBER_OF_GRIDS* MAX_NUMBER_OF_GRIDS> _vmapLoadFailed; // PLAYERBOT FIX: cache failed VMAP loads to prevent repeated attempts
std::bitset<MAX_NUMBER_OF_GRIDS* MAX_NUMBER_OF_GRIDS> _mmapLoadFailed; // PLAYERBOT FIX: cache failed MMAP loads to prevent repeated attempts
static constexpr Milliseconds CleanupInterval = 1min;
+7 -3
View File
@@ -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<OpcodeServer>(packet->GetOpcode())), uint32(conIdx), GetPlayerInfo());
TC_LOG_ERROR("network.opcode", "Prevented sending of {} to non existent socket {} to {}", GetOpcodeNameForLogging(static_cast<OpcodeServer>(packet->GetOpcode())), uint32(conIdx), GetPlayerInfo());
return;
}
+10 -4
View File
@@ -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);
@@ -43,6 +43,8 @@
#include "World.h"
#include <sstream>
#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
+13
View File
@@ -72,6 +72,8 @@
#include <numeric>
#include <sstream>
#include "../../modules/Playerbot/Core/PlayerBotHooks.h"
extern NonDefaultConstructible<SpellEffectHandlerFn> 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<class T>
+2 -2
View File
@@ -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());