|
@@ -1,15 +1,190 @@
|
|
|
#include "queuez_outcome_staging.h"
|
|
#include "queuez_outcome_staging.h"
|
|
|
|
|
|
|
|
|
|
+#include <algorithm>
|
|
|
#include <limits>
|
|
#include <limits>
|
|
|
|
|
+#include <optional>
|
|
|
|
|
|
|
|
#include "../../../../core/logging/log.h"
|
|
#include "../../../../core/logging/log.h"
|
|
|
|
|
+#include "../../../../middleware/datagen/family4/loadout/loadout_resolver.h"
|
|
|
#include "../../../../middleware/secure_channel/runtime.h"
|
|
#include "../../../../middleware/secure_channel/runtime.h"
|
|
|
-#include "../../../../state/runtime/runtime.h"
|
|
|
|
|
#include "queuez_state_validation.h"
|
|
#include "queuez_state_validation.h"
|
|
|
|
|
|
|
|
namespace sunrise::server::bap::encrypted::queuez {
|
|
namespace sunrise::server::bap::encrypted::queuez {
|
|
|
namespace {
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
+/** Extends the active feed overlay with the two identities touched by this equip. */
|
|
|
|
|
+[[nodiscard]] bool merge_equipment_presentation_rows(
|
|
|
|
|
+ std::span<const AcquisitionPresentationRow> existing,
|
|
|
|
|
+ const state::PendingEquipmentSwap& mutation,
|
|
|
|
|
+ std::array<AcquisitionPresentationRow, kAcquisitionPresentationRowCapacity>& output,
|
|
|
|
|
+ std::uint8_t& count) noexcept {
|
|
|
|
|
+ if (existing.size() > output.size()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ std::copy(existing.begin(), existing.end(), output.begin());
|
|
|
|
|
+ std::size_t used = existing.size();
|
|
|
|
|
+
|
|
|
|
|
+ const state::AccountState account = state::account_snapshot();
|
|
|
|
|
+ middleware::datagen::family4::loadout::ResolvedLoadout loadout{};
|
|
|
|
|
+ if (!mutation.prepared || mutation.characterIndex >= account.characterCount
|
|
|
|
|
+ || account.characters[mutation.characterIndex].soid != mutation.characterSoid
|
|
|
|
|
+ || !middleware::datagen::family4::loadout::resolve(
|
|
|
|
|
+ account, mutation.characterIndex, loadout)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const auto add = [&](std::uint64_t instanceSoid) noexcept -> bool {
|
|
|
|
|
+ if (instanceSoid == 0) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (std::size_t index = 0; index < used; ++index) {
|
|
|
|
|
+ if (output[index].instanceSoid == instanceSoid) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (used >= output.size()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (std::size_t index = 0; index < loadout.itemCount; ++index) {
|
|
|
|
|
+ const auto& item = loadout.items[index];
|
|
|
|
|
+ if (item.instance.instanceSoid != instanceSoid) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (std::size_t prior = 0; prior < used; ++prior) {
|
|
|
|
|
+ if (output[prior].inventoryRow == item.inventoryRow) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ output[used++] = AcquisitionPresentationRow{
|
|
|
|
|
+ instanceSoid, static_cast<std::uint16_t>(item.inventoryRow)};
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (!add(mutation.requestedInstanceSoid) || !add(mutation.previousInstanceSoid)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ count = static_cast<std::uint8_t>(used);
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Stages the character upsert and appended resident an item acquisition promised. */
|
|
|
|
|
+[[nodiscard]] bool
|
|
|
|
|
+stage_item_acquisition_push(Scratch& scratch,
|
|
|
|
|
+ const SessionState& before,
|
|
|
|
|
+ const ItemAcquisition& acquisition,
|
|
|
|
|
+ const state::PendingItemAcquisition& pending,
|
|
|
|
|
+ std::optional<std::uint16_t> pendingSeasonReward,
|
|
|
|
|
+ std::span<const AcquisitionPresentationRow> presentationRows,
|
|
|
|
|
+ std::span<const std::byte, state::kAesKeySize> key,
|
|
|
|
|
+ std::array<std::byte, state::kBapNonceSize>& nonce,
|
|
|
|
|
+ std::span<std::byte> response,
|
|
|
|
|
+ std::size_t& written,
|
|
|
|
|
+ SessionState& after) noexcept {
|
|
|
|
|
+ const std::size_t appendedIndex = before.family4ResidentCount;
|
|
|
|
|
+ bool preservedManifest = acquisition.after.family4ResidentCount == appendedIndex + 1U;
|
|
|
|
|
+ for (std::size_t index = 0; preservedManifest && index < appendedIndex; ++index) {
|
|
|
|
|
+ preservedManifest = acquisition.after.family4Residents[index].objectSoid
|
|
|
|
|
+ == before.family4Residents[index].objectSoid
|
|
|
|
|
+ && acquisition.after.family4Residents[index].definitionId
|
|
|
|
|
+ == before.family4Residents[index].definitionId;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!valid(acquisition.after) || !preservedManifest
|
|
|
|
|
+ || acquisition.accountSoid != pending.accountSoid
|
|
|
|
|
+ || acquisition.characterSoid != pending.characterSoid
|
|
|
|
|
+ || acquisition.acquiredInstanceSoid != pending.acquiredInstanceSoid
|
|
|
|
|
+ || acquisition.updatesAccount != (pending.profileChanged || pendingSeasonReward.has_value())
|
|
|
|
|
+ || acquisition.accountSoid != before.family4RootSoid
|
|
|
|
|
+ || acquisition.after.family4RootSoid != before.family4RootSoid
|
|
|
|
|
+ || before.family4ResidentCount >= before.family4Residents.size()
|
|
|
|
|
+ || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|
|
+ || acquisition.after.family4Version != before.family4Version + 1
|
|
|
|
|
+ || acquisition.after.family4Residents[appendedIndex].objectSoid
|
|
|
|
|
+ != acquisition.acquiredInstanceSoid
|
|
|
|
|
+ || acquisition.after.family4Residents[appendedIndex].definitionId
|
|
|
|
|
+ != acquisition.itemInstanceDefinitionId
|
|
|
|
|
+ || !push::append_item_acquisition_notification(scratch,
|
|
|
|
|
+ acquisition,
|
|
|
|
|
+ pending,
|
|
|
|
|
+ pendingSeasonReward,
|
|
|
|
|
+ presentationRows,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
+ after = acquisition.after;
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Stages the account upsert and optional manifest append a profile acquisition promised. */
|
|
|
|
|
+[[nodiscard]] bool
|
|
|
|
|
+stage_profile_item_acquisition_push(Scratch& scratch,
|
|
|
|
|
+ const SessionState& before,
|
|
|
|
|
+ const ProfileItemAcquisition& acquisition,
|
|
|
|
|
+ const state::PendingProfileItemAcquisition& pending,
|
|
|
|
|
+ std::optional<std::uint16_t> pendingSeasonReward,
|
|
|
|
|
+ std::span<const std::byte, state::kAesKeySize> key,
|
|
|
|
|
+ std::array<std::byte, state::kBapNonceSize>& nonce,
|
|
|
|
|
+ std::span<std::byte> response,
|
|
|
|
|
+ std::size_t& written,
|
|
|
|
|
+ SessionState& after) noexcept {
|
|
|
|
|
+ const std::size_t priorResidentCount = before.family4ResidentCount;
|
|
|
|
|
+ const std::size_t expectedResidentCount =
|
|
|
|
|
+ priorResidentCount + static_cast<std::size_t>(acquisition.appendedResident);
|
|
|
|
|
+ bool validManifest = expectedResidentCount <= acquisition.after.family4Residents.size()
|
|
|
|
|
+ && acquisition.after.family4ResidentCount == expectedResidentCount;
|
|
|
|
|
+ for (std::size_t index = 0; validManifest && index < priorResidentCount; ++index) {
|
|
|
|
|
+ validManifest = acquisition.after.family4Residents[index].objectSoid
|
|
|
|
|
+ == before.family4Residents[index].objectSoid
|
|
|
|
|
+ && acquisition.after.family4Residents[index].definitionId
|
|
|
|
|
+ == before.family4Residents[index].definitionId;
|
|
|
|
|
+ }
|
|
|
|
|
+ std::size_t priorProfileResidentMatches = 0;
|
|
|
|
|
+ for (std::size_t index = 0; index < priorResidentCount; ++index) {
|
|
|
|
|
+ const ResidentObject& resident = before.family4Residents[index];
|
|
|
|
|
+ priorProfileResidentMatches += static_cast<std::size_t>(
|
|
|
|
|
+ acquisition.acquiredInstanceSoid != 0
|
|
|
|
|
+ && resident.objectSoid == acquisition.acquiredInstanceSoid
|
|
|
|
|
+ && resident.definitionId == acquisition.itemInstanceDefinitionId);
|
|
|
|
|
+ }
|
|
|
|
|
+ const bool appendedResidentValid =
|
|
|
|
|
+ !acquisition.appendedResident
|
|
|
|
|
+ || (priorResidentCount < acquisition.after.family4Residents.size()
|
|
|
|
|
+ && acquisition.after.family4Residents[priorResidentCount].objectSoid
|
|
|
|
|
+ == acquisition.acquiredInstanceSoid
|
|
|
|
|
+ && acquisition.after.family4Residents[priorResidentCount].definitionId
|
|
|
|
|
+ == acquisition.itemInstanceDefinitionId
|
|
|
|
|
+ && priorProfileResidentMatches == 0);
|
|
|
|
|
+ const bool sourceIdentityValid =
|
|
|
|
|
+ acquisition.actionSource == (acquisition.acquiredInstanceSoid != 0)
|
|
|
|
|
+ && (acquisition.actionSource
|
|
|
|
|
+ ? acquisition.itemInstanceDefinitionId != 0 && appendedResidentValid
|
|
|
|
|
+ && (acquisition.appendedResident || priorProfileResidentMatches == 1)
|
|
|
|
|
+ : acquisition.itemInstanceDefinitionId == 0 && !acquisition.appendedResident
|
|
|
|
|
+ && priorProfileResidentMatches == 0);
|
|
|
|
|
+ if (!valid(acquisition.after) || !validManifest || !sourceIdentityValid
|
|
|
|
|
+ || acquisition.accountSoid != pending.accountSoid
|
|
|
|
|
+ || acquisition.acquiredInstanceSoid != pending.acquiredInstanceSoid
|
|
|
|
|
+ || acquisition.actionSource != pending.actionSource
|
|
|
|
|
+ || acquisition.appendedResident != (pending.appended && pending.actionSource)
|
|
|
|
|
+ || acquisition.accountSoid != before.family4RootSoid || before.family4ResidentCount == 0
|
|
|
|
|
+ || acquisition.accountDefinitionId != before.family4Residents.front().definitionId
|
|
|
|
|
+ || acquisition.after.family4RootSoid != before.family4RootSoid
|
|
|
|
|
+ || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|
|
+ || acquisition.after.family4Version != before.family4Version + 1
|
|
|
|
|
+ || !push::append_profile_item_acquisition_notification(
|
|
|
|
|
+ scratch, acquisition, pending, pendingSeasonReward, key, nonce, response, written)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
+ after = acquisition.after;
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Says whether equipping into one slot changes what the family-two member record publishes.
|
|
* Says whether equipping into one slot changes what the family-two member record publishes.
|
|
|
*
|
|
*
|
|
@@ -37,6 +212,8 @@ namespace {
|
|
|
bool stage_service_outcome(Scratch& scratch,
|
|
bool stage_service_outcome(Scratch& scratch,
|
|
|
const SessionState& before,
|
|
const SessionState& before,
|
|
|
const ServiceOutcome& outcome,
|
|
const ServiceOutcome& outcome,
|
|
|
|
|
+ bool preserveAcquisitionPresentation,
|
|
|
|
|
+ std::span<const AcquisitionPresentationRow> acquisitionPresentationRows,
|
|
|
std::span<const std::byte, state::kAesKeySize> key,
|
|
std::span<const std::byte, state::kAesKeySize> key,
|
|
|
std::array<std::byte, state::kBapNonceSize>& nonce,
|
|
std::array<std::byte, state::kBapNonceSize>& nonce,
|
|
|
std::span<std::byte> response,
|
|
std::span<std::byte> response,
|
|
@@ -49,20 +226,28 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
std::uint64_t bannerRoot = 0;
|
|
std::uint64_t bannerRoot = 0;
|
|
|
std::uint64_t socialRosterRoot = 0;
|
|
std::uint64_t socialRosterRoot = 0;
|
|
|
bool armsAbilityRefresh = false;
|
|
bool armsAbilityRefresh = false;
|
|
|
|
|
+ const auto* changeCharacter = transaction_if<ChangeCharacter>(outcome);
|
|
|
|
|
+ const auto* selectCharacter = transaction_if<SelectCharacter>(outcome);
|
|
|
const auto* equipment = transaction_if<EquipmentSwapTransaction>(outcome);
|
|
const auto* equipment = transaction_if<EquipmentSwapTransaction>(outcome);
|
|
|
const auto* subclassSelection = transaction_if<SubclassSelectionTransaction>(outcome);
|
|
const auto* subclassSelection = transaction_if<SubclassSelectionTransaction>(outcome);
|
|
|
const auto* itemState = transaction_if<ItemStateTransaction>(outcome);
|
|
const auto* itemState = transaction_if<ItemStateTransaction>(outcome);
|
|
|
const auto* currentActivity = transaction_if<CurrentActivityTransaction>(outcome);
|
|
const auto* currentActivity = transaction_if<CurrentActivityTransaction>(outcome);
|
|
|
|
|
+ const auto* artifactPurchase = transaction_if<ArtifactPurchaseTransaction>(outcome);
|
|
|
const auto* socket = transaction_if<SocketPlugTransaction>(outcome);
|
|
const auto* socket = transaction_if<SocketPlugTransaction>(outcome);
|
|
|
const auto* itemAcquisition = transaction_if<ItemAcquisitionTransaction>(outcome);
|
|
const auto* itemAcquisition = transaction_if<ItemAcquisitionTransaction>(outcome);
|
|
|
const auto* profileAcquisition = transaction_if<ProfileItemAcquisitionTransaction>(outcome);
|
|
const auto* profileAcquisition = transaction_if<ProfileItemAcquisitionTransaction>(outcome);
|
|
|
const auto* itemDismantle = transaction_if<ItemDismantleTransaction>(outcome);
|
|
const auto* itemDismantle = transaction_if<ItemDismantleTransaction>(outcome);
|
|
|
- const auto* allocation = transaction_if<state::activity::PendingAllocation>(outcome);
|
|
|
|
|
|
|
+ const auto* recordRewardGrant = transaction_if<RecordRewardGrantTransaction>(outcome);
|
|
|
|
|
+ const auto* seasonPassReward = transaction_if<SeasonPassRewardTransaction>(outcome);
|
|
|
|
|
+ const auto presentationRows = preserveAcquisitionPresentation
|
|
|
|
|
+ ? acquisitionPresentationRows
|
|
|
|
|
+ : std::span<const AcquisitionPresentationRow>{};
|
|
|
// Set before the branch chain rather than inside the equipment arm. That arm returns early
|
|
// Set before the branch chain rather than inside the equipment arm. That arm returns early
|
|
|
// when the staged after-image fails validation, and the equip has already moved State by
|
|
// when the staged after-image fails validation, and the equip has already moved State by
|
|
|
// then -- so the published record is stale on exactly the path the arm never finishes.
|
|
// then -- so the published record is stale on exactly the path the arm never finishes.
|
|
|
publication.rearmsSocialRosterRepush =
|
|
publication.rearmsSocialRosterRepush =
|
|
|
- equipment != nullptr && moves_social_roster(equipment->pending.equipmentSlotIndex);
|
|
|
|
|
|
|
+ equipment != nullptr && equipment->pending != nullptr
|
|
|
|
|
+ && moves_social_roster(equipment->pending->equipmentSlotIndex);
|
|
|
if (outcome.hasSubscription) {
|
|
if (outcome.hasSubscription) {
|
|
|
push::append_queuez_notification(scratch,
|
|
push::append_queuez_notification(scratch,
|
|
|
before,
|
|
before,
|
|
@@ -89,26 +274,48 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
} else if (outcome.hasChangeCharacter) {
|
|
} else if (outcome.hasChangeCharacter) {
|
|
|
// The reply already carries the version this patch promises. A patch that cannot be built
|
|
// The reply already carries the version this patch promises. A patch that cannot be built
|
|
|
// leaves the ladder where it is, instead of holding back that reply.
|
|
// leaves the ladder where it is, instead of holding back that reply.
|
|
|
- if (!push::append_change_character_notification(
|
|
|
|
|
- scratch, outcome.changeCharacter, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ if (changeCharacter == nullptr
|
|
|
|
|
+ || !push::append_change_character_notification(
|
|
|
|
|
+ scratch, *changeCharacter, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=change result=fail");
|
|
"ev=queuez stage=change result=fail");
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
|
- after = outcome.changeCharacter.after;
|
|
|
|
|
|
|
+ after = changeCharacter->after;
|
|
|
} else if (equipment != nullptr) {
|
|
} else if (equipment != nullptr) {
|
|
|
// Body processing already staged this exact after-image so the correlated opcode-403
|
|
// Body processing already staged this exact after-image so the correlated opcode-403
|
|
|
// response could promise its version. Reuse it here; staging a second revision would make
|
|
// response could promise its version. Reuse it here; staging a second revision would make
|
|
|
// the response and pushed Family-4 ladder disagree.
|
|
// the response and pushed Family-4 ladder disagree.
|
|
|
|
|
+ if (equipment->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *equipment->pending;
|
|
|
const EquipmentSwap& swap = equipment->update;
|
|
const EquipmentSwap& swap = equipment->update;
|
|
|
- if (!valid(swap.after) || swap.characterSoid != equipment->pending.characterSoid
|
|
|
|
|
|
|
+ std::array<AcquisitionPresentationRow, kAcquisitionPresentationRowCapacity>
|
|
|
|
|
+ mergedPresentationRows{};
|
|
|
|
|
+ std::uint8_t mergedPresentationRowCount = 0;
|
|
|
|
|
+ const bool hasPresentation =
|
|
|
|
|
+ preserveAcquisitionPresentation
|
|
|
|
|
+ && merge_equipment_presentation_rows(acquisitionPresentationRows,
|
|
|
|
|
+ pending,
|
|
|
|
|
+ mergedPresentationRows,
|
|
|
|
|
+ mergedPresentationRowCount);
|
|
|
|
|
+ if (preserveAcquisitionPresentation && !hasPresentation) {
|
|
|
|
|
+ core::log::write(core::log::Channel::server,
|
|
|
|
|
+ core::log::Level::warn,
|
|
|
|
|
+ "ev=queuez stage=equip_presentation result=fail");
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto equipmentPresentationRows =
|
|
|
|
|
+ hasPresentation ? std::span(mergedPresentationRows).first(mergedPresentationRowCount)
|
|
|
|
|
+ : std::span<const AcquisitionPresentationRow>{};
|
|
|
|
|
+ if (!valid(swap.after) || swap.characterSoid != pending.characterSoid
|
|
|
|| swap.after.family4RootSoid != before.family4RootSoid
|
|
|| swap.after.family4RootSoid != before.family4RootSoid
|
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|| swap.after.family4Version != before.family4Version + 1
|
|
|| swap.after.family4Version != before.family4Version + 1
|
|
|
|| !push::append_equipment_swap_notification(
|
|
|| !push::append_equipment_swap_notification(
|
|
|
- scratch, swap, equipment->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, swap, pending, equipmentPresentationRows, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=equip result=fail");
|
|
"ev=queuez stage=equip result=fail");
|
|
@@ -117,10 +324,17 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
}
|
|
}
|
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
|
after = swap.after;
|
|
after = swap.after;
|
|
|
- // Swapping the subclass slot invalidates the published ability buckets, the same way an
|
|
|
|
|
- // opcode-801 pick does. The rebuild is likewise asynchronous, so this owes the same
|
|
|
|
|
- // delayed re-derivation rather than racing whatever refresh runs below.
|
|
|
|
|
- if (equipment->pending.equipmentSlotIndex
|
|
|
|
|
|
|
+ if (preserveAcquisitionPresentation) {
|
|
|
|
|
+ if (hasPresentation) {
|
|
|
|
|
+ publication.acquisitionPresentationRows = mergedPresentationRows;
|
|
|
|
|
+ publication.acquisitionPresentationRowCount = mergedPresentationRowCount;
|
|
|
|
|
+ }
|
|
|
|
|
+ publication.updatesAcquisitionPresentationRows = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ // Swapping the subclass slot invalidates the published ability buckets the same way an
|
|
|
|
|
+ // opcode-801 pick does; the rebuild is likewise asynchronous, so this owes the same
|
|
|
|
|
+ // delayed re-derivation rather than risking a race with whatever refresh runs below.
|
|
|
|
|
+ if (pending.equipmentSlotIndex
|
|
|
== static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass)) {
|
|
== static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass)) {
|
|
|
armsAbilityRefresh = true;
|
|
armsAbilityRefresh = true;
|
|
|
}
|
|
}
|
|
@@ -129,10 +343,9 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
// in place: releasing and re-adding the same key tears down the ship/banner binding.
|
|
// in place: releasing and re-adding the same key tears down the ship/banner binding.
|
|
|
if (after.family0Active) {
|
|
if (after.family0Active) {
|
|
|
CharacterAppearanceRefresh refresh{};
|
|
CharacterAppearanceRefresh refresh{};
|
|
|
- if (!stage_character_appearance_refresh(
|
|
|
|
|
- after, equipment->pending.characterSoid, refresh)
|
|
|
|
|
|
|
+ if (!stage_character_appearance_refresh(after, pending.characterSoid, refresh)
|
|
|
|| !push::append_equipment_appearance_refresh_notification(
|
|
|| !push::append_equipment_appearance_refresh_notification(
|
|
|
- scratch, refresh, equipment->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, refresh, pending, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=equip_appearance result=fail");
|
|
"ev=queuez stage=equip_appearance result=fail");
|
|
@@ -144,13 +357,12 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
}
|
|
}
|
|
|
// Family three owns the orbit roster and a separate copy of the same appearance record.
|
|
// Family three owns the orbit roster and a separate copy of the same appearance record.
|
|
|
// Equipment movement changes both bodies, so publish character first and roster second at
|
|
// Equipment movement changes both bodies, so publish character first and roster second at
|
|
|
- // one exact +1 Family-3 revision. Failure keeps all three peer ladders and State private.
|
|
|
|
|
|
|
+ // one exact +1 Family-3 revision. Failure keeps all three peer ladders and State private.
|
|
|
if (after.family3Active) {
|
|
if (after.family3Active) {
|
|
|
RosterAppearanceRefresh refresh{};
|
|
RosterAppearanceRefresh refresh{};
|
|
|
- if (!stage_roster_appearance_refresh(
|
|
|
|
|
- after, equipment->pending.characterSoid, true, refresh)
|
|
|
|
|
|
|
+ if (!stage_roster_appearance_refresh(after, pending.characterSoid, true, refresh)
|
|
|
|| !push::append_equipment_roster_refresh_notification(
|
|
|| !push::append_equipment_roster_refresh_notification(
|
|
|
- scratch, refresh, equipment->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, refresh, pending, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=equip_roster result=fail");
|
|
"ev=queuez stage=equip_roster result=fail");
|
|
@@ -158,9 +370,41 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
}
|
|
}
|
|
|
after = refresh.after;
|
|
after = refresh.after;
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if (artifactPurchase != nullptr) {
|
|
|
|
|
+ if (artifactPurchase->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *artifactPurchase->pending;
|
|
|
|
|
+ const EquipmentSwap& update = artifactPurchase->update;
|
|
|
|
|
+ bool preservedManifest = update.after.family4ResidentCount == before.family4ResidentCount;
|
|
|
|
|
+ for (std::size_t index = 0; preservedManifest && index < before.family4ResidentCount;
|
|
|
|
|
+ ++index) {
|
|
|
|
|
+ preservedManifest = update.after.family4Residents[index].objectSoid
|
|
|
|
|
+ == before.family4Residents[index].objectSoid
|
|
|
|
|
+ && update.after.family4Residents[index].definitionId
|
|
|
|
|
+ == before.family4Residents[index].definitionId;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!valid(update.after) || !preservedManifest
|
|
|
|
|
+ || update.characterSoid != pending.characterSoid
|
|
|
|
|
+ || update.after.family4RootSoid != before.family4RootSoid
|
|
|
|
|
+ || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|
|
+ || update.after.family4Version != before.family4Version + 1
|
|
|
|
|
+ || !push::append_artifact_purchase_notification(
|
|
|
|
|
+ scratch, update, pending, presentationRows, key, nonce, response, written)) {
|
|
|
|
|
+ core::log::write(core::log::Channel::server,
|
|
|
|
|
+ core::log::Level::warn,
|
|
|
|
|
+ "ev=queuez stage=artifact result=fail");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
+ after = update.after;
|
|
|
} else if (itemState != nullptr) {
|
|
} else if (itemState != nullptr) {
|
|
|
// Item-state bits live in the selected-character inventory row. Publish only that
|
|
// Item-state bits live in the selected-character inventory row. Publish only that
|
|
|
// resident character body; item-instance, appearance, roster and manifest are unchanged.
|
|
// resident character body; item-instance, appearance, roster and manifest are unchanged.
|
|
|
|
|
+ if (itemState->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *itemState->pending;
|
|
|
const EquipmentSwap& update = itemState->update;
|
|
const EquipmentSwap& update = itemState->update;
|
|
|
bool preservedManifest = update.after.family4ResidentCount == before.family4ResidentCount;
|
|
bool preservedManifest = update.after.family4ResidentCount == before.family4ResidentCount;
|
|
|
for (std::size_t index = 0; preservedManifest && index < before.family4ResidentCount;
|
|
for (std::size_t index = 0; preservedManifest && index < before.family4ResidentCount;
|
|
@@ -171,12 +415,12 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
== before.family4Residents[index].definitionId;
|
|
== before.family4Residents[index].definitionId;
|
|
|
}
|
|
}
|
|
|
if (!valid(update.after) || !preservedManifest
|
|
if (!valid(update.after) || !preservedManifest
|
|
|
- || update.characterSoid != itemState->pending.characterSoid
|
|
|
|
|
|
|
+ || update.characterSoid != pending.characterSoid
|
|
|
|| update.after.family4RootSoid != before.family4RootSoid
|
|
|| update.after.family4RootSoid != before.family4RootSoid
|
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|| update.after.family4Version != before.family4Version + 1
|
|
|| update.after.family4Version != before.family4Version + 1
|
|
|
|| !push::append_item_state_notification(
|
|
|| !push::append_item_state_notification(
|
|
|
- scratch, update, itemState->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, update, pending, presentationRows, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=item_state result=fail");
|
|
"ev=queuez stage=item_state result=fail");
|
|
@@ -214,6 +458,10 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
// Body processing already staged the exact +1 revision opcode 801 promised. The instance
|
|
// Body processing already staged the exact +1 revision opcode 801 promised. The instance
|
|
|
// upsert goes first, then the appearance and roster refreshes, so gameplay reads the new
|
|
// upsert goes first, then the appearance and roster refreshes, so gameplay reads the new
|
|
|
// selection now rather than on the next unrelated poll.
|
|
// selection now rather than on the next unrelated poll.
|
|
|
|
|
+ if (subclassSelection->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *subclassSelection->pending;
|
|
|
const SubclassSelection& selection = subclassSelection->update;
|
|
const SubclassSelection& selection = subclassSelection->update;
|
|
|
bool preservedManifest =
|
|
bool preservedManifest =
|
|
|
selection.after.family4ResidentCount == before.family4ResidentCount;
|
|
selection.after.family4ResidentCount == before.family4ResidentCount;
|
|
@@ -229,14 +477,14 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
&& resident.definitionId == selection.itemInstanceDefinitionId);
|
|
&& resident.definitionId == selection.itemInstanceDefinitionId);
|
|
|
}
|
|
}
|
|
|
if (!valid(selection.after) || !preservedManifest || targetMatches != 1
|
|
if (!valid(selection.after) || !preservedManifest || targetMatches != 1
|
|
|
- || selection.accountSoid != subclassSelection->pending.accountSoid
|
|
|
|
|
- || selection.characterSoid != subclassSelection->pending.characterSoid
|
|
|
|
|
- || selection.subclassInstanceSoid != subclassSelection->pending.subclassInstanceSoid
|
|
|
|
|
|
|
+ || selection.accountSoid != pending.accountSoid
|
|
|
|
|
+ || selection.characterSoid != pending.characterSoid
|
|
|
|
|
+ || selection.subclassInstanceSoid != pending.subclassInstanceSoid
|
|
|
|| selection.after.family4RootSoid != before.family4RootSoid
|
|
|| selection.after.family4RootSoid != before.family4RootSoid
|
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|| selection.after.family4Version != before.family4Version + 1
|
|
|| selection.after.family4Version != before.family4Version + 1
|
|
|
|| !push::append_subclass_selection_notification(
|
|
|| !push::append_subclass_selection_notification(
|
|
|
- scratch, selection, subclassSelection->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, selection, pending, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=subclass_select result=fail");
|
|
"ev=queuez stage=subclass_select result=fail");
|
|
@@ -244,41 +492,17 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
}
|
|
}
|
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
|
after = selection.after;
|
|
after = selection.after;
|
|
|
- // The ability-bucket rebuild runs off the Client content-extraction pump, so the two
|
|
|
|
|
- // refreshes below can race it and carry empty buckets. The delayed re-derivation is owed
|
|
|
|
|
- // either way.
|
|
|
|
|
|
|
+ // Family zero and Family three no longer fit beside the enlarged upstream Family-four
|
|
|
|
|
+ // selection notification in this bounded reply. Publish them through the already-proven
|
|
|
|
|
+ // deferred refresh channel after the primary mutation commits.
|
|
|
armsAbilityRefresh = true;
|
|
armsAbilityRefresh = true;
|
|
|
- // A subclass is always equipped, so both the appearance and roster ability reads are
|
|
|
|
|
- // always owed a refresh once one is active.
|
|
|
|
|
- if (after.family0Active) {
|
|
|
|
|
- CharacterAppearanceRefresh refresh{};
|
|
|
|
|
- if (!stage_character_appearance_refresh(
|
|
|
|
|
- after, subclassSelection->pending.characterSoid, refresh)
|
|
|
|
|
- || !push::append_subclass_appearance_refresh_notification(
|
|
|
|
|
- scratch, refresh, subclassSelection->pending, key, nonce, response, written)) {
|
|
|
|
|
- core::log::write(core::log::Channel::server,
|
|
|
|
|
- core::log::Level::warn,
|
|
|
|
|
- "ev=queuez stage=subclass_appearance result=fail");
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- after = refresh.after;
|
|
|
|
|
- }
|
|
|
|
|
- if (after.family3Active) {
|
|
|
|
|
- RosterAppearanceRefresh refresh{};
|
|
|
|
|
- if (!stage_roster_appearance_refresh(
|
|
|
|
|
- after, subclassSelection->pending.characterSoid, false, refresh)
|
|
|
|
|
- || !push::append_subclass_roster_refresh_notification(
|
|
|
|
|
- scratch, refresh, subclassSelection->pending, key, nonce, response, written)) {
|
|
|
|
|
- core::log::write(core::log::Channel::server,
|
|
|
|
|
- core::log::Level::warn,
|
|
|
|
|
- "ev=queuez stage=subclass_roster result=fail");
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- after = refresh.after;
|
|
|
|
|
- }
|
|
|
|
|
} else if (socket != nullptr) {
|
|
} else if (socket != nullptr) {
|
|
|
// Body processing staged this exact +1 revision before encoding opcode 903's status pair.
|
|
// Body processing staged this exact +1 revision before encoding opcode 903's status pair.
|
|
|
// A socket selection changes only one already-resident item-instance body.
|
|
// A socket selection changes only one already-resident item-instance body.
|
|
|
|
|
+ if (socket->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *socket->pending;
|
|
|
const SocketPlug& socketPlug = socket->update;
|
|
const SocketPlug& socketPlug = socket->update;
|
|
|
bool preservedManifest =
|
|
bool preservedManifest =
|
|
|
socketPlug.after.family4ResidentCount == before.family4ResidentCount;
|
|
socketPlug.after.family4ResidentCount == before.family4ResidentCount;
|
|
@@ -298,15 +522,15 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
== socketPlug.accountDefinitionId);
|
|
== socketPlug.accountDefinitionId);
|
|
|
}
|
|
}
|
|
|
if (!valid(socketPlug.after) || !preservedManifest || accountMatches != 1
|
|
if (!valid(socketPlug.after) || !preservedManifest || accountMatches != 1
|
|
|
- || targetMatches != 1 || socketPlug.accountSoid != socket->pending.accountSoid
|
|
|
|
|
- || socketPlug.characterSoid != socket->pending.characterSoid
|
|
|
|
|
- || socketPlug.targetInstanceSoid != socket->pending.targetInstanceSoid
|
|
|
|
|
- || socketPlug.updatesAccount != socket->pending.profileChanged
|
|
|
|
|
|
|
+ || targetMatches != 1 || socketPlug.accountSoid != pending.accountSoid
|
|
|
|
|
+ || socketPlug.characterSoid != pending.characterSoid
|
|
|
|
|
+ || socketPlug.targetInstanceSoid != pending.targetInstanceSoid
|
|
|
|
|
+ || socketPlug.updatesAccount != pending.profileChanged
|
|
|
|| socketPlug.after.family4RootSoid != before.family4RootSoid
|
|
|| socketPlug.after.family4RootSoid != before.family4RootSoid
|
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|| socketPlug.after.family4Version != before.family4Version + 1
|
|
|| socketPlug.after.family4Version != before.family4Version + 1
|
|
|
|| !push::append_socket_plug_notification(
|
|
|| !push::append_socket_plug_notification(
|
|
|
- scratch, socketPlug, socket->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, socketPlug, pending, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=socket_plug result=fail");
|
|
"ev=queuez stage=socket_plug result=fail");
|
|
@@ -314,133 +538,186 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
}
|
|
}
|
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
|
after = socketPlug.after;
|
|
after = socketPlug.after;
|
|
|
- // Equipped plugs feed Family zero's material, overflow-hash and sandbox-perk banks. An
|
|
|
|
|
- // inventory-only socket change has no rendered character record to refresh.
|
|
|
|
|
- if (socket->pending.targetEquipped && after.family0Active) {
|
|
|
|
|
- CharacterAppearanceRefresh refresh{};
|
|
|
|
|
- if (!stage_character_appearance_refresh(after, socket->pending.characterSoid, refresh)
|
|
|
|
|
- || !push::append_socket_appearance_refresh_notification(
|
|
|
|
|
- scratch, refresh, socket->pending, key, nonce, response, written)) {
|
|
|
|
|
- core::log::write(core::log::Channel::server,
|
|
|
|
|
- core::log::Level::warn,
|
|
|
|
|
- "ev=queuez stage=socket_appearance result=fail");
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- after = refresh.after;
|
|
|
|
|
- }
|
|
|
|
|
- // A socket change can alter the rendered shader/perk banks in Family three, but it does not
|
|
|
|
|
- // change the roster's base-definition references. Only the character record is owed.
|
|
|
|
|
- if (socket->pending.targetEquipped && after.family3Active) {
|
|
|
|
|
- RosterAppearanceRefresh refresh{};
|
|
|
|
|
- if (!stage_roster_appearance_refresh(
|
|
|
|
|
- after, socket->pending.characterSoid, false, refresh)
|
|
|
|
|
- || !push::append_socket_roster_refresh_notification(
|
|
|
|
|
- scratch, refresh, socket->pending, key, nonce, response, written)) {
|
|
|
|
|
- core::log::write(core::log::Channel::server,
|
|
|
|
|
- core::log::Level::warn,
|
|
|
|
|
- "ev=queuez stage=socket_roster result=fail");
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
- after = refresh.after;
|
|
|
|
|
|
|
+ // Equipped plugs alter Family-zero and Family-three presentation. Those enlarged records
|
|
|
|
|
+ // are published separately so this mutation cannot fail after its primary frame is built.
|
|
|
|
|
+ if (pending.targetEquipped) {
|
|
|
|
|
+ armsAbilityRefresh = true;
|
|
|
}
|
|
}
|
|
|
} else if (itemAcquisition != nullptr) {
|
|
} else if (itemAcquisition != nullptr) {
|
|
|
// Body processing staged this exact manifest append before encoding the response version.
|
|
// Body processing staged this exact manifest append before encoding the response version.
|
|
|
// The character and new item objects must both fit or the State insertion is not committed.
|
|
// The character and new item objects must both fit or the State insertion is not committed.
|
|
|
- const ItemAcquisition& acquisition = itemAcquisition->update;
|
|
|
|
|
- const std::size_t appendedIndex = before.family4ResidentCount;
|
|
|
|
|
- bool preservedManifest = acquisition.after.family4ResidentCount == appendedIndex + 1U;
|
|
|
|
|
- for (std::size_t index = 0; preservedManifest && index < appendedIndex; ++index) {
|
|
|
|
|
- preservedManifest = acquisition.after.family4Residents[index].objectSoid
|
|
|
|
|
- == before.family4Residents[index].objectSoid
|
|
|
|
|
- && acquisition.after.family4Residents[index].definitionId
|
|
|
|
|
- == before.family4Residents[index].definitionId;
|
|
|
|
|
- }
|
|
|
|
|
- if (!valid(acquisition.after) || !preservedManifest
|
|
|
|
|
- || acquisition.accountSoid != itemAcquisition->pending.accountSoid
|
|
|
|
|
- || acquisition.characterSoid != itemAcquisition->pending.characterSoid
|
|
|
|
|
- || acquisition.acquiredInstanceSoid != itemAcquisition->pending.acquiredInstanceSoid
|
|
|
|
|
- || acquisition.updatesAccount != itemAcquisition->pending.profileChanged
|
|
|
|
|
- || acquisition.accountSoid != before.family4RootSoid
|
|
|
|
|
- || acquisition.after.family4RootSoid != before.family4RootSoid
|
|
|
|
|
- || before.family4ResidentCount >= before.family4Residents.size()
|
|
|
|
|
- || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|
|
- || acquisition.after.family4Version != before.family4Version + 1
|
|
|
|
|
- || acquisition.after.family4Residents[appendedIndex].objectSoid
|
|
|
|
|
- != acquisition.acquiredInstanceSoid
|
|
|
|
|
- || acquisition.after.family4Residents[appendedIndex].definitionId
|
|
|
|
|
- != acquisition.itemInstanceDefinitionId
|
|
|
|
|
- || !push::append_item_acquisition_notification(
|
|
|
|
|
- scratch, acquisition, itemAcquisition->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ if (itemAcquisition->pending == nullptr
|
|
|
|
|
+ || !stage_item_acquisition_push(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ itemAcquisition->update,
|
|
|
|
|
+ *itemAcquisition->pending,
|
|
|
|
|
+ std::nullopt,
|
|
|
|
|
+ presentationRows,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written,
|
|
|
|
|
+ after)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=acquire result=fail");
|
|
"ev=queuez stage=acquire result=fail");
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
- middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
- after = acquisition.after;
|
|
|
|
|
} else if (profileAcquisition != nullptr) {
|
|
} else if (profileAcquisition != nullptr) {
|
|
|
// A source-backed profile append creates one dependency before the account starts naming
|
|
// A source-backed profile append creates one dependency before the account starts naming
|
|
|
// it. Existing stacks and non-actionable currency rows preserve the complete manifest.
|
|
// it. Existing stacks and non-actionable currency rows preserve the complete manifest.
|
|
|
- const ProfileItemAcquisition& acquisition = profileAcquisition->update;
|
|
|
|
|
- const std::size_t priorResidentCount = before.family4ResidentCount;
|
|
|
|
|
- const std::size_t expectedResidentCount =
|
|
|
|
|
- priorResidentCount + static_cast<std::size_t>(acquisition.appendedResident);
|
|
|
|
|
- bool validManifest = expectedResidentCount <= acquisition.after.family4Residents.size()
|
|
|
|
|
- && acquisition.after.family4ResidentCount == expectedResidentCount;
|
|
|
|
|
- for (std::size_t index = 0; validManifest && index < priorResidentCount; ++index) {
|
|
|
|
|
- validManifest = acquisition.after.family4Residents[index].objectSoid
|
|
|
|
|
- == before.family4Residents[index].objectSoid
|
|
|
|
|
- && acquisition.after.family4Residents[index].definitionId
|
|
|
|
|
- == before.family4Residents[index].definitionId;
|
|
|
|
|
- }
|
|
|
|
|
- std::size_t priorProfileResidentMatches = 0;
|
|
|
|
|
- for (std::size_t index = 0; index < priorResidentCount; ++index) {
|
|
|
|
|
- const ResidentObject& resident = before.family4Residents[index];
|
|
|
|
|
- priorProfileResidentMatches += static_cast<std::size_t>(
|
|
|
|
|
- acquisition.acquiredInstanceSoid != 0
|
|
|
|
|
- && resident.objectSoid == acquisition.acquiredInstanceSoid
|
|
|
|
|
- && resident.definitionId == acquisition.itemInstanceDefinitionId);
|
|
|
|
|
- }
|
|
|
|
|
- const bool appendedResidentValid =
|
|
|
|
|
- !acquisition.appendedResident
|
|
|
|
|
- || (priorResidentCount < acquisition.after.family4Residents.size()
|
|
|
|
|
- && acquisition.after.family4Residents[priorResidentCount].objectSoid
|
|
|
|
|
- == acquisition.acquiredInstanceSoid
|
|
|
|
|
- && acquisition.after.family4Residents[priorResidentCount].definitionId
|
|
|
|
|
- == acquisition.itemInstanceDefinitionId
|
|
|
|
|
- && priorProfileResidentMatches == 0);
|
|
|
|
|
- const bool sourceIdentityValid =
|
|
|
|
|
- acquisition.actionSource == (acquisition.acquiredInstanceSoid != 0)
|
|
|
|
|
- && (acquisition.actionSource
|
|
|
|
|
- ? acquisition.itemInstanceDefinitionId != 0 && appendedResidentValid
|
|
|
|
|
- && (acquisition.appendedResident || priorProfileResidentMatches == 1)
|
|
|
|
|
- : acquisition.itemInstanceDefinitionId == 0 && !acquisition.appendedResident
|
|
|
|
|
- && priorProfileResidentMatches == 0);
|
|
|
|
|
- if (!valid(acquisition.after) || !validManifest || !sourceIdentityValid
|
|
|
|
|
- || acquisition.accountSoid != profileAcquisition->pending.accountSoid
|
|
|
|
|
- || acquisition.acquiredInstanceSoid != profileAcquisition->pending.acquiredInstanceSoid
|
|
|
|
|
- || acquisition.actionSource != profileAcquisition->pending.actionSource
|
|
|
|
|
- || acquisition.appendedResident
|
|
|
|
|
- != (profileAcquisition->pending.appended
|
|
|
|
|
- && profileAcquisition->pending.actionSource)
|
|
|
|
|
- || acquisition.accountSoid != before.family4RootSoid || before.family4ResidentCount == 0
|
|
|
|
|
- || acquisition.accountDefinitionId != before.family4Residents.front().definitionId
|
|
|
|
|
- || acquisition.after.family4RootSoid != before.family4RootSoid
|
|
|
|
|
- || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|
|
- || acquisition.after.family4Version != before.family4Version + 1
|
|
|
|
|
- || !push::append_profile_item_acquisition_notification(
|
|
|
|
|
- scratch, acquisition, profileAcquisition->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ if (profileAcquisition->pending == nullptr
|
|
|
|
|
+ || !stage_profile_item_acquisition_push(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ profileAcquisition->update,
|
|
|
|
|
+ *profileAcquisition->pending,
|
|
|
|
|
+ std::nullopt,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written,
|
|
|
|
|
+ after)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=profile_acquire result=fail");
|
|
"ev=queuez stage=profile_acquire result=fail");
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
+ } else if (recordRewardGrant != nullptr) {
|
|
|
|
|
+ if (recordRewardGrant->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!push::append_record_reward_notification(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ recordRewardGrant->update,
|
|
|
|
|
+ *recordRewardGrant->pending,
|
|
|
|
|
+ std::nullopt,
|
|
|
|
|
+ presentationRows,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written)) {
|
|
|
|
|
+ core::log::write(core::log::Channel::server,
|
|
|
|
|
+ core::log::Level::warn,
|
|
|
|
|
+ "ev=queuez stage=record_reward result=fail");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
|
- after = acquisition.after;
|
|
|
|
|
|
|
+ after = recordRewardGrant->update.after;
|
|
|
|
|
+ } else if (seasonPassReward != nullptr) {
|
|
|
|
|
+ if (seasonPassReward->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *seasonPassReward->pending;
|
|
|
|
|
+ bool staged = false;
|
|
|
|
|
+ if (const auto* itemUpdate = std::get_if<ItemAcquisition>(&seasonPassReward->update)) {
|
|
|
|
|
+ if (const auto* itemPending =
|
|
|
|
|
+ std::get_if<state::PendingItemAcquisition>(&pending.grant)) {
|
|
|
|
|
+ staged = stage_item_acquisition_push(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ *itemUpdate,
|
|
|
|
|
+ *itemPending,
|
|
|
|
|
+ pending.rewardIndex,
|
|
|
|
|
+ presentationRows,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written,
|
|
|
|
|
+ after);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (const auto* profileUpdate =
|
|
|
|
|
+ std::get_if<ProfileItemAcquisition>(&seasonPassReward->update)) {
|
|
|
|
|
+ if (const auto* profilePending =
|
|
|
|
|
+ std::get_if<state::PendingProfileItemAcquisition>(&pending.grant)) {
|
|
|
|
|
+ staged = stage_profile_item_acquisition_push(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ *profileUpdate,
|
|
|
|
|
+ *profilePending,
|
|
|
|
|
+ pending.rewardIndex,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written,
|
|
|
|
|
+ after);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (const auto* bundle =
|
|
|
|
|
+ std::get_if<state::PendingDirectItemBundle>(&pending.grant)) {
|
|
|
|
|
+ staged = push::append_season_pass_package_notification(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ *bundle,
|
|
|
|
|
+ pending.rewardIndex,
|
|
|
|
|
+ presentationRows,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written,
|
|
|
|
|
+ after);
|
|
|
|
|
+ if (staged) {
|
|
|
|
|
+ middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (const auto* resourceUpdate =
|
|
|
|
|
+ std::get_if<RecordRewardGrant>(&seasonPassReward->update)) {
|
|
|
|
|
+ if (const auto* resources =
|
|
|
|
|
+ std::get_if<state::PendingRecordRewardGrant>(&pending.grant)) {
|
|
|
|
|
+ staged = push::append_record_reward_notification(scratch,
|
|
|
|
|
+ before,
|
|
|
|
|
+ *resourceUpdate,
|
|
|
|
|
+ *resources,
|
|
|
|
|
+ pending.rewardIndex,
|
|
|
|
|
+ presentationRows,
|
|
|
|
|
+ key,
|
|
|
|
|
+ nonce,
|
|
|
|
|
+ response,
|
|
|
|
|
+ written);
|
|
|
|
|
+ if (staged) {
|
|
|
|
|
+ middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
+ after = resourceUpdate->after;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!staged) {
|
|
|
|
|
+ core::log::write(core::log::Channel::server,
|
|
|
|
|
+ core::log::Level::warn,
|
|
|
|
|
+ "ev=ws2400 stage=queuez_reward result=fail");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (outcome.hasArtifactReset) {
|
|
|
|
|
+ const state::AccountState account = state::account_snapshot();
|
|
|
|
|
+ std::uint64_t selectedCharacter = 0;
|
|
|
|
|
+ for (std::size_t index = 0; index < account.characterCount; ++index) {
|
|
|
|
|
+ if (account.characters[index].selected) {
|
|
|
|
|
+ selectedCharacter = account.characters[index].soid;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ EquipmentSwap reset{};
|
|
|
|
|
+ if (selectedCharacter == 0 || !stage_equipment_swap(before, selectedCharacter, reset)
|
|
|
|
|
+ || !push::append_artifact_reset_notification(
|
|
|
|
|
+ scratch, reset, key, nonce, response, written)) {
|
|
|
|
|
+ core::log::write(core::log::Channel::server,
|
|
|
|
|
+ core::log::Level::warn,
|
|
|
|
|
+ "ev=ws901 stage=artifact_reset_resync result=fail");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ middleware::secure_channel::advance_nonce(nonce);
|
|
|
|
|
+ after = reset.after;
|
|
|
|
|
+ } else if (outcome.hasRecordClaim) {
|
|
|
|
|
+ // A claim rewrites one byte of the account flag bank and leaves the manifest alone, so a
|
|
|
|
|
+ // full account snapshot at the next version carries it with no other staging.
|
|
|
|
|
+ if (!push::append_account_resync_notification(
|
|
|
|
|
+ scratch, before, presentationRows, key, nonce, response, written, after)) {
|
|
|
|
|
+ core::log::write(core::log::Channel::server,
|
|
|
|
|
+ core::log::Level::warn,
|
|
|
|
|
+ "ev=ws1801 stage=queuez_resync result=fail");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
} else if (itemDismantle != nullptr) {
|
|
} else if (itemDismantle != nullptr) {
|
|
|
// A dismantle removes exactly one resident while preserving the relative order of every
|
|
// A dismantle removes exactly one resident while preserving the relative order of every
|
|
|
// survivor. The character after-image and empty release descriptor must fit together or
|
|
// survivor. The character after-image and empty release descriptor must fit together or
|
|
|
// the State removal is not committed.
|
|
// the State removal is not committed.
|
|
|
|
|
+ if (itemDismantle->pending == nullptr) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const auto& pending = *itemDismantle->pending;
|
|
|
const ItemDismantle& dismantle = itemDismantle->update;
|
|
const ItemDismantle& dismantle = itemDismantle->update;
|
|
|
bool compactedManifest =
|
|
bool compactedManifest =
|
|
|
before.family4ResidentCount != 0
|
|
before.family4ResidentCount != 0
|
|
@@ -468,17 +745,17 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
&& afterIndex == dismantle.after.family4ResidentCount;
|
|
&& afterIndex == dismantle.after.family4ResidentCount;
|
|
|
|
|
|
|
|
if (!valid(dismantle.after) || !compactedManifest
|
|
if (!valid(dismantle.after) || !compactedManifest
|
|
|
- || dismantle.accountSoid != itemDismantle->pending.accountSoid
|
|
|
|
|
- || dismantle.characterSoid != itemDismantle->pending.characterSoid
|
|
|
|
|
- || dismantle.dismantledInstanceSoid != itemDismantle->pending.dismantledInstanceSoid
|
|
|
|
|
- || dismantle.updatesAccount != itemDismantle->pending.profileChanged
|
|
|
|
|
|
|
+ || dismantle.accountSoid != pending.accountSoid
|
|
|
|
|
+ || dismantle.characterSoid != pending.characterSoid
|
|
|
|
|
+ || dismantle.dismantledInstanceSoid != pending.dismantledInstanceSoid
|
|
|
|
|
+ || dismantle.updatesAccount != pending.profileChanged
|
|
|
|| dismantle.accountSoid != before.family4RootSoid || before.family4ResidentCount == 0
|
|
|| dismantle.accountSoid != before.family4RootSoid || before.family4ResidentCount == 0
|
|
|
|| dismantle.accountDefinitionId != before.family4Residents.front().definitionId
|
|
|| dismantle.accountDefinitionId != before.family4Residents.front().definitionId
|
|
|
|| dismantle.after.family4RootSoid != before.family4RootSoid
|
|
|| dismantle.after.family4RootSoid != before.family4RootSoid
|
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|| before.family4Version == (std::numeric_limits<std::int32_t>::max)()
|
|
|
|| dismantle.after.family4Version != before.family4Version + 1
|
|
|| dismantle.after.family4Version != before.family4Version + 1
|
|
|
|| !push::append_item_dismantle_notification(
|
|
|| !push::append_item_dismantle_notification(
|
|
|
- scratch, dismantle, itemDismantle->pending, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ scratch, dismantle, pending, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=dismantle result=fail");
|
|
"ev=queuez stage=dismantle result=fail");
|
|
@@ -489,22 +766,23 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
} else if (outcome.hasSelectCharacter) {
|
|
} else if (outcome.hasSelectCharacter) {
|
|
|
// The reply is the Client's task completion and the move is a separate frame. A move that
|
|
// The reply is the Client's task completion and the move is a separate frame. A move that
|
|
|
// cannot be built leaves the selection where it is, instead of holding back that reply.
|
|
// cannot be built leaves the selection where it is, instead of holding back that reply.
|
|
|
- if (!push::append_select_character_notification(
|
|
|
|
|
- scratch, outcome.selectCharacter, key, nonce, response, written)) {
|
|
|
|
|
|
|
+ if (selectCharacter == nullptr
|
|
|
|
|
+ || !push::append_select_character_notification(
|
|
|
|
|
+ scratch, *selectCharacter, key, nonce, response, written)) {
|
|
|
core::log::write(core::log::Channel::server,
|
|
core::log::write(core::log::Channel::server,
|
|
|
core::log::Level::warn,
|
|
core::log::Level::warn,
|
|
|
"ev=queuez stage=select result=fail");
|
|
"ev=queuez stage=select result=fail");
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
middleware::secure_channel::advance_nonce(nonce);
|
|
|
- after = outcome.selectCharacter.after;
|
|
|
|
|
|
|
+ after = selectCharacter->after;
|
|
|
// The banner pair follows the family-four move, so it is sent against the state that move
|
|
// The banner pair follows the family-four move, so it is sent against the state that move
|
|
|
// made. A pair that cannot be built leaves the emblem where it is.
|
|
// made. A pair that cannot be built leaves the emblem where it is.
|
|
|
const SessionState& bannerBefore = after;
|
|
const SessionState& bannerBefore = after;
|
|
|
SessionState bannerAfter{};
|
|
SessionState bannerAfter{};
|
|
|
if (push::append_banner_move_notification(scratch,
|
|
if (push::append_banner_move_notification(scratch,
|
|
|
bannerBefore,
|
|
bannerBefore,
|
|
|
- outcome.selectCharacter.selectedCharacterSoid,
|
|
|
|
|
|
|
+ selectCharacter->selectedCharacterSoid,
|
|
|
key,
|
|
key,
|
|
|
nonce,
|
|
nonce,
|
|
|
response,
|
|
response,
|
|
@@ -540,9 +818,6 @@ bool stage_service_outcome(Scratch& scratch,
|
|
|
bannerRoot = held.familyRootSoid;
|
|
bannerRoot = held.familyRootSoid;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } else if (allocation != nullptr) {
|
|
|
|
|
- // The launch arrives on the activity link, which holds no family-4 store and owes none.
|
|
|
|
|
- return true;
|
|
|
|
|
} else {
|
|
} else {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|