Core/Housing: stage 34 snapshot Initiative reward delivery

This commit is contained in:
Gexo91
2026-09-12 18:06:25 +03:00
parent effc57e7c0
commit 1df23abdfb
@@ -9,6 +9,7 @@
#include "HousingMgr.h" #include "HousingMgr.h"
#include "Log.h" #include "Log.h"
#include "Player.h" #include "Player.h"
#include <limits>
#include <mutex> #include <mutex>
namespace Housing namespace Housing
@@ -16,8 +17,11 @@ namespace Housing
namespace namespace
{ {
constexpr uint32 MaxArchivedRewardsPerNeighborhood = 32; constexpr uint32 MaxArchivedRewardsPerNeighborhood = 32;
constexpr uint32 MaxRewardEntriesPerClaim = 64;
constexpr uint8 RewardClaimPending = 0; constexpr uint8 RewardClaimPending = 0;
constexpr uint8 RewardClaimCompleted = 1; constexpr uint8 RewardClaimCompleted = 1;
constexpr uint8 RewardGrantPending = 0;
constexpr uint8 RewardGrantDelivered = 1;
struct ArchivedRewardState struct ArchivedRewardState
{ {
@@ -41,6 +45,14 @@ namespace Housing
uint64 CompletedAt = 0; uint64 CompletedAt = 0;
}; };
struct RewardCatalogEntry
{
uint32 EntryId = 0;
InitiativeRewardGrantType Type = InitiativeRewardGrantType::Item;
uint32 AssetId = 0;
uint32 Quantity = 0;
};
std::mutex InitiativeRewardClaimMutex; std::mutex InitiativeRewardClaimMutex;
bool PlayerOwnsNeighborhood(Player const* player, ObjectGuid const& neighborhoodGuid) bool PlayerOwnsNeighborhood(Player const* player, ObjectGuid const& neighborhoodGuid)
@@ -55,6 +67,12 @@ namespace Housing
return false; return false;
} }
bool IsSupportedGrantType(uint8 value)
{
return value == uint8(InitiativeRewardGrantType::Item) ||
value == uint8(InitiativeRewardGrantType::Currency);
}
bool LoadArchivedReward(ObjectGuid const& neighborhoodGuid, uint64 generationId, ArchivedRewardState& state) bool LoadArchivedReward(ObjectGuid const& neighborhoodGuid, uint64 generationId, ArchivedRewardState& state)
{ {
state = {}; state = {};
@@ -132,6 +150,75 @@ namespace Housing
return state.GenerationId != 0 && state.InitiativeId != 0 && state.CharacterGuid != 0; return state.GenerationId != 0 && state.InitiativeId != 0 && state.CharacterGuid != 0;
} }
InitiativeRewardClaimResult LoadCatalogEntries(uint16 rewardTier, std::vector<RewardCatalogEntry>& entries)
{
entries.clear();
QueryResult result = CharacterDatabase.PQuery(
"SELECT EntryID, GrantType, AssetID, Quantity FROM housing_initiative_reward_catalog "
"WHERE RewardTier = {} AND `Enabled` = 1 ORDER BY EntryID LIMIT {}",
uint32(rewardTier), MaxRewardEntriesPerClaim + 1);
if (!result)
return InitiativeRewardClaimResult::InvalidCatalog;
do
{
if (entries.size() >= MaxRewardEntriesPerClaim)
return InitiativeRewardClaimResult::SafetyLimitExceeded;
Field* fields = result->Fetch();
uint8 const grantType = fields[1].GetUInt8();
RewardCatalogEntry entry;
entry.EntryId = fields[0].GetUInt32();
entry.AssetId = fields[2].GetUInt32();
entry.Quantity = fields[3].GetUInt32();
if (!entry.EntryId || !IsSupportedGrantType(grantType) || !entry.AssetId || !entry.Quantity ||
entry.Quantity > uint32(std::numeric_limits<int32>::max()))
return InitiativeRewardClaimResult::InvalidCatalog;
entry.Type = InitiativeRewardGrantType(grantType);
entries.push_back(entry);
} while (result->NextRow());
return entries.empty() ? InitiativeRewardClaimResult::InvalidCatalog : InitiativeRewardClaimResult::Success;
}
InitiativeRewardClaimResult LoadDeliverySnapshot(uint64 claimId, std::vector<InitiativeRewardGrant>& grants)
{
grants.clear();
QueryResult result = CharacterDatabase.PQuery(
"SELECT EntryID, GrantType, AssetID, Quantity, State "
"FROM housing_initiative_reward_delivery WHERE ClaimID = {} ORDER BY EntryID LIMIT {}",
claimId, MaxRewardEntriesPerClaim + 1);
if (!result)
return InitiativeRewardClaimResult::Success;
do
{
if (grants.size() >= MaxRewardEntriesPerClaim)
return InitiativeRewardClaimResult::SafetyLimitExceeded;
Field* fields = result->Fetch();
uint8 const grantType = fields[1].GetUInt8();
uint8 const state = fields[4].GetUInt8();
InitiativeRewardGrant grant;
grant.EntryId = fields[0].GetUInt32();
grant.AssetId = fields[2].GetUInt32();
grant.Quantity = fields[3].GetUInt32();
if (!grant.EntryId || !IsSupportedGrantType(grantType) || !grant.AssetId || !grant.Quantity ||
grant.Quantity > uint32(std::numeric_limits<int32>::max()) ||
(state != RewardGrantPending && state != RewardGrantDelivered))
return InitiativeRewardClaimResult::PersistenceError;
grant.Type = InitiativeRewardGrantType(grantType);
grant.Delivered = state == RewardGrantDelivered;
grants.push_back(grant);
} while (result->NextRow());
return InitiativeRewardClaimResult::Success;
}
InitiativeRewardClaimResult PopulateReservation(Player const* player, ArchivedRewardState const& reward, InitiativeRewardClaimResult PopulateReservation(Player const* player, ArchivedRewardState const& reward,
RewardClaimState const& claim, InitiativeRewardClaimReservation& reservation) RewardClaimState const& claim, InitiativeRewardClaimReservation& reservation)
{ {
@@ -270,6 +357,132 @@ namespace Housing
return PopulateReservation(player, verifiedReward, claim, reservation); return PopulateReservation(player, verifiedReward, claim, reservation);
} }
InitiativeRewardClaimResult PrepareArchivedInitiativeRewardDelivery(Player const* player,
uint64 claimId, std::vector<InitiativeRewardGrant>& grants)
{
grants.clear();
if (!player || !claimId)
return InitiativeRewardClaimResult::InvalidRequest;
std::lock_guard<std::mutex> lock(InitiativeRewardClaimMutex);
RewardClaimState claim;
if (!LoadClaimById(claimId, claim))
return InitiativeRewardClaimResult::NoReward;
if (claim.CharacterGuid != player->GetGUID().GetCounter())
return InitiativeRewardClaimResult::PermissionDenied;
if (claim.State == RewardClaimCompleted)
return InitiativeRewardClaimResult::AlreadyClaimed;
ArchivedRewardState reward;
if (!LoadArchivedReward(claim.NeighborhoodGuid, claim.GenerationId, reward) ||
reward.InitiativeId != claim.InitiativeId)
return InitiativeRewardClaimResult::PersistenceError;
if (!reward.RewardAvailable)
return InitiativeRewardClaimResult::NoReward;
if (reward.RewardClaimed)
return InitiativeRewardClaimResult::AlreadyClaimed;
InitiativeRewardClaimResult const existingSnapshot = LoadDeliverySnapshot(claimId, grants);
if (existingSnapshot != InitiativeRewardClaimResult::Success)
return existingSnapshot;
if (!grants.empty())
return InitiativeRewardClaimResult::Success;
std::vector<RewardCatalogEntry> catalog;
InitiativeRewardClaimResult const catalogResult = LoadCatalogEntries(reward.RewardTier, catalog);
if (catalogResult != InitiativeRewardClaimResult::Success)
return catalogResult;
uint64 const now = uint64(GameTime::GetGameTime());
CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction();
for (RewardCatalogEntry const& entry : catalog)
{
transaction->PAppend(
"INSERT INTO housing_initiative_reward_delivery "
"(ClaimID, EntryID, GrantType, AssetID, Quantity, State, CreatedAt, UpdatedAt, DeliveredAt) "
"VALUES ({}, {}, {}, {}, {}, {}, {}, {}, 0) "
"ON DUPLICATE KEY UPDATE ClaimID = VALUES(ClaimID)",
claimId, entry.EntryId, uint32(entry.Type), entry.AssetId, entry.Quantity,
uint32(RewardGrantPending), now, now);
}
CharacterDatabase.DirectCommitTransaction(transaction);
InitiativeRewardClaimResult const snapshotResult = LoadDeliverySnapshot(claimId, grants);
if (snapshotResult != InitiativeRewardClaimResult::Success)
return snapshotResult;
if (grants.size() != catalog.size())
return InitiativeRewardClaimResult::PersistenceError;
for (std::size_t i = 0; i < grants.size(); ++i)
{
RewardCatalogEntry const& expected = catalog[i];
InitiativeRewardGrant const& actual = grants[i];
if (actual.EntryId != expected.EntryId || actual.Type != expected.Type ||
actual.AssetId != expected.AssetId || actual.Quantity != expected.Quantity || actual.Delivered)
return InitiativeRewardClaimResult::PersistenceError;
}
return InitiativeRewardClaimResult::Success;
}
InitiativeRewardClaimResult MarkArchivedInitiativeRewardGrantDelivered(Player const* player,
uint64 claimId, uint32 entryId)
{
if (!player || !claimId || !entryId)
return InitiativeRewardClaimResult::InvalidRequest;
std::lock_guard<std::mutex> lock(InitiativeRewardClaimMutex);
RewardClaimState claim;
if (!LoadClaimById(claimId, claim))
return InitiativeRewardClaimResult::NoReward;
if (claim.CharacterGuid != player->GetGUID().GetCounter())
return InitiativeRewardClaimResult::PermissionDenied;
if (claim.State == RewardClaimCompleted)
return InitiativeRewardClaimResult::AlreadyClaimed;
QueryResult row = CharacterDatabase.PQuery(
"SELECT GrantType, AssetID, Quantity, State FROM housing_initiative_reward_delivery "
"WHERE ClaimID = {} AND EntryID = {} LIMIT 1",
claimId, entryId);
if (!row)
return InitiativeRewardClaimResult::InvalidCatalog;
Field* fields = row->Fetch();
uint8 const grantType = fields[0].GetUInt8();
uint32 const assetId = fields[1].GetUInt32();
uint32 const quantity = fields[2].GetUInt32();
uint8 const state = fields[3].GetUInt8();
if (!IsSupportedGrantType(grantType) || !assetId || !quantity ||
quantity > uint32(std::numeric_limits<int32>::max()) ||
(state != RewardGrantPending && state != RewardGrantDelivered))
return InitiativeRewardClaimResult::PersistenceError;
if (state == RewardGrantDelivered)
return InitiativeRewardClaimResult::Success;
uint64 const now = uint64(GameTime::GetGameTime());
CharacterDatabase.DirectPExecute(
"UPDATE housing_initiative_reward_delivery SET State = {}, UpdatedAt = {}, "
"DeliveredAt = IF(DeliveredAt = 0, {}, DeliveredAt) "
"WHERE ClaimID = {} AND EntryID = {} AND State = {}",
uint32(RewardGrantDelivered), now, now, claimId, entryId, uint32(RewardGrantPending));
QueryResult verification = CharacterDatabase.PQuery(
"SELECT State, DeliveredAt FROM housing_initiative_reward_delivery "
"WHERE ClaimID = {} AND EntryID = {} LIMIT 1",
claimId, entryId);
if (!verification)
return InitiativeRewardClaimResult::PersistenceError;
Field* verified = verification->Fetch();
if (verified[0].GetUInt8() != RewardGrantDelivered || !verified[1].GetUInt64())
return InitiativeRewardClaimResult::PersistenceError;
return InitiativeRewardClaimResult::Success;
}
InitiativeRewardClaimResult FinalizeArchivedInitiativeReward(Player const* player, uint64 claimId) InitiativeRewardClaimResult FinalizeArchivedInitiativeReward(Player const* player, uint64 claimId)
{ {
if (!player || !claimId) if (!player || !claimId)
@@ -295,6 +508,22 @@ namespace Housing
if (!reward.RewardAvailable) if (!reward.RewardAvailable)
return InitiativeRewardClaimResult::NoReward; return InitiativeRewardClaimResult::NoReward;
QueryResult deliveryCount = CharacterDatabase.PQuery(
"SELECT COUNT(*), SUM(State = {}) FROM housing_initiative_reward_delivery WHERE ClaimID = {}",
uint32(RewardGrantPending), claimId);
if (!deliveryCount)
return InitiativeRewardClaimResult::PersistenceError;
Field* deliveryFields = deliveryCount->Fetch();
uint64 const totalEntries = deliveryFields[0].GetUInt64();
uint64 const pendingEntries = deliveryFields[1].GetUInt64();
if (!totalEntries)
return InitiativeRewardClaimResult::InvalidCatalog;
if (totalEntries > MaxRewardEntriesPerClaim)
return InitiativeRewardClaimResult::SafetyLimitExceeded;
if (pendingEntries)
return InitiativeRewardClaimResult::DeliveryIncomplete;
uint64 const now = uint64(GameTime::GetGameTime()); uint64 const now = uint64(GameTime::GetGameTime());
CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction(); CharacterDatabaseTransaction transaction = CharacterDatabase.BeginTransaction();
transaction->PAppend( transaction->PAppend(