Sfoglia il codice sorgente

Clarify quest acquisition contracts and SQLite lint policy

Gage Fulwood 2 giorni fa
parent
commit
2c1fc46dd6

+ 1 - 1
.clang-tidy

@@ -74,5 +74,5 @@ ExtraArgsBefore:
   - -Wdocumentation
 FormatStyle: file
 CheckOptions:
-  portability-restrict-system-includes.Includes: '-*,Windows.h,WinSock2.h,WS2tcpip.h,MSWSock.h,WinDNS.h,TlHelp32.h,Shellapi.h,bcrypt.h,d3d11.h,d3d11_1.h,d3dcompiler.h,detours.h,dxgi.h,wincodec.h,imgui.h,imgui_impl_dx11.h,imgui_impl_win32.h,intrin.h,algorithm,array,atomic,bit,bitset,cctype,cerrno,charconv,chrono,climits,cmath,compare,concepts,cstdarg,cstddef,cstdint,cstdio,cstdlib,cstring,cwchar,deque,functional,initializer_list,iterator,limits,map,memory,mutex,new,numeric,optional,queue,set,shared_mutex,span,sstream,string,string_view,tuple,type_traits,unordered_map,unordered_set,utility,variant,vector'
+  portability-restrict-system-includes.Includes: '-*,Windows.h,WinSock2.h,WS2tcpip.h,MSWSock.h,WinDNS.h,TlHelp32.h,Shellapi.h,bcrypt.h,d3d11.h,d3d11_1.h,d3dcompiler.h,detours.h,dxgi.h,wincodec.h,imgui.h,imgui_impl_dx11.h,imgui_impl_win32.h,intrin.h,sqlite3.h,algorithm,array,atomic,bit,bitset,cctype,cerrno,charconv,chrono,climits,cmath,compare,concepts,cstdarg,cstddef,cstdint,cstdio,cstdlib,cstring,cwchar,deque,functional,initializer_list,iterator,limits,map,memory,mutex,new,numeric,optional,queue,set,shared_mutex,span,sstream,string,string_view,tuple,type_traits,unordered_map,unordered_set,utility,variant,vector'
 ...

+ 5 - 3
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -50,7 +50,10 @@ bool ready() noexcept {
            && content::activity::entity_position_profiles::ready();
 }
 
-/** Publishes the dense item table from the installed packages, once. */
+/**
+ * Publishes missing package domains while retaining completed domains for later calls.
+ * @return True when every owned domain is ready; false leaves unfinished work for another call.
+ */
 bool build() noexcept {
     static Storage storage{};
     reader::BlockKeys keys{};
@@ -113,8 +116,7 @@ bool build() noexcept {
             }
             // The same root names the bucket and socket-list tables.
             storage.root = storage.child;
-            // Quest initialization, records, nodes, season rewards and catalysts share the
-            // unlock mapping tables, so they are read once here.
+            // Quest, record, node, season and catalyst reads share these unlock maps.
             if (!state::build_data::item_definitions_ready()
                 || !state::build_data::record_definitions_ready()
                 || !state::build_data::node_definitions_ready()

+ 11 - 2
Sunrise/src/client/content/items/packages/package_item_rows.cpp

@@ -47,7 +47,15 @@ bool exotic_catalysts_settled() noexcept {
     return state::build_data::exotic_catalysts_ready() || g_catalystsUnsupported;
 }
 
-/** Walks the located item index table, then publishes every domain that depends on it. */
+/**
+ * Publishes missing item domains from the located index table and its definitions.
+ * @param source Borrowed package source.
+ * @param storage Pass storage holding the table in child, root data, maps, and output rows.
+ * @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.
+ */
 bool build_item_rows(const reader::Source& source,
                      Storage& storage,
                      const tables::Array& table,
@@ -108,7 +116,8 @@ bool build_item_rows(const reader::Source& source,
             corrected_plug_category(item.definitionHash, item.plugCategoryHash);
         build_items::QuestInitialization quest{};
         const auto parentIndex = tables::items::quest_parent(storage.definition);
-        if (needDefinitions && itemClass == tables::kItemDefinitionClass && parentIndex < table.count) {
+        if (needDefinitions && itemClass == tables::kItemDefinitionClass
+            && parentIndex < table.count) {
             std::span<const std::byte> parent = storage.definition;
             tables::IndexRow parentRow{};
             std::uint32_t parentClass = itemClass;

+ 7 - 1
Sunrise/src/client/content/items/packages/package_node_build.cpp

@@ -110,7 +110,13 @@ read_slot_map(std::span<const std::byte> blob, std::size_t descriptor, SlotMap&
 
 } // namespace
 
-/** Reads both unlock mapping tables. A gate names a slot; the bank index is the row naming it. */
+/**
+ * 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 root Investment root bytes naming the flag and value mapping tables.
+ * @return True when both account maps read; a character map may remain unmapped.
+ */
 bool read_unlock_slot_maps(const reader::Source& source,
                            Storage& storage,
                            std::span<const std::byte> root) noexcept {

+ 63 - 17
Sunrise/src/middleware/content/packages/tables/quest_initialization_reader.cpp

@@ -10,47 +10,73 @@ namespace {
 
 using Quest = state::build_data::items::QuestInitialization;
 
-// Serialized layout offsets are relative to the definition, block, or row named below,
-// not process addresses. Class IDs identify the expected serialized block/element type.
+/** Policy: shorter item blobs stay outside the supported quest layout. */
 constexpr std::size_t kMinimumQuestDefinitionSize = 0xF0;
+/** A serialized block's 32-bit class ID sits immediately before its payload. */
 constexpr std::size_t kBlockClassPrefixSize = sizeof(std::uint32_t);
+/** Policy: keep the full fixed block prefix before reading nested arrays. */
 constexpr std::size_t kMinimumQuestBlockSize = 0x20;
 
+/** Item +0x30 holds a signed 64-bit offset relative to that field. */
 constexpr std::size_t kItemObjectiveBlockOffset = 0x30;
+/** This block holds objective indices and the item index that owns the quest set. */
 constexpr std::uint32_t kItemObjectiveBlockClass = 0x808077EBU;
+/** Objective block +0x1C holds a 16-bit item-table index, not a definition hash. */
 constexpr std::size_t kObjectiveParentItemOffset = 0x1C;
+/** Objective array entries are 16-bit table indices. */
 constexpr std::size_t kObjectiveReferenceStride = sizeof(std::uint16_t);
 
+/** Item +0x60 holds the quest-set block offset relative to that field. */
 constexpr std::size_t kItemQuestSetBlockOffset = 0x60;
+/** This block holds ordered quest members and the value slot that selects the active step. */
 constexpr std::uint32_t kQuestSetBlockClass = 0x808077C8U;
+/** Set +0x10 holds a 16-bit unlock value slot; a map supplies its saved bank row. */
 constexpr std::size_t kQuestSetValueSlotOffset = 0x10;
+/** Set +0x1C holds a one-byte mode separate from the member values. */
 constexpr std::size_t kQuestSetModeOffset = 0x1C;
-/** Only this authored mode is supported; no semantics are assumed for other modes. */
+/** Policy: only mode 1 permits first-step writes; the other modes are not decoded. */
 constexpr std::uint8_t kSupportedQuestSetMode = 1;
+/** Each member pairs a signed step value with the item that represents it. */
 constexpr std::uint32_t kQuestSetMemberClass = 0x808077CAU;
+/** A member is a 32-bit value, a 16-bit item index, then a 16-bit reserved field. */
 constexpr std::size_t kQuestSetMemberStride = 8;
+/** Member +0 holds the signed step identifier; values are not ordered progress counts. */
 constexpr std::size_t kQuestSetMemberValueOffset = 0;
+/** Member +4 holds the item-table index for that step. */
 constexpr std::size_t kQuestSetMemberItemOffset = 4;
+/** Only member rows whose final 16 bits are zero are supported. */
 constexpr std::size_t kQuestSetMemberReservedOffset = 6;
 
+/** Item +0x90 holds the unlock block offset relative to that field; zero means absent. */
 constexpr std::size_t kItemUnlockBlockOffset = 0x90;
+/** This block's first array names the flags supplied by item presence. */
 constexpr std::uint32_t kItemUnlockBlockClass = 0x808077ABU;
+/** Presence-flag entries hold unlock slot indices, not saved bank rows. */
 constexpr std::uint32_t kItemPresenceFlagClass = 0x80807D4BU;
+/** Each presence-flag entry occupies one 16-bit slot. */
 constexpr std::size_t kItemPresenceFlagStride = sizeof(std::uint16_t);
 /** Authored value/flag slots must fit the nonnegative range of a signed 16-bit mapping. */
 constexpr std::uint16_t kUnlockSlotLimit = 0x8000U;
-/** Separate objective-free roots are supported only in this native bucket. */
+/** Policy: pursuits without presence flags need a bucket-37 root with no objective block. */
 constexpr std::uint8_t kSeparateQuestRootBucketId = 37;
 
-/** The remaining maps are scanned to reject matches outside account/character state. */
+/** Map +40 has no supported save bank; a matching slot makes initialization unsafe. */
 constexpr std::size_t kThirdValueMapDescriptor = 40;
+/** Map +56 is also checked for duplicate slots but has no supported save bank. */
 constexpr std::size_t kFourthValueMapDescriptor = 56;
-constexpr std::size_t kValueMapRowStride = 8;
-constexpr std::size_t kValueMapSlotOffset = 4;
+/** A matching map row is supported only when its final 16 bits are zero. */
 constexpr std::size_t kValueMapReservedOffset = 6;
+/** The all-one 16-bit row is reserved and cannot name saved state. */
 constexpr std::uint16_t kUnavailableValueMapRow = 0xFFFFU;
 
-/** These are serialized block pointers, not count/relative array descriptors. */
+/**
+ * A block pointer is relative to its own field; its class ID precedes the payload.
+ * @param bytes Blob containing the pointer and block.
+ * @param field Offset of the signed 64-bit block pointer.
+ * @param expectedClass Required serialized block class.
+ * @param offset Receives the payload offset; use only on success.
+ * @return False for absent, out-of-bounds, short, or wrong-class blocks.
+ */
 [[nodiscard]] bool block(std::span<const std::byte> bytes,
                          std::size_t field,
                          std::uint32_t expectedClass,
@@ -72,7 +98,15 @@ constexpr std::uint16_t kUnavailableValueMapRow = 0xFFFFU;
     return read(bytes, offset - kBlockClassPrefixSize, actualClass) && actualClass == expectedClass;
 }
 
-/** Bounds an authored array using its element class and fixed stride. */
+/**
+ * The whole fixed-stride array must fit the blob before any row is read.
+ * @param bytes Blob containing the descriptor and rows.
+ * @param field Offset of the array descriptor.
+ * @param expectedClass Required serialized element class.
+ * @param stride Nonzero byte width of one row.
+ * @param rows Receives the array bounds; use only on success.
+ * @return False when the descriptor, class, or row bounds are invalid.
+ */
 [[nodiscard]] bool array(std::span<const std::byte> bytes,
                          std::size_t field,
                          std::uint32_t expectedClass,
@@ -85,6 +119,11 @@ constexpr std::uint16_t kUnavailableValueMapRow = 0xFFFFU;
 
 } // namespace
 
+/**
+ * Only an objective-bearing pursuit can name a quest-set owner.
+ * @param definition Item definition bytes, including its nested blocks.
+ * @return The set owner's item-table index, or kUnavailableQuestParent on rejection.
+ */
 std::uint16_t quest_parent(std::span<const std::byte> definition) noexcept {
     std::uint8_t bucket = 0;
     std::size_t objective = 0;
@@ -105,6 +144,15 @@ std::uint16_t quest_parent(std::span<const std::byte> definition) noexcept {
     return parent;
 }
 
+/**
+ * Only a unique first member with one supported save-bank mapping may start a quest.
+ * @param definition Pursuit item being acquired.
+ * @param itemIndex Pursuit's item-table index.
+ * @param parent Set-owner bytes selected by quest_parent; may be definition itself.
+ * @param itemCount Exclusive bound for item-table indices.
+ * @param valueMap Blob containing all four unlock value maps.
+ * @return The first-step value and bank row, or an empty plan for unsupported content.
+ */
 Quest read_quest_initialization(std::span<const std::byte> definition,
                                 std::uint16_t itemIndex,
                                 std::span<const std::byte> parent,
@@ -145,9 +193,7 @@ Quest read_quest_initialization(std::span<const std::byte> definition,
             return {};
         }
     }
-    // A separate bucket-37 set root can track a pursuit without an item-presence flag.
-    // Only the objective-free root / character-value form is supported here; this is
-    // first-step initialization, not a general interaction or eligibility evaluator.
+    // Without presence flags, require a separate objective-free root and a character value.
     const bool separateRoot = flags.count == 0;
     if (separateRoot) {
         std::uint8_t parentBucket = 0;
@@ -177,14 +223,14 @@ Quest read_quest_initialization(std::span<const std::byte> definition,
             ++matches;
             quest.value = value;
         } else if (i != 0 && matches != 0 && value == quest.value) {
-            return {}; // Two steps cannot give the initial identifier an unambiguous meaning.
+            return {}; // The first value must identify only one step.
         }
     }
     if (matches != 1) {
         return {};
     }
 
-    // Resolve across all four maps: a context/roster-lane match or duplicate is unsupported.
+    // A slot must match once across all maps, including maps with no supported save bank.
     matches = 0;
     for (const std::size_t descriptor : {kAccountValueMapDescriptor,
                                          kCharacterValueMapDescriptor,
@@ -192,17 +238,17 @@ Quest read_quest_initialization(std::span<const std::byte> definition,
                                          kFourthValueMapDescriptor}) {
         Array rows{};
         if (!find_optional_array_at(valueMap, descriptor, rows) || rows.dataOffset > valueMap.size()
-            || rows.count > (valueMap.size() - rows.dataOffset) / kValueMapRowStride) {
+            || rows.count > (valueMap.size() - rows.dataOffset) / kUnlockMapRowStride) {
             return {};
         }
         for (std::size_t i = 0; i < rows.count; ++i) {
             std::int16_t mappedSlot = -1;
             std::uint16_t reserved = 0;
             if (!read(valueMap,
-                      rows.dataOffset + i * kValueMapRowStride + kValueMapSlotOffset,
+                      rows.dataOffset + i * kUnlockMapRowStride + kUnlockMapDestinationSlotOffset,
                       mappedSlot)
                 || !read(valueMap,
-                         rows.dataOffset + i * kValueMapRowStride + kValueMapReservedOffset,
+                         rows.dataOffset + i * kUnlockMapRowStride + kValueMapReservedOffset,
                          reserved)) {
                 return {};
             }

+ 13 - 9
Sunrise/src/middleware/content/packages/tables/quest_initialization_reader.h

@@ -8,20 +8,24 @@
 
 namespace sunrise::middleware::content::packages::tables::items {
 
-/** No supported set-bearing item was resolved from the pursuit definition. */
+/** The all-one item index cannot name a quest-set owner. */
 inline constexpr std::uint16_t kUnavailableQuestParent = 0xFFFFU;
 
-/** Returns the objective block's set-bearing item index, or kUnavailableQuestParent. */
+/**
+ * Only an objective-bearing pursuit can name a quest-set owner.
+ * @param definition Item definition bytes, including its nested blocks.
+ * @return The owner's item-table index, or kUnavailableQuestParent on rejection.
+ */
 [[nodiscard]] std::uint16_t quest_parent(std::span<const std::byte> definition) noexcept;
 
 /**
- * Resolves the first member of a supported quest set to its saved value-bank row.
- * @param definition Objective-bearing pursuit definition.
- * @param itemIndex Definition's ordinal in the item table.
- * @param parent Set-bearing definition named by quest_parent, possibly definition itself.
- * @param itemCount Bounds for authored item indices.
- * @param valueMap Serialized unlock value mapping table.
- * @return An empty plan for unsupported or malformed content; no guessed state write.
+ * Only a unique first member with one supported save-bank mapping may start a quest.
+ * @param definition Pursuit item being acquired.
+ * @param itemIndex Pursuit's item-table index.
+ * @param parent Set-owner bytes selected by quest_parent; may be definition itself.
+ * @param itemCount Exclusive bound for item-table indices.
+ * @param valueMap Blob containing all four unlock value maps.
+ * @return The first-step value and bank row, or an empty plan for unsupported content.
  */
 [[nodiscard]] state::build_data::items::QuestInitialization
 read_quest_initialization(std::span<const std::byte> definition,

+ 7 - 0
Sunrise/src/middleware/datagen/family4/account/account_encoder.cpp

@@ -79,6 +79,13 @@ bool encode(const state::AccountState& state, std::span<std::byte> output) noexc
     return state::unlocks::snapshot(unlocks) && encode(state, output, unlocks);
 }
 
+/**
+ * Account-wide unlocks use the supplied snapshot; per-character flags still use saved state.
+ * @param state Account identity, roster, preferences, and inventory to encode.
+ * @param output Receives the account object; unchanged on failure.
+ * @param unlocks Account unlocks from the same live or prepared view as state.
+ * @return False when state, saved flags, mappings, or output bounds are invalid.
+ */
 bool encode(const state::AccountState& state,
             std::span<std::byte> output,
             const state::unlocks::Table& unlocks) noexcept {

+ 7 - 1
Sunrise/src/middleware/datagen/family4/account/account_encoder.h

@@ -15,7 +15,13 @@ namespace sunrise::middleware::datagen::family4::account {
  */
 [[nodiscard]] bool encode(const state::AccountState& state, std::span<std::byte> output) noexcept;
 
-/** Encodes a prepared unlock after-image without first writing it to the live save. */
+/**
+ * Account-wide unlocks use the supplied snapshot; per-character flags still use saved state.
+ * @param state Account identity, roster, preferences, and inventory to encode.
+ * @param output Receives the account object; unchanged on failure.
+ * @param unlocks Account unlocks from the same live or prepared view as state.
+ * @return False when state, saved flags, mappings, or output bounds are invalid.
+ */
 [[nodiscard]] bool encode(const state::AccountState& state,
                           std::span<std::byte> output,
                           const state::unlocks::Table& unlocks) noexcept;

+ 10 - 2
Sunrise/src/middleware/datagen/family4/character/character_encoder.cpp

@@ -246,6 +246,15 @@ bool encode(const state::CharacterState& state,
            && encode(state, resolvedLoadout, lightEvaluation, output, unlocks);
 }
 
+/**
+ * Character unlocks must match the live or prepared inventory view being encoded.
+ * @param state Character identity and inventory to encode.
+ * @param resolvedLoadout Item mappings for this character's inventory.
+ * @param lightEvaluation Equipment light values for the same loadout.
+ * @param output Receives the character object; unchanged on failure.
+ * @param unlocks Unlock snapshot for this character, including any prepared quest value.
+ * @return False when state, mappings, light values, or output bounds are invalid.
+ */
 bool encode(const state::CharacterState& state,
             const loadout::ResolvedLoadout& resolvedLoadout,
             const state::equipment::light::Evaluation& lightEvaluation,
@@ -288,8 +297,7 @@ bool encode(const state::CharacterState& state,
     for (layout::ItemStackRow& stack : object.itemStacks) {
         stack.selector = kEmptyItemStackSelector;
     }
-    // Acquired flags and objective progress are live world state, written by the request that
-    // changed them.
+    // Use the supplied quest state even when the acquisition has not committed yet.
     for (std::size_t index = 0; index < object.acquiredFlags.size(); ++index) {
         object.acquiredFlags[index] = static_cast<std::byte>(
             index < unlocks.characterObjectFlags.size() ? unlocks.characterObjectFlags[index]

+ 9 - 1
Sunrise/src/middleware/datagen/family4/character/character_encoder.h

@@ -22,7 +22,15 @@ namespace sunrise::middleware::datagen::family4::character {
                           const state::equipment::light::Evaluation& lightEvaluation,
                           std::span<std::byte> output) noexcept;
 
-/** Encodes a prepared unlock after-image without first writing it to the live save. */
+/**
+ * Character unlocks must match the live or prepared inventory view being encoded.
+ * @param state Character identity and inventory to encode.
+ * @param resolvedLoadout Item mappings for this character's inventory.
+ * @param lightEvaluation Equipment light values for the same loadout.
+ * @param output Receives the character object; unchanged on failure.
+ * @param unlocks Unlock snapshot for this character, including any prepared quest value.
+ * @return False when state, mappings, light values, or output bounds are invalid.
+ */
 [[nodiscard]] bool encode(const state::CharacterState& state,
                           const loadout::ResolvedLoadout& resolvedLoadout,
                           const state::equipment::light::Evaluation& lightEvaluation,

+ 8 - 3
Sunrise/src/state/build_data/cache/records/cache_record_codec.cpp

@@ -51,8 +51,8 @@ bool decode(const NamedRecord& record, content::Definition& value) noexcept {
 /**
  * Encodes one installed-build item mapping with its padding zeroed.
  * @param value Runtime row.
- * @param record Receives the packed disk row.
- * @return True when the optional quest initialization has a supported scope and bank row.
+ * @param record Receives the packed disk row; use only on success.
+ * @return True for empty quest state or a supported first-step value, scope, and bank row.
  */
 bool encode(const items::Definition& value, ItemRecord& record) noexcept {
     record = {
@@ -72,7 +72,12 @@ bool encode(const items::Definition& value, ItemRecord& record) noexcept {
     return items::valid(value.questInitialization);
 }
 
-/** Decodes one installed-build item mapping. */
+/**
+ * Cached quest state must fit the same bank limits as freshly read item metadata.
+ * @param record Packed disk row.
+ * @param value Receives the runtime item mapping; use only on success.
+ * @return True for empty quest state or a supported first-step value, scope, and bank row.
+ */
 bool decode(const ItemRecord& record, items::Definition& value) noexcept {
     value = {record.definitionHash,
              record.definitionIndex,

+ 1 - 5
Sunrise/src/state/build_data/cache/records/format.h

@@ -30,11 +30,7 @@ namespace sunrise::state::build_data::cache::records {
 
 /** These 8 ASCII bytes mark a Sunrise build-data file. */
 inline constexpr std::array<char, 8> kCacheMagic{'S', 'U', 'N', 'R', 'I', 'S', 'E', 'B'};
-/**
- * Current build-data cache format. Any other version on disk is rebuilt rather than read.
- * Bump it when a stored shape changes or when the extraction filling it changes what it writes,
- * because a cached row survives a code change and a corrected walk keeps publishing old rows.
- */
+/** Bump when stored layouts or extracted values change; other versions are rebuilt. */
 inline constexpr std::uint32_t kCacheFormatVersion = 65;
 /** Signed -1 on disk means there is no equipment slot. */
 inline constexpr std::int8_t kAbsentEquipmentSlot = -1;

+ 1 - 0
Sunrise/src/state/build_data/items/item_catalog.h

@@ -37,6 +37,7 @@ struct Definition {
     /** Item index of the plug this one stands for, or kUnavailableLinkedPlugIndex when it stands
      * alone. */
     std::uint16_t linkedPlugIndex{kUnavailableLinkedPlugIndex};
+    /** Empty unless this item is the first member of a supported quest set. */
     QuestInitialization questInitialization{};
 };
 

+ 13 - 3
Sunrise/src/state/build_data/items/quest_initialization.h

@@ -8,8 +8,9 @@ namespace sunrise::state::build_data::items {
 
 /** Native pursuit bucket shared by quest items and bounties. */
 inline constexpr std::uint8_t kPursuitBucketId = 40;
-/** Zero is unset state; -1 is excluded as an authored first-step identifier. */
+/** Missing saved quest rows read as zero; only this value permits a first-step write. */
 inline constexpr std::int32_t kUnsetQuestValue = 0;
+/** Policy: -1 cannot start a quest; existing -1 state must still be preserved. */
 inline constexpr std::int32_t kInvalidQuestInitialValue = -1;
 
 /** Authored initial value and bank row for the first member of a supported quest set. */
@@ -22,7 +23,11 @@ struct QuestInitialization {
     bool operator==(const QuestInitialization&) const = default;
 };
 
-/** An empty plan is valid; a supported plan must fit its persistent value bank. */
+/**
+ * An empty plan is valid; a nonempty plan must fit its saved bank.
+ * @param quest First-step value, row, and scope from item metadata.
+ * @return True for an empty plan or a supported value within its bank's capacity.
+ */
 [[nodiscard]] constexpr bool valid(const QuestInitialization& quest) noexcept {
     using Scope = QuestInitialization::Scope;
     if (quest.scope == Scope::none) {
@@ -34,7 +39,12 @@ struct QuestInitialization {
                    && quest.row < unlocks::kCharacterObjectValueCapacity));
 }
 
-/** Set values are identifiers, not a numerically ordered progress counter. */
+/**
+ * Preserve every nonzero value; step identifiers have no numeric progress order.
+ * @param quest Validated first-step plan; may be empty.
+ * @param before Current saved quest value.
+ * @return The first-step value only when the plan is nonempty and saved state is unset.
+ */
 [[nodiscard]] constexpr std::int32_t initialized_value(const QuestInitialization& quest,
                                                        std::int32_t before) noexcept {
     return quest.scope != QuestInitialization::Scope::none && before == kUnsetQuestValue

+ 14 - 7
Sunrise/src/state/runtime/runtime.h

@@ -140,7 +140,10 @@ struct PendingItemAcquisition {
     std::int32_t previousQuestValue{};
     bool prepared{};
 
-    /** Account-scoped initialization must publish even when acquisition charges no materials. */
+    /**
+     * Account-scoped quest writes need an account update even when no materials were charged.
+     * @return True for a profile inventory change or an unset account-scoped quest value.
+     */
     [[nodiscard]] bool updates_account() const noexcept {
         return profileChanged
                || (questInitialization.scope
@@ -575,17 +578,21 @@ set_selected_title(std::uint16_t recordIndex, std::uint64_t& characterSoid, bool
 [[nodiscard]] bool
 reserve_selected_character_inventory_serial(std::int32_t& mutationSerial) noexcept;
 
-/** Builds account and unlock after-images without committing the prepared acquisition. */
+/**
+ * Preview inventory and quest values together without changing the save.
+ * @param mutation Prepared acquisition checked against current saved state.
+ * @param after Receives the candidate account; use only on success.
+ * @param afterUnlocks Receives matching account and selected-character unlocks on success.
+ * @return False when the acquisition is stale or its saved unlocks cannot be read.
+ */
 [[nodiscard]] bool preview_item_acquisition(const PendingItemAcquisition& mutation,
                                             AccountState& after,
                                             unlocks::Table& afterUnlocks) noexcept;
 
 /**
- * Commits a prepared inventory insertion only while its selected character, existing loadout,
- * and next inventory serial still match the prepare-time view.
- *
- * @param mutation Prepared mutation, always cleared before this function returns.
- * @return True when the insertion commits atomically and leaves the whole account valid.
+ * Inventory and first-step state share one transaction; failure rolls both back.
+ * @param mutation Prepared grant consumed on either success or failure.
+ * @return True when both writes commit against the unchanged prepared state.
  */
 [[nodiscard]] bool commit_item_acquisition(PendingItemAcquisition& mutation) noexcept;
 

+ 52 - 8
Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp

@@ -25,13 +25,21 @@ namespace runtime::detail {
 
 using Quest = build_data::items::QuestInitialization;
 
-/** Selects the persistent bank after the nonempty initialization plan has been validated. */
+/**
+ * The plan must already be valid and nonempty before selecting a save bank.
+ * @param quest First-step plan with account or character scope.
+ * @return The persistent value bank for that scope.
+ */
 [[nodiscard]] investment::store::Bank quest_bank(const Quest& quest) noexcept {
     return quest.scope == Quest::Scope::account ? investment::store::Bank::objectiveValues
                                                 : investment::store::Bank::characterObjectValues;
 }
 
-/** The caller holds the investment lock and has checked the selected character. */
+/**
+ * Hold investment::store::g_mutex and validate the selected character before this check.
+ * @param mutation Prepared acquisition with the prior saved quest value.
+ * @return True only while item metadata and the saved quest value still match.
+ */
 [[nodiscard]] bool quest_current(const PendingItemAcquisition& mutation) noexcept {
     build_data::items::Definition definition{};
     if (!build_data::find_item_definition_hash(mutation.acquiredDefinitionHash, definition)
@@ -59,7 +67,16 @@ using Quest = build_data::items::QuestInitialization;
     return account.characters.size();
 }
 
-/** Stages the common selected-character insertion path. */
+/**
+ * Hold investment::store::g_mutex while capturing inventory and quest state together.
+ * @param account State before any acquisition charge.
+ * @param chargedAccount State after the prepared material charge.
+ * @param definitionHash Item definition to grant.
+ * @param profileChanged Whether the charge changed profile inventory.
+ * @param source Grant identity and material requirements for commit checks.
+ * @param mutation Receives a pending grant; use only on success.
+ * @return False when the item, inventory, mapping, or saved quest state is invalid.
+ */
 [[nodiscard]] bool finalize_item_acquisition(const AccountState& account,
                                              const AccountState& chargedAccount,
                                              std::uint32_t definitionHash,
@@ -144,7 +161,13 @@ using Quest = build_data::items::QuestInitialization;
 
 } // namespace runtime::detail
 
-/** Prepares one native-row-checked selected-character inventory insertion. */
+/**
+ * Inventory and quest state must come from the same locked save view.
+ * @param collectibleIndex Collections row, or kNoCollectibleIndex for an item-only grant.
+ * @param definitionHash Item definition to grant.
+ * @param mutation Receives a pending grant; prepared is set only on success.
+ * @return False when identity, costs, capacity, or saved state prevent the grant.
+ */
 bool prepare_item_acquisition(std::uint16_t collectibleIndex,
                               std::uint32_t definitionHash,
                               PendingItemAcquisition& mutation) noexcept {
@@ -194,7 +217,12 @@ bool prepare_item_acquisition(std::uint16_t collectibleIndex,
         mutation);
 }
 
-/** Prepares one direct selected-character inventory grant, with no Collections row or charge. */
+/**
+ * Direct grants share quest-state checks but do not charge Collections materials.
+ * @param itemDefinitionIndex Item-table row to grant to the selected character.
+ * @param mutation Receives a pending grant; prepared is set only on success.
+ * @return False when the item, inventory, mapping, or saved quest state is invalid.
+ */
 bool prepare_item_acquisition_for_item(std::uint16_t itemDefinitionIndex,
                                        PendingItemAcquisition& mutation) noexcept {
     const std::lock_guard lock(investment::store::g_mutex);
@@ -388,7 +416,13 @@ valid_item_acquisition_source(const PendingItemAcquisition& mutation) noexcept {
            && definition.definitionHash == mutation.acquiredDefinitionHash;
 }
 
-/** Applies an insertion and checks its saved quest value while the caller holds the State lock. */
+/**
+ * Hold investment::store::g_mutex; the selected character and saved state must still match.
+ * @param current Current account from the locked save view.
+ * @param mutation Prepared inventory insertion and prior quest state.
+ * @param after Receives the candidate account; use only on success.
+ * @return False for stale state or an invalid resulting inventory.
+ */
 [[nodiscard]] bool materialize_item_acquisition(const AccountState& current,
                                                 const PendingItemAcquisition& mutation,
                                                 AccountState& after) noexcept {
@@ -495,7 +529,13 @@ valid_item_acquisition_source(const PendingItemAcquisition& mutation) noexcept {
 
 } // namespace runtime::detail
 
-/** Produces the full account after-image while a prepared character pull remains current. */
+/**
+ * Preview inventory and quest values together without changing the save.
+ * @param mutation Prepared acquisition checked against current saved state.
+ * @param after Receives the candidate account; use only on success.
+ * @param afterUnlocks Receives matching account and selected-character unlocks on success.
+ * @return False when the acquisition is stale or its saved unlocks cannot be read.
+ */
 bool preview_item_acquisition(const PendingItemAcquisition& mutation,
                               AccountState& after,
                               unlocks::Table& afterUnlocks) noexcept {
@@ -524,7 +564,11 @@ bool preview_direct_item_bundle(const PendingDirectItemBundle& mutation,
     return materialize_direct_item_bundle(account_snapshot(), mutation, after);
 }
 
-/** Commits inventory and initial quest state together while the prepared view remains current. */
+/**
+ * Inventory and first-step state share one transaction; failure rolls both back.
+ * @param mutation Prepared grant consumed on either success or failure.
+ * @return True when both writes commit against the unchanged prepared state.
+ */
 bool commit_item_acquisition(PendingItemAcquisition& mutation) noexcept {
     const PendingItemAcquisition& prepared = mutation;
     const PendingConsumption consume{mutation};

+ 13 - 5
Sunrise/src/state/runtime/state_account_transaction_helpers.h

@@ -116,9 +116,14 @@ struct GrantSource {
 /** @return The selected character's index, or the character count when none is selected. */
 [[nodiscard]] std::size_t selected_character_index(const AccountState& account) noexcept;
 /**
- * Stages the common selected-character insertion path.
- * @param chargedAccount Account after any material cost, or account itself when nothing is charged.
- * @return False when the character has no free row or the after-image does not resolve.
+ * Hold investment::store::g_mutex while capturing inventory and quest state together.
+ * @param account State before any acquisition charge.
+ * @param chargedAccount State after the prepared material charge.
+ * @param definitionHash Item definition to grant.
+ * @param profileChanged Whether the charge changed profile inventory.
+ * @param source Grant identity and material requirements for commit checks.
+ * @param mutation Receives a pending grant; use only on success.
+ * @return False when the item, inventory, mapping, or saved quest state is invalid.
  */
 [[nodiscard]] bool finalize_item_acquisition(const AccountState& account,
                                              const AccountState& chargedAccount,
@@ -142,8 +147,11 @@ finalize_profile_item_acquisition(const AccountState& account,
                                   const GrantSource& source,
                                   PendingProfileItemAcquisition& mutation) noexcept;
 /**
- * Applies one validated insertion over an exact current account without taking State locks.
- * @return False when the account moved since the mutation was prepared.
+ * Hold investment::store::g_mutex; the selected character and saved state must still match.
+ * @param current Current account from the locked save view.
+ * @param mutation Prepared inventory insertion and prior quest state.
+ * @param after Receives the candidate account; use only on success.
+ * @return False for stale state or an invalid resulting inventory.
  */
 [[nodiscard]] bool materialize_item_acquisition(const AccountState& current,
                                                 const PendingItemAcquisition& mutation,