Core/Housing: stage 38 repair persistent decor storage GUIDs
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user