Core/Housing: stage 23 make decor storage mutations atomic

This commit is contained in:
Gexo91
2026-09-12 14:38:14 +03:00
parent 5ff3afc24c
commit 9262b50134
@@ -3,10 +3,14 @@
*/
#include "HousingDecorSession.h"
#include "DatabaseEnv.h"
#include "HousingMgr.h"
#include "HousingPackets.h"
#include "Player.h"
#include "WorldSession.h"
#include <algorithm>
#include <utility>
#include <vector>
namespace Housing
{
@@ -138,4 +142,173 @@ namespace Housing
session->HandleHousingDecorSetDyeSlots(packet);
}
void HandleDecorDeleteFromStorageStage23(WorldSession* session,
WorldPackets::Housing::HousingDecorDeleteFromStorage& packet)
{
if (!session)
return;
WorldPackets::Housing::HousingDecorDeleteFromStorageResponse response;
response.Result = ToDecorPacketResult(Result::Success);
uint32 const battlenetAccountId = session->GetBattlenetAccountId();
std::vector<std::pair<ObjectGuid, uint32>> validatedDecor;
validatedDecor.reserve(packet.DecorGUIDs.size());
// Validate the entire wire batch before beginning the transaction. Reject
// duplicate GUIDs so one storage instance can never decrement its license twice.
for (ObjectGuid const& decorGuid : packet.DecorGUIDs)
{
if (!IsDecorGuid(decorGuid) || !decorGuid.GetEntry() ||
std::find_if(validatedDecor.begin(), validatedDecor.end(),
[&decorGuid](auto const& entry) { return entry.first == decorGuid; }) != validatedDecor.end())
{
response.Result = ToDecorPacketResult(Result::InvalidDecorItem);
break;
}
QueryResult storedDecor = CharacterDatabase.PQuery(
"SELECT i.DecorID FROM housing_decor_instance i "
"INNER JOIN account_housing_decor a ON a.BattlenetAccountId = i.BattlenetAccountId "
"AND a.DecorId = i.DecorID "
"WHERE i.BattlenetAccountId = {} AND i.DecorGuid = '{}' "
"AND i.PlacementStatus = 0 AND i.HouseGuid = '' AND a.Quantity > 0 LIMIT 1",
battlenetAccountId, decorGuid.ToString());
if (!storedDecor)
{
response.Result = ToDecorPacketResult(Result::InvalidDecorItem);
break;
}
uint32 const decorId = storedDecor->Fetch()[0].GetUInt32();
if (!decorId || decorId != decorGuid.GetEntry())
{
response.Result = ToDecorPacketResult(Result::InvalidDecorItem);
break;
}
validatedDecor.emplace_back(decorGuid, decorId);
}
if (response.Result == ToDecorPacketResult(Result::Success) && !validatedDecor.empty())
{
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
for (auto const& [decorGuid, decorId] : validatedDecor)
{
trans->PAppend(
"DELETE FROM housing_decor_instance WHERE BattlenetAccountId = {} "
"AND DecorGuid = '{}' AND DecorID = {} AND PlacementStatus = 0 AND HouseGuid = ''",
battlenetAccountId, decorGuid.ToString(), decorId);
trans->PAppend(
"UPDATE account_housing_decor SET Quantity = Quantity - 1 "
"WHERE BattlenetAccountId = {} AND DecorId = {} AND Quantity > 0",
battlenetAccountId, decorId);
}
trans->PAppend(
"DELETE FROM account_housing_decor WHERE BattlenetAccountId = {} AND Quantity = 0",
battlenetAccountId);
CharacterDatabase.DirectCommitTransaction(trans);
}
session->SendPacket(response.Write());
}
void HandleDecorRedeemDeferredDecorStage23(WorldSession* session,
WorldPackets::Housing::HousingDecorRedeemDeferredDecor& packet)
{
if (!session)
return;
WorldPackets::Housing::HousingRedeemDeferredDecorResponse response;
response.TransactionID = packet.TransactionID;
response.Result = ToDecorPacketResult(Result::InvalidDecorItem);
Player* player = session->GetPlayer();
if (!player || !packet.DecorID || !packet.TransactionID)
{
session->SendPacket(response.Write());
return;
}
uint32 const battlenetAccountId = session->GetBattlenetAccountId();
QueryResult existing = CharacterDatabase.PQuery(
"SELECT DecorID, DecorGuid FROM housing_deferred_decor_redemption "
"WHERE BattlenetAccountId = {} AND TransactionID = {} LIMIT 1",
battlenetAccountId, packet.TransactionID);
if (existing)
{
Field* fields = existing->Fetch();
uint32 const persistedDecorId = fields[0].GetUInt32();
ObjectGuid const persistedDecorGuid = ObjectGuid::FromString(fields[1].GetString());
if (persistedDecorId != packet.DecorID || !IsDecorGuid(persistedDecorGuid) ||
persistedDecorGuid.GetEntry() != packet.DecorID)
{
session->SendPacket(response.Write());
return;
}
response.DecorGUID = persistedDecorGuid;
// Heal only rows that can be reconstructed without guessing quantities.
// Existing license quantities are preserved; a missing license is restored
// to the minimum one implied by the persisted redemption record.
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
trans->PAppend(
"INSERT INTO account_housing_decor (BattlenetAccountId, DecorId, Quantity) VALUES ({}, {}, 1) "
"ON DUPLICATE KEY UPDATE Quantity = Quantity",
battlenetAccountId, packet.DecorID);
trans->PAppend(
"INSERT INTO housing_decor_instance "
"(BattlenetAccountId, DecorGuid, DecorID, HouseGuid, PlacementStatus, SourceType, SourceValue) "
"VALUES ({}, '{}', {}, '', 0, 0, '') "
"ON DUPLICATE KEY UPDATE DecorID = VALUES(DecorID)",
battlenetAccountId, response.DecorGUID.ToString(), packet.DecorID);
CharacterDatabase.DirectCommitTransaction(trans);
}
else
{
response.DecorGUID = ObjectGuid::Create<HighGuid::Housing>(
1, packet.DecorID, packet.TransactionID, player->GetGUID().GetCounter());
if (!IsDecorGuid(response.DecorGUID))
{
session->SendPacket(response.Write());
return;
}
// The redemption marker, licensed quantity and concrete storage instance
// are one logical mutation and must either all persist or all roll back.
CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction();
trans->PAppend(
"INSERT INTO housing_deferred_decor_redemption "
"(BattlenetAccountId, TransactionID, DecorID, DecorGuid) VALUES ({}, {}, {}, '{}')",
battlenetAccountId, packet.TransactionID, packet.DecorID, response.DecorGUID.ToString());
trans->PAppend(
"INSERT INTO account_housing_decor (BattlenetAccountId, DecorId, Quantity) VALUES ({}, {}, 1) "
"ON DUPLICATE KEY UPDATE Quantity = Quantity + 1",
battlenetAccountId, packet.DecorID);
trans->PAppend(
"INSERT INTO housing_decor_instance "
"(BattlenetAccountId, DecorGuid, DecorID, HouseGuid, PlacementStatus, SourceType, SourceValue) "
"VALUES ({}, '{}', {}, '', 0, 0, '')",
battlenetAccountId, response.DecorGUID.ToString(), packet.DecorID);
CharacterDatabase.DirectCommitTransaction(trans);
}
QueryResult committed = CharacterDatabase.PQuery(
"SELECT 1 FROM housing_deferred_decor_redemption r "
"INNER JOIN housing_decor_instance i ON i.BattlenetAccountId = r.BattlenetAccountId "
"AND i.DecorGuid = r.DecorGuid AND i.DecorID = r.DecorID "
"INNER JOIN account_housing_decor a ON a.BattlenetAccountId = r.BattlenetAccountId "
"AND a.DecorId = r.DecorID AND a.Quantity > 0 "
"WHERE r.BattlenetAccountId = {} AND r.TransactionID = {} "
"AND r.DecorID = {} AND r.DecorGuid = '{}' LIMIT 1",
battlenetAccountId, packet.TransactionID, packet.DecorID, response.DecorGUID.ToString());
if (committed)
response.Result = ToDecorPacketResult(Result::Success);
session->SendPacket(response.Write());
}
}