diff --git a/src/modules/Playerbot/CMakeLists.txt b/src/modules/Playerbot/CMakeLists.txt index 6986fee4f..44b1d81f4 100644 --- a/src/modules/Playerbot/CMakeLists.txt +++ b/src/modules/Playerbot/CMakeLists.txt @@ -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 diff --git a/src/modules/Playerbot/Lifecycle/PlayerbotStartupCleanup.cpp b/src/modules/Playerbot/Lifecycle/PlayerbotStartupCleanup.cpp new file mode 100644 index 000000000..74c0b2bb8 --- /dev/null +++ b/src/modules/Playerbot/Lifecycle/PlayerbotStartupCleanup.cpp @@ -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> botsToDelete; + do + { + Field* fields = result->Fetch(); + ObjectGuid::LowType guidLow = fields[0].GetUInt64(); + uint32 accountId = fields[1].GetUInt32(); + ObjectGuid botGuid = ObjectGuid::Create(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 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 diff --git a/src/modules/Playerbot/Lifecycle/PlayerbotStartupCleanup.h b/src/modules/Playerbot/Lifecycle/PlayerbotStartupCleanup.h new file mode 100644 index 000000000..bceb9ad8a --- /dev/null +++ b/src/modules/Playerbot/Lifecycle/PlayerbotStartupCleanup.h @@ -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 + +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 diff --git a/src/modules/Playerbot/PlayerbotModule.cpp b/src/modules/Playerbot/PlayerbotModule.cpp index 7350c46fe..bcc9c0589 100644 --- a/src/modules/Playerbot/PlayerbotModule.cpp +++ b/src/modules/Playerbot/PlayerbotModule.cpp @@ -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()) { diff --git a/src/server/game/Entities/GameObject/GameObject.cpp b/src/server/game/Entities/GameObject/GameObject.cpp index 4ac40e248..c2b8e3a5e 100644 --- a/src/server/game/Entities/GameObject/GameObject.cpp +++ b/src/server/game/Entities/GameObject/GameObject.cpp @@ -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(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); +}