Core/Housing: stage 23 make decor storage mutations atomic
This commit is contained in:
@@ -3,10 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "HousingDecorSession.h"
|
#include "HousingDecorSession.h"
|
||||||
|
#include "DatabaseEnv.h"
|
||||||
#include "HousingMgr.h"
|
#include "HousingMgr.h"
|
||||||
#include "HousingPackets.h"
|
#include "HousingPackets.h"
|
||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "WorldSession.h"
|
#include "WorldSession.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Housing
|
namespace Housing
|
||||||
{
|
{
|
||||||
@@ -138,4 +142,173 @@ namespace Housing
|
|||||||
|
|
||||||
session->HandleHousingDecorSetDyeSlots(packet);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user