Core/Housing: stage 35 execute Initiative reward grants recoverably

This commit is contained in:
Gexo91
2026-09-12 18:16:06 +03:00
parent 6d5684fc2a
commit 02ca8b8788
@@ -0,0 +1,298 @@
/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*/
#include "HousingInitiativeRewardExecutor.h"
#include "DatabaseEnv.h"
#include "GameTime.h"
#include "HousingInitiativeReward.h"
#include "Log.h"
#include "Player.h"
#include <limits>
#include <mutex>
#include <vector>
namespace Housing
{
namespace
{
constexpr uint8 RewardExecutionApplying = 0;
constexpr uint8 RewardExecutionDelivered = 1;
constexpr uint8 RewardExecutionAmbiguous = 2;
struct RewardExecutionState
{
bool Exists = false;
InitiativeRewardGrantType Type = InitiativeRewardGrantType::Item;
uint32 AssetId = 0;
uint32 Quantity = 0;
uint8 State = RewardExecutionApplying;
uint64 ObservedBefore = 0;
uint64 ExpectedAfter = 0;
uint64 ObservedAfter = 0;
};
std::mutex InitiativeRewardExecutionMutex;
InitiativeRewardExecutionResult MapClaimResult(InitiativeRewardClaimResult result)
{
switch (result)
{
case InitiativeRewardClaimResult::Success:
return InitiativeRewardExecutionResult::Success;
case InitiativeRewardClaimResult::InvalidRequest:
return InitiativeRewardExecutionResult::InvalidRequest;
case InitiativeRewardClaimResult::PermissionDenied:
case InitiativeRewardClaimResult::ClaimInProgress:
return InitiativeRewardExecutionResult::PermissionDenied;
case InitiativeRewardClaimResult::NoReward:
return InitiativeRewardExecutionResult::NoReward;
case InitiativeRewardClaimResult::AlreadyClaimed:
return InitiativeRewardExecutionResult::AlreadyClaimed;
case InitiativeRewardClaimResult::DeliveryIncomplete:
return InitiativeRewardExecutionResult::RetryRequired;
case InitiativeRewardClaimResult::SafetyLimitExceeded:
case InitiativeRewardClaimResult::InvalidCatalog:
case InitiativeRewardClaimResult::PersistenceError:
return InitiativeRewardExecutionResult::PersistenceError;
}
return InitiativeRewardExecutionResult::PersistenceError;
}
bool IsSupportedGrantType(uint8 value)
{
return value == uint8(InitiativeRewardGrantType::Item) ||
value == uint8(InitiativeRewardGrantType::Currency);
}
uint64 ObserveGrantQuantity(Player const* player, InitiativeRewardGrant const& grant)
{
if (!player)
return 0;
switch (grant.Type)
{
case InitiativeRewardGrantType::Item:
return player->GetItemCount(grant.AssetId, true);
case InitiativeRewardGrantType::Currency:
return player->GetCurrencyQuantity(grant.AssetId);
}
return 0;
}
bool ApplyGrant(Player* player, InitiativeRewardGrant const& grant)
{
if (!player || !grant.AssetId || !grant.Quantity)
return false;
switch (grant.Type)
{
case InitiativeRewardGrantType::Item:
return player->AddItem(grant.AssetId, grant.Quantity);
case InitiativeRewardGrantType::Currency:
player->AddCurrency(grant.AssetId, grant.Quantity);
return true;
}
return false;
}
bool LoadExecutionState(uint64 claimId, uint32 entryId, RewardExecutionState& execution)
{
execution = {};
QueryResult result = CharacterDatabase.PQuery(
"SELECT GrantType, AssetID, Quantity, State, ObservedBefore, ExpectedAfter, ObservedAfter "
"FROM housing_initiative_reward_execution WHERE ClaimID = {} AND EntryID = {} LIMIT 1",
claimId, entryId);
if (!result)
return true;
Field* fields = result->Fetch();
uint8 const grantType = fields[0].GetUInt8();
uint8 const state = fields[3].GetUInt8();
if (!IsSupportedGrantType(grantType) || state > RewardExecutionAmbiguous)
return false;
execution.Exists = true;
execution.Type = InitiativeRewardGrantType(grantType);
execution.AssetId = fields[1].GetUInt32();
execution.Quantity = fields[2].GetUInt32();
execution.State = state;
execution.ObservedBefore = fields[4].GetUInt64();
execution.ExpectedAfter = fields[5].GetUInt64();
execution.ObservedAfter = fields[6].GetUInt64();
return execution.AssetId != 0 && execution.Quantity != 0;
}
bool ExecutionMatchesGrant(RewardExecutionState const& execution, InitiativeRewardGrant const& grant)
{
return execution.Exists && execution.Type == grant.Type && execution.AssetId == grant.AssetId &&
execution.Quantity == grant.Quantity;
}
bool PersistApplyingState(uint64 claimId, InitiativeRewardGrant const& grant,
uint64 observedBefore, uint64 expectedAfter, RewardExecutionState& execution)
{
uint64 const now = uint64(GameTime::GetGameTime());
CharacterDatabase.DirectPExecute(
"INSERT INTO housing_initiative_reward_execution "
"(ClaimID, EntryID, GrantType, AssetID, Quantity, State, ObservedBefore, ExpectedAfter, ObservedAfter, StartedAt, UpdatedAt) "
"VALUES ({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) "
"ON DUPLICATE KEY UPDATE ClaimID = VALUES(ClaimID)",
claimId, grant.EntryId, uint32(grant.Type), grant.AssetId, grant.Quantity,
uint32(RewardExecutionApplying), observedBefore, expectedAfter, observedBefore, now, now);
if (!LoadExecutionState(claimId, grant.EntryId, execution) || !ExecutionMatchesGrant(execution, grant))
return false;
return execution.State == RewardExecutionApplying && execution.ObservedBefore == observedBefore &&
execution.ExpectedAfter == expectedAfter;
}
bool PersistExecutionState(uint64 claimId, uint32 entryId, uint8 state, uint64 observedAfter)
{
if (state > RewardExecutionAmbiguous)
return false;
uint64 const now = uint64(GameTime::GetGameTime());
CharacterDatabase.DirectPExecute(
"UPDATE housing_initiative_reward_execution SET State = {}, ObservedAfter = {}, UpdatedAt = {} "
"WHERE ClaimID = {} AND EntryID = {}",
uint32(state), observedAfter, now, claimId, entryId);
RewardExecutionState verification;
if (!LoadExecutionState(claimId, entryId, verification) || !verification.Exists)
return false;
return verification.State == state && verification.ObservedAfter == observedAfter;
}
InitiativeRewardExecutionResult SealDeliveredGrant(Player const* player, uint64 claimId,
InitiativeRewardGrant const& grant, uint64 observedAfter)
{
if (!PersistExecutionState(claimId, grant.EntryId, RewardExecutionDelivered, observedAfter))
return InitiativeRewardExecutionResult::PersistenceError;
InitiativeRewardClaimResult const markResult =
MarkArchivedInitiativeRewardGrantDelivered(player, claimId, grant.EntryId);
return MapClaimResult(markResult);
}
InitiativeRewardExecutionResult ExecuteGrant(Player* player, uint64 claimId, InitiativeRewardGrant const& grant)
{
if (!player || !claimId || !grant.EntryId || !grant.AssetId || !grant.Quantity)
return InitiativeRewardExecutionResult::InvalidRequest;
if (grant.Delivered)
return InitiativeRewardExecutionResult::Success;
std::lock_guard<std::mutex> lock(InitiativeRewardExecutionMutex);
RewardExecutionState execution;
if (!LoadExecutionState(claimId, grant.EntryId, execution))
return InitiativeRewardExecutionResult::PersistenceError;
uint64 const observedNow = ObserveGrantQuantity(player, grant);
if (execution.Exists)
{
if (!ExecutionMatchesGrant(execution, grant))
return InitiativeRewardExecutionResult::PersistenceError;
if (execution.State == RewardExecutionDelivered)
return SealDeliveredGrant(player, claimId, grant, observedNow);
if (execution.State == RewardExecutionAmbiguous)
return InitiativeRewardExecutionResult::AmbiguousState;
if (observedNow == execution.ExpectedAfter)
return SealDeliveredGrant(player, claimId, grant, observedNow);
if (observedNow != execution.ObservedBefore)
{
if (!PersistExecutionState(claimId, grant.EntryId, RewardExecutionAmbiguous, observedNow))
return InitiativeRewardExecutionResult::PersistenceError;
TC_LOG_ERROR("housing",
"Initiative reward claim {} entry {} cannot be reconciled safely: observed {} expected before {} or after {}",
claimId, grant.EntryId, observedNow, execution.ObservedBefore, execution.ExpectedAfter);
return InitiativeRewardExecutionResult::AmbiguousState;
}
}
else
{
if (observedNow > std::numeric_limits<uint32>::max() - uint64(grant.Quantity))
return InitiativeRewardExecutionResult::GrantFailed;
uint64 const expectedAfter = observedNow + grant.Quantity;
if (!PersistApplyingState(claimId, grant, observedNow, expectedAfter, execution))
return InitiativeRewardExecutionResult::PersistenceError;
}
bool const grantApplied = ApplyGrant(player, grant);
uint64 const observedAfterGrant = ObserveGrantQuantity(player, grant);
if (!grantApplied || observedAfterGrant != execution.ExpectedAfter)
{
if (observedAfterGrant == execution.ObservedBefore)
{
TC_LOG_DEBUG("housing", "Initiative reward claim {} entry {} could not be granted; retry remains safe",
claimId, grant.EntryId);
return InitiativeRewardExecutionResult::GrantFailed;
}
if (!PersistExecutionState(claimId, grant.EntryId, RewardExecutionAmbiguous, observedAfterGrant))
return InitiativeRewardExecutionResult::PersistenceError;
TC_LOG_ERROR("housing",
"Initiative reward claim {} entry {} changed quantity unexpectedly from {} to {} while expecting {}",
claimId, grant.EntryId, execution.ObservedBefore, observedAfterGrant, execution.ExpectedAfter);
return InitiativeRewardExecutionResult::AmbiguousState;
}
// Persist inventory/currency state synchronously before sealing the delivery
// ledger. If the process stops before this commit, retry observes the old
// quantity; if it stops afterwards, retry observes ExpectedAfter and seals it.
CharacterDatabaseTransaction playerSave = CharacterDatabase.BeginTransaction();
player->SaveToDB(playerSave);
CharacterDatabase.DirectCommitTransaction(playerSave);
uint64 const verifiedQuantity = ObserveGrantQuantity(player, grant);
if (verifiedQuantity != execution.ExpectedAfter)
{
if (!PersistExecutionState(claimId, grant.EntryId, RewardExecutionAmbiguous, verifiedQuantity))
return InitiativeRewardExecutionResult::PersistenceError;
return InitiativeRewardExecutionResult::AmbiguousState;
}
return SealDeliveredGrant(player, claimId, grant, verifiedQuantity);
}
}
InitiativeRewardExecutionResult ExecuteArchivedInitiativeReward(Player* player, uint64 claimId)
{
if (!player || !claimId)
return InitiativeRewardExecutionResult::InvalidRequest;
std::vector<InitiativeRewardGrant> grants;
InitiativeRewardClaimResult const prepareResult =
PrepareArchivedInitiativeRewardDelivery(player, claimId, grants);
if (prepareResult != InitiativeRewardClaimResult::Success)
return MapClaimResult(prepareResult);
if (grants.empty())
return InitiativeRewardExecutionResult::PersistenceError;
for (InitiativeRewardGrant const& grant : grants)
{
InitiativeRewardExecutionResult const result = ExecuteGrant(player, claimId, grant);
if (result != InitiativeRewardExecutionResult::Success)
return result;
}
InitiativeRewardClaimResult const finalizeResult = FinalizeArchivedInitiativeReward(player, claimId);
return MapClaimResult(finalizeResult);
}
}