diff --git a/Sunrise/Sunrise.vcxproj b/Sunrise/Sunrise.vcxproj
index 6cd0414ae..563404e2a 100644
--- a/Sunrise/Sunrise.vcxproj
+++ b/Sunrise/Sunrise.vcxproj
@@ -99,6 +99,11 @@
+
+
+
+
+
@@ -642,7 +647,7 @@
-
+
@@ -1254,6 +1259,11 @@
+
+
+
+
+
@@ -1510,7 +1520,7 @@
-
+
diff --git a/Sunrise/src/client/content/items/packages/internal.h b/Sunrise/src/client/content/items/packages/internal.h
index 33aff9e42..c2ace82e7 100644
--- a/Sunrise/src/client/content/items/packages/internal.h
+++ b/Sunrise/src/client/content/items/packages/internal.h
@@ -24,6 +24,7 @@
#include "../../../../state/build_data/runtime.h"
#include "../../../../state/build_data/season_pass/definition.h"
#include "../../../../state/build_data/sobjects/sobject_catalog.h"
+#include "package_reward_build.h"
namespace sunrise::client::content::items::packages {
@@ -70,9 +71,10 @@ static_assert(kUnmappedSlot
/** Bank index per unlock slot, indexed by slot. The first mapping row of a slot wins. */
using SlotMap = std::array;
-/** The four maps one root's two unlock mapping tables carry. 256 KiB together. */
+/** Saved-bank indices resolved from the root's flag and value mapping tables. */
struct SlotMaps {
SlotMap accountFlag{};
+ SlotMap profileFlag{};
SlotMap characterFlag{};
SlotMap accountValue{};
SlotMap characterValue{};
@@ -83,6 +85,8 @@ struct Storage {
reader::Scratch scratch{};
/** Read once per root. Every domain resolves its unlock slots through these. */
SlotMaps slotMaps{};
+ /** Reward pools, wrappers and condition tables read alongside the item walk. */
+ RewardBuild rewardBuild{};
/** Node rows held until the value slot and owned records are resolved. */
std::array
nodeRows{};
@@ -154,15 +158,11 @@ struct Storage {
state::build_data::progressions::kStepCapacity>
progressionSteps{};
std::size_t progressionStepCount{};
- /** Season pass reward rows and the wrapper items they grant. */
+ /** Season pass reward rows in native claim order. */
std::array
seasonPassRewards{};
- std::array
- seasonPassPackages{};
std::size_t seasonPassRewardCount{};
- std::size_t seasonPassPackageCount{};
/** Repeatable bounty rows, keyed by the item-type pair their pool shares. */
std::array
@@ -207,6 +207,9 @@ struct Storage {
/** @return True when the catalyst catalog is published or cannot exist on this executable. */
[[nodiscard]] bool exotic_catalysts_settled() noexcept;
+/** @return True when the reward graph is published or can never be. */
+[[nodiscard]] bool reward_definitions_settled() noexcept;
+
/** Adds one native definition index to the deduplicated request set. */
void request(std::uint16_t definitionIndex, DetailRequests& requested) noexcept;
@@ -324,7 +327,7 @@ read_investment_constants(const reader::Source& source,
/**
* Reads one root's two unlock mapping tables into the pass slot maps.
* @param source Package source.
- * @param storage Pass storage receiving the four maps.
+ * @param storage Pass storage receiving the five maps.
* @param root Investment root bytes.
* @return True when both account maps read. A character map may stay unmapped.
*/
@@ -379,7 +382,7 @@ read_investment_constants(const reader::Source& source,
std::span steps,
std::size_t& stepCount) noexcept;
-/** Reads the season pass reward list and the wrapper items it grants. */
+/** Reads the season pass reward list, keeping an unreadable row as an unavailable one. */
[[nodiscard]] bool build_season_pass(const reader::Source& source,
Storage& storage,
std::span root) noexcept;
diff --git a/Sunrise/src/client/content/items/packages/package_item_build.cpp b/Sunrise/src/client/content/items/packages/package_item_build.cpp
index e039beb82..0fa0b810d 100644
--- a/Sunrise/src/client/content/items/packages/package_item_build.cpp
+++ b/Sunrise/src/client/content/items/packages/package_item_build.cpp
@@ -17,6 +17,7 @@
#include "../../vendors/vendor_build.h"
#include "build.h"
#include "internal.h"
+#include "package_reward_build.h"
#include "package_socket_plug_build.h"
namespace sunrise::client::content::items::packages {
@@ -34,7 +35,7 @@ namespace {
&& state::build_data::ability_buckets_ready()
&& state::build_data::socket_entry_buckets_ready()
&& state::build_data::progression_definitions_ready()
- && state::build_data::season_pass_ready()
+ && state::build_data::season_pass_ready() && reward_definitions_settled()
&& state::build_data::repeatable_bounties_ready()
&& state::build_data::record_definitions_ready()
&& state::build_data::node_definitions_ready()
@@ -126,7 +127,8 @@ bool build() noexcept {
if (!state::build_data::item_definitions_ready()
|| !state::build_data::record_definitions_ready()
|| !state::build_data::node_definitions_ready()
- || !state::build_data::season_pass_ready() || !exotic_catalysts_settled()) {
+ || !state::build_data::season_pass_ready() || !reward_definitions_settled()
+ || !exotic_catalysts_settled()) {
reason = "unlock_maps";
if (!read_unlock_slot_maps(
source, storage, std::span{storage.root})) {
@@ -184,11 +186,18 @@ bool build() noexcept {
std::span(storage.progressionSteps).first(storage.progressionStepCount));
}
}
- if (!state::build_data::season_pass_ready()
- && build_season_pass(source, storage, std::span{storage.root})) {
- (void)state::build_data::publish_season_pass(
- std::span(storage.seasonPassRewards).first(storage.seasonPassRewardCount),
- std::span(storage.seasonPassPackages).first(storage.seasonPassPackageCount));
+ // Pass rows bind through the reward condition tables, which load before the pools.
+ if (!reward_definitions_settled() || !state::build_data::season_pass_ready()) {
+ (void)storage.rewardBuild.load(
+ source, storage.scratch, storage.root, storage.slotMaps);
+ }
+ if (!state::build_data::season_pass_ready() && storage.rewardBuild.conditions.loaded()
+ && build_season_pass(source, storage, std::span{storage.root})
+ && !state::build_data::publish_season_pass(
+ std::span(storage.seasonPassRewards).first(storage.seasonPassRewardCount))) {
+ core::log::write(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=season_pass result=fail reason=publish");
}
// Records are read before nodes: a node's lore-book flag and its parent bar come
// from the records it owns, so the record rows must already be in pass storage.
diff --git a/Sunrise/src/client/content/items/packages/package_item_rows.cpp b/Sunrise/src/client/content/items/packages/package_item_rows.cpp
index 1ffa18f8c..4c8db5eb0 100644
--- a/Sunrise/src/client/content/items/packages/package_item_rows.cpp
+++ b/Sunrise/src/client/content/items/packages/package_item_rows.cpp
@@ -54,13 +54,16 @@ bool exotic_catalysts_settled() noexcept {
* @param table Located item index array within storage.child.
* @param rowCount Starts at zero; receives the rows retained even if publication fails.
* @param reason Receives the last stage reached or its failure reason.
- * @return True when this pass's required item domains are ready; failure may retain prior results.
+ * @return True when required non-reward item domains are ready. Failure may retain prior results.
*/
bool build_item_rows(const reader::Source& source,
Storage& storage,
const tables::Array& table,
std::size_t& rowCount,
const char*& reason) noexcept {
+ const bool needRewards = !reward_definitions_settled();
+ const bool rewardStorageReady =
+ needRewards && storage.rewardBuild.begin_items(static_cast(table.count));
const bool needDefinitions = !state::build_data::item_definitions_ready();
const bool needDetails = !state::build_data::configured_item_details_ready();
const bool needSocketPlugs = !state::build_data::socket_plug_rules_ready();
@@ -71,7 +74,7 @@ bool build_item_rows(const reader::Source& source,
const bool needDetailRows = needDetails || needSocketRows;
// Bucket equipment slots are derived from this same complete item walk, so a partial retry
// must still revisit the table even when definitions and detail domains already published.
- const bool needRows = needDefinitions || needDetailRows || needBuckets;
+ const bool needRows = needDefinitions || needDetailRows || needBuckets || rewardStorageReady;
bool published = !needRows;
if (retainDetails && storage.details.size() != kDetailCapacity) {
storage.details.assign(kDetailCapacity, build_details::Definition{});
@@ -107,11 +110,23 @@ bool build_item_rows(const reader::Source& source,
item.definitionHash = row.definitionHash;
item.definitionIndex = static_cast(index);
std::uint32_t itemClass = 0;
- if (!reader::read_tag(source, storage.scratch, row.targetTag, storage.definition, itemClass)
- || !tables::items::read_definition(std::span{storage.definition},
- item)) {
+ if (!reader::read_tag(
+ source, storage.scratch, row.targetTag, storage.definition, itemClass)) {
+ // The published catalog read this row before, so its reward waits for a retry; a row
+ // the catalog lacks stays pruned from both.
+ state::build_data::items::Definition cataloged{};
+ if (rewardStorageReady
+ && state::build_data::find_item_definition_index(item.definitionIndex, cataloged)) {
+ storage.rewardBuild.item_unread(item.definitionIndex);
+ }
+ continue;
+ }
+ if (!tables::items::read_definition(std::span{storage.definition}, item)) {
continue;
}
+ if (rewardStorageReady && itemClass == tables::kItemDefinitionClass) {
+ storage.rewardBuild.item(item.definitionIndex, item.definitionHash, storage.definition);
+ }
const std::uint32_t plugCategoryHash =
corrected_plug_category(item.definitionHash, item.plugCategoryHash);
build_items::QuestInitialization quest{};
@@ -175,6 +190,13 @@ bool build_item_rows(const reader::Source& source,
published =
state::build_data::publish_item_definitions(std::span(storage.rows).first(rowCount));
}
+ // A reward refusal is reported by RewardBuild and does not block the other item domains.
+ // Rewards publish only beside the item catalog and at its row count, so both omit the same
+ // unread rows.
+ const bool rewardsPublished =
+ !needRewards
+ || (rewardStorageReady && state::build_data::item_definitions_ready()
+ && storage.rewardBuild.publish(state::build_data::item_definition_count()));
if (!published) {
reason = !detailStorageReady ? "detail_storage"
: !requestsFit ? "detail_capacity"
@@ -301,6 +323,10 @@ bool build_item_rows(const reader::Source& source,
report_ability_count(abilityCount);
}
}
+ // Named last; later stages overwrite the reason as they start.
+ if (published && !rewardsPublished) {
+ reason = "rewards";
+ }
return published && state::build_data::item_definitions_ready()
&& state::build_data::configured_item_details_ready()
&& state::build_data::socket_plug_rules_ready() && exotic_catalysts_settled()
diff --git a/Sunrise/src/client/content/items/packages/package_node_build.cpp b/Sunrise/src/client/content/items/packages/package_node_build.cpp
index a629db783..1584d0b39 100644
--- a/Sunrise/src/client/content/items/packages/package_node_build.cpp
+++ b/Sunrise/src/client/content/items/packages/package_node_build.cpp
@@ -30,9 +30,10 @@ void clear_slot_map(SlotMap& output) noexcept {
output.fill(kUnmappedSlot);
}
-/** Clears all four so an unread table cannot leave a prior pass's indexes addressable. */
+/** An unread table must not leave a prior pass's indices addressable. */
void clear_slot_maps(SlotMaps& maps) noexcept {
clear_slot_map(maps.accountFlag);
+ clear_slot_map(maps.profileFlag);
clear_slot_map(maps.characterFlag);
clear_slot_map(maps.accountValue);
clear_slot_map(maps.characterValue);
@@ -113,7 +114,7 @@ read_slot_map(std::span blob, std::size_t descriptor, SlotMap&
/**
* A gate names a slot; its saved bank index is the mapping row that names that slot.
* @param source Borrowed package source.
- * @param storage Receives four slot maps and retained value-map bytes; may be partial on failure.
+ * @param storage Receives slot maps and retained value-map bytes; may be partial on failure.
* @param root Investment root bytes naming the flag and value mapping tables.
* @return True when both account maps read; a character map may remain unmapped.
*/
@@ -131,6 +132,7 @@ bool read_unlock_slot_maps(const reader::Source& source,
const std::span flagMap{storage.child};
const bool flagMapRead =
read_slot_map(flagMap, tables::kAccountFlagMapDescriptor, maps.accountFlag);
+ (void)read_slot_map(flagMap, tables::kProfileFlagMapDescriptor, maps.profileFlag);
(void)read_slot_map(flagMap, tables::kCharacterFlagMapDescriptor, maps.characterFlag);
std::uint32_t valueMapTag = 0;
diff --git a/Sunrise/src/client/content/items/packages/package_reward_build.cpp b/Sunrise/src/client/content/items/packages/package_reward_build.cpp
new file mode 100644
index 000000000..ef9e0a272
--- /dev/null
+++ b/Sunrise/src/client/content/items/packages/package_reward_build.cpp
@@ -0,0 +1,811 @@
+#include "package_reward_build.h"
+
+#include
+#include
+#include
+
+#include "../../../../core/logging/log.h"
+#include "../../../../middleware/content/packages/tables/definition_index_table.h"
+#include "../../../../middleware/content/packages/tables/field_reader.h"
+#include "../../../../state/build_data/runtime.h"
+#include "../../../../state/unlocks/unlocks_expression.h"
+#include "internal.h"
+
+namespace sunrise::client::content::items::packages {
+namespace {
+
+namespace reader = middleware::content::packages::reader;
+namespace tables = middleware::content::packages::tables;
+namespace domain = state::build_data::rewards;
+namespace unlocks = state::unlocks;
+
+/** Investment-root slots identify reward pools and unlock-slot bindings. */
+constexpr std::size_t kSupplementalRewardSlot = 84;
+constexpr std::size_t kPoolSlot = 88;
+constexpr std::size_t kExpressionSlot = 109;
+constexpr std::size_t kFlagSlot = 112;
+constexpr std::size_t kValueSlot = 114;
+/** Only the omitted-bank sentinel permits a no-op; other invalid tags leave effects unresolved. */
+constexpr std::uint32_t kAbsentTableTag = 0xFFFFFFFFU;
+/** Native reward schema classes and fixed row sizes. */
+constexpr std::uint32_t kPoolClass = 0x80807553U;
+constexpr std::uint32_t kPoolRowClass = 0x8080748CU;
+constexpr std::uint32_t kEntryClass = 0x8080748EU;
+constexpr std::uint32_t kExpressionClass = 0x80807D31U;
+constexpr std::uint32_t kModifierClass = 0x80807490U;
+constexpr std::uint32_t kSocketClass = 0x80803062U;
+constexpr std::uint32_t kWrapperClass = 0x808077CCU;
+constexpr std::uint32_t kSelectionClass = 0x808077CFU;
+constexpr std::uint32_t kFlagTableClass = 0x80807D49U;
+constexpr std::uint32_t kFlagRowClass = 0x80807D4FU;
+constexpr std::uint32_t kValueTableClass = 0x80807C92U;
+constexpr std::uint32_t kValueRowClass = 0x80807C96U;
+constexpr std::uint32_t kExpressionTableClass = 0x80807C49U;
+constexpr std::uint32_t kExpressionRowClass = 0x80807C4FU;
+constexpr std::uint32_t kConditionClass = 0x80807D2FU;
+constexpr std::size_t kPoolStride = 24;
+constexpr std::size_t kEntryStride = 80;
+constexpr std::size_t kModifierStride = 24;
+constexpr std::size_t kBindingStride = 8;
+constexpr std::size_t kExpressionRowStride = 24;
+constexpr std::size_t kSocketStride = 12;
+constexpr std::size_t kSelectionStride = 12;
+/** Item headers hold a relative wrapper pointer and an acquired-unlock slot. */
+constexpr std::size_t kWrapperField = 0x58;
+constexpr std::size_t kAcquiredFlagField = 0xDA;
+
+/** Byte offsets inside the serialized reward rows. */
+constexpr std::size_t kEntryQuantityOffset = 4;
+constexpr std::size_t kEntryPoolOffset = 8;
+constexpr std::size_t kEntrySupplementalOffset = 10;
+constexpr std::size_t kEntryCategoryOffset = 20;
+constexpr std::size_t kEntryWeightOffset = 24;
+constexpr std::size_t kEntryConditionOffset = 32;
+constexpr std::size_t kEntryModifiersOffset = 48;
+constexpr std::size_t kEntrySocketsOffset = 64;
+constexpr std::size_t kModifierValueIndexOffset = 16;
+constexpr std::size_t kModifierValueOffset = 20;
+constexpr std::size_t kPoolEntriesOffset = 8;
+constexpr std::size_t kExpressionBodyOffset = 8;
+constexpr std::size_t kWrapperSelectionsOffset = 8;
+constexpr std::size_t kWrapperFlagsOffset = 24;
+constexpr std::size_t kSelectionCountOffset = 4;
+constexpr std::size_t kSocketPlugOffset = 2;
+constexpr std::size_t kSocketPlugSetOffset = 4;
+constexpr std::size_t kSocketRollSetOffset = 6;
+constexpr std::size_t kSocketSelectionOffset = 8;
+/** Relative definition bodies are preceded by their four-byte schema class. */
+constexpr std::size_t kDefinitionClassPrefixSize = sizeof(std::uint32_t);
+
+/** Class rows name a default finisher whose use gate supplies the computed class flag. */
+constexpr std::size_t kClassSlot = 12;
+constexpr std::size_t kClassStride = 32;
+constexpr std::size_t kDefaultFinisherOffset = 24;
+constexpr std::uint32_t kClassTable = 0x808075BEU;
+constexpr std::uint32_t kClassRow = 0x808074FAU;
+constexpr std::size_t kUseConditionPointer = 24;
+constexpr std::uint32_t kUseConditionClass = 0x80802980U;
+
+/** Set when the installed data refuses the reward tables or graph; no retry can change that. */
+bool g_rewardsUnsupported = false;
+
+/** Logs one refused reward load, sizing or publication. */
+void report_refusal(const char* reason) noexcept {
+ core::log::writef(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=rewards result=fail reason=%s",
+ reason);
+}
+
+/** Logs a refusal the installed data decides and settles the domain once. */
+void settle_unsupported(const char* reason) noexcept {
+ report_refusal(reason);
+ if (!g_rewardsUnsupported) {
+ g_rewardsUnsupported = true;
+ core::log::write(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=rewards result=unsupported");
+ }
+}
+
+/** An unread item row keeps a zero hash. */
+[[nodiscard]] bool item_read(std::span items, std::uint16_t index) noexcept {
+ return index < items.size() && items[index].definitionHash != 0;
+}
+
+/** Appends within a bank's capacity; an allocation failure is refused rather than thrown. */
+template bool append(std::vector& bank, T value, std::size_t capacity) noexcept {
+ if (bank.size() >= capacity) {
+ return false;
+ }
+ try {
+ bank.push_back(value);
+ return true;
+ } catch (...) {
+ return false;
+ }
+}
+
+/** Package reads can fail temporarily. Only decoded schema failures settle a domain. */
+enum class TableRead { loaded, readFailed, unsupported };
+
+/** Reads the table an investment-root slot names, checking its class when one is expected. */
+TableRead root_table(const reader::Source& source,
+ reader::Scratch& scratch,
+ std::span root,
+ std::size_t slot,
+ std::vector& blob,
+ std::uint32_t expectedClass) noexcept {
+ std::uint32_t tag = 0;
+ std::uint32_t cls = 0;
+ if (!tables::slot_tag(root, slot, tag) || tables::package_of(tag) == tables::kAbsentPackageId) {
+ return TableRead::unsupported;
+ }
+ if (!reader::read_tag(source, scratch, tag, blob, cls)) {
+ return TableRead::readFailed;
+ }
+ return expectedClass == 0 || cls == expectedClass ? TableRead::loaded : TableRead::unsupported;
+}
+
+/** Required reward tables settle only after a decoded schema failure, leaving reads retryable. */
+bool load_reward_table(const reader::Source& source,
+ reader::Scratch& scratch,
+ std::span root,
+ std::size_t slot,
+ std::vector& blob,
+ std::uint32_t expectedClass,
+ const char* reason) noexcept {
+ const auto result = root_table(source, scratch, root, slot, blob, expectedClass);
+ if (result == TableRead::unsupported) {
+ settle_unsupported(reason);
+ } else if (result == TableRead::readFailed) {
+ report_refusal(reason);
+ }
+ return result == TableRead::loaded;
+}
+
+/** Reads one entry's fixed fields; a negative or non-finite weight is refused. */
+bool read_reward_entry(std::span blob,
+ std::size_t at,
+ domain::Entry& entry) noexcept {
+ return tables::read(blob, at, entry.itemIndex)
+ && tables::read(blob, at + kEntryQuantityOffset, entry.quantity)
+ && tables::read(blob, at + kEntryPoolOffset, entry.poolIndex)
+ && tables::read(blob, at + kEntrySupplementalOffset, entry.supplementalIndex)
+ && tables::read(blob, at + kEntryCategoryOffset, entry.categoryHash)
+ && tables::read(blob, at + kEntryWeightOffset, entry.weight)
+ && std::isfinite(entry.weight) && entry.weight >= 0;
+}
+
+/** Reads the single flag that gates use of the class's default finisher. */
+bool read_class_flag(std::span blob, std::uint16_t& output) noexcept {
+ std::size_t at = 0;
+ std::uint32_t cls = 0;
+ tables::Array expression{};
+ if (!tables::relative(blob, kUseConditionPointer, at) || at < kDefinitionClassPrefixSize
+ || !tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kUseConditionClass
+ || !tables::read_array(
+ blob, at, kExpressionClass, tables::kUnlockInstructionStride, expression)
+ || expression.count != 1) {
+ return false;
+ }
+ std::uint32_t opcode = 0;
+ std::uint32_t flag = domain::kAbsent;
+ if (!tables::read(blob, expression.dataOffset, opcode)
+ || opcode != tables::kUnlockReadFlagOpcode
+ || !tables::read(
+ blob, expression.dataOffset + tables::kUnlockInstructionOperandOffset, flag)
+ || flag >= domain::kAbsent) {
+ return false;
+ }
+ output = static_cast(flag);
+ return true;
+}
+
+} // namespace
+
+/** @return True when the reward graph is published or can never be. */
+bool reward_definitions_settled() noexcept {
+ return state::build_data::reward_definitions_ready() || g_rewardsUnsupported;
+}
+
+bool RewardBuild::entry(std::span blob,
+ std::size_t at,
+ BankFailure& failure,
+ const char*& reason) noexcept {
+ domain::Entry out{};
+ failure = BankFailure::none;
+ if (!read_reward_entry(blob, at, out)
+ || !conditions.read(
+ blob, at + kEntryConditionOffset, instructions_, out.condition, failure)) {
+ reason = failure == BankFailure::full ? "instruction_capacity" : "instruction_storage";
+ return false;
+ }
+ out.supplementalMissing = supplementalMissing_ && out.supplementalIndex != domain::kAbsent;
+ tables::Array rows{};
+ if (!tables::read_array(
+ blob, at + kEntryModifiersOffset, kModifierClass, kModifierStride, rows)) {
+ return false;
+ }
+ out.modifiers = {static_cast(modifiers_.size()),
+ static_cast(rows.count)};
+ for (std::size_t i = 0; i < rows.count; ++i) {
+ const auto offset = rows.dataOffset + i * kModifierStride;
+ domain::Modifier modifier{};
+ if (!conditions.read(blob, offset, instructions_, modifier.condition, failure)
+ || !tables::read(blob, offset + kModifierValueIndexOffset, modifier.valueIndex)
+ || !tables::read(blob, offset + kModifierValueOffset, modifier.value)
+ || !std::isfinite(modifier.value)) {
+ reason = failure == BankFailure::full ? "instruction_capacity" : "instruction_storage";
+ return false;
+ }
+ if (modifiers_.size() >= domain::kModifierCapacity) {
+ failure = BankFailure::full;
+ reason = "modifier_capacity";
+ return false;
+ }
+ if (!append(modifiers_, modifier, domain::kModifierCapacity)) {
+ failure = BankFailure::storage;
+ reason = "modifier_storage";
+ return false;
+ }
+ }
+ std::array overrides{};
+ std::size_t count = 0;
+ if (!read_reward_sockets(blob, at + kEntrySocketsOffset, overrides, count)) {
+ return false;
+ }
+ if (count > domain::kSocketOverrideCapacity - sockets_.size()) {
+ failure = BankFailure::full;
+ reason = "socket_capacity";
+ return false;
+ }
+ out.sockets = {static_cast(sockets_.size()), static_cast(count)};
+ for (std::size_t i = 0; i < count; ++i) {
+ if (overrides[i].socketType == domain::kAbsent) {
+ return false;
+ }
+ if (!append(sockets_, overrides[i], domain::kSocketOverrideCapacity)) {
+ failure = BankFailure::storage;
+ reason = "socket_storage";
+ return false;
+ }
+ }
+ if (entries_.size() >= domain::kEntryCapacity) {
+ failure = BankFailure::full;
+ reason = "entry_capacity";
+ return false;
+ }
+ if (!append(entries_, out, domain::kEntryCapacity)) {
+ failure = BankFailure::storage;
+ reason = "entry_storage";
+ return false;
+ }
+ return true;
+}
+
+bool RewardConditions::load_class_flags(const reader::Source& source,
+ reader::Scratch& scratch,
+ std::span root) noexcept {
+ classFlags_.fill(domain::kAbsent);
+ std::vector classes;
+ std::vector index;
+ std::vector blob;
+ tables::Array classRows{};
+ tables::Array itemRows{};
+ if (!load_reward_table(source, scratch, root, kClassSlot, classes, kClassTable, "class_table")
+ || !load_reward_table(
+ source, scratch, root, tables::kItemTableSlot, index, 0, "class_item_table")) {
+ return false;
+ }
+ if (!tables::read_array(
+ classes, tables::kTableArrayDescriptor, kClassRow, kClassStride, classRows)
+ || classRows.count != classFlags_.size()
+ || !tables::read_array(index,
+ tables::kTableArrayDescriptor,
+ tables::kItemIndexTableClass,
+ tables::kItemIndexRowStride,
+ itemRows)) {
+ settle_unsupported("class_schema");
+ return false;
+ }
+ for (std::size_t i = 0; i < classFlags_.size(); ++i) {
+ std::uint16_t itemIndex = domain::kAbsent;
+ tables::IndexRow item{};
+ std::uint32_t cls = 0;
+ if (!tables::read(std::span{classes},
+ classRows.dataOffset + i * kClassStride + kDefaultFinisherOffset,
+ itemIndex)
+ || !tables::index_row(index, itemRows, itemIndex, item)) {
+ settle_unsupported("class_finisher_index");
+ return false;
+ }
+ if (!reader::read_tag(source, scratch, item.targetTag, blob, cls)) {
+ core::log::writef(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=class_flags class=%zu result=fail reason=finisher_read",
+ i);
+ return false;
+ }
+ if (cls != tables::kItemDefinitionClass || !read_class_flag(blob, classFlags_[i])) {
+ settle_unsupported("class_finisher_schema");
+ return false;
+ }
+ }
+ return true;
+}
+
+bool RewardConditions::load(const reader::Source& source,
+ reader::Scratch& scratch,
+ std::span root,
+ const SlotMaps& maps) noexcept {
+ loaded_ = false;
+ maps_ = &maps;
+ if (!load_class_flags(source, scratch, root)
+ || !load_reward_table(
+ source, scratch, root, kFlagSlot, flags_, kFlagTableClass, "flag_table")
+ || !load_reward_table(
+ source, scratch, root, kValueSlot, values_, kValueTableClass, "value_table")
+ || !load_reward_table(source,
+ scratch,
+ root,
+ kExpressionSlot,
+ expressions_,
+ kExpressionTableClass,
+ "expression_table")) {
+ return false;
+ }
+ // Shared expressions are expanded before the runtime evaluates reward conditions.
+ if (!tables::read_array(
+ flags_, tables::kTableArrayDescriptor, kFlagRowClass, kBindingStride, flagRows_)
+ || !tables::read_array(
+ values_, tables::kTableArrayDescriptor, kValueRowClass, kBindingStride, valueRows_)
+ || !tables::read_array(expressions_,
+ tables::kTableArrayDescriptor,
+ kExpressionRowClass,
+ kExpressionRowStride,
+ expressionRows_)) {
+ settle_unsupported("condition_schema");
+ return false;
+ }
+ loaded_ = true;
+ return true;
+}
+
+bool RewardConditions::loaded() const noexcept {
+ return loaded_;
+}
+
+bool RewardConditions::bind(std::uint32_t native,
+ std::uint32_t operand,
+ domain::Instruction& instruction) const noexcept {
+ const bool flag = native == tables::kUnlockReadFlagOpcode;
+ if (!flag && native != tables::kUnlockReadValueOpcode) {
+ instruction = {{}, unlocks::Bank::none, operand};
+ return unlocks::decode_opcode(native, instruction.opcode);
+ }
+ const auto& rows = flag ? flagRows_ : valueRows_;
+ const std::span blob = flag ? flags_ : values_;
+ std::uint32_t hash = 0;
+ if (operand >= rows.count
+ || !tables::read(blob, rows.dataOffset + operand * kBindingStride, hash)) {
+ return false;
+ }
+ instruction.opcode = flag ? unlocks::Opcode::flag : unlocks::Opcode::loadValue;
+ if (flag) {
+ for (std::size_t characterClass = 0; characterClass < classFlags_.size();
+ ++characterClass) {
+ if (classFlags_[characterClass] != domain::kAbsent
+ && operand == classFlags_[characterClass]) {
+ instruction.bank = unlocks::Bank::characterClass;
+ instruction.operand = static_cast(characterClass);
+ return true;
+ }
+ }
+ }
+ const auto slot = static_cast(operand);
+ const auto accountIndex = bank_index(flag ? maps_->accountFlag : maps_->accountValue, slot);
+ const auto profileIndex = flag ? bank_index(maps_->profileFlag, slot) : kUnmappedSlot;
+ const auto characterIndex =
+ bank_index(flag ? maps_->characterFlag : maps_->characterValue, slot);
+ if (accountIndex != kUnmappedSlot) {
+ instruction.bank = unlocks::Bank::account;
+ instruction.operand = accountIndex;
+ } else if (profileIndex != kUnmappedSlot) {
+ instruction.bank = unlocks::Bank::profile;
+ instruction.operand = profileIndex;
+ } else if (characterIndex != kUnmappedSlot) {
+ instruction.bank = unlocks::Bank::character;
+ instruction.operand = characterIndex;
+ } else {
+ instruction.bank = unlocks::Bank::external;
+ instruction.operand = hash;
+ }
+ return true;
+}
+
+bool RewardConditions::append_expression(std::span blob,
+ std::size_t at,
+ std::vector& bank,
+ std::size_t depth,
+ BankFailure& failure) const noexcept {
+ tables::Array rows{};
+ if (depth >= domain::kTraversalDepth
+ || !tables::read_array(
+ blob, at, kExpressionClass, tables::kUnlockInstructionStride, rows)) {
+ return false;
+ }
+ for (std::size_t i = 0; i < rows.count; ++i) {
+ const std::size_t row = rows.dataOffset + i * tables::kUnlockInstructionStride;
+ std::uint32_t native = 0;
+ std::uint32_t operand = 0;
+ if (!tables::read(blob, row, native)
+ || !tables::read(blob, row + tables::kUnlockInstructionOperandOffset, operand)) {
+ return false;
+ }
+ if (native == tables::kUnlockExpressionOpcode) {
+ const auto before = bank.size();
+ if (operand >= expressionRows_.count
+ || !append_expression(expressions_,
+ expressionRows_.dataOffset + operand * kExpressionRowStride
+ + kExpressionBodyOffset,
+ bank,
+ depth + 1,
+ failure)
+ || bank.size() == before) {
+ return false;
+ }
+ continue;
+ }
+ domain::Instruction instruction{};
+ if (!bind(native, operand, instruction) || !unlocks::valid(instruction)) {
+ return false;
+ }
+ if (bank.size() >= domain::kInstructionCapacity) {
+ failure = BankFailure::full;
+ return false;
+ }
+ if (!append(bank, instruction, domain::kInstructionCapacity)) {
+ failure = BankFailure::storage;
+ return false;
+ }
+ }
+ return true;
+}
+
+bool RewardConditions::read(std::span blob,
+ std::size_t at,
+ std::vector& bank,
+ domain::Range& range,
+ BankFailure& failure) const noexcept {
+ const auto first = bank.size();
+ failure = BankFailure::none;
+ if (!append_expression(blob, at, bank, 0, failure)) {
+ bank.resize(first);
+ return false;
+ }
+ range = {static_cast(first), static_cast(bank.size() - first)};
+ return true;
+}
+
+bool RewardConditions::read_list(std::span blob,
+ std::size_t at,
+ std::span output,
+ std::size_t& count,
+ BankFailure& failure) const noexcept {
+ count = 0;
+ failure = BankFailure::none;
+ tables::Array rows{};
+ std::vector instructions;
+ if (!tables::read_array(blob, at, kConditionClass, tables::kUnlockExpressionFieldSize, rows)) {
+ return false;
+ }
+ for (std::size_t i = 0; i < rows.count; ++i) {
+ domain::Range expression{};
+ if (!read(blob,
+ rows.dataOffset + i * tables::kUnlockExpressionFieldSize,
+ instructions,
+ expression,
+ failure)
+ || expression.count == 0) {
+ return false;
+ }
+ // Every expression attached to a progression reward must hold.
+ if (i != 0) {
+ if (instructions.size() >= output.size()) {
+ return false;
+ }
+ if (!append(instructions,
+ domain::Instruction{unlocks::Opcode::logicalAnd, unlocks::Bank::none, 0},
+ output.size())) {
+ failure = BankFailure::storage;
+ return false;
+ }
+ }
+ if (instructions.size() > output.size()) {
+ return false;
+ }
+ }
+ std::copy(instructions.begin(), instructions.end(), output.begin());
+ count = instructions.size();
+ return true;
+}
+
+bool read_reward_sockets(std::span blob,
+ std::size_t at,
+ std::span output,
+ std::size_t& count) noexcept {
+ count = 0;
+ tables::Array rows{};
+ if (!tables::read_array(blob, at, kSocketClass, kSocketStride, rows)
+ || rows.count > output.size()) {
+ return false;
+ }
+ for (std::size_t i = 0; i < rows.count; ++i) {
+ const std::size_t p = rows.dataOffset + i * kSocketStride;
+ auto& socket = output[i];
+ if (!tables::read(blob, p, socket.socketType)
+ || !tables::read(blob, p + kSocketPlugOffset, socket.plugItem)
+ || !tables::read(blob, p + kSocketPlugSetOffset, socket.plugSet)
+ || !tables::read(blob, p + kSocketRollSetOffset, socket.rollSet)
+ || !tables::read(blob, p + kSocketSelectionOffset, socket.selection)) {
+ return false;
+ }
+ }
+ count = static_cast(rows.count);
+ return true;
+}
+
+bool RewardBuild::load(const reader::Source& source,
+ reader::Scratch& scratch,
+ std::span root,
+ const SlotMaps& maps) noexcept {
+ loaded_ = false;
+ supplementalMissing_ = false;
+ pools_.clear();
+ entries_.clear();
+ instructions_.clear();
+ modifiers_.clear();
+ sockets_.clear();
+ std::vector blob;
+ tables::Array rows{};
+ std::uint32_t supplementalTag = 0;
+ // Conditions load first; the season pass binds through them even if the pools are refused.
+ if (!conditions.load(source, scratch, root, maps)) {
+ return false;
+ }
+ if (!tables::slot_tag(root, kSupplementalRewardSlot, supplementalTag)) {
+ settle_unsupported("supplemental_slot");
+ return false;
+ }
+ if (!load_reward_table(source, scratch, root, kPoolSlot, blob, kPoolClass, "pool_table")) {
+ return false;
+ }
+ if (!tables::read_array(
+ blob, tables::kTableArrayDescriptor, kPoolRowClass, kPoolStride, rows)) {
+ settle_unsupported("pool_schema");
+ return false;
+ }
+ if (rows.count == 0 || rows.count > domain::kPoolCapacity) {
+ settle_unsupported("pool_count");
+ return false;
+ }
+ supplementalMissing_ = supplementalTag == kAbsentTableTag;
+ std::size_t skipped = 0;
+ for (std::size_t i = 0; i < rows.count; ++i) {
+ const auto at = rows.dataOffset + i * kPoolStride;
+ domain::Pool pool{};
+ tables::Array members{};
+ if (tables::read(blob, at, pool.definitionHash) && pool.definitionHash != 0
+ && tables::read_array(
+ blob, at + kPoolEntriesOffset, kEntryClass, kEntryStride, members)) {
+ pool.entries.first = static_cast(entries_.size());
+ for (std::size_t j = 0; j < members.count; ++j) {
+ const auto beforeInstructions = instructions_.size();
+ const auto beforeModifiers = modifiers_.size();
+ const auto beforeSockets = sockets_.size();
+ auto failure = BankFailure::none;
+ const char* reason = nullptr;
+ if (!entry(blob, members.dataOffset + j * kEntryStride, failure, reason)) {
+ // Skipping a bank failure would publish a pool missing some of its entries.
+ if (failure == BankFailure::full) {
+ settle_unsupported(reason);
+ return false;
+ }
+ if (failure == BankFailure::storage) {
+ report_refusal(reason);
+ return false;
+ }
+ instructions_.resize(beforeInstructions);
+ modifiers_.resize(beforeModifiers);
+ sockets_.resize(beforeSockets);
+ ++skipped;
+ }
+ }
+ pool.entries.count = static_cast(entries_.size()) - pool.entries.first;
+ } else {
+ pool = {};
+ ++skipped;
+ }
+ if (!append(pools_, pool, domain::kPoolCapacity)) {
+ report_refusal("pool_storage");
+ return false;
+ }
+ }
+ core::log::writef(core::log::Channel::client,
+ skipped == 0 ? core::log::Level::info : core::log::Level::warn,
+ "ev=pkg stage=rewards pools=%zu entries=%zu instructions=%zu modifiers=%zu "
+ "sockets=%zu skipped=%zu",
+ pools_.size(),
+ entries_.size(),
+ instructions_.size(),
+ modifiers_.size(),
+ sockets_.size(),
+ skipped);
+ loaded_ = true;
+ return true;
+}
+
+bool RewardBuild::read_item(std::uint32_t hash,
+ std::span blob,
+ domain::Item& item) noexcept {
+ std::uint16_t acquired = domain::kAbsent;
+ std::int64_t wrapper = 0;
+ std::uint32_t cls = 0;
+ if (!tables::read(blob, kWrapperField, wrapper)
+ || !tables::read(blob, kAcquiredFlagField, acquired)) {
+ return false;
+ }
+ item.definitionHash = hash;
+ if (acquired != domain::kAbsent) {
+ domain::Instruction flag{};
+ if (!conditions.bind(tables::kUnlockReadFlagOpcode, acquired, flag)
+ || !unlocks::valid(flag)) {
+ // The grant is kept; only its acquisition flag is unknown.
+ ++unboundAcquiredFlags_;
+ } else if (flag.bank == unlocks::Bank::account) {
+ item.acquiredFlag = static_cast(flag.operand);
+ } else {
+ ++discardedAcquiredFlags_;
+ }
+ }
+ // A zero delta means the item is not a wrapper.
+ if (wrapper != 0) {
+ std::size_t at = 0;
+ tables::Array selections{};
+ if (!tables::relative(blob, kWrapperField, at) || at < kDefinitionClassPrefixSize
+ || !tables::read(blob, at - kDefinitionClassPrefixSize, cls) || cls != kWrapperClass
+ || !tables::read(blob, at, item.poolIndex)
+ || !tables::read(blob, at + kWrapperFlagsOffset, item.flags)
+ || !tables::read_array(
+ blob, at + kWrapperSelectionsOffset, kSelectionClass, kSelectionStride, selections)
+ || selections.count > item.selections.size()) {
+ return false;
+ }
+ item.selectionCount = static_cast(selections.count);
+ for (std::size_t j = 0; j < selections.count; ++j) {
+ auto& selection = item.selections[j];
+ const auto p = selections.dataOffset + j * kSelectionStride;
+ if (!tables::read(blob, p, selection.categoryHash)
+ || !tables::read(blob, p + kSelectionCountOffset, selection.count)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool RewardBuild::begin_items(std::size_t count) noexcept {
+ unboundAcquiredFlags_ = 0;
+ discardedAcquiredFlags_ = 0;
+ unreadItems_ = 0;
+ // A refused load has already reported itself.
+ if (!loaded_) {
+ return false;
+ }
+ try {
+ if (count == 0 || count > domain::kItemCapacity) {
+ report_refusal("item_storage");
+ return false;
+ }
+ items_.assign(count, {});
+ return true;
+ } catch (...) {
+ report_refusal("item_storage");
+ return false;
+ }
+}
+
+void RewardBuild::item(std::uint16_t index,
+ std::uint32_t hash,
+ std::span blob) noexcept {
+ domain::Item parsed{};
+ if (index < items_.size() && read_item(hash, blob, parsed)) {
+ items_[index] = parsed;
+ }
+}
+
+void RewardBuild::item_unread(std::uint16_t index) noexcept {
+ if (unreadItems_++ == 0) {
+ firstUnreadItem_ = index;
+ }
+}
+
+bool RewardBuild::publish(std::size_t catalogItemCount) noexcept {
+ if (!loaded_) {
+ return false;
+ }
+ // Publishing now would prune every entry that names an unread item.
+ if (unreadItems_ != 0) {
+ core::log::writef(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=rewards result=fail reason=item_read unread_items=%zu "
+ "first_index=%u",
+ unreadItems_,
+ static_cast(firstUnreadItem_));
+ return false;
+ }
+ // A catalog that lost trailing rows is shorter than the table, and the cache pairs each of its
+ // items with one reward row.
+ if (catalogItemCount < items_.size()) {
+ items_.resize(catalogItemCount);
+ }
+ // Cleared first so the entry pass also drops draws of a cleared wrapper.
+ for (auto& item : items_) {
+ if (item.poolIndex != domain::kAbsent && item.poolIndex >= pools_.size()) {
+ item = {};
+ }
+ }
+ std::size_t write = 0;
+ std::size_t socketWrite = 0;
+ for (auto& pool : pools_) {
+ const auto range = pool.entries;
+ pool.entries.first = static_cast(write);
+ for (std::size_t i = range.first; i < range.first + range.count; ++i) {
+ const auto& row = entries_[i];
+ bool valid = (row.itemIndex == domain::kAbsent || item_read(items_, row.itemIndex))
+ && (row.poolIndex == domain::kAbsent || row.poolIndex < pools_.size());
+ for (const auto& socket :
+ std::span(sockets_).subspan(row.sockets.first, row.sockets.count)) {
+ valid &=
+ domain::valid_socket(socket, items_.size())
+ && (socket.plugItem == domain::kAbsent || item_read(items_, socket.plugItem));
+ }
+ if (valid) {
+ auto retained = row;
+ retained.sockets.first = static_cast(socketWrite);
+ for (const auto& socket :
+ std::span(sockets_).subspan(row.sockets.first, row.sockets.count)) {
+ sockets_[socketWrite++] = socket;
+ }
+ entries_[write++] = retained;
+ }
+ }
+ pool.entries.count = static_cast(write) - pool.entries.first;
+ }
+ const std::size_t droppedEntries = entries_.size() - write;
+ entries_.resize(write);
+ sockets_.resize(socketWrite);
+ if (unboundAcquiredFlags_ != 0 || discardedAcquiredFlags_ != 0 || droppedEntries != 0) {
+ core::log::writef(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=rewards result=partial unbound_acquired_flags=%zu "
+ "discarded_acquired_flags=%zu dropped_entries=%zu",
+ unboundAcquiredFlags_,
+ discardedAcquiredFlags_,
+ droppedEntries);
+ }
+ const domain::View graph{pools_, entries_, items_, instructions_, modifiers_, sockets_};
+ if (!state::build_data::valid_reward_definitions(graph)) {
+ settle_unsupported("graph");
+ return false;
+ }
+ // A cache write can still fail, so this stays retryable.
+ if (!state::build_data::publish_reward_definitions(graph)) {
+ report_refusal("publish");
+ return false;
+ }
+ return true;
+}
+
+} // namespace sunrise::client::content::items::packages
diff --git a/Sunrise/src/client/content/items/packages/package_reward_build.h b/Sunrise/src/client/content/items/packages/package_reward_build.h
new file mode 100644
index 000000000..08015f885
--- /dev/null
+++ b/Sunrise/src/client/content/items/packages/package_reward_build.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include "../../../../middleware/content/packages/reader/reader.h"
+#include "../../../../middleware/content/packages/tables/definition_index_table.h"
+#include "../../../../state/account/account_state.h"
+#include "../../../../state/build_data/rewards/definition.h"
+
+namespace sunrise::client::content::items::packages {
+
+struct SlotMaps;
+
+/** A full reward bank settles the domain; failed storage is retried on a later pass. */
+enum class BankFailure : std::uint8_t { none, full, storage };
+
+/** Resolves expression references and unlock slots once for a package pass. */
+class RewardConditions {
+public:
+ /** Reads the root's flag, value, expression and class tables; false when one is missing. */
+ [[nodiscard]] bool load(const middleware::content::packages::reader::Source& source,
+ middleware::content::packages::reader::Scratch& scratch,
+ std::span root,
+ const SlotMaps& maps) noexcept;
+ /** @return True when the last load() succeeded. */
+ [[nodiscard]] bool loaded() const noexcept;
+ /** Binds one native instruction; flag and value slots resolve to their saved bank. */
+ [[nodiscard]] bool bind(std::uint32_t native,
+ std::uint32_t operand,
+ state::build_data::rewards::Instruction& instruction) const noexcept;
+ /** Appends one expanded expression; the bank is unchanged on failure. */
+ [[nodiscard]] bool read(std::span blob,
+ std::size_t at,
+ std::vector& bank,
+ state::build_data::rewards::Range& range,
+ BankFailure& failure) const noexcept;
+ /** Expands a list of expressions into one program where every expression must hold. */
+ [[nodiscard]] bool read_list(std::span blob,
+ std::size_t at,
+ std::span output,
+ std::size_t& count,
+ BankFailure& failure) const noexcept;
+
+private:
+ [[nodiscard]] bool append_expression(std::span blob,
+ std::size_t at,
+ std::vector& bank,
+ std::size_t depth,
+ BankFailure& failure) const noexcept;
+ /** Reads every class binding before any reward condition can be published. */
+ [[nodiscard]] bool load_class_flags(const middleware::content::packages::reader::Source& source,
+ middleware::content::packages::reader::Scratch& scratch,
+ std::span root) noexcept;
+ std::array classFlags_{};
+ const SlotMaps* maps_{};
+ bool loaded_{};
+ std::vector flags_;
+ std::vector values_;
+ std::vector expressions_;
+ middleware::content::packages::tables::Array flagRows_{};
+ middleware::content::packages::tables::Array valueRows_{};
+ middleware::content::packages::tables::Array expressionRows_{};
+};
+
+/** Reward metadata collected alongside the existing item-definition walk. */
+class RewardBuild {
+public:
+ RewardConditions conditions;
+ /** Reads every reward pool; unreadable entries are skipped, a full bank refuses the load. */
+ [[nodiscard]] bool load(const middleware::content::packages::reader::Source& source,
+ middleware::content::packages::reader::Scratch& scratch,
+ std::span root,
+ const SlotMaps& maps) noexcept;
+ /** Sizes the dense item rows before the item walk. */
+ [[nodiscard]] bool begin_items(std::size_t count) noexcept;
+ /** Records one item's wrapper and acquisition flag; an unreadable item stays unavailable. */
+ void item(std::uint16_t index, std::uint32_t hash, std::span blob) noexcept;
+ /** Notes an item whose definition failed to read; publication waits for a clean walk. */
+ void item_unread(std::uint16_t index) noexcept;
+ /** Trims item rows to the catalog, drops rows naming unread items or pools, then publishes. */
+ [[nodiscard]] bool publish(std::size_t catalogItemCount) noexcept;
+
+private:
+ /** Appends one entry; a bank failure also names the bank and what refused it. */
+ [[nodiscard]] bool entry(std::span blob,
+ std::size_t at,
+ BankFailure& failure,
+ const char*& reason) noexcept;
+ [[nodiscard]] bool read_item(std::uint32_t hash,
+ std::span blob,
+ state::build_data::rewards::Item& item) noexcept;
+ bool loaded_{};
+ bool supplementalMissing_{};
+ std::size_t unboundAcquiredFlags_{};
+ std::size_t discardedAcquiredFlags_{};
+ std::size_t unreadItems_{};
+ std::uint16_t firstUnreadItem_{};
+ std::vector pools_;
+ std::vector entries_;
+ std::vector items_;
+ std::vector instructions_;
+ std::vector modifiers_;
+ std::vector sockets_;
+};
+
+/** Progression rewards and pool entries use the same socket-override layout. */
+[[nodiscard]] bool read_reward_sockets(std::span blob,
+ std::size_t at,
+ std::span output,
+ std::size_t& count) noexcept;
+
+} // namespace sunrise::client::content::items::packages
diff --git a/Sunrise/src/client/content/items/packages/package_season_pass_build.cpp b/Sunrise/src/client/content/items/packages/package_season_pass_build.cpp
index 2dcc13bd9..9a689b186 100644
--- a/Sunrise/src/client/content/items/packages/package_season_pass_build.cpp
+++ b/Sunrise/src/client/content/items/packages/package_season_pass_build.cpp
@@ -1,73 +1,94 @@
#include
-#include
#include
+#include "../../../../core/logging/log.h"
+#include "../../../../middleware/content/packages/tables/field_reader.h"
+#include "../../../../state/progression/season_pass_reward_catalog.h"
#include "internal.h"
+#include "package_reward_build.h"
namespace sunrise::client::content::items::packages {
namespace {
namespace domain = state::build_data::season_pass;
-/** Account progression the installed Season of Arrivals pass declares its rewards on. */
-constexpr std::uint16_t kPassProgressionIndex = 40;
-
-/** @param blob Source bytes. @param offset Field offset. @param value Receives the field. */
-template
-[[nodiscard]] bool
-read(std::span blob, std::size_t offset, Value& value) noexcept {
- if (offset > blob.size() || blob.size() - offset < sizeof value) {
- return false;
- }
- std::memcpy(&value, blob.data() + offset, sizeof value);
- return true;
-}
-
/**
- * Reads the item set one reward's wrapper item opens into.
- * @param definition Whole item definition blob.
- * @param itemTable Item index table blob.
- * @param itemRows Located item index array.
- * @param package Receives the wrapper's items, or stays empty when the item opens into nothing.
- * @return True when the item declares no set, or declares one that reads back whole.
+ * Reads one pass row: item, rank, quantity and claim flag, then its sockets and conditions.
+ * @return False when any field is unreadable or out of range.
*/
-[[nodiscard]] bool read_package(std::span definition,
- std::span itemTable,
- const tables::Array& itemRows,
- domain::Package& package) noexcept {
- tables::Array set{};
- if (!tables::find_optional_array_at(definition, tables::kGearsetItemField, set)) {
+bool read_season_reward(Storage& storage,
+ std::span progressionTable,
+ const tables::Array& itemRows,
+ std::size_t at,
+ domain::Reward& reward,
+ BankFailure& failure) noexcept {
+ std::uint32_t rank = 0;
+ std::uint32_t itemIndex = 0;
+ std::uint32_t claimSlot = 0;
+ if (!tables::read(progressionTable, at + tables::kProgressionRewardRankOffset, rank)
+ || !tables::read(
+ progressionTable, at + tables::kProgressionRewardItemIndexOffset, itemIndex)
+ || !tables::read(
+ progressionTable, at + tables::kProgressionRewardQuantityOffset, reward.quantity)
+ || !tables::read(
+ progressionTable, at + tables::kProgressionRewardClaimSlotOffset, claimSlot)) {
return false;
}
- if (set.count == 0) {
- return true;
- }
- if (set.elementClass != tables::kGearsetItemRowClass || set.count > domain::kPackageItemCapacity
- || set.dataOffset + static_cast(set.count) * tables::kGearsetItemStride
- > definition.size()) {
+
+ // Native fields are 32-bit, the stored row keeps the narrower native index widths.
+ tables::IndexRow entry{};
+ if (rank > (std::numeric_limits::max)()
+ || itemIndex > (std::numeric_limits::max)()
+ || claimSlot > (std::numeric_limits::max)()
+ || !tables::index_row(storage.itemIndexTable, itemRows, itemIndex, entry)) {
return false;
}
- for (std::uint64_t member = 0; member < set.count; ++member) {
- std::uint16_t itemIndex = 0;
- tables::IndexRow entry{};
- if (!read(definition,
- set.dataOffset + static_cast(member) * tables::kGearsetItemStride,
- itemIndex)
- || !tables::index_row(itemTable, itemRows, itemIndex, entry)) {
+ reward.itemHash = entry.definitionHash;
+ reward.itemIndex = static_cast(itemIndex);
+ reward.requiredRank = static_cast(rank);
+
+ // A reward with no claim flag carries slot 0, any other slot must map to the account bank.
+ if (claimSlot != 0) {
+ reward.claimFlagIndex =
+ bank_index(storage.slotMaps.accountFlag, static_cast(claimSlot));
+ if (reward.claimFlagIndex == domain::kUnavailableFlagIndex) {
return false;
}
- package.items[package.itemCount++] = entry.definitionHash;
}
- return true;
+
+ std::size_t socketCount = 0;
+ std::size_t conditionCount = 0;
+ if (!read_reward_sockets(progressionTable,
+ at + tables::kProgressionRewardSocketsOffset,
+ reward.sockets,
+ socketCount)
+ || !storage.rewardBuild.conditions.read_list(
+ progressionTable,
+ at + tables::kProgressionRewardConditionsOffset,
+ reward.condition,
+ conditionCount,
+ failure)) {
+ return false;
+ }
+ reward.socketCount = static_cast(socketCount);
+ reward.conditionCount = static_cast(conditionCount);
+ // Checked per row so one refused row stays unavailable.
+ return state::build_data::valid_season_pass_rewards(std::span(&reward, 1))
+ && std::all_of(reward.sockets.begin(),
+ reward.sockets.begin() + reward.socketCount,
+ [&itemRows](const state::build_data::rewards::SocketOverride& socket) {
+ return state::build_data::rewards::valid_socket(socket,
+ itemRows.count);
+ });
}
} // namespace
/**
- * Reads the season pass reward list and the wrapper items it grants.
- * A reward names an item index and a claim flag slot; both become the values a grant needs.
+ * Reads the season pass reward list in native order, which the opcode-2400 claim indexes.
+ * A row that fails to read is kept as an unavailable row so later claim indices stay aligned.
* @param source Installed package source.
- * @param storage Pass storage receiving the reward rows and the wrapper packages.
+ * @param storage Pass storage receiving the reward rows.
* @param root Investment root bytes.
* @return True when the reward list read and produced at least one row.
*/
@@ -75,7 +96,6 @@ bool build_season_pass(const reader::Source& source,
Storage& storage,
std::span root) noexcept {
storage.seasonPassRewardCount = 0;
- storage.seasonPassPackageCount = 0;
std::uint32_t itemTableTag = 0;
tables::Array itemRows{};
@@ -87,7 +107,6 @@ bool build_season_pass(const reader::Source& source,
|| itemRows.elementClass != tables::kItemIndexTableClass) {
return false;
}
- const std::span itemTable{storage.itemIndexTable};
std::uint32_t tableTag = 0;
tables::Array progressions{};
@@ -97,74 +116,49 @@ bool build_season_pass(const reader::Source& source,
tables::kTableArrayDescriptor,
progressions)
|| progressions.elementClass != tables::kProgressionTableClass
- || progressions.count <= kPassProgressionIndex) {
+ || progressions.count <= state::progression::season_pass::kProgressionDefinitionIndex) {
return false;
}
const std::span progressionTable{storage.progressionTable};
- const std::size_t passAt =
- progressions.dataOffset + kPassProgressionIndex * tables::kProgressionRowStride;
+ const std::size_t passAt = progressions.dataOffset
+ + state::progression::season_pass::kProgressionDefinitionIndex
+ * tables::kProgressionRowStride;
tables::Array rewards{};
- if (!tables::find_optional_array_at(
- progressionTable, passAt + tables::kProgressionRewardField, rewards)
- || rewards.count == 0 || rewards.elementClass != tables::kProgressionRewardRowClass
- || rewards.count > domain::kRewardCapacity
- || rewards.dataOffset
- + static_cast(rewards.count) * tables::kProgressionRewardStride
- > progressionTable.size()) {
+ if (!tables::read_array(progressionTable,
+ passAt + tables::kProgressionRewardField,
+ tables::kProgressionRewardRowClass,
+ tables::kProgressionRewardStride,
+ rewards)
+ || rewards.count == 0 || rewards.count > domain::kRewardCapacity) {
return false;
}
+ std::size_t skipped = 0;
for (std::uint64_t row = 0; row < rewards.count; ++row) {
const std::size_t at =
rewards.dataOffset + static_cast(row) * tables::kProgressionRewardStride;
- std::uint32_t rank = 0;
- std::uint32_t itemIndex = 0;
- std::uint32_t claimSlot = 0;
- domain::Reward& reward = storage.seasonPassRewards[storage.seasonPassRewardCount];
+ auto& reward = storage.seasonPassRewards[row];
reward = {};
- tables::IndexRow entry{};
- if (!read(progressionTable, at + tables::kProgressionRewardRankOffset, rank)
- || !read(progressionTable, at + tables::kProgressionRewardItemIndexOffset, itemIndex)
- || !read(
- progressionTable, at + tables::kProgressionRewardQuantityOffset, reward.quantity)
- || !read(progressionTable, at + tables::kProgressionRewardClaimSlotOffset, claimSlot)
- || rank > (std::numeric_limits::max)()
- || itemIndex > (std::numeric_limits::max)()
- || claimSlot > (std::numeric_limits::max)()
- || !tables::index_row(itemTable, itemRows, itemIndex, entry)) {
- storage.seasonPassRewardCount = 0;
- return false;
+ auto failure = BankFailure::none;
+ if (!read_season_reward(storage, progressionTable, itemRows, at, reward, failure)) {
+ // A row is left unavailable only for its data, never for failed storage.
+ if (failure == BankFailure::storage) {
+ core::log::write(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=season_pass result=fail reason=condition_storage");
+ return false;
+ }
+ reward = {};
+ ++skipped;
}
- reward.itemHash = entry.definitionHash;
- reward.itemIndex = static_cast(itemIndex);
- reward.requiredRank = static_cast(rank);
- // A reward with no claim flag carries slot 0.
- if (claimSlot != 0) {
- reward.claimFlagIndex =
- bank_index(storage.slotMaps.accountFlag, static_cast(claimSlot));
- }
-
- // A wrapper reward opens into a set; a plain reward declares none and keeps zero items.
- // One wrapper can be granted at several ranks, so it is recorded once.
- const std::span held =
- std::span(storage.seasonPassPackages).first(storage.seasonPassPackageCount);
- domain::Package package{};
- package.definitionHash = entry.definitionHash;
- if (storage.seasonPassPackageCount < domain::kPackageCapacity
- && std::none_of(
- held.begin(),
- held.end(),
- [&entry](const domain::Package& row) {
- return row.definitionHash == entry.definitionHash;
- })
- && reader::read_tag(source, storage.scratch, entry.targetTag, storage.definition)
- && read_package(
- std::span{storage.definition}, itemTable, itemRows, package)
- && package.itemCount != 0) {
- storage.seasonPassPackages[storage.seasonPassPackageCount++] = package;
- }
- ++storage.seasonPassRewardCount;
}
+ if (skipped != 0) {
+ core::log::writef(core::log::Channel::client,
+ core::log::Level::warn,
+ "ev=pkg stage=season_pass skipped=%zu",
+ skipped);
+ }
+ storage.seasonPassRewardCount = static_cast(rewards.count);
return storage.seasonPassRewardCount != 0;
}
diff --git a/Sunrise/src/middleware/content/packages/tables/activity_table.h b/Sunrise/src/middleware/content/packages/tables/activity_table.h
index c10becf27..8613c76b5 100644
--- a/Sunrise/src/middleware/content/packages/tables/activity_table.h
+++ b/Sunrise/src/middleware/content/packages/tables/activity_table.h
@@ -4,6 +4,7 @@
#include "../../../../state/build_data/activities/activity_catalog.h"
#include "definition_index_table.h"
+#include "field_reader.h"
namespace sunrise::middleware::content::packages::tables::activities {
/** Public activity index, NOT the package-definition index in root slot 29. */
@@ -32,26 +33,6 @@ template
std::memcpy(&value, bytes.data() + offset, sizeof(T));
return true;
}
-/**
- * Resolves a self-relative field.
- * @param field Offset of the 8-byte signed delta.
- * @param target Receives the resolved offset.
- * @return False when the delta is zero or the target falls outside the blob.
- */
-[[nodiscard]] inline bool
-relative(std::span bytes, std::size_t field, std::size_t& target) noexcept {
- std::int64_t delta{};
- if (!read(bytes, field, delta) || delta == 0
- || field > static_cast((std::numeric_limits::max)())) {
- return false;
- }
- const auto base = static_cast(field);
- if (delta < -base || delta > (std::numeric_limits::max)() - base) {
- return false;
- }
- target = static_cast(base + delta);
- return target < bytes.size();
-}
/**
* Parses the dense public activity table.
* @param bytes The table blob.
diff --git a/Sunrise/src/middleware/content/packages/tables/definition_index_table.cpp b/Sunrise/src/middleware/content/packages/tables/definition_index_table.cpp
index e2e83ab68..e926ff11a 100644
--- a/Sunrise/src/middleware/content/packages/tables/definition_index_table.cpp
+++ b/Sunrise/src/middleware/content/packages/tables/definition_index_table.cpp
@@ -1,8 +1,9 @@
#include "definition_index_table.h"
-#include
#include
+#include "field_reader.h"
+
namespace sunrise::middleware::content::packages::tables {
namespace {
@@ -31,17 +32,6 @@ constexpr std::size_t kIndexRowTagOffset = 16;
/** Arrays larger than this are not definition tables. */
constexpr std::uint64_t kMaximumArrayCount = 300000;
-/** @param blob Source bytes. @param offset Field offset. @param value Receives the field. */
-template
-[[nodiscard]] bool
-read(std::span blob, std::size_t offset, Value& value) noexcept {
- if (offset > blob.size() || blob.size() - offset < sizeof value) {
- return false;
- }
- std::memcpy(&value, blob.data() + offset, sizeof value);
- return true;
-}
-
/**
* Checks one array header reached from a descriptor.
* @param blob Whole definition bytes.
@@ -137,6 +127,18 @@ bool find_optional_array_at(std::span blob,
return resolve_descriptor(blob, descriptorOffset, true, output);
}
+/** Reads an optional typed array and checks every fixed-stride row lies in its blob. */
+bool read_array(std::span blob,
+ std::size_t at,
+ std::uint32_t elementClass,
+ std::size_t stride,
+ Array& rows) noexcept {
+ return stride != 0 && find_optional_array_at(blob, at, rows)
+ && (rows.count == 0
+ || (rows.elementClass == elementClass && rows.dataOffset <= blob.size()
+ && rows.count <= (blob.size() - rows.dataOffset) / stride));
+}
+
/** Finds the first array whose header names one element class. */
bool find_array(std::span blob,
std::uint32_t elementClass,
diff --git a/Sunrise/src/middleware/content/packages/tables/definition_index_table.h b/Sunrise/src/middleware/content/packages/tables/definition_index_table.h
index a1aa1502e..7cd162b7f 100644
--- a/Sunrise/src/middleware/content/packages/tables/definition_index_table.h
+++ b/Sunrise/src/middleware/content/packages/tables/definition_index_table.h
@@ -96,7 +96,10 @@ inline constexpr std::size_t kUnlockInstructionOperandOffset = 4;
/** An expression field is a 64-bit count then a 64-bit self-relative offset. */
inline constexpr std::size_t kUnlockExpressionFieldSize = 16;
inline constexpr std::size_t kUnlockExpressionPointerOffset = 8;
-/** Opcodes run to fifteen; anything wider means the field is not an expression. */
+/**
+ * Record and node field detection accepts a field only when every opcode is at most 20. This is a
+ * detection bound, not the opcode range: installed reward conditions also use opcode 22.
+ */
inline constexpr std::uint32_t kUnlockOpcodeCeiling = 20;
/** The opcode that reads a value slot. */
inline constexpr std::uint32_t kUnlockReadValueOpcode = 10;
@@ -104,6 +107,8 @@ inline constexpr std::uint32_t kUnlockReadValueOpcode = 10;
inline constexpr std::uint32_t kUnlockReadFlagOpcode = 1;
/** The opcode that pushes a literal. */
inline constexpr std::uint32_t kUnlockLiteralOpcode = 11;
+/** The opcode that expands a shared expression row. Its operand is the row index. */
+inline constexpr std::uint32_t kUnlockExpressionOpcode = 12;
/** The opcode that tests greater than or equal. */
inline constexpr std::uint32_t kUnlockGreaterEqualOpcode = 14;
/** The opcode that inverts the value on top of the stack. Its operand is unused. */
@@ -130,6 +135,11 @@ inline constexpr std::size_t kNodeExpressionFieldAlternate = 48;
/** Records a node owns, four bytes each as a row then a gate. */
inline constexpr std::size_t kNodeChildRecordField = 136;
inline constexpr std::size_t kNodeChildRecordStride = 4;
+/**
+ * Array descriptor of the profile flag mapping table, between the account (+8) and character (+40)
+ * maps. Profile flags are account-wide but saved in their own bank, not the account flag bank.
+ */
+inline constexpr std::size_t kProfileFlagMapDescriptor = 24;
/** Array descriptor of the character object's flag mapping table, sized to that bank. */
inline constexpr std::size_t kCharacterFlagMapDescriptor = 40;
/** Array descriptor of the character object's value mapping table, sized to that bank. */
@@ -223,10 +233,9 @@ inline constexpr std::size_t kProgressionRewardItemIndexOffset = 4;
inline constexpr std::size_t kProgressionRewardQuantityOffset = 8;
/** Unlock flag slot a reward's claim sets. The slots run dense in reward order. */
inline constexpr std::size_t kProgressionRewardClaimSlotOffset = 20;
-/** Items a wrapper item opens into, two bytes each as an item-definition index. */
-inline constexpr std::size_t kGearsetItemField = 392;
-inline constexpr std::uint32_t kGearsetItemRowClass = 0x808087DBU;
-inline constexpr std::size_t kGearsetItemStride = 2;
+/** Eligibility expressions and socket overrides carried by one progression reward. */
+inline constexpr std::size_t kProgressionRewardConditionsOffset = 24;
+inline constexpr std::size_t kProgressionRewardSocketsOffset = 40;
/** Expression that makes one collectible acquired, which a purchase or a grant sets. */
inline constexpr std::size_t kCollectibleAcquiredExpressionField = 112;
@@ -283,6 +292,13 @@ using RowVisitor = bool (*)(void* context, std::uint32_t index, const IndexRow&
std::size_t descriptorOffset,
Array& output) noexcept;
+/** Reads an optional typed array and checks every fixed-stride row lies in its blob. */
+[[nodiscard]] bool read_array(std::span blob,
+ std::size_t at,
+ std::uint32_t elementClass,
+ std::size_t stride,
+ Array& rows) noexcept;
+
/**
* Finds the first array whose header names one element class.
* @param blob Whole definition bytes.
diff --git a/Sunrise/src/middleware/content/packages/tables/field_reader.h b/Sunrise/src/middleware/content/packages/tables/field_reader.h
new file mode 100644
index 000000000..55ed6d27e
--- /dev/null
+++ b/Sunrise/src/middleware/content/packages/tables/field_reader.h
@@ -0,0 +1,49 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace sunrise::middleware::content::packages::tables {
+/**
+ * Reads one little-endian field that must lie inside the blob.
+ * @param blob Whole definition bytes.
+ * @param offset Field offset.
+ * @param value Receives the field.
+ * @return True when the whole field is inside the blob.
+ */
+template
+[[nodiscard]] inline bool
+read(std::span blob, std::size_t offset, Value& value) noexcept {
+ // Subtracting rather than adding keeps a large offset from wrapping past the size.
+ if (offset > blob.size() || blob.size() - offset < sizeof value) {
+ return false;
+ }
+ std::memcpy(&value, blob.data() + offset, sizeof value);
+ return true;
+}
+
+/**
+ * Resolves a self-relative field.
+ * @param field Offset of the 8-byte signed delta.
+ * @param target Receives the resolved offset.
+ * @return False when the delta is zero or the target falls outside the blob.
+ */
+[[nodiscard]] inline bool
+relative(std::span bytes, std::size_t field, std::size_t& target) noexcept {
+ std::int64_t delta{};
+ if (!read(bytes, field, delta) || delta == 0
+ || field > static_cast((std::numeric_limits::max)())) {
+ return false;
+ }
+ const auto base = static_cast(field);
+ if (delta < -base || delta > (std::numeric_limits::max)() - base) {
+ return false;
+ }
+ target = static_cast(base + delta);
+ return target < bytes.size();
+}
+
+} // namespace sunrise::middleware::content::packages::tables
diff --git a/Sunrise/src/middleware/content/packages/tables/internal.h b/Sunrise/src/middleware/content/packages/tables/internal.h
index 393ac00d0..2384f4996 100644
--- a/Sunrise/src/middleware/content/packages/tables/internal.h
+++ b/Sunrise/src/middleware/content/packages/tables/internal.h
@@ -2,28 +2,11 @@
#include
#include
-#include
#include
-namespace sunrise::middleware::content::packages::tables {
+#include "field_reader.h"
-/**
- * Reads one little-endian field that must lie inside the blob.
- * @param blob Whole definition bytes.
- * @param offset Field offset.
- * @param value Receives the field.
- * @return True when the whole field is inside the blob.
- */
-template
-[[nodiscard]] inline bool
-read(std::span blob, std::size_t offset, Value& value) noexcept {
- // Subtracting rather than adding keeps a large offset from wrapping past the size.
- if (offset > blob.size() || blob.size() - offset < sizeof value) {
- return false;
- }
- std::memcpy(&value, blob.data() + offset, sizeof value);
- return true;
-}
+namespace sunrise::middleware::content::packages::tables {
/**
* Finds the offset of one fixed-stride array element.
diff --git a/Sunrise/src/server/bap/bap_world_reward_queue.cpp b/Sunrise/src/server/bap/bap_world_reward_queue.cpp
index 6c27c8ea6..3cbd24b66 100644
--- a/Sunrise/src/server/bap/bap_world_reward_queue.cpp
+++ b/Sunrise/src/server/bap/bap_world_reward_queue.cpp
@@ -1,4 +1,10 @@
+#include
+#include
+#include
+#include
+
#include "../../core/logging/log.h"
+#include "../../middleware/crypto/random_bytes.h"
#include "../../state/build_data/runtime.h"
#include "../../state/investment/store_internal.h"
#include "internal.h"
@@ -6,25 +12,72 @@
namespace sunrise::server::bap {
namespace {
-/** Grants and retires one earned reward in the same database transaction. */
-bool commit_world_reward(const WorldRewardRequest& request) noexcept {
+/** Logged for a saved row whose hash is not in the item table. */
+constexpr std::uint16_t kUnresolvedItemIndex = (std::numeric_limits::max)();
+
+/** Outcome of one settlement attempt. */
+enum class WorldRewardSettlement : std::uint8_t { granted, retired, kept };
+
+/** Grants and retires one earned reward in one transaction, or retires one State rules out. */
+WorldRewardSettlement commit_world_reward(const WorldRewardRequest& request) noexcept {
state::investment::store::Transaction transaction;
if (!transaction.ready()) {
- return false;
+ return WorldRewardSettlement::kept;
}
bool committed = false;
+ bool unresolvable = false;
+ const char* reason = nullptr;
if (request.kind == WorldRewardKind::item) {
- state::PendingItemAcquisition acquisition;
- committed =
- state::prepare_item_acquisition_for_item(request.itemDefinitionIndex, acquisition)
- && state::commit_item_acquisition(acquisition);
+ if (state::item_grant_route(request.itemDefinitionIndex) == state::ItemGrantRoute::quest) {
+ if (request.quantity != 1) {
+ unresolvable = true;
+ reason = "instance_quantity";
+ } else {
+ state::PendingItemAcquisition acquisition;
+ committed = state::prepare_item_acquisition_for_item(request.itemDefinitionIndex,
+ acquisition)
+ && state::commit_item_acquisition(acquisition);
+ }
+ } else {
+ const std::unique_ptr grant(
+ new (std::nothrow) state::PendingRecordRewardGrant);
+ std::uint64_t seed = 0;
+ const bool seeded =
+ middleware::crypto::random::fill(std::as_writable_bytes(std::span(&seed, 1)));
+ reason = seeded ? "reward_allocation" : "random_source";
+ auto preparation = state::RewardPreparation::deferred;
+ if (seeded && grant) {
+ preparation =
+ state::prepare_item_reward(request.itemDefinitionIndex,
+ static_cast(request.quantity),
+ seed,
+ *grant,
+ &reason);
+ }
+ unresolvable = preparation == state::RewardPreparation::unresolvable;
+ committed = preparation == state::RewardPreparation::prepared;
+ if (committed) {
+ reason = "inventory_commit";
+ committed = state::commit_record_reward(*grant);
+ }
+ if (!committed && !unresolvable) {
+ report_reward_refusal("world_settle", request.itemDefinitionIndex, reason);
+ }
+ }
} else {
state::PendingProfileItemAcquisition acquisition;
committed = state::prepare_profile_item_acquisition_for_item(
request.itemDefinitionIndex, request.quantity, acquisition)
&& state::commit_profile_item_acquisition(acquisition);
}
- return committed && complete_world_reward(request.id) && transaction.commit();
+ if (unresolvable) {
+ return retire_world_reward(transaction, request, reason) ? WorldRewardSettlement::retired
+ : WorldRewardSettlement::kept;
+ }
+ if (!committed || !complete_world_reward(request.id) || !transaction.commit()) {
+ return WorldRewardSettlement::kept;
+ }
+ return WorldRewardSettlement::granted;
}
/** Saves the reward under its stable definition hash and earning character. */
@@ -45,6 +98,16 @@ bool enqueue_world_reward(std::uint16_t definitionIndex,
} // namespace
+/** Logs one refused reward stage with its native index and a specific reason. */
+void report_reward_refusal(const char* stage, std::uint16_t index, const char* reason) noexcept {
+ core::log::writef(core::log::Channel::server,
+ core::log::Level::warn,
+ "ev=reward stage=%s index=%u result=refused reason=%s",
+ stage,
+ static_cast(index),
+ reason != nullptr ? reason : "unknown");
+}
+
/** Saves one item reward before its pickup presentation is queued. */
bool arm_world_item_acquisition(std::uint16_t itemDefinitionIndex) noexcept {
return enqueue_world_reward(itemDefinitionIndex, 1, WorldRewardKind::item);
@@ -57,40 +120,82 @@ bool arm_world_profile_item_acquisition(std::uint16_t itemDefinitionIndex,
&& enqueue_world_reward(itemDefinitionIndex, quantity, WorldRewardKind::profileItem);
}
-/** Restores the oldest reward belonging to the selected character. */
+/** Restores the selected character's oldest reward, retiring rows that can never be granted. */
bool current_world_reward(WorldRewardRequest& request) noexcept {
- request = {};
state::investment::store::PendingReward saved;
- state::build_data::items::Definition definition;
- if (!state::investment::store::next_reward(saved)
- || !state::build_data::find_item_definition_hash(saved.definitionHash, definition)) {
- return false;
+ while (state::investment::store::next_reward(saved)) {
+ request = {};
+ request.id = saved.id;
+ request.definitionHash = saved.definitionHash;
+ request.quantity = saved.quantity;
+ request.kind = static_cast(saved.kind);
+ const bool itemsPublished = state::build_data::item_definitions_ready();
+ state::build_data::items::Definition definition;
+ const char* verdict = "reward_quantity";
+ if (state::build_data::find_item_definition_hash(saved.definitionHash, definition)) {
+ request.itemDefinitionIndex = definition.definitionIndex;
+ if (request.quantity > 0) {
+ return true;
+ }
+ } else if (!itemsPublished || !state::build_data::item_definitions_ready()) {
+ // An unpublished item table proves nothing; retry later.
+ break;
+ } else {
+ request.itemDefinitionIndex = kUnresolvedItemIndex;
+ verdict = "item_definition";
+ }
+ state::investment::store::Transaction transaction;
+ if (!transaction.ready() || !retire_world_reward(transaction, request, verdict)) {
+ break;
+ }
}
- request.id = saved.id;
- request.quantity = saved.quantity;
- request.itemDefinitionIndex = definition.definitionIndex;
- request.kind = static_cast(saved.kind);
- return true;
+ request = {};
+ return false;
}
-/** Acknowledges exactly the reward whose inventory grant is being committed. */
+/** Acknowledges exactly the reward being granted or retired. */
bool complete_world_reward(std::uint64_t id) noexcept {
return state::investment::store::complete_reward(id);
}
-/** A failed grant stays saved for a later attempt. */
+/** Deletes the row on the caller's open savepoint, commits, and logs why. */
+bool retire_world_reward(state::investment::store::Transaction& transaction,
+ const WorldRewardRequest& request,
+ const char* reason) noexcept {
+ const bool retired = complete_world_reward(request.id) && transaction.commit();
+ core::log::writef(core::log::Channel::server,
+ core::log::Level::warn,
+ "ev=reward stage=world_retire id=%llu hash=0x%08X index=%u quantity=%d "
+ "kind=%u result=%s reason=%s",
+ static_cast(request.id),
+ static_cast(request.definitionHash),
+ static_cast(request.itemDefinitionIndex),
+ static_cast(request.quantity),
+ static_cast(request.kind),
+ retired ? "retired" : "kept",
+ reason != nullptr ? reason : "unknown");
+ return retired;
+}
+
+/** A retired reward lets the next one take its turn; any other failed grant stays saved. */
void settle_world_reward() noexcept {
WorldRewardRequest request;
- if (current_world_reward(request) && commit_world_reward(request)) {
- arm_account_resync_everywhere();
+ while (current_world_reward(request)) {
+ const WorldRewardSettlement settlement = commit_world_reward(request);
+ if (settlement == WorldRewardSettlement::granted) {
+ arm_account_resync_everywhere();
+ }
+ if (settlement != WorldRewardSettlement::retired) {
+ return;
+ }
}
}
-/** Shutdown may grant ready rewards; rewards for other characters stay in the database. */
+/** Shutdown grants or retires rewards until one must wait; other characters' rewards stay saved. */
void drain_world_rewards() noexcept {
WorldRewardRequest request;
while (current_world_reward(request)) {
- if (!commit_world_reward(request)) {
+ if (commit_world_reward(request) == WorldRewardSettlement::kept) {
break;
}
}
diff --git a/Sunrise/src/server/bap/encrypted/activity_message/receipts/activity_incident_grants.cpp b/Sunrise/src/server/bap/encrypted/activity_message/receipts/activity_incident_grants.cpp
index 148327cea..5b8873d6a 100644
--- a/Sunrise/src/server/bap/encrypted/activity_message/receipts/activity_incident_grants.cpp
+++ b/Sunrise/src/server/bap/encrypted/activity_message/receipts/activity_incident_grants.cpp
@@ -380,10 +380,7 @@ void grant_random_moon_loot() noexcept {
grant_lost_ghost_reward();
// One found ghost pays this much seasonal experience.
constexpr std::int32_t kBaseExperienceReward = 2500;
- const bool queued = bap::arm_seasonal_experience_presentation(kBaseExperienceReward);
- if (!queued) {
- (void)state::grant_seasonal_experience(kBaseExperienceReward);
- }
+ (void)bap::arm_seasonal_experience_presentation(kBaseExperienceReward);
}
return outcome;
}
diff --git a/Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp b/Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp
index 80baf576e..4901881ef 100644
--- a/Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp
+++ b/Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp
@@ -529,22 +529,9 @@ bool process(const ServiceRoute& route,
}
if (recordRewardGrant != nullptr) {
auto* transaction = emplace_transaction(outcome);
- std::array residents{};
- std::size_t residentCount = 0;
- for (std::size_t index = 0; index < recordRewardGrant->rewardCount; ++index) {
- const auto& reward = recordRewardGrant->rewards[index];
- if (reward.kind == state::RecordRewardKind::characterInstance
- || reward.appendedProfileResident) {
- residents[residentCount++] = reward.instanceSoid;
- }
- }
- const bool staged =
- transaction != nullptr
- && queuez::stage_record_reward_grant(queuezState,
- recordRewardGrant->accountSoid,
- recordRewardGrant->characterSoid,
- std::span(residents).first(residentCount),
- transaction->update);
+ const bool staged = transaction != nullptr
+ && queuez::stage_record_reward_grant(
+ queuezState, *recordRewardGrant, transaction->update);
if (!staged) {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
@@ -577,43 +564,9 @@ bool process(const ServiceRoute& route,
"ev=ws2400 stage=reward_preflight result=fail");
return refuse_web_action(message, output, written);
}
- bool staged = false;
- std::int32_t stagedVersion = 0;
- if (const auto* itemGrant =
- std::get_if(&seasonPassReward->grant)) {
- auto& update = transaction->update.emplace();
- staged = queuez::stage_item_acquisition(queuezState,
- itemGrant->accountSoid,
- itemGrant->characterSoid,
- itemGrant->acquiredInstanceSoid,
- true,
- update);
- stagedVersion = update.after.family4Version;
- } else if (const auto* profileGrant = std::get_if(
- &seasonPassReward->grant)) {
- auto& update = transaction->update.emplace();
- staged = queuez::stage_profile_item_acquisition(queuezState,
- profileGrant->accountSoid,
- profileGrant->acquiredInstanceSoid,
- profileGrant->actionSource,
- profileGrant->appended,
- update);
- stagedVersion = update.after.family4Version;
- } else if (const auto* bundle =
- std::get_if(&seasonPassReward->grant)) {
- staged = queuez::stage_direct_item_bundle(queuezState,
- bundle->accountSoid,
- bundle->characterSoid,
- bundle->firstInstanceSoid,
- bundle->itemCount,
- stagedVersion);
- } else if (const auto* resources =
- std::get_if(&seasonPassReward->grant)) {
- auto& update = transaction->update.emplace();
- staged = queuez::stage_record_reward_grant(
- queuezState, resources->accountSoid, resources->characterSoid, {}, update);
- stagedVersion = update.after.family4Version;
- }
+ const bool staged = queuez::stage_record_reward_grant(
+ queuezState, seasonPassReward->grant, transaction->update);
+ const auto stagedVersion = transaction->update.after.family4Version;
if (!staged) {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
diff --git a/Sunrise/src/server/bap/encrypted/internal.h b/Sunrise/src/server/bap/encrypted/internal.h
index 197bde99d..5ba1a0301 100644
--- a/Sunrise/src/server/bap/encrypted/internal.h
+++ b/Sunrise/src/server/bap/encrypted/internal.h
@@ -141,15 +141,7 @@ struct SeasonPassRewardTransaction {
std::unique_ptr pending{};
- std::variant
-
- update{};
+ queuez::RecordRewardGrant update{};
};
/** Optional side effect produced while decoding one authenticated service body. */
@@ -481,30 +473,6 @@ void append_queuez_notification(Scratch& scratch,
queuez::SessionState& after) noexcept;
-/** Publishes one Season package as new residents plus their acquisition descriptors. */
-
-[[nodiscard]] bool append_season_pass_package_notification(
-
- Scratch& scratch,
-
- const queuez::SessionState& before,
-
- const state::PendingDirectItemBundle& mutation,
-
- std::uint16_t rewardIndex,
-
- std::span acquisitionPresentationRows,
-
- std::span key,
-
- std::span nonce,
-
- std::span response,
-
- std::size_t& written,
-
- queuez::SessionState& after) noexcept;
-
/** Publishes every item and the claimed record in one Family-4 revision. */
[[nodiscard]] bool append_record_reward_notification(
diff --git a/Sunrise/src/server/bap/encrypted/push/queuez/queuez_select_character.cpp b/Sunrise/src/server/bap/encrypted/push/queuez/queuez_select_character.cpp
index 5ab320f8d..3a0e59e8d 100644
--- a/Sunrise/src/server/bap/encrypted/push/queuez/queuez_select_character.cpp
+++ b/Sunrise/src/server/bap/encrypted/push/queuez/queuez_select_character.cpp
@@ -178,59 +178,6 @@ bool append_seasonal_experience_notification(
return true;
}
-/** Publishes every item in one rank-one class package as an ordinary acquisition. */
-bool append_season_pass_package_notification(
- Scratch& scratch,
- const queuez::SessionState& before,
- const state::PendingDirectItemBundle& mutation,
- std::uint16_t rewardIndex,
- std::span acquisitionPresentationRows,
- std::span key,
- std::span nonce,
- std::span response,
- std::size_t& written,
- queuez::SessionState& after) noexcept {
- after = before;
- snapshot::Prepared prepared{};
- const std::size_t itemCount = mutation.itemCount;
- // The item objects are indexed below, and every new resident must fit the peer's manifest.
- if (!snapshot::prepare_season_pass_package(
- scratch, before, mutation, rewardIndex, acquisitionPresentationRows, prepared)
- || itemCount == 0 || prepared.family.objects.size() != itemCount + 2U
- || before.family4ResidentCount + itemCount > before.family4Residents.size()) {
- return false;
- }
- for (std::size_t index = 0; index < itemCount; ++index) {
- const middleware::queuez::Object& object = prepared.family.objects[index];
- for (std::size_t residentIndex = 0; residentIndex < before.family4ResidentCount;
- ++residentIndex) {
- if (before.family4Residents[residentIndex].objectSoid == object.version) {
- return false;
- }
- }
- after.family4Residents[after.family4ResidentCount++] =
- queuez::ResidentObject{object.version, object.id};
- }
- const middleware::queuez::Object& characterObject = prepared.family.objects[itemCount];
- std::size_t characterMatches = 0;
- for (std::size_t index = 0; index < before.family4ResidentCount; ++index) {
- const queuez::ResidentObject& resident = before.family4Residents[index];
- characterMatches +=
- static_cast(resident.objectSoid == characterObject.version
- && resident.definitionId == characterObject.id);
- }
- if (characterMatches != 1) {
- return false;
- }
- after.family4Version = prepared.family.version;
- if (!queuez::valid(after)
- || !queuez_frame::append_prepared(scratch, prepared, key, nonce, response, written)) {
- after = before;
- return false;
- }
- return true;
-}
-
/** Publishes one prepared record-reward batch. */
bool append_record_reward_notification(
Scratch& scratch,
diff --git a/Sunrise/src/server/bap/encrypted/push/snapshot/family4_reward_updates.cpp b/Sunrise/src/server/bap/encrypted/push/snapshot/family4_reward_updates.cpp
index f517e2753..b28fead96 100644
--- a/Sunrise/src/server/bap/encrypted/push/snapshot/family4_reward_updates.cpp
+++ b/Sunrise/src/server/bap/encrypted/push/snapshot/family4_reward_updates.cpp
@@ -1,12 +1,10 @@
-/** Family-4 reward updates: season pass packages and record rewards, one revision each. */
+/** Family-4 reward updates: record rewards, one revision each. */
#include
#include
#include
-#include
#include
-#include "../../../../../middleware/datagen/definitions.h"
#include "../../../../../middleware/datagen/family4/account/account_encoder.h"
#include "../../../../../middleware/datagen/family4/account/layout.h"
#include "../../../../../middleware/datagen/family4/character/character_encoder.h"
@@ -21,191 +19,10 @@ namespace sunrise::server::bap::encrypted::push::snapshot {
namespace family4_datagen = middleware::datagen::family4;
-/** Builds one atomic Season package update with one acquisition record per granted item. */
-bool prepare_season_pass_package(
- Scratch& scratch,
- const queuez::SessionState& before,
- const state::PendingDirectItemBundle& mutation,
- std::uint16_t rewardIndex,
- std::span acquisitionPresentationRows,
- Prepared& prepared) noexcept {
- namespace character_layout = family4_datagen::character::layout;
- const std::size_t itemCount = mutation.itemCount;
- state::build_data::season_pass::Reward reward{};
- state::build_data::season_pass::Package package{};
- if (itemCount == 0 || itemCount > character_layout::kInventoryChangeRecordCapacity
- || !mutation.prepared || !state::build_data::find_season_pass_reward(rewardIndex, reward)
- || reward.quantity != 1 || reward.itemHash != mutation.sourceDefinitionHash
- || !state::build_data::find_season_pass_package(reward.itemHash, package)
- || !queuez::valid(before) || !before.family4Active || before.family4ResidentCount == 0
- || before.family4Version == (std::numeric_limits::max)()
- || before.family4ResidentCount + itemCount > before.family4Residents.size()) {
- return report_failure("season_package_session");
- }
-
- state::AccountState account{};
- if (!state::preview_direct_item_bundle(mutation, account)) {
- return report_failure("season_package_preview");
- }
- const std::optional selectedIndex = find_character_index(account);
- Resolved selected{};
- if (!state::account::valid(account) || account.primarySoid != before.family4RootSoid
- || account.primarySoid != mutation.accountSoid || !selectedIndex.has_value()
- || *selectedIndex != mutation.characterIndex
- || !resolve(account, *selectedIndex, selected)) {
- return report_failure("season_package_account");
- }
- const state::CharacterState& character = account.characters[*selectedIndex];
- if (character.soid != mutation.characterSoid
- || mutation.expectedInventoryCount > character.inventory.count
- || character.inventory.count != mutation.expectedInventoryCount + itemCount) {
- return report_failure("season_package_count");
- }
-
- std::uint32_t itemInstanceObjectId = 0;
- if (!middleware::datagen::object_id(queuez::kAccountFamilyType,
- middleware::datagen::kItemInstanceSlot,
- itemInstanceObjectId)
- || before.family4Residents.front().objectSoid != account.primarySoid
- || before.family4Residents.front().definitionId == 0) {
- return report_failure("season_package_definitions");
- }
- std::size_t characterResidentMatches = 0;
- for (std::size_t index = 0; index < before.family4ResidentCount; ++index) {
- const queuez::ResidentObject& resident = before.family4Residents[index];
- characterResidentMatches +=
- static_cast(resident.objectSoid == character.soid
- && resident.definitionId == selected.characterObjectId);
- }
- if (characterResidentMatches != 1) {
- return report_failure("season_package_character_resident");
- }
-
- family4_datagen::loadout::ResolvedInstances acquired{};
- const std::size_t firstGranted = mutation.expectedInventoryCount;
- for (std::size_t grantIndex = 0; grantIndex < itemCount; ++grantIndex) {
- const auto& granted = character.inventory.values[firstGranted + grantIndex];
- if (granted.instanceSoid != mutation.firstInstanceSoid + grantIndex
- || granted.mutationSerial < 0) {
- return report_failure("season_package_item_identity");
- }
- for (std::size_t residentIndex = 0; residentIndex < before.family4ResidentCount;
- ++residentIndex) {
- if (before.family4Residents[residentIndex].objectSoid == granted.instanceSoid) {
- return report_failure("season_package_item_resident");
- }
- }
- std::size_t matches = 0;
- for (std::size_t loadoutIndex = 0; loadoutIndex < selected.loadout.itemCount;
- ++loadoutIndex) {
- const auto& resolvedItem = selected.loadout.items[loadoutIndex];
- if (resolvedItem.instance.instanceSoid != granted.instanceSoid) {
- continue;
- }
- acquired.items[grantIndex].equipmentSlot = resolvedItem.equipmentSlot;
- acquired.items[grantIndex].instance = resolvedItem.instance;
- if (resolvedItem.mutationSerial != granted.mutationSerial) {
- return report_failure("season_package_item_serial");
- }
- ++matches;
- }
- if (matches != 1) {
- return report_failure("season_package_item_loadout");
- }
- }
- acquired.itemCount = itemCount;
-
- const Reservation reservation = reserve_prior(scratch, prepared);
- const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
- if (character_layout::kObjectSize > rawStorage.size()
- || family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
- return report_failure("season_package_storage");
- }
-
- Prepared staged{};
- std::size_t compressedExtent = reservation.compressedWriteOffset;
- std::size_t itemCursor = 0;
- if (!append_items(scratch,
- rawStorage,
- itemInstanceObjectId,
- acquired,
- 0,
- staged,
- itemCursor,
- compressedExtent)
- || itemCursor != itemCount) {
- clear_after(scratch, reservation);
- return report_failure("season_package_item_objects");
- }
-
- const auto characterBytes = rawStorage.first(character_layout::kObjectSize);
- if (!family4_datagen::character::encode(
- character, selected.loadout, selected.lightEvaluation, characterBytes)) {
- clear_after(scratch, reservation);
- return report_failure("season_package_character_encode");
- }
- auto& characterObject = *reinterpret_cast(characterBytes.data());
- if (characterObject.inventoryChanges.writeSlot != 0
- || characterObject.inventoryChanges.nextSequence != 0
- || !std::all_of(characterObject.inventoryChanges.records.cbegin(),
- characterObject.inventoryChanges.records.cend(),
- kChangeRecordIsZero)) {
- clear_after(scratch, reservation);
- return report_failure("season_package_change_state");
- }
- for (std::size_t index = 0; index < itemCount; ++index) {
- auto& change = characterObject.inventoryChanges.records[index];
- change.sequence = static_cast(index);
- change.mutationSerial = character.inventory.values[firstGranted + index].mutationSerial;
- change.kind = kChangeKind;
- change.flags = kChangeFlags;
- }
- characterObject.inventoryChanges.writeSlot = static_cast(itemCount);
- characterObject.inventoryChanges.nextSequence = static_cast(itemCount);
- if (!apply_acquisition_presentation(
- characterBytes, selected.loadout, acquisitionPresentationRows)) {
- clear_after(scratch, reservation);
- return report_failure("season_package_presentation");
- }
- if (!append_object(scratch,
- characterBytes,
- selected.characterObjectId,
- character.soid,
- staged.objects[itemCount],
- compressedExtent)) {
- clear_after(scratch, reservation);
- return report_failure("season_package_character_object");
- }
-
- const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
- if (!family4_datagen::account::encode(account, accountBytes)
- || !append_object(scratch,
- accountBytes,
- before.family4Residents.front().definitionId,
- account.primarySoid,
- staged.objects[itemCount + 1U],
- compressedExtent)) {
- clear_after(scratch, reservation);
- return report_failure("season_package_account_object");
- }
-
- staged.rawClearSize =
- (std::max)(reservation.rawClearSize,
- reservation.rawWriteOffset
- + (std::max)(character_layout::kObjectSize,
- family4_datagen::account::layout::kObjectSize));
- staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
- staged.family = middleware::queuez::Family{queuez::kAccountFamilyType,
- account.primarySoid,
- before.family4Version + 1,
- 0,
- std::span(staged.objects).first(itemCount + 2U)};
- if (!commit(staged, prepared)) {
- clear_after(scratch, reservation);
- return report_failure("season_package_commit");
- }
- return true;
-}
+// The grant array holds both change lists in full; State bounds each list separately.
+static_assert(state::kRecordRewardGrantCapacity
+ == family4_datagen::character::layout::kInventoryChangeRecordCapacity
+ + family4_datagen::account::layout::kProfileInventoryChangeRecordCapacity);
/** Builds all record rewards and the pending claim into one Family-4 revision. */
bool prepare_record_reward_grant(
@@ -217,7 +34,7 @@ bool prepare_record_reward_grant(
Prepared& prepared) noexcept {
namespace account_layout = family4_datagen::account::layout;
namespace character_layout = family4_datagen::character::layout;
- if (!mutation.prepared || mutation.rewardCount == 0
+ if (!mutation.prepared || (mutation.rewardCount == 0 && mutation.wrapperFlags.empty())
|| mutation.rewardCount > mutation.rewards.size() || !queuez::valid(before)
|| !queuez::valid(update.after) || !before.family4Active || before.family4ResidentCount == 0
|| before.family4Version == (std::numeric_limits::max)()
@@ -346,7 +163,8 @@ bool prepare_record_reward_grant(
std::size_t characterChanges = 0;
for (std::size_t rewardIndex = 0; rewardIndex < mutation.rewardCount; ++rewardIndex) {
const state::PreparedRecordReward& reward = mutation.rewards[rewardIndex];
- if (reward.kind == state::RecordRewardKind::profileStack) {
+ if (reward.kind == state::RecordRewardKind::profileStack
+ || reward.kind == state::RecordRewardKind::accountUnlock) {
continue;
}
state::build_data::items::Definition definition{};
@@ -402,7 +220,9 @@ bool prepare_record_reward_grant(
}
const auto accountBytes = rawStorage.first(account_layout::kObjectSize);
- if (!family4_datagen::account::encode(account, accountBytes)) {
+ state::unlocks::Table unlocks{};
+ if (!state::preview_reward_unlocks(mutation, unlocks)
+ || !family4_datagen::account::encode(account, accountBytes, unlocks)) {
clear_after(scratch, reservation);
return report_failure("record_reward_account_encode");
}
diff --git a/Sunrise/src/server/bap/encrypted/push/snapshot/internal.h b/Sunrise/src/server/bap/encrypted/push/snapshot/internal.h
index 12721fa37..7411bca73 100644
--- a/Sunrise/src/server/bap/encrypted/push/snapshot/internal.h
+++ b/Sunrise/src/server/bap/encrypted/push/snapshot/internal.h
@@ -236,15 +236,6 @@ prepare_profile_item_acquisition(Scratch& scratch,
std::span acquisitionPresentationRows,
Prepared& prepared) noexcept;
-/** Builds one package increment containing its new instances, character, and account objects. */
-[[nodiscard]] bool prepare_season_pass_package(
- Scratch& scratch,
- const queuez::SessionState& before,
- const state::PendingDirectItemBundle& mutation,
- std::uint16_t rewardIndex,
- std::span acquisitionPresentationRows,
- Prepared& prepared) noexcept;
-
/** Builds one atomic record-reward batch in resident, character, account order. */
[[nodiscard]] bool prepare_record_reward_grant(
Scratch& scratch,
diff --git a/Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp b/Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp
index a35d2f946..fc8d3a119 100644
--- a/Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp
+++ b/Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp
@@ -2,8 +2,12 @@
#include
#include
+#include
+#include
+#include
#include "../../../../core/logging/log.h"
+#include "../../../../middleware/crypto/random_bytes.h"
#include "../../../../middleware/secure_channel/runtime.h"
#include "../../../../state/account/account_state.h"
#include "../../../../state/activity/destination/definition.h"
@@ -52,6 +56,84 @@ selected_character(const state::AccountState& account) noexcept {
return nullptr;
}
+/** Publishes and commits one world reward resolved through its installed reward definition. */
+[[nodiscard]] bool consume_world_record_reward(const WorldRewardRequest& request,
+ Session& session,
+ Scratch& scratch,
+ std::span response,
+ std::size_t& written,
+ bool& touchesScratch) noexcept {
+ const auto fail = [&](const char* reason) noexcept {
+ report_reward_refusal("world_publish", request.itemDefinitionIndex, reason);
+ return false;
+ };
+ // A draw is not retained, so it waits for a buffer its whole frame is sure to fit.
+ if (response.size() < scratch.framed.size()) {
+ return false;
+ }
+ const std::unique_ptr pending(
+ new (std::nothrow) state::PendingRecordRewardGrant);
+ if (!pending) {
+ return fail("reward_allocation");
+ }
+ std::uint64_t seed = 0;
+ if (!middleware::crypto::random::fill(std::as_writable_bytes(std::span(&seed, 1)))) {
+ return fail("random_source");
+ }
+ const char* reason = "transaction";
+ state::investment::store::Transaction transaction;
+ if (!transaction.ready()) {
+ return fail(reason);
+ }
+ const auto preparation =
+ state::prepare_item_reward(request.itemDefinitionIndex,
+ static_cast(request.quantity),
+ seed,
+ *pending,
+ &reason);
+ if (preparation == state::RewardPreparation::unresolvable) {
+ (void)bap::retire_world_reward(transaction, request, reason);
+ return false;
+ }
+ if (preparation != state::RewardPreparation::prepared) {
+ return fail(reason);
+ }
+ queuez::RecordRewardGrant update{};
+ touchesScratch = true;
+ std::size_t framedSize = 0;
+ const bool staged = queuez::stage_record_reward_grant(session.queuez, *pending, update);
+ const bool encoded =
+ staged
+ && push::append_record_reward_notification(scratch,
+ session.queuez,
+ update,
+ *pending,
+ active_acquisition_presentation_rows(session),
+ session.sessionKey,
+ session.sendNonce,
+ scratch.framed,
+ framedSize)
+ && framedSize != 0;
+ if (!state::commit_record_reward(*pending)) {
+ return fail("inventory_commit");
+ }
+ if (!bap::complete_world_reward(request.id) || !transaction.commit()) {
+ return fail("queue_commit");
+ }
+ // No buffer fixes a failed stage or encode, so the grant commits and the row retires.
+ if (!encoded) {
+ bap::arm_account_resync_everywhere();
+ return fail(!staged ? "inventory_stage" : "inventory_encode");
+ }
+ std::copy_n(scratch.framed.begin(), framedSize, response.begin());
+ written = framedSize;
+ middleware::secure_channel::advance_nonce(session.sendNonce);
+ session.queuez = update.after;
+ bap::arm_account_resync_elsewhere(session);
+ bap::arm_acquisition_presentation_hold(session);
+ return true;
+}
+
/** Publishes and commits one character-inventory world reward. */
[[nodiscard]] bool consume_world_item_acquisition(const WorldRewardRequest& request,
Session& session,
@@ -59,6 +141,10 @@ selected_character(const state::AccountState& account) noexcept {
std::span response,
std::size_t& written,
bool& touchesScratch) noexcept {
+ if (state::item_grant_route(request.itemDefinitionIndex) != state::ItemGrantRoute::quest) {
+ return consume_world_record_reward(
+ request, session, scratch, response, written, touchesScratch);
+ }
state::investment::store::Transaction transaction;
if (!transaction.ready()) {
return false;
@@ -69,38 +155,33 @@ selected_character(const state::AccountState& account) noexcept {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
"ev=queuez stage=world_acquisition result=fail reason=prepare");
- bap::settle_world_reward();
return false;
}
touchesScratch = true;
queuez::ItemAcquisition acquisition{};
- if (!queuez::stage_item_acquisition(session.queuez,
- pending.accountSoid,
- pending.characterSoid,
- pending.acquiredInstanceSoid,
- pending.updates_account(),
- acquisition)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=world_acquisition result=fail reason=stage");
- bap::settle_world_reward();
- return false;
- }
+ const bool staged = queuez::stage_item_acquisition(session.queuez,
+ pending.accountSoid,
+ pending.characterSoid,
+ pending.acquiredInstanceSoid,
+ pending.updates_account(),
+ acquisition);
auto nextSendNonce = session.sendNonce;
std::size_t framedSize = 0;
- if (!push::append_item_acquisition_notification(scratch,
- acquisition,
- pending,
- active_acquisition_presentation_rows(session),
- session.sessionKey,
- nextSendNonce,
- scratch.framed,
- framedSize)
- || framedSize == 0 || framedSize > response.size()) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=world_acquisition result=fail reason=encode");
- bap::settle_world_reward();
+ const bool encoded =
+ staged
+ && push::append_item_acquisition_notification(scratch,
+ acquisition,
+ pending,
+ active_acquisition_presentation_rows(session),
+ session.sessionKey,
+ nextSendNonce,
+ scratch.framed,
+ framedSize)
+ && framedSize != 0;
+ // Only a piggyback slot can be short; the grant waits for a full poll.
+ if (encoded && framedSize > response.size()) {
+ report_reward_refusal(
+ "world_acquisition", request.itemDefinitionIndex, "response_capacity");
return false;
}
if (!state::commit_item_acquisition(pending) || !bap::complete_world_reward(request.id)
@@ -108,7 +189,14 @@ selected_character(const state::AccountState& account) noexcept {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
"ev=queuez stage=world_acquisition result=fail reason=commit");
- bap::settle_world_reward();
+ return false;
+ }
+ // No buffer fixes a failed stage or encode, so the grant commits and the row retires.
+ if (!encoded) {
+ bap::arm_account_resync_everywhere();
+ report_reward_refusal("world_acquisition",
+ request.itemDefinitionIndex,
+ !staged ? "inventory_stage" : "inventory_encode");
return false;
}
std::copy_n(scratch.framed.begin(), framedSize, response.begin());
@@ -139,37 +227,31 @@ selected_character(const state::AccountState& account) noexcept {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
"ev=queuez stage=world_profile_acquisition result=fail reason=prepare");
- bap::settle_world_reward();
return false;
}
touchesScratch = true;
queuez::ProfileItemAcquisition acquisition{};
- if (!queuez::stage_profile_item_acquisition(session.queuez,
- pending.accountSoid,
- pending.acquiredInstanceSoid,
- pending.actionSource,
- pending.appended,
- acquisition)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=world_profile_acquisition result=fail reason=stage");
- bap::settle_world_reward();
- return false;
- }
+ const bool staged = queuez::stage_profile_item_acquisition(session.queuez,
+ pending.accountSoid,
+ pending.acquiredInstanceSoid,
+ pending.actionSource,
+ pending.appended,
+ acquisition);
auto nextSendNonce = session.sendNonce;
std::size_t framedSize = 0;
- if (!push::append_profile_item_acquisition_notification(scratch,
- acquisition,
- pending,
- session.sessionKey,
- nextSendNonce,
- scratch.framed,
- framedSize)
- || framedSize == 0 || framedSize > response.size()) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=queuez stage=world_profile_acquisition result=fail reason=encode");
- bap::settle_world_reward();
+ const bool encoded = staged
+ && push::append_profile_item_acquisition_notification(scratch,
+ acquisition,
+ pending,
+ session.sessionKey,
+ nextSendNonce,
+ scratch.framed,
+ framedSize)
+ && framedSize != 0;
+ // Only a piggyback slot can be short; the grant waits for a full poll.
+ if (encoded && framedSize > response.size()) {
+ report_reward_refusal(
+ "world_profile_acquisition", request.itemDefinitionIndex, "response_capacity");
return false;
}
if (!state::commit_profile_item_acquisition(pending) || !bap::complete_world_reward(request.id)
@@ -177,7 +259,14 @@ selected_character(const state::AccountState& account) noexcept {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
"ev=queuez stage=world_profile_acquisition result=fail reason=commit");
- bap::settle_world_reward();
+ return false;
+ }
+ // No buffer fixes a failed stage or encode, so the grant commits and the row retires.
+ if (!encoded) {
+ bap::arm_account_resync_everywhere();
+ report_reward_refusal("world_profile_acquisition",
+ request.itemDefinitionIndex,
+ !staged ? "inventory_stage" : "inventory_encode");
return false;
}
std::copy_n(scratch.framed.begin(), framedSize, response.begin());
@@ -696,6 +785,9 @@ bool consume_deferred(Session& session,
if (published) {
return true;
}
+ if (session.accountResyncArmed) {
+ return false;
+ }
}
if (consume_seasonal_experience_presentation(
session, scratch, response, written, touchesScratch)) {
diff --git a/Sunrise/src/server/bap/encrypted/queuez/queuez_reward_staging.cpp b/Sunrise/src/server/bap/encrypted/queuez/queuez_reward_staging.cpp
index 0789ee199..29c5b445c 100644
--- a/Sunrise/src/server/bap/encrypted/queuez/queuez_reward_staging.cpp
+++ b/Sunrise/src/server/bap/encrypted/queuez/queuez_reward_staging.cpp
@@ -2,9 +2,6 @@
#include "queuez_reward_staging.h"
-#include
-#include
-
#include "../../../../core/logging/log.h"
#include "../../../../middleware/secure_channel/runtime.h"
@@ -56,70 +53,24 @@ stage_profile_item_acquisition_push(Scratch& scratch,
return true;
}
-/** Stages the one grant a season pass rank owes, whichever of the four shapes it carries. */
+/** Publishes a reward batch before advancing the nonce and peer revision. */
[[nodiscard]] bool
-stage_season_pass_reward(Scratch& scratch,
+stage_record_reward_push(Scratch& scratch,
const SessionState& before,
- const SeasonPassRewardTransaction& reward,
+ const RecordRewardGrant& update,
+ const state::PendingRecordRewardGrant& pending,
std::span presentationRows,
std::span key,
std::array& nonce,
std::span response,
std::size_t& written,
SessionState& after) noexcept {
- const auto& pending = *reward.pending;
- if (const auto* itemUpdate = std::get_if(&reward.update)) {
- const auto* itemPending = std::get_if(&pending.grant);
- return itemPending != nullptr
- && stage_item_acquisition_push(scratch,
- *itemUpdate,
- *itemPending,
- presentationRows,
- key,
- nonce,
- response,
- written,
- after);
- }
- if (const auto* profileUpdate = std::get_if(&reward.update)) {
- const auto* profilePending =
- std::get_if(&pending.grant);
- return profilePending != nullptr
- && stage_profile_item_acquisition_push(
- scratch, *profileUpdate, *profilePending, key, nonce, response, written, after);
- }
- if (const auto* bundle = std::get_if(&pending.grant)) {
- if (!push::append_season_pass_package_notification(scratch,
- before,
- *bundle,
- pending.rewardIndex,
- presentationRows,
- key,
- nonce,
- response,
- written,
- after)) {
- return false;
- }
- middleware::secure_channel::advance_nonce(nonce);
- return true;
- }
- const auto* resourceUpdate = std::get_if(&reward.update);
- const auto* resources = std::get_if(&pending.grant);
- if (resourceUpdate == nullptr || resources == nullptr
- || !push::append_record_reward_notification(scratch,
- before,
- *resourceUpdate,
- *resources,
- presentationRows,
- key,
- nonce,
- response,
- written)) {
+ if (!push::append_record_reward_notification(
+ scratch, before, update, pending, presentationRows, key, nonce, response, written)) {
return false;
}
middleware::secure_channel::advance_nonce(nonce);
- after = resourceUpdate->after;
+ after = update.after;
return true;
}
@@ -184,28 +135,35 @@ bool stage_reward_outcome(Scratch& scratch,
}
if (const auto* record = transaction_if(outcome)) {
if (record->pending == nullptr
- || !push::append_record_reward_notification(scratch,
- before,
- record->update,
- *record->pending,
- presentationRows,
- key,
- nonce,
- response,
- written)) {
+ || !stage_record_reward_push(scratch,
+ before,
+ record->update,
+ *record->pending,
+ presentationRows,
+ key,
+ nonce,
+ response,
+ written,
+ after)) {
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);
- after = record->update.after;
return true;
}
const auto* seasonPass = transaction_if(outcome);
if (seasonPass == nullptr || seasonPass->pending == nullptr
- || !stage_season_pass_reward(
- scratch, before, *seasonPass, presentationRows, key, nonce, response, written, after)) {
+ || !stage_record_reward_push(scratch,
+ before,
+ seasonPass->update,
+ seasonPass->pending->grant,
+ presentationRows,
+ key,
+ nonce,
+ response,
+ written,
+ after)) {
core::log::write(core::log::Channel::server,
core::log::Level::warn,
"ev=ws2400 stage=queuez_reward result=fail");
diff --git a/Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h b/Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h
index 2e32fa88a..489777526 100644
--- a/Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h
+++ b/Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h
@@ -7,6 +7,10 @@
#include "../../../../middleware/queuez/subscription.h"
#include "definition.h"
+namespace sunrise::state {
+struct PendingRecordRewardGrant;
+}
+
namespace sunrise::server::bap::encrypted::queuez {
/** @return True when one peer queuez state is canonical for the implemented versions. */
@@ -158,14 +162,6 @@ namespace sunrise::server::bap::encrypted::queuez {
bool updatesAccount,
ItemAcquisition& acquisition) noexcept;
-/** Validates one same-version bundle append and returns the revision its response may promise. */
-[[nodiscard]] bool stage_direct_item_bundle(const SessionState& before,
- std::uint64_t accountSoid,
- std::uint64_t characterSoid,
- std::uint64_t firstInstanceSoid,
- std::size_t itemCount,
- std::int32_t& family4Version) noexcept;
-
/**
* Stages one Family-4 version increment for a full resident account-object upsert.
* A profile row with a nonzero action-source SOID must already be resident when its stack grows,
@@ -192,6 +188,11 @@ namespace sunrise::server::bap::encrypted::queuez {
std::span appendedResidents,
RecordRewardGrant& grant) noexcept;
+/** Stages a prepared record-reward grant, appending the residents its new instances need. */
+[[nodiscard]] bool stage_record_reward_grant(const SessionState& before,
+ const state::PendingRecordRewardGrant& pending,
+ RecordRewardGrant& grant) noexcept;
+
/**
* Stages one Family-4 increment that removes an item resident and updates its character.
*
diff --git a/Sunrise/src/server/bap/encrypted/queuez/staging/queuez_character_staging.cpp b/Sunrise/src/server/bap/encrypted/queuez/staging/queuez_character_staging.cpp
index f5ed8b243..e945bb314 100644
--- a/Sunrise/src/server/bap/encrypted/queuez/staging/queuez_character_staging.cpp
+++ b/Sunrise/src/server/bap/encrypted/queuez/staging/queuez_character_staging.cpp
@@ -7,6 +7,7 @@
#include "../../../../../core/logging/log.h"
#include "../../../../../middleware/datagen/definitions.h"
+#include "../../../../../state/runtime/runtime.h"
#include "../queuez_state_validation.h"
namespace sunrise::server::bap::encrypted::queuez {
@@ -472,55 +473,30 @@ bool stage_item_acquisition(const SessionState& before,
return staged;
}
-/** Checks that a fixed bundle can append all residents in one Family-4 increment. */
-bool stage_direct_item_bundle(const SessionState& before,
- std::uint64_t accountSoid,
- std::uint64_t characterSoid,
- std::uint64_t firstInstanceSoid,
- std::size_t itemCount,
- std::int32_t& family4Version) noexcept {
- family4Version = 0;
- std::uint32_t accountDefinitionId = 0;
- std::uint32_t characterDefinitionId = 0;
- std::uint32_t itemDefinitionId = 0;
- if (!valid(before) || !before.family4Active || accountSoid == 0 || characterSoid == 0
- || firstInstanceSoid == 0 || itemCount == 0 || accountSoid != before.family4RootSoid
- || before.family4ResidentCount == 0
- || itemCount > before.family4Residents.size() - before.family4ResidentCount
- || itemCount - 1U > (std::numeric_limits::max)() - firstInstanceSoid
- || before.family4Version == (std::numeric_limits::max)()
- || !middleware::datagen::object_id(
- kAccountFamilyType, middleware::datagen::kAccountSlot, accountDefinitionId)
- || !middleware::datagen::object_id(
- kAccountFamilyType, middleware::datagen::kCharacterSlot, characterDefinitionId)
- || !middleware::datagen::object_id(
- kAccountFamilyType, middleware::datagen::kItemInstanceSlot, itemDefinitionId)) {
+/** Stages one atomic record-reward manifest update. */
+bool stage_record_reward_grant(const SessionState& before,
+ const state::PendingRecordRewardGrant& pending,
+ RecordRewardGrant& grant) noexcept {
+ grant = {};
+ if (!pending.prepared || pending.rewardCount > pending.rewards.size()) {
return false;
}
-
- std::size_t accountMatches = 0;
- std::size_t characterMatches = 0;
- for (std::size_t residentIndex = 0; residentIndex < before.family4ResidentCount;
- ++residentIndex) {
- const ResidentObject& resident = before.family4Residents[residentIndex];
- accountMatches += static_cast(resident.objectSoid == accountSoid
- && resident.definitionId == accountDefinitionId);
- characterMatches += static_cast(
- resident.objectSoid == characterSoid && resident.definitionId == characterDefinitionId);
- for (std::size_t itemIndex = 0; itemIndex < itemCount; ++itemIndex) {
- if (resident.objectSoid == firstInstanceSoid + itemIndex) {
- return false;
- }
+ std::array residents{};
+ std::size_t count = 0;
+ for (std::size_t i = 0; i < pending.rewardCount; ++i) {
+ const auto& reward = pending.rewards[i];
+ if (reward.kind == state::RecordRewardKind::characterInstance
+ || reward.appendedProfileResident) {
+ residents[count++] = reward.instanceSoid;
}
}
- if (accountMatches != 1 || characterMatches != 1) {
- return false;
- }
- family4Version = before.family4Version + 1;
- return true;
+ return stage_record_reward_grant(before,
+ pending.accountSoid,
+ pending.characterSoid,
+ std::span(residents).first(count),
+ grant);
}
-/** Stages one atomic record-reward manifest update. */
bool stage_record_reward_grant(const SessionState& before,
std::uint64_t accountSoid,
std::uint64_t characterSoid,
diff --git a/Sunrise/src/server/bap/internal.h b/Sunrise/src/server/bap/internal.h
index ac99ff4b5..e265b7bdb 100644
--- a/Sunrise/src/server/bap/internal.h
+++ b/Sunrise/src/server/bap/internal.h
@@ -25,6 +25,10 @@
#include "encrypted/queuez/definition.h"
#include "runtime.h"
+namespace sunrise::state::investment::store {
+class Transaction;
+}
+
namespace sunrise::server::bap {
/** One session per transport peer slot, so a connection id indexes this array directly. */
@@ -323,6 +327,9 @@ struct ReplicationEpochPublication {
bool staged{};
};
+/** Logs one refused reward stage with its native index and a specific reason. */
+void report_reward_refusal(const char* stage, std::uint16_t index, const char* reason) noexcept;
+
/** Which inventory one world reward lands in. */
enum class WorldRewardKind : std::uint8_t {
item,
@@ -332,6 +339,7 @@ enum class WorldRewardKind : std::uint8_t {
/** One reward earned in world, held until a Family-4 peer can publish it. */
struct WorldRewardRequest {
std::uint64_t id{};
+ std::uint32_t definitionHash{};
std::int32_t quantity{};
std::uint16_t itemDefinitionIndex{};
WorldRewardKind kind{};
@@ -536,7 +544,7 @@ unique_activity_link_locked(const state::activity::SessionBinding& binding,
session_scenario_layout(const Session& session,
state::build_data::scenarios::Definition& output) noexcept;
-/** Commits every queued world reward with no presentation and empties the queue. */
+/** Commits saved world rewards with no presentation until one must wait. */
void drain_world_rewards() noexcept;
/** Arms every other Family-4 peer after the origin publishes a complete account mutation. */
@@ -555,13 +563,18 @@ void arm_acquisition_presentation_hold(Session& session) noexcept;
[[nodiscard]] bool arm_world_profile_item_acquisition(std::uint16_t itemDefinitionIndex,
std::int32_t quantity) noexcept;
-/** Reads the oldest queued world reward without removing it. */
+/** Reads the oldest saved world reward that can still be granted, retiring rows that cannot. */
[[nodiscard]] bool current_world_reward(WorldRewardRequest& request) noexcept;
/** Removes the world reward returned by current_world_reward. */
[[nodiscard]] bool complete_world_reward(std::uint64_t id) noexcept;
-/** Commits the queued reward with no flyout once its presentation cannot be built. */
+/** Deletes an ungrantable reward on the caller's open savepoint, commits, and logs why. */
+[[nodiscard]] bool retire_world_reward(state::investment::store::Transaction& transaction,
+ const WorldRewardRequest& request,
+ const char* reason) noexcept;
+
+/** Grants the oldest saved reward with no flyout, retiring rewards that can never be granted. */
void settle_world_reward() noexcept;
/** Queues one transient XP item update so the native HUD presents a seasonal XP gain. */
diff --git a/Sunrise/src/server/web_service/web_service_record_actions.cpp b/Sunrise/src/server/web_service/web_service_record_actions.cpp
index 34dd754be..e8ca97d84 100644
--- a/Sunrise/src/server/web_service/web_service_record_actions.cpp
+++ b/Sunrise/src/server/web_service/web_service_record_actions.cpp
@@ -1,6 +1,5 @@
/** Triumph, title and season-pass reward actions the web service prepares from one request. */
-#include
#include
#include
#include
@@ -8,12 +7,9 @@
#include "../../core/logging/log.h"
#include "../../middleware/crypto/random_bytes.h"
-#include "../../middleware/encoding/byte_order.h"
#include "../../middleware/web_service/messages/opcode1801.h"
#include "../../middleware/web_service/messages/opcode1821.h"
#include "../../middleware/web_service/messages/opcode2400.h"
-#include "../../state/account/account_state.h"
-#include "../../state/build_data/items/item_catalog.h"
#include "../../state/build_data/runtime.h"
#include "../../state/progression/season_pass_reward_catalog.h"
#include "../../state/runtime/runtime.h"
@@ -23,226 +19,6 @@
namespace sunrise::server::web_service {
-namespace {
-
-/** Prepares one rank-one class package without changing account State. */
-[[nodiscard]] bool
-prepare_premium_class_package(const state::build_data::season_pass::Package& package,
- state::PendingDirectItemBundle& mutation) noexcept {
- std::array itemIndices{};
- if (package.itemCount == 0 || package.itemCount > itemIndices.size()) {
- return false;
- }
- for (std::size_t index = 0; index < package.itemCount; ++index) {
- state::build_data::items::Definition definition{};
- if (!state::build_data::find_item_definition_hash(package.items[index], definition)) {
- return false;
- }
- itemIndices[index] = definition.definitionIndex;
- }
- return state::prepare_direct_item_bundle(
- package.definitionHash, std::span(itemIndices).first(package.itemCount), mutation);
-}
-
-/** Expands the manifest-authored destination package into all nine material stacks. */
-[[nodiscard]] bool
-prepare_destination_resource_bundle(state::PendingSeasonPassReward& grant) noexcept {
- namespace pass = state::progression::season_pass;
- std::array rewards{};
- for (std::size_t index = 0; index < rewards.size(); ++index) {
- state::build_data::items::Definition definition{};
- if (!state::build_data::find_item_definition_hash(pass::kDestinationResourceHashes[index],
- definition)) {
- return false;
- }
- rewards[index] = {definition.definitionIndex, pass::kDestinationResourceQuantity};
- }
- auto& mutation = grant.grant.emplace();
- return state::prepare_record_reward_grant(rewards, {}, mutation);
-}
-
-/** Chooses one installed weapon or selected-class armour item from an auto-decrypting engram. */
-template
-[[nodiscard]] std::span
-class_armour_pool(state::CharacterClass characterClass,
- const std::array