Core/Housing: stage 35 execute Initiative reward grants recoverably
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user