This commit is contained in:
luis
2026-05-08 22:29:58 -03:00
parent 0e96bc63c8
commit b367d85bc7
5 changed files with 494 additions and 0 deletions
+4
View File
@@ -883,6 +883,10 @@ set(PLAYERBOT_LIFECYCLE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/Lifecycle/PopulationLifecycleController.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Lifecycle/PopulationLifecycleController.h
# Startup Cleanup System
${CMAKE_CURRENT_SOURCE_DIR}/Lifecycle/PlayerbotStartupCleanup.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Lifecycle/PlayerbotStartupCleanup.h
# Instance Bot Pool System
${CMAKE_CURRENT_SOURCE_DIR}/Lifecycle/Instance/PoolSlotState.h
${CMAKE_CURRENT_SOURCE_DIR}/Lifecycle/Instance/InstanceBotSlot.h
@@ -0,0 +1,253 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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.
*/
#ifdef BUILD_PLAYERBOT
#include "PlayerbotStartupCleanup.h"
#include "Database/PlayerbotDatabase.h"
#include "Chat/BotChatManager.h"
#include "Log.h"
#include "Player.h"
#include "ObjectAccessor.h"
namespace Playerbot
{
// Initialize static members
PlayerbotStartupCleanup::CleanupStats PlayerbotStartupCleanup::_stats;
bool PlayerbotStartupCleanup::RunStartupCleanup()
{
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Starting comprehensive startup cleanup...");
// Reset statistics
_stats = CleanupStats();
// Run all cleanup procedures
// Each procedure handles its own error recovery
CleanupOrphanedJITBots();
CleanupStaleLFGAssignments();
CleanupOldConversations();
CleanupOrphanedAccounts();
CleanupInactiveData();
// Log final statistics
LogCleanupStatistics();
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Startup cleanup completed");
return true;
}
bool PlayerbotStartupCleanup::CleanupOrphanedJITBots()
{
try
{
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Cleaning up orphaned JIT bot characters...");
// Query for orphaned JIT bots from tracking table
QueryResult result = sPlayerbotDatabase->Query(
"SELECT jb.bot_guid, jb.account_id FROM playerbot_jit_bots jb");
if (!result)
{
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: No orphaned JIT bots found");
return true;
}
std::vector<std::pair<ObjectGuid, uint32>> botsToDelete;
do
{
Field* fields = result->Fetch();
ObjectGuid::LowType guidLow = fields[0].GetUInt64();
uint32 accountId = fields[1].GetUInt32();
ObjectGuid botGuid = ObjectGuid::Create<HighGuid::Player>(guidLow);
botsToDelete.push_back({botGuid, accountId});
} while (result->NextRow());
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Found {} orphaned JIT bots to delete", botsToDelete.size());
// Delete each orphaned bot
for (auto const& [botGuid, accountId] : botsToDelete)
{
try
{
Player::DeleteFromDB(botGuid, accountId, false, true);
++_stats.jitBotsDeleted;
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: Deleted orphaned JIT bot {}", botGuid.ToString());
}
catch (std::exception const& e)
{
TC_LOG_WARN("module.playerbot", "PlayerbotStartupCleanup: Failed to delete JIT bot {}: {}",
botGuid.ToString(), e.what());
}
}
// Clear the JIT bots tracking table
sPlayerbotDatabase->Execute("TRUNCATE TABLE playerbot_jit_bots");
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Successfully cleaned up {} orphaned JIT bots",
_stats.jitBotsDeleted);
return true;
}
catch (std::exception const& e)
{
TC_LOG_ERROR("module.playerbot", "PlayerbotStartupCleanup: Exception during JIT bot cleanup: {}", e.what());
return true; // Don't fail startup on cleanup errors
}
}
bool PlayerbotStartupCleanup::CleanupStaleLFGAssignments()
{
try
{
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Cleaning up stale LFG assignments...");
// NOTE: LFG assignments are stored in-memory and not persisted to database
// They are automatically cleared on server restart, so no database cleanup needed
// However, we verify that LFGBotManager is ready to start fresh
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: LFG assignments are in-memory and will be cleared at startup");
return true;
}
catch (std::exception const& e)
{
TC_LOG_WARN("module.playerbot", "PlayerbotStartupCleanup: Exception during LFG cleanup: {}", e.what());
return true; // Don't fail startup on cleanup errors
}
}
bool PlayerbotStartupCleanup::CleanupOldConversations()
{
try
{
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Cleaning up old AI conversations...");
// Check if BotChatManager is available and call its cleanup
if (Playerbot::BotChatManager::instance())
{
Playerbot::BotChatManager::instance()->Cleanup();
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Old conversations cleaned up");
return true;
}
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: BotChatManager not available, skipping conversation cleanup");
return true;
}
catch (std::exception const& e)
{
TC_LOG_ERROR("module.playerbot", "PlayerbotStartupCleanup: Exception during conversation cleanup: {}", e.what());
return true; // Don't fail startup on cleanup errors
}
}
bool PlayerbotStartupCleanup::CleanupOrphanedAccounts()
{
try
{
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Cleaning up orphaned bot accounts...");
// Find bot accounts that:
// 1. Have no characters in the characters table, OR
// 2. Have been inactive for > 30 days
QueryResult result = sPlayerbotDatabase->Query(
"SELECT pa.account_id, pa.created_at "
"FROM playerbot_accounts pa "
"LEFT JOIN characters c ON pa.account_id = c.account "
"WHERE c.guid IS NULL "
"OR (pa.is_active = 1 AND DATE_ADD(pa.last_login, INTERVAL 30 DAY) < NOW())");
if (!result)
{
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: No orphaned bot accounts found");
return true;
}
std::vector<uint32> orphanedAccounts;
do
{
Field* fields = result->Fetch();
uint32 accountId = fields[0].GetUInt32();
orphanedAccounts.push_back(accountId);
} while (result->NextRow());
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Found {} orphaned bot accounts", orphanedAccounts.size());
// Mark orphaned accounts as inactive
for (uint32 accountId : orphanedAccounts)
{
try
{
sPlayerbotDatabase->Execute(
"UPDATE playerbot_accounts SET is_active = 0 WHERE account_id = {}", accountId);
++_stats.orphanedAccountsDeleted;
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: Marked orphaned account {} as inactive", accountId);
}
catch (std::exception const& e)
{
TC_LOG_WARN("module.playerbot", "PlayerbotStartupCleanup: Failed to mark account {} as inactive: {}",
accountId, e.what());
}
}
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Deactivated {} orphaned bot accounts",
_stats.orphanedAccountsDeleted);
return true;
}
catch (std::exception const& e)
{
TC_LOG_ERROR("module.playerbot", "PlayerbotStartupCleanup: Exception during account cleanup: {}", e.what());
return true; // Don't fail startup on cleanup errors
}
}
bool PlayerbotStartupCleanup::CleanupInactiveData()
{
try
{
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Cleaning up inactive bot data...");
// Clean up old pool statistics (keep last 7 days)
if (sPlayerbotDatabase->Query("SELECT 1 FROM information_schema.TABLES WHERE TABLE_NAME='playerbot_pool_statistics' LIMIT 1"))
{
sPlayerbotDatabase->Execute(
"DELETE FROM playerbot_pool_statistics WHERE recorded_at < DATE_SUB(NOW(), INTERVAL 7 DAY)");
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: Cleaned old pool statistics");
}
// Clean up old template statistics (keep last 30 days)
if (sPlayerbotDatabase->Query("SELECT 1 FROM information_schema.TABLES WHERE TABLE_NAME='playerbot_template_statistics' LIMIT 1"))
{
sPlayerbotDatabase->Execute(
"DELETE FROM playerbot_template_statistics WHERE last_updated < DATE_SUB(NOW(), INTERVAL 30 DAY)");
TC_LOG_DEBUG("module.playerbot", "PlayerbotStartupCleanup: Cleaned old template statistics");
}
TC_LOG_INFO("module.playerbot", "PlayerbotStartupCleanup: Inactive data cleanup completed");
return true;
}
catch (std::exception const& e)
{
TC_LOG_WARN("module.playerbot", "PlayerbotStartupCleanup: Exception during inactive data cleanup: {}", e.what());
return true; // Don't fail startup on cleanup errors
}
}
void PlayerbotStartupCleanup::LogCleanupStatistics()
{
TC_LOG_INFO("module.playerbot", "");
TC_LOG_INFO("module.playerbot", "=== Playerbot Startup Cleanup Statistics ===");
TC_LOG_INFO("module.playerbot", " JIT Bots Deleted: {}", _stats.jitBotsDeleted);
TC_LOG_INFO("module.playerbot", " Stale LFG Removed: {}", _stats.staleLFGsRemoved);
TC_LOG_INFO("module.playerbot", " Orphaned Accounts: {}", _stats.orphanedAccountsDeleted);
TC_LOG_INFO("module.playerbot", "============================================");
TC_LOG_INFO("module.playerbot", "");
}
} // namespace Playerbot
#endif // BUILD_PLAYERBOT
@@ -0,0 +1,97 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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.
*/
#ifndef PLAYERBOT_STARTUP_CLEANUP_H
#define PLAYERBOT_STARTUP_CLEANUP_H
#include "Define.h"
#include <string>
namespace Playerbot
{
/**
* @brief Startup cleanup manager for playerbot accounts, characters, and data
*
* This manager handles comprehensive cleanup of playerbot data during server startup:
* - Orphaned bot characters from crashed sessions
* - Stale LFG assignments
* - Old AI chat conversations
* - Orphaned playerbot accounts
* - Inactive playerbot data
*/
class TC_GAME_API PlayerbotStartupCleanup
{
public:
/**
* @brief Run all startup cleanup procedures
* @return true if all cleanups completed successfully, false if critical error occurred
*/
static bool RunStartupCleanup();
private:
/**
* @brief Clean up orphaned JIT bot characters from previous runs
*
* These are bot characters that were created by JITBotFactory but not
* properly deleted due to server crashes or unclean shutdowns.
*/
static bool CleanupOrphanedJITBots();
/**
* @brief Clean up stale LFG bot assignments
*
* Removes bots that are still marked as queued but no longer exist
* in the world, or whose queue time has expired.
*/
static bool CleanupStaleLFGAssignments();
/**
* @brief Clean up old AI chat conversations
*
* Removes cached conversations older than the configured retention period
* to free up memory and database space.
*/
static bool CleanupOldConversations();
/**
* @brief Clean up orphaned playerbot accounts
*
* Removes bot accounts that have no associated characters or have been
* inactive for an extended period.
*/
static bool CleanupOrphanedAccounts();
/**
* @brief Clean up inactive bot data
*
* Removes stale bot statistics, performance metrics, and other
* accumulated data from bots that are no longer active.
*/
static bool CleanupInactiveData();
/**
* @brief Log cleanup statistics
*/
static void LogCleanupStatistics();
// Statistics tracking
struct CleanupStats
{
uint32 jitBotsDeleted = 0;
uint32 staleLFGsRemoved = 0;
uint32 conversationsDeleted = 0;
uint32 orphanedAccountsDeleted = 0;
uint64 bytesFreed = 0;
};
static CleanupStats _stats;
};
}
#endif // PLAYERBOT_STARTUP_CLEANUP_H
@@ -28,6 +28,7 @@
#include "Lifecycle/Demand/PlayerActivityTracker.h"
#include "Lifecycle/Demand/DemandCalculator.h"
#include "Lifecycle/PopulationLifecycleController.h"
#include "Lifecycle/PlayerbotStartupCleanup.h"
// Instance Bot Pool System (Hybrid Warm Pool + Elastic Overflow)
#include "Lifecycle/Instance/InstanceBotPool.h"
@@ -172,6 +173,11 @@ bool PlayerbotModule::Initialize()
// This ensures schema synchronization between builds
PlayerbotMigrationMgr::instance()->CheckVersionMismatch();
// Run startup cleanup to remove orphaned bots and stale data
TC_LOG_INFO("module.playerbot", "Running playerbot startup cleanup...");
Playerbot::PlayerbotStartupCleanup::RunStartupCleanup();
TC_LOG_INFO("module.playerbot", "Playerbot startup cleanup completed");
// Initialize all sub-managers
if (!InitializeManagers())
{
@@ -4147,6 +4147,8 @@ void GameObject::ValuesUpdateForPlayerWithMaskSender::operator()(Player const* p
void GameObject::ClearValuesChangesMask()
{
m_values.ClearChangesMask(&GameObject::m_gameObjectData);
m_values.ClearChangesMask(&GameObject::m_housingCornerstoneData);
m_values.ClearChangesMask(&GameObject::m_mirroredPositionData);
WorldObject::ClearValuesChangesMask();
}
@@ -4678,3 +4680,135 @@ SpellInfo const* GameObject::GetSpellForLock(Player const* player) const
return nullptr;
}
void GameObject::InitHousingCornerstoneData(uint64 cost, int32 plotIndex)
{
if (m_housingCornerstoneData.has_value())
return;
SetUpdateFieldValue(m_values.ModifyValue(&GameObject::m_housingCornerstoneData, 0)
.ModifyValue(&UF::HousingCornerstoneData::Cost), cost);
SetUpdateFieldValue(m_values.ModifyValue(&GameObject::m_housingCornerstoneData, 0)
.ModifyValue(&UF::HousingCornerstoneData::PlotIndex), plotIndex);
m_entityFragments.Add(WowCS::EntityFragment::FJamHousingCornerstone_C, IsInWorld(),
WowCS::GetRawFragmentData(m_housingCornerstoneData));
TC_LOG_DEBUG("housing", "GameObject::InitHousingCornerstoneData: entry={} guid={} cost={} plotIndex={} "
"isInWorld={} fragmentCount={} updateableCount={}",
GetEntry(), GetGUID().ToString(), cost, plotIndex,
IsInWorld(), m_entityFragments.Count, m_entityFragments.UpdateableCount);
}
void GameObject::InitHousingDecorData(ObjectGuid decorGuid, ObjectGuid houseGuid,
uint8 flags, ObjectGuid attachParent /*= ObjectGuid::Empty*/,
uint8 sourceType /*= 0*/, std::string sourceValue /*= {}*/)
{
if (m_housingDecorData.has_value())
return;
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
.ModifyValue(&UF::HousingDecorData::DecorGUID), decorGuid);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
.ModifyValue(&UF::HousingDecorData::AttachParentGUID), attachParent);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
.ModifyValue(&UF::HousingDecorData::Flags), flags);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingDecorData, 0)
.ModifyValue(&UF::HousingDecorData::TargetGameObjectGUID), GetGUID());
// Set persisted data (house ownership + source tracking)
auto persistedRef = m_values.ModifyValue(&Object::m_housingDecorData, 0)
.ModifyValue(&UF::HousingDecorData::PersistedData, 0);
SetUpdateFieldValue(persistedRef.ModifyValue(&UF::DecorStoragePersistedData::HouseGUID), houseGuid);
SetUpdateFieldValue(persistedRef.ModifyValue(&UF::DecorStoragePersistedData::SourceType), sourceType);
if (!sourceValue.empty())
SetUpdateFieldValue(persistedRef.ModifyValue(&UF::DecorStoragePersistedData::SourceValue), std::move(sourceValue));
m_entityFragments.Add(WowCS::EntityFragment::FHousingDecor_C, IsInWorld(),
WowCS::GetRawFragmentData(m_housingDecorData));
// 12.0.5 added Tag_HousingDecorProxyGameObject (=226) to mark a GameObject that is
// serving as a housing-decor proxy (chair/chest/mailbox/etc. placed as decor).
// Attach it alongside FHousingDecor_C so the client treats this entity as housing
// decor in addition to its normal GO behavior.
m_entityFragments.Add(WowCS::EntityFragment::Tag_HousingDecorProxyGameObject, IsInWorld());
TC_LOG_DEBUG("housing", "GameObject::InitHousingDecorData: entry={} goGuid={} decorGuid={} houseGuid={} flags={} "
"isInWorld={} fragmentCount={}",
GetEntry(), GetGUID().ToString(), decorGuid.ToString(), houseGuid.ToString(), flags,
IsInWorld(), m_entityFragments.Count);
}
void GameObject::InitHousingDecorMirroredPosition(Position const& localPos, QuaternionData const& localRot,
float localScale, ObjectGuid attachParent, uint8 attachFlags /*= 3*/)
{
// Retail sniff-verified: GameObject decor carries FMirroredPositionData_C fragment
// with AttachParent=room entity and local-space position.
auto posData = m_values.ModifyValue(&GameObject::m_mirroredPositionData)
.ModifyValue(&UF::MirroredPositionData::PositionData);
SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::AttachParentGUID), attachParent);
SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::PositionLocalSpace),
TaggedPosition<Position::XYZ>(localPos.GetPositionX(), localPos.GetPositionY(), localPos.GetPositionZ()));
SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::RotationLocalSpace), localRot);
SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::ScaleLocalSpace), localScale);
SetUpdateFieldValue(posData.ModifyValue(&UF::MirroredMeshObjectData::AttachmentFlags), attachFlags);
m_entityFragments.Add(WowCS::EntityFragment::FMirroredPositionData_C, IsInWorld(),
WowCS::GetRawFragmentData(m_mirroredPositionData));
TC_LOG_DEBUG("housing", "GameObject::InitHousingDecorMirroredPosition: entry={} goGuid={} "
"localPos=({:.2f},{:.2f},{:.2f}) attachParent={} attachFlags={}",
GetEntry(), GetGUID().ToString(),
localPos.GetPositionX(), localPos.GetPositionY(), localPos.GetPositionZ(),
attachParent.ToString(), attachFlags);
}
void GameObject::InitHousingFixtureData(ObjectGuid houseGuid, int32 exteriorComponentID, int32 houseExteriorWmoDataID,
uint8 exteriorComponentType /*= 9*/, uint8 houseSize /*= 2*/, int32 exteriorComponentHookID /*= -1*/)
{
if (m_housingFixtureData.has_value())
return;
// Sniff-verified field values (11.2 retail MeshObject with FHousingFixture_C):
// ExteriorComponentID: 141 (Stucco Base, small Human house)
// HouseExteriorWmoDataID: 9 (Human/Generic theme, NOT 32)
// ExteriorComponentHookID: -1 (base piece, no hook)
// ExteriorComponentType: 9 (Base)
// Field_59: 1
// Size: 2 (small)
// GameObjectGUID: 0 (empty)
// Guid: MeshObject GUID (we use Housing GUID as safe substitute)
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentID), exteriorComponentID);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::HouseExteriorWmoDataID), houseExteriorWmoDataID);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentHookID), exteriorComponentHookID);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::HouseGUID), houseGuid);
// Guid must be a Housing-type GUID (HighGuid::Housing, type 55). Client GUID resolver
// crashes if it receives a non-Housing, non-null GUID here (e.g. HighGuid::GameObject = 11)
// because it enters a conversion path that returns null, then dereferences at +0x64.
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::Guid), houseGuid);
// GameObjectGUID: sniff confirms 0x0 (empty) for all fixture pieces
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::ExteriorComponentType), exteriorComponentType);
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::Field_59), uint8(1)); // sniff: always 1
SetUpdateFieldValue(m_values.ModifyValue(&Object::m_housingFixtureData, 0)
.ModifyValue(&UF::HousingFixtureData::Size), houseSize);
m_entityFragments.Add(WowCS::EntityFragment::FHousingFixture_C, IsInWorld(),
WowCS::GetRawFragmentData(m_housingFixtureData));
TC_LOG_DEBUG("housing", "GameObject::InitHousingFixtureData: entry={} goGuid={} houseGuid={} "
"exteriorComponentID={} wmoDataID={} hookID={} componentType={} size={} field59=1 "
"isInWorld={} fragmentCount={}",
GetEntry(), GetGUID().ToString(), houseGuid.ToString(),
exteriorComponentID, houseExteriorWmoDataID, exteriorComponentHookID,
exteriorComponentType, houseSize,
IsInWorld(), m_entityFragments.Count);
}