Core/Housing: stage 38 repair persistent decor storage GUIDs

This commit is contained in:
Gexo91
2026-09-12 21:07:25 +03:00
parent 07ea25023b
commit 565d1176ee
@@ -0,0 +1,304 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*/
#include "HousingStorageSession.h"
#include "DatabaseEnv.h"
#include "HousingDefines.h"
#include "HousingPackets.h"
#include "Log.h"
#include "RealmList.h"
#include "WorldSession.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace Housing
{
namespace
{
constexpr uint32 MaxDecorInstancesPerAccount = 10000;
struct LicensedDecor
{
uint32 DecorId = 0;
uint32 Quantity = 0;
};
uint32 GetDecorGuidNamespace()
{
uint32 const realmId = sRealmList->GetCurrentRealmId().Realm & 0xFFFFu;
return realmId ? realmId : 1u;
}
uint64 StableInstanceId(std::string_view value)
{
// FNV-1a provides a stable server-side instance key for repairing GUIDs
// that were persisted with the old, reversed Housing/Decor layout.
uint64 hash = UI64LIT(14695981039346656037);
for (char c : value)
{
hash ^= uint8(c);
hash *= UI64LIT(1099511628211);
}
return hash ? hash : UI64LIT(1);
}
ObjectGuid MakeDecorGuid(uint32 decorId, uint64 instanceId)
{
if (!decorId || !instanceId)
return ObjectGuid::Empty;
return ObjectGuid::Create<HighGuid::Housing>(1, GetDecorGuidNamespace(), decorId, instanceId);
}
bool RepairPersistentDecorGuids(uint32 battlenetAccountId)
{
QueryResult result = CharacterDatabase.PQuery(
"SELECT DecorGuid, DecorID FROM housing_decor_instance "
"WHERE BattlenetAccountId = {} ORDER BY DecorID, DecorGuid LIMIT {}",
battlenetAccountId, MaxDecorInstancesPerAccount + 1);
if (!result)
return true;
struct Repair
{
std::string OldGuid;
std::string NewGuid;
uint32 DecorId = 0;
};
std::vector<Repair> repairs;
repairs.reserve(32);
uint32 rowCount = 0;
do
{
if (++rowCount > MaxDecorInstancesPerAccount)
{
TC_LOG_ERROR("housing", "Battle.net account {} exceeds the decor storage safety cap; GUID repair aborted",
battlenetAccountId);
return false;
}
Field* fields = result->Fetch();
std::string const oldGuidString = fields[0].GetString();
uint32 const decorId = fields[1].GetUInt32();
ObjectGuid const oldGuid = ObjectGuid::FromString(oldGuidString);
if (!decorId || oldGuid == ObjectGuid::FromStringFailed || !IsDecorGuid(oldGuid))
{
TC_LOG_ERROR("housing", "Invalid persisted decor instance '{}' for Battle.net account {}",
oldGuidString, battlenetAccountId);
return false;
}
if (GetDecorId(oldGuid) == decorId)
continue;
ObjectGuid const newGuid = MakeDecorGuid(decorId, StableInstanceId(oldGuidString));
if (!IsDecorGuid(newGuid) || GetDecorId(newGuid) != decorId)
return false;
std::string const newGuidString = newGuid.ToString();
QueryResult collision = CharacterDatabase.PQuery(
"SELECT 1 FROM housing_decor_instance WHERE DecorGuid = '{}' LIMIT 1",
newGuidString);
if (collision)
{
TC_LOG_ERROR("housing", "Cannot repair decor GUID '{}' because target GUID '{}' already exists",
oldGuidString, newGuidString);
return false;
}
repairs.push_back({ oldGuidString, newGuidString, decorId });
} while (result->NextRow());
if (repairs.empty())
return true;
CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction();
for (Repair const& repair : repairs)
{
transaction->PAppend(
"UPDATE housing_placed_decor SET DecorGuid = '{}' WHERE DecorGuid = '{}'",
repair.NewGuid, repair.OldGuid);
transaction->PAppend(
"UPDATE housing_deferred_decor_redemption SET DecorGuid = '{}' WHERE DecorGuid = '{}'",
repair.NewGuid, repair.OldGuid);
transaction->PAppend(
"UPDATE housing_decor_instance SET DecorGuid = '{}' "
"WHERE BattlenetAccountId = {} AND DecorGuid = '{}' AND DecorID = {}",
repair.NewGuid, battlenetAccountId, repair.OldGuid, repair.DecorId);
}
CharacterDatabase.DirectCommitTransaction(transaction);
for (Repair const& repair : repairs)
{
QueryResult verified = CharacterDatabase.PQuery(
"SELECT 1 FROM housing_decor_instance WHERE BattlenetAccountId = {} "
"AND DecorGuid = '{}' AND DecorID = {} LIMIT 1",
battlenetAccountId, repair.NewGuid, repair.DecorId);
if (!verified)
{
TC_LOG_ERROR("housing", "Decor GUID repair verification failed for '{}' -> '{}'",
repair.OldGuid, repair.NewGuid);
return false;
}
}
TC_LOG_INFO("housing", "Repaired {} Housing decor GUIDs for Battle.net account {}",
repairs.size(), battlenetAccountId);
return true;
}
void ReconcileLicensedQuantities(uint32 battlenetAccountId)
{
// A persisted decor instance is authoritative evidence that the account
// owns that copy. Preserve any larger licensed quantity already present.
CharacterDatabase.DirectPExecute(
"INSERT INTO account_housing_decor (BattlenetAccountId, DecorId, Quantity) "
"SELECT BattlenetAccountId, DecorID, COUNT(*) FROM housing_decor_instance "
"WHERE BattlenetAccountId = {} GROUP BY BattlenetAccountId, DecorID "
"ON DUPLICATE KEY UPDATE Quantity = GREATEST(Quantity, VALUES(Quantity))",
battlenetAccountId);
}
bool LoadLicensedDecor(uint32 battlenetAccountId, std::vector<LicensedDecor>& licensed)
{
licensed.clear();
QueryResult result = CharacterDatabase.PQuery(
"SELECT DecorId, Quantity FROM account_housing_decor "
"WHERE BattlenetAccountId = {} AND Quantity > 0 ORDER BY DecorId LIMIT {}",
battlenetAccountId, MaxDecorInstancesPerAccount + 1);
if (!result)
return true;
uint64 totalQuantity = 0;
do
{
if (licensed.size() >= MaxDecorInstancesPerAccount)
return false;
Field* fields = result->Fetch();
LicensedDecor entry;
entry.DecorId = fields[0].GetUInt32();
entry.Quantity = fields[1].GetUInt32();
if (!entry.DecorId || !entry.Quantity)
return false;
totalQuantity += entry.Quantity;
if (totalQuantity > MaxDecorInstancesPerAccount)
return false;
licensed.push_back(entry);
} while (result->NextRow());
return true;
}
bool MaterializeMissingDecorInstances(uint32 battlenetAccountId)
{
std::vector<LicensedDecor> licensed;
if (!LoadLicensedDecor(battlenetAccountId, licensed))
{
TC_LOG_ERROR("housing", "Licensed decor data for Battle.net account {} exceeds storage safety limits",
battlenetAccountId);
return false;
}
if (licensed.empty())
return true;
CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction();
uint32 pendingInserts = 0;
uint32 serial = 1;
for (LicensedDecor const& entry : licensed)
{
QueryResult countResult = CharacterDatabase.PQuery(
"SELECT COUNT(*) FROM housing_decor_instance WHERE BattlenetAccountId = {} AND DecorID = {}",
battlenetAccountId, entry.DecorId);
uint32 const existingCount = countResult ? countResult->Fetch()[0].GetUInt32() : 0;
if (existingCount >= entry.Quantity)
continue;
uint32 const missing = entry.Quantity - existingCount;
for (uint32 i = 0; i < missing; ++i)
{
ObjectGuid decorGuid;
std::string decorGuidString;
do
{
uint32 const token = 0x80000000u | serial++;
uint64 const instanceId = (uint64(battlenetAccountId) << 32) | uint64(token);
decorGuid = MakeDecorGuid(entry.DecorId, instanceId);
decorGuidString = decorGuid.ToString();
} while (CharacterDatabase.PQuery(
"SELECT 1 FROM housing_decor_instance WHERE DecorGuid = '{}' LIMIT 1",
decorGuidString));
transaction->PAppend(
"INSERT INTO housing_decor_instance "
"(BattlenetAccountId, DecorGuid, DecorID, HouseGuid, PlacementStatus, SourceType, SourceValue) "
"VALUES ({}, '{}', {}, '', 0, 0, '')",
battlenetAccountId, decorGuidString, entry.DecorId);
++pendingInserts;
}
}
if (!pendingInserts)
return true;
CharacterDatabase.DirectCommitTransaction(transaction);
for (LicensedDecor const& entry : licensed)
{
QueryResult countResult = CharacterDatabase.PQuery(
"SELECT COUNT(*) FROM housing_decor_instance WHERE BattlenetAccountId = {} AND DecorID = {}",
battlenetAccountId, entry.DecorId);
if (!countResult || countResult->Fetch()[0].GetUInt32() < entry.Quantity)
{
TC_LOG_ERROR("housing", "Decor instance materialization verification failed for account {} decor {}",
battlenetAccountId, entry.DecorId);
return false;
}
}
TC_LOG_INFO("housing", "Materialized {} missing Housing decor instances for Battle.net account {}",
pendingInserts, battlenetAccountId);
return true;
}
}
void HandleDecorRequestStorage(WorldSession* session,
WorldPackets::Housing::HousingDecorRequestStorage& packet)
{
if (!session)
return;
// The 69587 request carries the Battle.net account GUID. Never mutate or
// expose another account's storage if the packet does not match the session.
if (packet.BnetAccountGUID != session->GetBattlenetAccountGUID())
{
session->HandleHousingDecorRequestStorage(packet);
return;
}
uint32 const battlenetAccountId = session->GetBattlenetAccountId();
if (!RepairPersistentDecorGuids(battlenetAccountId))
TC_LOG_ERROR("housing", "Decor storage GUID repair was incomplete for Battle.net account {}",
battlenetAccountId);
ReconcileLicensedQuantities(battlenetAccountId);
if (!MaterializeMissingDecorInstances(battlenetAccountId))
TC_LOG_ERROR("housing", "Decor storage materialization was incomplete for Battle.net account {}",
battlenetAccountId);
// Delegate wire serialization and per-session create/update state to the
// established member handler after persistence has been normalized.
session->HandleHousingDecorRequestStorage(packet);
}
}