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