stan 3 هفته پیش
والد
کامیت
211774f88b
58فایلهای تغییر یافته به همراه583 افزوده شده و 734 حذف شده
  1. 1 0
      README.md
  2. 5 0
      Sunrise/Sunrise.vcxproj
  3. 2 2
      Sunrise/src/client/content/investment/investment_refresh.cpp
  4. 2 3
      Sunrise/src/client/content/investment/worker.h
  5. 20 30
      Sunrise/src/client/content/items/packages/internal.h
  6. 12 23
      Sunrise/src/client/content/items/packages/package_ability_build.cpp
  7. 4 6
      Sunrise/src/client/content/items/packages/package_item_rows.cpp
  8. 39 42
      Sunrise/src/client/content/items/packages/package_subclass_build.cpp
  9. 0 3
      Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_frame.cpp
  10. 7 0
      Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_lifecycle.cpp
  11. 90 90
      Sunrise/src/client/hooks/inactivity/inactivity_override.cpp
  12. 7 24
      Sunrise/src/client/hooks/inactivity/inactivity_override.h
  13. 55 41
      Sunrise/src/client/hooks/membership_probe/membership_probe.cpp
  14. 8 16
      Sunrise/src/client/inactivity/inactivity_settings_store.h
  15. 4 2
      Sunrise/src/client/ui/player/player_panel.cpp
  16. 1 0
      Sunrise/src/core/settings/settings_runtime.cpp
  17. 3 7
      Sunrise/src/core/settings/state/account_rows_parser.cpp
  18. 2 0
      Sunrise/src/core/settings/state/account_settings_parser.cpp
  19. 6 7
      Sunrise/src/core/settings/state/display_parser.cpp
  20. 1 1
      Sunrise/src/core/ui/theme/sunrise_ui_theme.cpp
  21. 5 0
      Sunrise/src/middleware/content/packages/tables/item_definition_reader.cpp
  22. 3 8
      Sunrise/src/middleware/content/packages/tables/items.h
  23. 3 5
      Sunrise/src/middleware/datagen/character_record/appearance/character_appearance_abilities.cpp
  24. 2 3
      Sunrise/src/middleware/datagen/family4/account/preferences/preferences_encoder.cpp
  25. 7 9
      Sunrise/src/middleware/datagen/family4/loadout/subclass_socket_selection.cpp
  26. 6 8
      Sunrise/src/middleware/datagen/family4/loadout/subclass_socket_selection.h
  27. 4 6
      Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp
  28. 8 8
      Sunrise/src/server/bap/encrypted/internal.h
  29. 2 3
      Sunrise/src/server/bap/encrypted/push/activity/activity_keepalive_push.cpp
  30. 13 14
      Sunrise/src/server/bap/encrypted/push/queuez/queuez_banner_push.cpp
  31. 1 2
      Sunrise/src/server/bap/encrypted/push/queuez/queuez_select_character.cpp
  32. 11 28
      Sunrise/src/server/bap/encrypted/push/snapshot/internal.h
  33. 2 6
      Sunrise/src/server/bap/encrypted/queuez/definition.h
  34. 23 19
      Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp
  35. 11 20
      Sunrise/src/server/bap/encrypted/queuez/queuez_outcome_staging.cpp
  36. 1 3
      Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h
  37. 5 6
      Sunrise/src/server/bap/encrypted/queuez/staging/queuez_character_staging.cpp
  38. 5 6
      Sunrise/src/server/bap/encrypted/transactions/service_outcome_commit.cpp
  39. 5 9
      Sunrise/src/server/bap/internal.h
  40. 67 78
      Sunrise/src/server/gameplay/peer/peer_transport.cpp
  41. 1 1
      Sunrise/src/server/gameplay/physics/host/physics_session.cpp
  42. 18 17
      Sunrise/src/server/web_service/web_service_actions.cpp
  43. 2 4
      Sunrise/src/server/web_service/web_service_runtime.cpp
  44. 3 5
      Sunrise/src/state/account/account_state.h
  45. 5 8
      Sunrise/src/state/account/inventory/inventory_state.h
  46. 4 9
      Sunrise/src/state/build_data/items/socket_plugs/socket_plug_build_data_runtime.cpp
  47. 9 17
      Sunrise/src/state/build_data/runtime.h
  48. 1 2
      Sunrise/src/state/build_data/runtime/build_data_catalog_runtime.cpp
  49. 2 4
      Sunrise/src/state/build_data/socket_entry_buckets/definition.h
  50. 7 1
      Sunrise/src/state/gameplay/physics/physics_state_runtime.cpp
  51. 14 30
      Sunrise/src/state/runtime/runtime.h
  52. 16 18
      Sunrise/src/state/runtime/state_account_dismantle_staging.cpp
  53. 2 5
      Sunrise/src/state/runtime/state_account_equipment_runtime.cpp
  54. 2 3
      Sunrise/src/state/runtime/state_account_item_action_runtime.cpp
  55. 28 42
      Sunrise/src/state/runtime/state_account_runtime.cpp
  56. 6 12
      Sunrise/src/state/runtime/state_account_socket_runtime.cpp
  57. 4 0
      Sunrise/src/state/runtime/state_rolled_socket_plugs.cpp
  58. 6 18
      Sunrise/src/state/runtime/state_rolled_socket_plugs.h

+ 1 - 0
README.md

@@ -50,6 +50,7 @@ Make sure you have `git`, `cmake`, `clang`, `ninja`, `llvm`, and `xwin` installe
 $ git clone https://github.com/stanuwu/Sunrise
 $ cd Sunrise
 ```
+
 2. Download Windows headers:
 ```bash
 $ xwin --accept-license splat --include-debug-libs --sdk-version 10.0.26100 --output .xwin-cache

+ 5 - 0
Sunrise/Sunrise.vcxproj

@@ -794,6 +794,8 @@
     <ClCompile Include="src\middleware\gameplay\descriptor\join_descriptor.cpp" />
     <ClCompile Include="src\server\gameplay\gameplay_advertisement.cpp" />
     <ClCompile Include="src\middleware\web_service\messages\opcode901\opcode901_codec.cpp" />
+    <ClCompile Include="src\middleware\web_service\messages\opcode801_codec.cpp" />
+    <ClCompile Include="src\state\build_data\socket_entry_buckets\socket_entry_bucket_catalog.cpp" />
     <ClCompile Include="src\middleware\gameplay\peer\reliable_assembly.cpp" />
     <ClCompile Include="src\middleware\gameplay\peer\join_messages.cpp" />
     <ClCompile Include="src\middleware\gameplay\group\session_messages.cpp" />
@@ -1389,6 +1391,9 @@
     <ClInclude Include="src\middleware\gameplay\descriptor\join_descriptor.h" />
     <ClInclude Include="src\server\gameplay\gameplay_advertisement.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode901\opcode901_codec.h" />
+    <ClInclude Include="src\middleware\web_service\messages\opcode801.h" />
+    <ClInclude Include="src\state\build_data\socket_entry_buckets\definition.h" />
+    <ClInclude Include="src\state\build_data\socket_entry_buckets\socket_entry_bucket_catalog.h" />
     <ClInclude Include="src\middleware\gameplay\peer\reliable_assembly.h" />
     <ClInclude Include="src\middleware\gameplay\peer\join_messages.h" />
     <ClInclude Include="src\middleware\gameplay\group\session_messages.h" />

+ 2 - 2
Sunrise/src/client/content/investment/investment_refresh.cpp

@@ -49,8 +49,8 @@ bool refresh() noexcept {
         // calls, so a held thread stopped inside one would deadlock the freeze below.
         AcquireSRWLockExclusive(&g_refreshLock);
         const bool persisted = state::ensure_profile_item_identities()
-                                && state::ensure_character_subclasses()
-                                && state::build_data::persist();
+                               && state::ensure_character_subclasses()
+                               && state::build_data::persist();
         // Nothing reads a package again until the next boot, so the open files and the held
         // tables go back now rather than at process exit.
         middleware::content::packages::reader::release_caches();

+ 2 - 3
Sunrise/src/client/content/investment/worker.h

@@ -18,9 +18,8 @@ void reset() noexcept;
 
 /**
  * Makes the next `service` pump take another refresh slice even though a prior one completed.
- * A committed mutation that invalidates one already-published build-data domain needs this: the
- * completion latch that keeps a steady session from re-running `refresh` every pump would
- * otherwise never notice the domain came back stale.
+ * A committed mutation that invalidates an already-published build-data domain needs this: the
+ * completion latch would otherwise never notice the domain came back stale.
  */
 void request_slice() noexcept;
 

+ 20 - 30
Sunrise/src/client/content/items/packages/internal.h

@@ -128,10 +128,8 @@ void request(std::uint16_t definitionIndex, DetailRequests& requested) noexcept;
 /**
  * Adds every socket lane's initial plug to the requested set.
  * A lane using native defaults falls back to this plug, so its detail must exist.
- * @param row
- * Item row already read from its definition blob.
- * @param itemDefinitionCount Installed
- * item-table bound.
+ * @param row Item row already read from its definition blob.
+ * @param itemDefinitionCount Installed item-table bound.
  * @param requested Requested-set storage.
  */
 void append_initial_plugs(const tables::items::Row& row,
@@ -140,13 +138,10 @@ void append_initial_plugs(const tables::items::Row& row,
 
 /**
  * Materializes requested native indices in ascending order.
- * @param requested Deduplicated
- * native-index set.
+ * @param requested Deduplicated native-index set.
  * @param output Fixed detail-index storage.
- * @param count Receives the
- * number of selected rows, or zero when output is too small.
- * @return True when every selected
- * row fits.
+ * @param count Receives the number of selected rows, or zero when output is too small.
+ * @return True when every selected row fits.
  */
 [[nodiscard]] bool materialize_requests(const DetailRequests& requested,
                                         std::span<std::uint16_t> output,
@@ -212,18 +207,17 @@ read_investment_constants(const reader::Source& source,
  * @param count Receives the number of rows built.
  * @return True when the table reads; a subclass that fails is skipped, not fatal.
  */
-[[nodiscard]] bool
-build_character_abilities(const reader::Source& source,
-                          reader::Scratch& scratch,
-                          std::span<const std::byte> root,
-                          std::vector<std::byte>& table,
-                          std::vector<std::byte>& definition,
-                          std::vector<std::byte>& blob,
-                          std::span<state::build_data::abilities::Definition> output,
-                          std::size_t& count,
-                          std::span<state::build_data::socket_entry_buckets::Definition>
-                              entryBucketOutput,
-                          std::size_t& entryBucketCount) noexcept;
+[[nodiscard]] bool build_character_abilities(
+    const reader::Source& source,
+    reader::Scratch& scratch,
+    std::span<const std::byte> root,
+    std::vector<std::byte>& table,
+    std::vector<std::byte>& definition,
+    std::vector<std::byte>& blob,
+    std::span<state::build_data::abilities::Definition> output,
+    std::size_t& count,
+    std::span<state::build_data::socket_entry_buckets::Definition> entryBucketOutput,
+    std::size_t& entryBucketCount) noexcept;
 
 /**
  * Resolves which of the 12 semantic ability buckets every entry in one socket-entry list reaches.
@@ -353,15 +347,11 @@ void report(std::size_t published, const char* reason) noexcept;
 
 /**
  * Reads and publishes the root's dense collectible-to-item mapping table.
- * @param source
- * Package source.
+ * @param source Package source.
  * @param storage Pass storage, including scratch bytes and bounded row storage.
-
- * * @param root Investment root bytes.
- * @param itemDefinitionCount Number of rows in the
- * installed item index table.
- * @return True when every tag, class, bound, and item link validates
- * and publishes.
+ * @param root Investment root bytes.
+ * @param itemDefinitionCount Number of rows in the installed item index table.
+ * @return True when every tag, class, bound, and item link validates and publishes.
  */
 [[nodiscard]] bool build_collectibles(const reader::Source& source,
                                       Storage& storage,

+ 12 - 23
Sunrise/src/client/content/items/packages/package_ability_build.cpp

@@ -109,11 +109,8 @@ selector_destination(const Walk& walk, std::uint8_t entryIndex, std::uint8_t& bu
 
 /**
  * Chooses the active plug source of every entry group, and any bundled siblings a pick carries.
- * An entry group holds alternatives, and the summary selection names which one the character has.
- * A pick can also bundle several consecutive same-group entries that publish together (an
- * Attunement's melee, plus the passive nodes it carries with it); those siblings normally carry
- * their own distinct plug source, so they are marked forced-active directly rather than relying on
- * a plug-source match.
+ * A group holds alternatives and the summary selection names one. A pick can also bundle same-group
+ * entries that publish together; those carry their own source, so they are marked forced-active.
  * @param walk Subclass walk state.
  * @param sources Receives one active plug source per group, keyed by group.
  * @param forcedActive Receives which entries are active regardless of plug source.
@@ -123,11 +120,9 @@ void chosen_sources(const Walk& walk,
                     std::array<bool, pool::kEntryCapacity>& forcedActive) noexcept {
     sources.fill(pool::kNoPlugSource);
     forcedActive.fill(false);
-    // A group of 2 or 3 entries (grenade, movement, class ability) is an ordinary set of mutually
-    // exclusive alternatives: exactly one contributes its hashes. An Attunement's group is far
-    // wider (it packs several 4-node options into one group id), so a population past the widest
-    // single bundle is the signal that this group's members activate in same-sized runs rather
-    // than as lone alternatives.
+    // A group of 2 or 3 entries is mutually exclusive alternatives: exactly one contributes its
+    // hashes. An Attunement's group packs several 4-node options into one group id, so a
+    // population past the widest single bundle means its members activate in same-sized runs.
     std::array<std::uint16_t, 256> groupPopulation{};
     for (std::size_t index = 0; index < walk.entryCount; ++index) {
         ++groupPopulation[walk.entries[index].group];
@@ -137,7 +132,8 @@ void chosen_sources(const Walk& walk,
             continue;
         }
         const pool::Entry& entry = walk.entries[entryIndex];
-        if (entry.plugSource == pool::kNoPlugSource || sources[entry.group] != pool::kNoPlugSource) {
+        if (entry.plugSource == pool::kNoPlugSource
+            || sources[entry.group] != pool::kNoPlugSource) {
             continue;
         }
         sources[entry.group] = entry.plugSource;
@@ -200,14 +196,9 @@ void chosen_sources(const Walk& walk,
 }
 
 /**
- * Claims a bucket kind for every forced-active bundle sibling the 6 canonical selections do not
- * already cover. An Attunement bundle can carry a member that fully replaces an ability (Phoenix
- * Dive replacing the class ability, rather than an ordinary Rift variant) under its own distinct
- * bucket, separate from the class's ordinary one, even though it is not itself one of the 6
- * summary picks. Its hash would otherwise never be filed, because nothing ever claims that
- * bucket's kind. A sibling with no destination bucket (a passive node) or one whose bucket is
- * already claimed is skipped rather than treated as a failure, since most bundle members are
- * exactly that.
+ * Claims a bucket kind for every forced-active bundle sibling the 6 canonical selections miss.
+ * A bundle member can replace an ability outright under its own bucket, and nothing else ever
+ * claims that kind. A sibling with no bucket, or one already claimed, is skipped, not failed.
  * @param walk Subclass walk state.
  * @param forcedActive Entries active regardless of plug source, from a bundled pick.
  * @param output Bucket kinds, extended in place.
@@ -301,10 +292,8 @@ bool build_ability_buckets(const reader::Source& source,
 
 /**
  * Resolves which of the 12 semantic ability buckets every entry in one socket-entry list reaches.
- * A pick's table position does not say which ability slot it fills; a bundled group (an
- * Attunement, for example) can freely mix its members across slots. Only the selector chain each
- * entry's own pool declares says where it lands, so this walks every entry once and records it,
- * independent of any character's current selection.
+ * Table position does not say which slot an entry fills and a bundled group can mix slots; only
+ * the selector chain does, so every entry is walked once, independent of any character.
  * @param source Package source.
  * @param scratch Reader scratch.
  * @param listDefinition One socket-entry list's definition bytes.

+ 4 - 6
Sunrise/src/client/content/items/packages/package_item_rows.cpp

@@ -56,9 +56,8 @@ bool build_item_rows(const reader::Source& source,
     const std::span<const std::byte> container{storage.child};
     reason = "rows";
     // The detail closure is gathered during this one walk. Collections can name any installed
-    // item row, including profile-owned shaders and modifications, so retain every readable row
-    // rather than only startup-authored/equippable definitions. The fixed request bitset still
-    // bounds this to the installed 16-bit item-table domain.
+    // item row, including profile-owned shaders and modifications, so retain every readable row.
+    // The fixed request bitset bounds this to the installed 16-bit item-table domain.
     storage.detailRequests.reset();
     storage.specialPlugCategories.fill(0);
     std::size_t detailCount = 0;
@@ -170,9 +169,8 @@ bool build_item_rows(const reader::Source& source,
         }
     }
     // Ability buckets read the socket entry list table again and depend on the detail domain, so
-    // they run last. The entry-bucket table is resolved in the same pass but never joins the
-    // on-disk cache, so a warm boot that finds the ability buckets already cached still has to
-    // run this once to fill it in for the session.
+    // they run last. The entry-bucket table never joins the on-disk cache, so a warm boot still
+    // has to run this once to fill it in for the session.
     if (published
         && (!state::build_data::ability_buckets_ready()
             || !state::build_data::socket_entry_buckets_ready())) {

+ 39 - 42
Sunrise/src/client/content/items/packages/package_subclass_build.cpp

@@ -59,9 +59,8 @@ constexpr std::size_t kSubclassSlot =
 }
 
 /** @param rows Rows built so far. @return True when this list already has a resolved row. */
-[[nodiscard]] bool
-held(std::span<const state::build_data::socket_entry_buckets::Definition> rows,
-     std::uint16_t socketEntryListIndex) noexcept {
+[[nodiscard]] bool held(std::span<const state::build_data::socket_entry_buckets::Definition> rows,
+                        std::uint16_t socketEntryListIndex) noexcept {
     for (const auto& existing : rows) {
         if (existing.socketEntryListIndex == socketEntryListIndex) {
             return true;
@@ -71,8 +70,7 @@ held(std::span<const state::build_data::socket_entry_buckets::Definition> rows,
 }
 
 /** @param item Authored subclass item. @return Its 5 selected socket entries. */
-[[nodiscard]] domain::Selection
-selection_of(const state::account::inventory::Item& item) noexcept {
+[[nodiscard]] domain::Selection selection_of(const state::account::inventory::Item& item) noexcept {
     return {item.movementAbilityEntry,
             item.grenadeAbilityEntry,
             item.superAbilityEntry,
@@ -83,17 +81,17 @@ selection_of(const state::account::inventory::Item& item) noexcept {
 } // namespace
 
 /** Builds one ability bucket row per distinct subclass and ability selection in use. */
-bool build_character_abilities(const reader::Source& source,
-                               reader::Scratch& scratch,
-                               std::span<const std::byte> root,
-                               std::vector<std::byte>& table,
-                               std::vector<std::byte>& definition,
-                               std::vector<std::byte>& blob,
-                               std::span<state::build_data::abilities::Definition> output,
-                               std::size_t& count,
-                               std::span<state::build_data::socket_entry_buckets::Definition>
-                                   entryBucketOutput,
-                               std::size_t& entryBucketCount) noexcept {
+bool build_character_abilities(
+    const reader::Source& source,
+    reader::Scratch& scratch,
+    std::span<const std::byte> root,
+    std::vector<std::byte>& table,
+    std::vector<std::byte>& definition,
+    std::vector<std::byte>& blob,
+    std::span<state::build_data::abilities::Definition> output,
+    std::size_t& count,
+    std::span<state::build_data::socket_entry_buckets::Definition> entryBucketOutput,
+    std::size_t& entryBucketCount) noexcept {
     count = 0;
     entryBucketCount = 0;
     std::uint32_t tableTag = 0;
@@ -112,11 +110,9 @@ bool build_character_abilities(const reader::Source& source,
         return false;
     }
     const state::AccountState account = state::account_snapshot();
-    // First-option defaults every shipped subclass starts at (see account_state.h). Equipping a
-    // subclass always resets its picks to these (state_account_runtime.cpp), so every owned
-    // subclass publishes a row at this fixed selection regardless of which one is equipped right
-    // now. That row has to exist synchronously, ahead of time: the equip response that needs it is
-    // built inline with the commit, with no room to wait on a later refresh slice.
+    // First-option defaults every shipped subclass starts at. Equipping one resets its picks to
+    // these, so every owned subclass publishes a row at this selection. The row must exist
+    // synchronously: the equip response is built inline with the commit.
     const domain::Selection defaultSelection{state::kDefaultMovementAbilityEntry,
                                              state::kDefaultGrenadeAbilityEntry,
                                              state::kDefaultSuperAbilityEntry,
@@ -137,13 +133,15 @@ bool build_character_abilities(const reader::Source& source,
             return;
         }
         tables::IndexRow indexRow{};
-        if (!tables::index_row(std::span<const std::byte>{table}, rows, socketEntryListIndex, indexRow)
+        if (!tables::index_row(
+                std::span<const std::byte>{table}, rows, socketEntryListIndex, indexRow)
             || indexRow.targetTag == 0) {
             report_ability_failure("index_row", character, socketEntryListIndex, rows.count);
             return;
         }
         if (!reader::read_tag(source, scratch, indexRow.targetTag, definition)) {
-            report_ability_failure("definition_read", character, socketEntryListIndex, indexRow.targetTag);
+            report_ability_failure(
+                "definition_read", character, socketEntryListIndex, indexRow.targetTag);
             return;
         }
         // A bundled group can freely mix which ability slot each of its members fills (an
@@ -153,19 +151,21 @@ bool build_character_abilities(const reader::Source& source,
             && !held(entryBucketOutput.first(entryBucketCount), socketEntryListIndex)) {
             state::build_data::socket_entry_buckets::Definition entryBuckets{};
             entryBuckets.socketEntryListIndex = socketEntryListIndex;
-            if (resolve_entry_buckets(
-                    source, scratch, std::span<const std::byte>{definition}, blob,
-                    entryBuckets.buckets)) {
+            if (resolve_entry_buckets(source,
+                                      scratch,
+                                      std::span<const std::byte>{definition},
+                                      blob,
+                                      entryBuckets.buckets)) {
                 entryBucketOutput[entryBucketCount++] = entryBuckets;
             }
         }
         if (!build_ability_buckets(
                 source, scratch, std::span<const std::byte>{definition}, blob, selection, row)) {
-            const std::size_t packedSelection = selection.movementEntry
-                                                | (selection.grenadeEntry << 8U)
-                                                | (selection.superEntry << 16U)
-                                                | (selection.meleeEntry << 24U);
-            report_ability_failure("bucket_build", character, socketEntryListIndex, packedSelection);
+            const std::size_t packedSelection =
+                selection.movementEntry | (selection.grenadeEntry << 8U)
+                | (selection.superEntry << 16U) | (selection.meleeEntry << 24U);
+            report_ability_failure(
+                "bucket_build", character, socketEntryListIndex, packedSelection);
             return;
         }
         output[count++] = row;
@@ -185,14 +185,13 @@ bool build_character_abilities(const reader::Source& source,
         const auto& equippedSlot = account.characters[character].equipment.slots[kSubclassSlot];
         state::build_data::items::Definition equippedItem{};
         if (!state::build_data::find_item_definition_hash(equippedSlot->definitionHash,
-                                                           equippedItem)) {
+                                                          equippedItem)) {
             continue;
         }
 
         // Every subclass the character owns publishes a row, not just the equipped one, so a
-        // later equip swap always lands on an already-built row instead of racing the next
-        // refresh slice. When the group cannot be resolved, at least the equipped one still
-        // publishes, matching the prior single-row behaviour.
+        // later equip swap lands on an already-built row instead of racing the next refresh
+        // slice. When the group cannot be resolved, the equipped one still publishes.
         std::array<std::uint16_t, state::build_data::kSubclassGroupSize> group{};
         std::array<std::uint16_t, state::build_data::kSubclassGroupSize> members{};
         std::size_t memberCount = 1;
@@ -206,15 +205,13 @@ bool build_character_abilities(const reader::Source& source,
             const std::uint16_t memberDefinitionIndex = members[member];
             state::build_data::items::details::Definition memberDetail{};
             if (!state::build_data::find_configured_item_detail(memberDefinitionIndex,
-                                                                 memberDetail)) {
+                                                                memberDetail)) {
                 continue;
             }
             publish(character, memberDetail.socketEntryListIndex, defaultSelection);
-            // Each owned subclass remembers its own picks now, not just the equipped one, so
-            // every member is checked for a non-default selection to publish on top of the
-            // default row every member gets: a fresh boot that never swapped needs its actual
-            // selection to resolve, not the shared default, for whichever subclasses were
-            // already configured before this boot.
+            // Each owned subclass remembers its own picks, so every member is checked for a
+            // non-default selection to publish on top of its default row. A fresh boot needs the
+            // actual selection for subclasses configured before this boot.
             const domain::Selection* memberSelection = nullptr;
             domain::Selection resolvedSelection{};
             if (memberDefinitionIndex == equippedItem.definitionIndex) {
@@ -223,7 +220,7 @@ bool build_character_abilities(const reader::Source& source,
             } else {
                 state::build_data::items::Definition memberItemDefinition{};
                 if (state::build_data::find_item_definition_index(memberDefinitionIndex,
-                                                                   memberItemDefinition)) {
+                                                                  memberItemDefinition)) {
                     const auto& inventory = account.characters[character].inventory;
                     for (std::size_t itemIndex = 0; itemIndex < inventory.count; ++itemIndex) {
                         if (inventory.values[itemIndex].definitionHash

+ 0 - 3
Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_frame.cpp

@@ -13,7 +13,6 @@
 #include "../../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../../../core/ui/scaling/dpi/ui_dpi_scaling.h"
 #include "../../../../core/ui/theme/sunrise_ui_theme.h"
-#include "../../inactivity/inactivity_override.h"
 #include "../input/input.h"
 #include "graphics_renderer_report.h"
 #include "state.h"
@@ -127,8 +126,6 @@ void render_frame_locked() noexcept {
     if (!fully_active_locked()) {
         return;
     }
-    // A steady tick with the game fully up, which is all the timeout hold needs.
-    hooks::inactivity::poll();
     if (core::ui::scaling::dpi::update(g_resources.window)) {
         // Style and text scale change together, before the backend sets up the frame.
         core::ui::theme::apply();

+ 7 - 0
Sunrise/src/client/hooks/graphics/renderer/graphics_renderer_lifecycle.cpp

@@ -7,6 +7,7 @@
 #include "../../../../core/ui/layout/layout.h"
 #include "../../../../core/ui/runtime/ui_visibility_runtime.h"
 #include "../../cursor/runtime.h"
+#include "../../inactivity/inactivity_override.h"
 #include "../../polled_input/runtime.h"
 #include "../input/input.h"
 #include "graphics_renderer_report.h"
@@ -315,11 +316,17 @@ void present(IDXGISwapChain* swapChain) noexcept {
     if (g_resources.swapChain == nullptr) {
         (void)initialize_locked(swapChain);
     }
+    bool framed = false;
     if (g_resources.swapChain == swapChain && fully_active_locked()) {
         render_frame_locked();
+        framed = true;
     }
     ReleaseSRWLockExclusive(&g_rendererLock);
 
+    if (framed) {
+        // The timeout hold enters game code, so it runs only after the renderer lock is gone.
+        inactivity::poll();
+    }
     // The cursor policy calls Win32, so it runs only after the renderer lock is gone.
     const bool visible = core::ui::runtime::snapshot().visible;
     cursor::apply_visibility(visible);

+ 90 - 90
Sunrise/src/client/hooks/inactivity/inactivity_override.cpp

@@ -1,15 +1,10 @@
 /**
  * Inactivity timeout override.
  *
- * The Client keeps one timeout per activity lane and ends a session whose controller has been
- * idle for longer. The lanes are not image data: they sit at a fixed offset inside a live object,
- * and the pointer to that object is stored obfuscated, so the Client reaches it through a getter
- * that decodes the pointer on each call. This module resolves that getter by signature and calls
- * it the same way, which is the same shape the camera pose block is reached by.
- *
- * No lane the Client authors is written down anywhere. A block that is not the one this module
- * last wrote is the Client's own, so reading before each hold both takes the value a lane is put
- * back to and follows an activity change, which re-authors the whole block.
+ * The Client keeps 14 activity timeouts in one live object and ends a session idle for longer.
+ * The pointer to that object is obfuscated, so this module resolves the Client's own getter by
+ * signature and calls it. Lanes are re-applied on an interval because an activity change
+ * re-authors the whole block.
  */
 
 #include "inactivity_override.h"
@@ -37,10 +32,9 @@ using patterns::signature;
 using patterns::signature_length;
 
 /**
- * The activity config getter. Its body is the shared shape every obfuscated pointer getter has,
- * so the load of its own global is what tells it apart: a RIP-relative displacement encodes a
- * distance rather than an address, carries no position-dependent bytes, and is the only part of
- * this prologue unique to this getter. Call and branch displacements stay wildcarded.
+ * The activity config getter. Every obfuscated pointer getter shares this prologue, so the load
+ * of its own global is the only part that tells this one apart: wildcarding that displacement
+ * takes the pattern from one match to 242. It is image-relative, so ASLR does not move it.
  */
 constexpr std::string_view kConfigGetterText =
     "40 53 48 83 EC 20 48 8B 1D 2B 10 1A 02 48 85 DB 0F 84 ? ? ? ? 48 89 5C 24 30 "
@@ -48,30 +42,28 @@ constexpr std::string_view kConfigGetterText =
 /** Compiled pattern bytes of the config getter signature. */
 constexpr auto kConfigGetter = signature<signature_length(kConfigGetterText)>(kConfigGetterText);
 
-/**
- * The controlled player's index, which the idle clock is keyed by. Stopped at its own ret, because
- * the bytes after it belong to the next function and would tie this pattern to that one's layout.
- */
-constexpr std::string_view kControlledIndexText =
-    "48 8B 05 39 0F 26 02 8B 80 60 04 00 00 C3";
+/** The controlled player's index, which the idle clock is keyed by. */
+constexpr std::string_view kControlledIndexText = "48 8B 05 ? ? ? ? 8B 80 60 04 00 00 C3";
+/** Compiled pattern bytes of the controlled index signature. */
 constexpr auto kControlledIndex =
     signature<signature_length(kControlledIndexText)>(kControlledIndexText);
 
-/** The idle clock. It answers in the lanes' own unit, which is what makes the two comparable. */
+/** The idle clock, in the lanes' own unit. Runs to the 0x1B0 stride, which makes it unique. */
 constexpr std::string_view kIdleClockText =
-    "40 53 48 83 EC 20 48 63 D9 48 8D 0D C8 99 A8 01 8B D3 E8 ? ? ? ? 84 C0";
+    "40 53 48 83 EC 20 48 63 D9 48 8D 0D ? ? ? ? 8B D3 E8 ? ? ? ? 84 C0 74 ? "
+    "48 8B 05 ? ? ? ? 48 69 CB B0 01 00 00";
+/** Compiled pattern bytes of the idle clock signature. */
 constexpr auto kIdleClock = signature<signature_length(kIdleClockText)>(kIdleClockText);
 
 /** The session clock, which is what the grace is measured against. */
 constexpr std::string_view kSessionClockText =
-    "48 83 EC 28 E8 ? ? ? ? 48 85 C0 74 ? 80 3D 4B 63 88 01 00 48 89 5C 24 20";
+    "48 83 EC 28 E8 ? ? ? ? 48 85 C0 74 ? 80 3D ? ? ? ? 00 48 89 5C 24 20";
+/** Compiled pattern bytes of the session clock signature. */
 constexpr auto kSessionClock = signature<signature_length(kSessionClockText)>(kSessionClockText);
 
 /**
- * The session grace, in the object the getter returns.
- *
- * Read so the interface can explain a lane that has not fired yet, and never written: this module
- * exists to stop a kick, and the only thing a shorter grace can do is bring one forward.
+ * The session grace. Read for the interface and never written, because shortening it can only
+ * bring a kick forward. It gates the matchmade lanes only, not orbit.
  */
 constexpr std::size_t kGraceOffset = 0x84;
 
@@ -94,8 +86,6 @@ using SessionGetter = std::uint64_t(__fastcall*)();
 
 SRWLOCK g_lock{SRWLOCK_INIT};
 ConfigGetter g_getter{};
-/** The object the last call returned, kept only so the interface can show it. */
-std::uintptr_t g_object{};
 std::uint64_t g_nextHoldTick{};
 /** The block this module last wrote. Anything else in the object is the Client's own. */
 Lanes g_applied{};
@@ -120,6 +110,10 @@ IndexGetter g_indexGetter{};
 IdleGetter g_idleGetter{};
 SessionGetter g_sessionGetter{};
 
+/** Last sample, taken only while a caller is asking for one. */
+Timers g_timers{};
+bool g_timersWanted{};
+
 /**
  * Calls the getter without faulting. The body is obfuscated game code, and it runs before the
  * Client has published its global on an early frame.
@@ -144,11 +138,8 @@ SessionGetter g_sessionGetter{};
  */
 [[nodiscard]] bool read_block(const std::byte* object, Lanes& values) noexcept {
     SIZE_T read = 0;
-    return ReadProcessMemory(GetCurrentProcess(),
-                             object + kTimeoutBlockOffset,
-                             values.data(),
-                             kBlockBytes,
-                             &read)
+    return ReadProcessMemory(
+               GetCurrentProcess(), object + kTimeoutBlockOffset, values.data(), kBlockBytes, &read)
                != FALSE
            && read == kBlockBytes;
 }
@@ -188,11 +179,10 @@ SessionGetter g_sessionGetter{};
  * @param current Block just read out of the object.
  */
 void capture_locked(const Lanes& current) noexcept {
-    // A zero lane is an object the Client has published but not authored yet.
-    const bool authored =
-        std::none_of(current.begin(), current.end(), [](std::uint32_t value) noexcept {
-            return value == 0;
-        });
+    // An all-zero block is an object the Client has published but not filled in. One zero lane is
+    // the Client's own way of switching that lane off, so it is captured like any other value.
+    const bool authored = std::any_of(
+        current.begin(), current.end(), [](std::uint32_t value) noexcept { return value != 0; });
     if (!authored || (g_appliedValid && current == g_applied)) {
         return;
     }
@@ -233,11 +223,12 @@ void capture_locked(const Lanes& current) noexcept {
 }
 
 /**
- * Caller holds the lock. Failure is not propagated, because these are reported and never acted on:
- * a build whose signatures have moved should still install and still hold its lanes.
+ * Caller holds the lock. Failure is not propagated: a build whose signatures have moved should
+ * still install and still hold its lanes.
  */
 void resolve_clocks_locked() noexcept {
-    std::byte* const index = scan_main_image_unique(kControlledIndex, "inactivity_controlled_index");
+    std::byte* const index =
+        scan_main_image_unique(kControlledIndex, "inactivity_controlled_index");
     std::byte* const idle = scan_main_image_unique(kIdleClock, "inactivity_idle_clock");
     std::byte* const session = scan_main_image_unique(kSessionClock, "inactivity_session_clock");
     if (index == nullptr || idle == nullptr || session == nullptr) {
@@ -249,9 +240,44 @@ void resolve_clocks_locked() noexcept {
     g_indexGetter = reinterpret_cast<IndexGetter>(index);
     g_idleGetter = reinterpret_cast<IdleGetter>(idle);
     g_sessionGetter = reinterpret_cast<SessionGetter>(session);
-    core::log::write(core::log::Channel::client,
-                     core::log::Level::info,
-                     "ev=inactivity stage=clocks result=ok");
+    core::log::write(
+        core::log::Channel::client, core::log::Level::info, "ev=inactivity stage=clocks result=ok");
+}
+
+/**
+ * Refreshes the cached clocks when a caller asked for them. The getters are obfuscated Client
+ * code, so they are called with no lock held and guarded against an early frame.
+ */
+void sample_timers() noexcept {
+    AcquireSRWLockExclusive(&g_lock);
+    const bool wanted = g_timersWanted;
+    g_timersWanted = false;
+    const IndexGetter index = g_indexGetter;
+    const IdleGetter idle = g_idleGetter;
+    const SessionGetter session = g_sessionGetter;
+    ReleaseSRWLockExclusive(&g_lock);
+    if (!wanted || index == nullptr || idle == nullptr || session == nullptr) {
+        return;
+    }
+    Timers sampled{};
+    __try {
+        const std::int32_t controlled = index();
+        if (controlled >= 0) {
+            sampled.idleMs = idle(controlled);
+            sampled.idleValid = true;
+        }
+    } __except (EXCEPTION_EXECUTE_HANDLER) {
+        sampled.idleValid = false;
+    }
+    __try {
+        sampled.sessionMs = session();
+        sampled.sessionValid = true;
+    } __except (EXCEPTION_EXECUTE_HANDLER) {
+        sampled.sessionValid = false;
+    }
+    AcquireSRWLockExclusive(&g_lock);
+    g_timers = sampled;
+    ReleaseSRWLockExclusive(&g_lock);
 }
 
 } // namespace
@@ -272,8 +298,7 @@ bool install() noexcept {
         return false;
     }
     g_getter = reinterpret_cast<ConfigGetter>(match);
-    // Scanned here rather than on first display, because the scan walks the whole image and the
-    // interface asks for these from the render thread.
+    // Scanned here rather than on first display, because the scan walks the whole image.
     resolve_clocks_locked();
     ReleaseSRWLockExclusive(&g_lock);
     core::log::write(core::log::Channel::client,
@@ -293,7 +318,6 @@ void uninstall() noexcept {
     g_indexGetter = nullptr;
     g_idleGetter = nullptr;
     g_sessionGetter = nullptr;
-    g_object = 0;
     g_nextHoldTick = 0;
     g_applied = Lanes{};
     g_appliedValid = false;
@@ -307,11 +331,14 @@ void uninstall() noexcept {
     g_liveValid = false;
     g_liveGrace = 0;
     g_liveGraceValid = false;
+    g_timers = Timers{};
+    g_timersWanted = false;
     ReleaseSRWLockExclusive(&g_lock);
 }
 
 /** Holds the configured milliseconds, or puts back the ones the Client authored. */
 void poll() noexcept {
+    sample_timers();
     const settings::Settings configured = settings::get();
     const bool holding = holds(configured);
     const Lanes desired = held_lanes(configured);
@@ -325,14 +352,12 @@ void poll() noexcept {
         return;
     }
     g_nextHoldTick = now + kHoldIntervalMs;
-    // Recorded before the object is reached, so neither a poll that finds no activity nor a write
-    // the Client refuses can leave the intent looking changed and skip the interval on every later
-    // frame.
+    // Recorded before the object is reached, so a poll that finds no activity cannot leave the
+    // intent looking changed and skip the interval on every later frame.
     g_intentLanes = desired;
     g_intentHolding = holding;
     g_intentValid = true;
     std::byte* const object = config_object();
-    g_object = reinterpret_cast<std::uintptr_t>(object);
     if (object == nullptr) {
         ReleaseSRWLockExclusive(&g_lock);
         return;
@@ -349,7 +374,6 @@ void poll() noexcept {
     if (!holding) {
         const bool released = release_locked(object);
         ReleaseSRWLockExclusive(&g_lock);
-        // Logged as its own event, so a reader can see a hold end rather than only see one start.
         // Nothing to put back is not a failure: it is the ordinary state with the feature off.
         if (changed) {
             core::log::write(core::log::Channel::client,
@@ -366,19 +390,20 @@ void poll() noexcept {
         g_appliedValid = true;
         g_holding = true;
     }
-    // The Client picks which lane to time by at runtime, so the shortest one is the only figure
-    // that says when a kick can first happen without naming a lane that may not be in force.
+    // The Client picks its lane at runtime, so the shortest is the only figure that says when a
+    // kick can first happen without naming a lane that may not be in force.
     const std::uint32_t shortest = *std::min_element(desired.begin(), desired.end());
     ReleaseSRWLockExclusive(&g_lock);
     if (changed) {
         // Only on a change, so a steady hold does not fill the log every interval.
         std::array<char, 128> line{};
-        const int length = std::snprintf(line.data(),
-                                         line.size(),
-                                         "ev=inactivity stage=hold mode=%s shortest_ms=%u result=%s",
-                                         configured.enabled ? "disable" : "set",
-                                         shortest,
-                                         wrote ? "ok" : "fail");
+        const int length =
+            std::snprintf(line.data(),
+                          line.size(),
+                          "ev=inactivity stage=hold mode=%s shortest_ms=%u result=%s",
+                          configured.enabled ? "disable" : "set",
+                          shortest,
+                          wrote ? "ok" : "fail");
         if (length > 0) {
             core::log::write(core::log::Channel::client,
                              core::log::Level::info,
@@ -391,9 +416,6 @@ void poll() noexcept {
 Status status() noexcept {
     Status output{};
     AcquireSRWLockShared(&g_lock);
-    output.resolved = g_getter != nullptr;
-    output.address = g_object;
-    output.captured = g_capturedValid;
     output.live = g_live;
     output.liveValid = g_liveValid;
     output.liveGraceMs = g_liveGrace;
@@ -402,34 +424,12 @@ Status status() noexcept {
     return output;
 }
 
+/** Reads the last sample and arms the next one. */
 Timers timers() noexcept {
-    Timers output{};
-    AcquireSRWLockShared(&g_lock);
-    const IndexGetter index = g_indexGetter;
-    const IdleGetter idle = g_idleGetter;
-    const SessionGetter session = g_sessionGetter;
-    ReleaseSRWLockShared(&g_lock);
-    if (index == nullptr || idle == nullptr || session == nullptr) {
-        return output;
-    }
-    output.resolved = true;
-    // Called outside the lock and guarded, because these bodies are obfuscated Client code and
-    // run before the Client has published the globals they read on an early frame.
-    __try {
-        const std::int32_t controlled = index();
-        if (controlled >= 0) {
-            output.idleMs = idle(controlled);
-            output.idleValid = true;
-        }
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        output.idleValid = false;
-    }
-    __try {
-        output.sessionMs = session();
-        output.sessionValid = true;
-    } __except (EXCEPTION_EXECUTE_HANDLER) {
-        output.sessionValid = false;
-    }
+    AcquireSRWLockExclusive(&g_lock);
+    g_timersWanted = true;
+    const Timers output = g_timers;
+    ReleaseSRWLockExclusive(&g_lock);
     return output;
 }
 

+ 7 - 24
Sunrise/src/client/hooks/inactivity/inactivity_override.h

@@ -7,44 +7,27 @@
 
 namespace sunrise::client::hooks::inactivity {
 
-/** What the override reached, which is what a lane not taking says. */
+/** What the Client is holding in its lanes now, read back rather than assumed. */
 struct Status {
-    /** Read back rather than assumed, so a hold that never reached the Client still reads true. */
     std::array<std::uint32_t, client::inactivity::kActivityCount> live{};
-    /** Address of the activity config object, or zero until the Client publishes one. */
-    std::uintptr_t address{};
-    /** Set once the config getter has been found in the image. */
-    bool resolved{};
-    /** Set once the Client's own lanes have been read back. */
-    bool captured{};
     bool liveValid{};
-    /** Zero is meaningful: it is the value that stops the Client gating on it at all. */
+    /** Session grace. Zero stops the Client gating on it at all. */
     std::uint32_t liveGraceMs{};
     bool liveGraceValid{};
 };
 
-/**
- * The Client's own two clocks, read through the same getters it uses.
- *
- * A lane times out when idle passes its milliseconds, and nothing times out at all until the
- * session passes the grace.
- */
+/** The Client's own two clocks. A lane fires once idle passes its milliseconds. */
 struct Timers {
     /** Input resets this, so it does not track the session and the two can diverge widely. */
     std::uint64_t idleMs{};
     std::uint64_t sessionMs{};
-    bool resolved{};
     bool idleValid{};
     bool sessionValid{};
 };
 
 /**
- * Reads the Client's idle and session clocks.
- *
- * Every call enters Client code, so this is deliberately kept out of poll(): a caller pays for it
- * only while it is displaying the result, and nothing pays for it otherwise. A caller that draws
- * every frame does call it every frame. It writes nothing, and reports nothing when install could
- * not resolve the getters.
+ * Reads the clocks poll() last sampled and asks it to sample again next frame.
+ * A sample enters Client code, so it is taken from poll() and never from a draw.
  * @return The clocks, with a validity flag for each.
  */
 [[nodiscard]] Timers timers() noexcept;
@@ -59,8 +42,8 @@ struct Timers {
 void uninstall() noexcept;
 
 /**
- * Holds the configured milliseconds in the activity config object, or puts back the ones the
- * Client authored. Call once a frame from any steady tick.
+ * Holds the configured milliseconds, or puts back the ones the Client authored.
+ * Enters Client code, so call it once a frame from a tick that holds no lock.
  */
 void poll() noexcept;
 

+ 55 - 41
Sunrise/src/client/hooks/membership_probe/membership_probe.cpp

@@ -38,15 +38,14 @@ using patterns::signature_length;
 constexpr std::string_view kReceiveText = "40 55 53 41 56 41 57 48 8D AC 24 ? ? ? ? B8 C8 96 05 00";
 constexpr auto kReceive = signature<signature_length(kReceiveText)>(kReceiveText);
 
-/** Status word the handler writes. `RE/25 "The +304 status word, mapped"` owns its bits. */
+/** Status word the handler writes, one bit per membership step. */
 constexpr std::size_t kStatusWordOffset = 304;
 /** Membership header. Its leading qword is the member key the client matches itself by. */
 constexpr std::size_t kMembershipHeaderOffset = 27696;
 /**
  * The two slot axes and the printable label the constructor builds from them.
- * Axis 1 is PRIVATE or PUBLIC, axis 2 is CURRENT or TARGET, and the slot map indexes them as
- * `axis1 + 2 * axis2`. A TARGET slot is index 2 or 3, which the public-first current-slot pick
- * never reads -- so the label says outright which readers can ever see this client.
+ * Axis 1 is PRIVATE or PUBLIC, axis 2 is CURRENT or TARGET, indexed as `axis1 + 2 * axis2`. A
+ * TARGET slot is index 2 or 3, which the public-first current-slot pick never reads.
  */
 constexpr std::size_t kSlotAxisOneOffset = 24;
 constexpr std::size_t kSlotAxisTwoOffset = 28;
@@ -87,16 +86,31 @@ constexpr std::uint64_t kSampleIntervalMs = 2'000;
 /** Clients the probe tracks at once. One private and one public target is the live shape. */
 constexpr std::size_t kTrackedCapacity = 4;
 
-using Receive = char(__fastcall*)(std::int64_t, std::int64_t, int);
+using Receive = char(__fastcall*)(const std::byte*, std::int64_t, int);
 
 /** One ActivityClient seen carrying a membership body, sampled until its window closes. */
 struct Tracked {
-    std::int64_t client{};
+    const std::byte* client{};
     std::uint64_t expiresAt{};
     std::uint64_t nextSample{};
     bool occupied{};
 };
 
+/**
+ * Reads one field out of the client.
+ * @param client ActivityClient.
+ * @param offset Byte offset of the field.
+ * @return The field's value.
+ */
+template <typename T> [[nodiscard]] T field(const std::byte* client, std::size_t offset) noexcept {
+    return *reinterpret_cast<const T*>(client + offset);
+}
+
+/** @param client ActivityClient. @return Its address, for a log line. */
+[[nodiscard]] unsigned long long address_of(const std::byte* client) noexcept {
+    return static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(client));
+}
+
 hooking::detour::Handle g_handle{};
 std::atomic_bool g_installed{false};
 /** The detour runs on the client's network thread and the sampler on the callback pump. */
@@ -109,19 +123,18 @@ std::array<Tracked, kTrackedCapacity> g_tracked{};
  * @param before Status word before the call.
  * @param after Status word after it.
  */
-void report(std::int64_t client, std::uint16_t before, std::uint16_t after) noexcept {
+void report(const std::byte* client, std::uint16_t before, std::uint16_t after) noexcept {
     std::array<char, core::log::kLineCapacity> line{};
-    const std::uint64_t memberKey =
-        *reinterpret_cast<const std::uint64_t*>(client + kMembershipHeaderOffset);
-    const auto axisOne = *reinterpret_cast<const std::uint32_t*>(client + kSlotAxisOneOffset);
-    const auto axisTwo = *reinterpret_cast<const std::uint32_t*>(client + kSlotAxisTwoOffset);
+    const auto memberKey = field<std::uint64_t>(client, kMembershipHeaderOffset);
+    const auto axisOne = field<std::uint32_t>(client, kSlotAxisOneOffset);
+    const auto axisTwo = field<std::uint32_t>(client, kSlotAxisTwoOffset);
     const auto* label = reinterpret_cast<const char*>(client + kSlotLabelOffset);
     const int written = std::snprintf(line.data(),
                                       line.size(),
                                       "ev=probe stage=msg12 result=received client=0x%llX "
                                       "slot=%u label=%.*s member=0x%016llX "
                                       "status=0x%04X->0x%04X flag=%u",
-                                      static_cast<unsigned long long>(client),
+                                      address_of(client),
                                       axisOne + 2U * axisTwo,
                                       static_cast<int>(kSlotLabelCapacity),
                                       label,
@@ -142,19 +155,17 @@ void report(std::int64_t client, std::uint16_t before, std::uint16_t after) noex
  * of the public-first pick, so between them they name which reader can ever see this client.
  * @param client ActivityClient.
  */
-void report_bind_inputs(std::int64_t client) noexcept {
+void report_bind_inputs(const std::byte* client) noexcept {
     std::array<char, core::log::kLineCapacity> line{};
-    const auto established =
-        *reinterpret_cast<const std::uint64_t*>(client + kEstablishedSessionOffset);
-    const auto slotRecord = *reinterpret_cast<const std::uint64_t*>(client + kSlotRecordOffset);
-    const auto rosterContainer =
-        *reinterpret_cast<const std::uint64_t*>(client + kRosterContainerOffset);
-    const auto receipt = *reinterpret_cast<const std::uint8_t*>(client + kBindReceiptOffset);
+    const auto established = field<std::uint64_t>(client, kEstablishedSessionOffset);
+    const auto slotRecord = field<std::uint64_t>(client, kSlotRecordOffset);
+    const auto rosterContainer = field<std::uint64_t>(client, kRosterContainerOffset);
+    const auto receipt = field<std::uint8_t>(client, kBindReceiptOffset);
     const int written = std::snprintf(line.data(),
                                       line.size(),
                                       "ev=probe stage=bind client=0x%llX established=0x%016llX "
                                       "slotrec=0x%llX roster=0x%llX receipt=%u",
-                                      static_cast<unsigned long long>(client),
+                                      address_of(client),
                                       static_cast<unsigned long long>(established),
                                       static_cast<unsigned long long>(slotRecord),
                                       static_cast<unsigned long long>(rosterContainer),
@@ -167,7 +178,7 @@ void report_bind_inputs(std::int64_t client) noexcept {
 }
 
 /** @param client ActivityClient. @return Entity-slot bits it holds but has not applied. */
-[[nodiscard]] std::size_t pending_slots(std::int64_t client) noexcept {
+[[nodiscard]] std::size_t pending_slots(const std::byte* client) noexcept {
     const auto* mask = reinterpret_cast<const std::uint8_t*>(client + kPendingMaskOffset);
     std::size_t count = 0;
     for (std::size_t index = 0; index < kPendingMaskSize; ++index) {
@@ -177,7 +188,7 @@ void report_bind_inputs(std::int64_t client) noexcept {
 }
 
 /** Opens or refreshes the sampling window for one client. */
-void track(std::int64_t client, std::uint64_t now) noexcept {
+void track(const std::byte* client, std::uint64_t now) noexcept {
     AcquireSRWLockExclusive(&g_lock);
     Tracked* free = nullptr;
     for (Tracked& entry : g_tracked) {
@@ -197,14 +208,17 @@ void track(std::int64_t client, std::uint64_t now) noexcept {
 }
 
 /** Reads the status word, defers to the original, then reads it again. */
-char __fastcall receive(std::int64_t client, std::int64_t body, int size) noexcept {
+char __fastcall receive(const std::byte* client, std::int64_t body, int size) noexcept {
     auto* original = reinterpret_cast<Receive>(g_handle.original);
-    if (original == nullptr || client == 0) {
-        return original != nullptr ? original(client, body, size) : 0;
+    if (original == nullptr) {
+        return 0;
     }
-    const auto before = *reinterpret_cast<const std::uint16_t*>(client + kStatusWordOffset);
+    if (client == nullptr) {
+        return original(client, body, size);
+    }
+    const auto before = field<std::uint16_t>(client, kStatusWordOffset);
     const char result = original(client, body, size);
-    const auto after = *reinterpret_cast<const std::uint16_t*>(client + kStatusWordOffset);
+    const auto after = field<std::uint16_t>(client, kStatusWordOffset);
     report(client, before, after);
     report_bind_inputs(client);
     track(client, GetTickCount64());
@@ -215,20 +229,20 @@ char __fastcall receive(std::int64_t client, std::int64_t body, int size) noexce
  * Reports what one client did with its grant after the message.
  * @param client ActivityClient.
  */
-void sample(std::int64_t client) noexcept {
+void sample(const std::byte* client) noexcept {
     std::array<char, core::log::kLineCapacity> line{};
-    const auto status = *reinterpret_cast<const std::uint16_t*>(client + kStatusWordOffset);
-    const auto dirty = *reinterpret_cast<const std::uint8_t*>(client + kGrantDirtyOffset);
-    const int written = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=probe stage=grant client=0x%llX status=0x%04X "
-        "pending=%zu dirty=%u receipt=%u",
-        static_cast<unsigned long long>(client),
-        static_cast<unsigned>(status),
-        pending_slots(client),
-        static_cast<unsigned>(dirty),
-        static_cast<unsigned>(*reinterpret_cast<const std::uint8_t*>(client + kBindReceiptOffset)));
+    const auto status = field<std::uint16_t>(client, kStatusWordOffset);
+    const auto dirty = field<std::uint8_t>(client, kGrantDirtyOffset);
+    const int written =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=probe stage=grant client=0x%llX status=0x%04X "
+                      "pending=%zu dirty=%u receipt=%u",
+                      address_of(client),
+                      static_cast<unsigned>(status),
+                      pending_slots(client),
+                      static_cast<unsigned>(dirty),
+                      static_cast<unsigned>(field<std::uint8_t>(client, kBindReceiptOffset)));
     if (written > 0) {
         core::log::write(core::log::Channel::client,
                          core::log::Level::info,
@@ -278,7 +292,7 @@ void service(std::uint64_t now) noexcept {
     }
     // Copied under the lock, sampled outside it: a read walks 1024 bytes and must not hold a lock
     // the detour needs on the client's own thread.
-    std::array<std::int64_t, kTrackedCapacity> due{};
+    std::array<const std::byte*, kTrackedCapacity> due{};
     std::size_t count = 0;
     AcquireSRWLockExclusive(&g_lock);
     for (Tracked& entry : g_tracked) {

+ 8 - 16
Sunrise/src/client/inactivity/inactivity_settings_store.h

@@ -11,23 +11,18 @@ namespace sunrise::client::inactivity {
 inline constexpr std::size_t kActivityCount = 14;
 
 /**
- * Shortest timeout offered, in milliseconds.
- *
- * The Client will not time any lane out until the session has outlived its own grace, which is
- * around a minute on this build and which this module does not write. A shorter lane could not
- * fire any sooner, so offering one would only look like a hold that is not working. A file
- * carrying a smaller value is clamped up to this rather than refused.
+ * Shortest timeout offered, in milliseconds. A file carrying less is clamped up, not refused.
+ * The matchmade lanes cannot fire until the session outlives the grace, about a minute here, so
+ * a shorter value would only look like a hold that is not working.
  */
 inline constexpr std::uint32_t kMinimumTimeoutMs = 60000;
 /** Longest timeout offered, in milliseconds. A day outlasts any session. */
 inline constexpr std::uint32_t kMaximumTimeoutMs = 86400000;
 
 /**
- * The orbit lane.
- *
- * A timeout that fires in orbit drops the session, and this Client cannot establish another one:
- * the next screen is a marrionberry error and the process has to be restarted. The lane is held
- * at its longest whenever the hold is on, and no field or file value reaches it.
+ * The orbit lane. It is the one lane the Client reads without the grace gate, so a short value
+ * fires at once, drops the session and leaves a marionberry error the process cannot recover
+ * from. Held at its longest whenever the hold is on, and no field or file value reaches it.
  */
 inline constexpr std::size_t kOrbitLane = 13;
 
@@ -69,13 +64,10 @@ inline constexpr std::array<ActivityInfo, kActivityCount> kActivities{{
 /** Compiled lanes a fresh install holds. */
 inline constexpr std::array<std::uint32_t, kActivityCount> kDefaultTimeouts = longest_timeouts();
 
-
 /**
  * Runtime inactivity configuration. This module owns it; Core settings do not carry it.
- *
- * The two switches are exclusive, because they describe opposite behaviour: one removes every
- * timeout and the other replaces each with a chosen one. Neither set leaves the Client's own
- * timeouts in place.
+ * The two switches are exclusive: one removes every timeout, the other replaces each with a
+ * chosen one. Neither leaves the Client's own timeouts in place.
  */
 struct Settings {
     /** Milliseconds per lane, in block order. Held only while custom is set. */

+ 4 - 2
Sunrise/src/client/ui/player/player_panel.cpp

@@ -18,6 +18,7 @@ namespace {
 namespace inactivity = client::inactivity;
 namespace toggle = core::ui::components::toggle;
 
+/** Grid width. Seven columns lays the fourteen lanes out in two rows. */
 constexpr int kLaneColumns = 7;
 
 /**
@@ -96,8 +97,8 @@ void draw_inactivity_clocks(const hooks::inactivity::Status& status) noexcept {
     if (!status.liveGraceValid) {
         return;
     }
-    const bool passed = status.liveGraceMs == 0
-                        || (timers.sessionValid && timers.sessionMs > status.liveGraceMs);
+    const bool passed =
+        status.liveGraceMs == 0 || (timers.sessionValid && timers.sessionMs > status.liveGraceMs);
     ImGui::SameLine();
     // Reported, never written.
     const double grace = static_cast<double>(status.liveGraceMs) / 1000.0;
@@ -108,6 +109,7 @@ void draw_inactivity_clocks(const hooks::inactivity::Status& status) noexcept {
     }
 }
 
+/** Draws the inactivity section: the main switch, the lane grid and the Client's clocks. */
 void draw_inactivity() noexcept {
     inactivity::Settings configured = inactivity::get();
     // Taken once, so every line below and the grid all describe the same poll.

+ 1 - 0
Sunrise/src/core/settings/settings_runtime.cpp

@@ -181,6 +181,7 @@ void report_upgrade(bool stored) noexcept {
  * @return The same text with any BOM removed.
  */
 [[nodiscard]] std::string_view without_byte_order_mark(std::string_view document) noexcept {
+    // UTF-8 byte order mark. An editor writes it and the parser must not see it.
     constexpr std::string_view kMark = "\xEF\xBB\xBF";
     return document.starts_with(kMark) ? document.substr(kMark.size()) : document;
 }

+ 3 - 7
Sunrise/src/core/settings/state/account_rows_parser.cpp

@@ -306,13 +306,9 @@ bool Parser::character(state::CharacterState& output) noexcept {
             }
         } else if (key == "movement_ability" || key == "grenade_ability" || key == "super_ability"
                    || key == "melee_ability" || key == "class_ability") {
-            // Deliberately ignored on load: the subclass screen's first paint each login reads
-            // whatever the game's own UI initializes itself to before any interaction, which is
-            // always the ability-entry struct defaults below, not whatever State last committed.
-            // Restoring a persisted non-default pick here would leave that first paint showing
-            // something different from what is actually equipped until the player made any
-            // change and forced a redraw. Resetting every login keeps the two in sync from the
-            // start; the value is still written back out (see the writer), just never read back.
+            // Deliberately ignored on load. The subclass screen's first paint each login shows
+            // the ability-entry struct defaults below, so restoring a persisted pick would leave
+            // that paint disagreeing with what is equipped. Still written out, never read back.
             if (!skip_value(0)) {
                 return false;
             }

+ 2 - 0
Sunrise/src/core/settings/state/account_settings_parser.cpp

@@ -25,6 +25,8 @@ bool Parser::account_settings(state::account::settings::AccountSettings& output)
         return false;
     }
     std::bitset<static_cast<std::size_t>(Group::count)> supplied;
+    // Tracked apart from the bitset because this key is optional: an enumerated group is required
+    // by the supplied.all() below, and an older settings file does not carry it.
     bool hasKeyBindingSource = false;
     const auto mark = [&supplied](Group group) noexcept {
         const std::size_t index = static_cast<std::size_t>(group);

+ 6 - 7
Sunrise/src/core/settings/state/display_parser.cpp

@@ -1,14 +1,9 @@
 #include <bitset>
+#include <limits>
 
 #include "../parser.h"
 
 namespace sunrise::core::settings::parser {
-namespace {
-
-/** DXGI accepts sync intervals from immediate presentation through every fourth refresh. */
-constexpr std::uint64_t kMaximumVerticalSyncInterval = 4;
-
-} // namespace
 
 /** Parses screen and renderer settings under stable Sunrise-owned names. */
 bool Parser::display_settings(state::account::settings::Display& output) noexcept {
@@ -26,6 +21,8 @@ bool Parser::display_settings(state::account::settings::Display& output) noexcep
         return false;
     }
     std::bitset<static_cast<std::size_t>(Field::count)> supplied;
+    // Tracked apart from the bitset because these two keys are optional: an enumerated field is
+    // required by the supplied.all() below, and an older settings file does not carry them.
     bool hasVerticalSyncInterval = false;
     bool hasFieldOfView = false;
     const auto mark = [&supplied](Field field) noexcept {
@@ -58,8 +55,10 @@ bool Parser::display_settings(state::account::settings::Display& output) noexcep
             }
         } else if (key == "vertical_sync_interval") {
             std::uint64_t value = 0;
+            // Bounds the narrowing only. The 0 to 4 presentation domain is checked with the rest
+            // of the account settings, so it is stated once.
             if (hasVerticalSyncInterval || !unsigned_integer(value)
-                || value > kMaximumVerticalSyncInterval) {
+                || value > (std::numeric_limits<std::uint8_t>::max)()) {
                 return false;
             }
             output.verticalSyncInterval = static_cast<std::uint8_t>(value);

+ 1 - 1
Sunrise/src/core/ui/theme/sunrise_ui_theme.cpp

@@ -109,7 +109,7 @@ void apply() noexcept {
     const float scale = scaling::dpi::current();
     style.ScaleAllSizes(scale);
     // ScaleAllSizes truncates this one to a whole number, so any factor below 1 zeroes it and the
-    // cursor draws with no area. Below the authored geometry it holds its authored size instead.
+    // cursor draws with no area. Held at 1 instead, which is the size it is authored at.
     style.MouseCursorScale = (std::max)(1.0F, style.MouseCursorScale);
     style.FontSizeBase = fontSizeBase;
     style.FontScaleMain = scale;

+ 5 - 0
Sunrise/src/middleware/content/packages/tables/item_definition_reader.cpp

@@ -280,6 +280,11 @@ void read_plug_block(std::span<const std::byte> definition, Row& row) noexcept {
     }
 }
 
+/**
+ * Reads the investment stat block into the row.
+ * @param definition Whole item definition record.
+ * @param row Receives the stats, left empty when the block is absent or malformed.
+ */
 void read_stats(std::span<const std::byte> definition, Row& row) noexcept {
     row.statCount = 0;
     std::int64_t blockRelative = 0;

+ 3 - 8
Sunrise/src/middleware/content/packages/tables/items.h

@@ -99,19 +99,14 @@ void read_appearance(std::span<const std::byte> definition, Row& row) noexcept;
 using AllowedPlugVisitor = bool (*)(void* context, std::uint32_t itemDefinitionIndex) noexcept;
 
 /**
- * Visits the embedded, reusable, and randomized plug-list members declared for one socket
- * lane.
+ * Visits the embedded, reusable, and randomized plug-list members declared for one socket lane.
  * The initial plug is a separate fixed field and is intentionally left to the caller.
- *
  * @param definition Whole base-item definition bytes.
- * @param plugSetTable Whole shared plug-set
- * definition table from investment-root slot 51.
+ * @param plugSetTable Whole shared plug-set definition table from investment-root slot 51.
  * @param lane Ordinary socket lane to inspect.
- *
  * @param visitor Required bounded consumer.
  * @param context Opaque consumer state.
- * @return
- * True when every referenced array is structurally valid and accepted by the visitor.
+ * @return True when every referenced array is structurally valid and accepted by the visitor.
  */
 [[nodiscard]] bool visit_allowed_plugs(std::span<const std::byte> definition,
                                        std::span<const std::byte> plugSetTable,

+ 3 - 5
Sunrise/src/middleware/datagen/character_record/appearance/character_appearance_abilities.cpp

@@ -42,11 +42,9 @@ bool apply_ability_buckets(const state::CharacterState& character,
         }
         if (!state::build_data::find_ability_buckets(
                 detail.socketEntryListIndex, selection_of(*subclassItem), published)) {
-            // The domain has not caught up with this selection yet (a fresh pick invalidates it
-            // until the next investment refresh slice republishes). Publish empty buckets for
-            // this one encode, the same as a character with no subclass equipped, instead of
-            // failing the whole record: a hard failure here aborts the entire Family-0/3 snapshot
-            // and surfaces as a request error even though the underlying selection did commit.
+            // The domain has not caught up with this selection yet. Publish empty buckets for
+            // this encode, like a character with no subclass, instead of failing: a hard failure
+            // aborts the whole Family-0/3 snapshot even though the selection did commit.
             return true;
         }
         for (std::size_t bucket = 0; bucket < appearance.abilityBuckets.size(); ++bucket) {

+ 2 - 3
Sunrise/src/middleware/datagen/family4/account/preferences/preferences_encoder.cpp

@@ -11,9 +11,8 @@ constexpr std::uint16_t kUnboundInputCode = 0x0074;
 constexpr std::int32_t kOpenSeedVersion = 0;
 /**
  * Seed version 1 closes a gate so the client keeps the replicated values behind it.
- * The keybinding gate now stays closed because Sunrise supplies the modeled preferences behind it.
- * The post-processing gate stays closed, or local cvars would overwrite its 3 replicated fields on
- * every sign-in.
+ * The keybinding gate stays closed because Sunrise supplies the modeled preferences. The
+ * post-processing gate stays closed, or local cvars overwrite its 3 replicated fields each login.
  */
 constexpr std::int32_t kClosedSeedVersion = 1;
 /** Source 0 makes later input reads use the replicated keybinding array. */

+ 7 - 9
Sunrise/src/middleware/datagen/family4/loadout/subclass_socket_selection.cpp

@@ -57,11 +57,9 @@ void resolve_socket_states(
     SubclassSelection selection{};
     subclass_selection(item, characterClass, selection);
 
-    // A group of 2 or 3 entries (grenade, movement, class ability) is an ordinary set of mutually
-    // exclusive alternatives: exactly one is meant to light up. An Attunement's group is far
-    // wider (it packs several 4-node options into one group id), so a population past the widest
-    // single bundle is the signal that this group's members activate in same-sized runs rather
-    // than as lone alternatives.
+    // A group of 2 or 3 entries is mutually exclusive alternatives: exactly one lights up. An
+    // Attunement's group packs several 4-node options into one group id, so a population past the
+    // widest single bundle means its members activate in same-sized runs.
     std::array<std::uint16_t, build_socket_lists::kEntryCapacity> groupPopulation{};
     for (std::size_t index = 0; index < definition.entryCount; ++index) {
         const std::uint8_t group = entries.entries[index].group;
@@ -90,12 +88,12 @@ void resolve_socket_states(
             continue;
         }
         // A pick can bundle several consecutive entries under the same group, all publishing
-        // together (an Attunement's melee, plus the passive nodes it carries with it). Siblings
-        // normally carry their own distinct plug source, so force the whole contiguous run active
-        // rather than relying on the plug-source match below to find them.
+        // together. Siblings carry their own plug source, so force the whole contiguous run
+        // active rather than relying on the plug-source match below.
         forcedActive[selected.entry] = true;
         for (std::size_t offset = 1;
-             offset < state::kMaxAttunementBundleSize && selected.entry + offset < definition.entryCount
+             offset < state::kMaxAttunementBundleSize
+             && selected.entry + offset < definition.entryCount
              && entries.entries[selected.entry + offset].group == entry.group;
              ++offset) {
             forcedActive[selected.entry + offset] = true;

+ 6 - 8
Sunrise/src/middleware/datagen/family4/loadout/subclass_socket_selection.h

@@ -27,10 +27,9 @@ struct SubclassSelection {
 };
 
 /**
- * Builds the selection for one subclass item. Only sprint is fixed; the grenade, super, melee,
- * movement and class entries are that item's own authored choices (each owned subclass remembers
- * its own picks independently), and the bucket the class ability publishes into follows the
- * owning character's class.
+ * Builds the selection for one subclass item. Only sprint is fixed; grenade, super, melee,
+ * movement and class entries are that item's own authored choices, and the class-ability bucket
+ * follows the owning character's class.
  * @param item Authored subclass item carrying its own ability choices.
  * @param characterClass Owning character's class, which the class-ability bucket follows.
  * @param output Receives the 6 selected entries.
@@ -41,11 +40,10 @@ void subclass_selection(const state::account::inventory::Item& item,
 
 /**
  * Resolves one item's socket-entry states and selector lanes.
- * Only a list that carries a super lane belongs to a subclass, so every other item keeps its
- * absent and ready states and publishes no selector.
+ * Only a list carrying a super lane belongs to a subclass, so every other item keeps its absent
+ * and ready states and publishes no selector.
  * @param definition Installed socket-entry-list mapping.
- * @param item Authored item being resolved; only its own ability choices matter when it is a
- * subclass.
+ * @param item Authored item being resolved; only a subclass carries ability choices.
  * @param characterClass Owning character's class, which the class-ability bucket follows.
  * @param acquiredSubclassAbilityMask Owning character's runtime acquired-entry mask.
  * @param output Receives the state of every fixed lane.

+ 4 - 6
Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp

@@ -213,9 +213,8 @@ bool process(const ServiceRoute& route,
             web_service::mutation_if<state::PendingItemDismantle>(webOutcome);
         if (equipmentSwap != nullptr) {
             // Equip is an optimistic Character-screen action. Its status-pair value is the exact
-            // Family-4 revision whose following Queuez frame makes the action authoritative. Stage
-            // that revision before encoding the reply so the Client cannot complete the action
-            // against the old object store.
+            // Family-4 revision whose following Queuez frame makes it authoritative. Stage that
+            // revision before encoding the reply, or the Client completes against the old store.
             auto& transaction = outcome.transaction.emplace<EquipmentSwapTransaction>();
             if (!queuez::stage_equipment_swap(
                     queuezState, equipmentSwap->characterSoid, transaction.update)) {
@@ -416,9 +415,8 @@ bool process(const ServiceRoute& route,
         }
         if (itemDismantle != nullptr) {
             // Dismantle is another optimistic Character-screen action. Promise only the exact
-            // Family-4 revision that carries both the character after-image and the empty
-            // item-instance release descriptor; otherwise retain the generic sentinel reply and
-            // publish no removal.
+            // Family-4 revision carrying both the character after-image and the empty release
+            // descriptor; otherwise keep the generic sentinel reply and publish no removal.
             auto& transaction = outcome.transaction.emplace<ItemDismantleTransaction>();
             if (!queuez::stage_item_dismantle(queuezState,
                                               itemDismantle->accountSoid,

+ 8 - 8
Sunrise/src/server/bap/encrypted/internal.h

@@ -429,14 +429,14 @@ append_socket_plug_notification(Scratch& scratch,
                                 std::size_t& written) noexcept;
 
 /** Appends the opcode-801 Family-4 subclass item-instance upsert. */
-[[nodiscard]] bool append_subclass_selection_notification(
-    Scratch& scratch,
-    const queuez::SubclassSelection& selection,
-    const state::PendingSubclassSelection& mutation,
-    std::span<const std::byte, state::kAesKeySize> key,
-    std::span<const std::byte, state::kBapNonceSize> nonce,
-    std::span<std::byte> response,
-    std::size_t& written) noexcept;
+[[nodiscard]] bool
+append_subclass_selection_notification(Scratch& scratch,
+                                       const queuez::SubclassSelection& selection,
+                                       const state::PendingSubclassSelection& mutation,
+                                       std::span<const std::byte, state::kAesKeySize> key,
+                                       std::span<const std::byte, state::kBapNonceSize> nonce,
+                                       std::span<std::byte> response,
+                                       std::size_t& written) noexcept;
 
 /** Appends a Family-4 character upsert plus newly acquired item-instance upsert. */
 [[nodiscard]] bool

+ 2 - 3
Sunrise/src/server/bap/encrypted/push/activity/activity_keepalive_push.cpp

@@ -137,9 +137,8 @@ bool consume_activity_keepalive(Session& session,
         // and the citizen advertisement inside the body is already gated on `privateCurrent`.
         //
         // It still owes one membership body. The client's msg 12 handler is the only writer of the
-        // flag that binds a world container to this ActivityClient, and until that bind lands the
-        // entity-slot grant sent at join has no view to reach:
-        // `RE/31 "A grant reaches a view only through a bound world container"`.
+        // flag that binds a world container to this ActivityClient, and a grant reaches a view
+        // only through a bound container, so until that bind lands the join grant has no view.
         //
         // Exactly one body per binding. The flag the client sets is one-way, it never acknowledges
         // one on this link, and a link that joined a session it did not allocate never reports a

+ 13 - 14
Sunrise/src/server/bap/encrypted/push/queuez/queuez_banner_push.cpp

@@ -216,10 +216,9 @@ bool append_banner_notification(Scratch& scratch,
 }
 
 /**
- * Appends the family-zero pair that follows an opcode-504 pick.
- * The Client holds the objIdx-1 buffer for one character at a time, allocated from the character
- * the anchor names, so the pair moves with the pick or the banner keeps the old emblem. A pick
- * naming the character the pair already holds republishes it in place.
+ * Appends the family-zero pair that follows an opcode-504 pick. The Client holds the objIdx-1
+ * buffer for one character at a time, so the pair moves with the pick or the banner keeps the old
+ * emblem. A pick on the character it already holds republishes in place.
  * @param scratch Lock-owned transform buffers.
  * @param before Queuez state after the family-four move.
  * @param selectedCharacter Character the pick named.
@@ -378,6 +377,7 @@ bool append_subclass_appearance_refresh_notification(
     std::array<std::byte, state::kBapNonceSize>& nonce,
     std::span<std::byte> response,
     std::size_t& written) noexcept {
+    // Index of the subclass slot in the authored equipment array.
     constexpr std::size_t kSubclassSlot =
         static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass);
     if (!mutation.prepared || mutation.characterSoid != refresh.characterSoid
@@ -388,8 +388,7 @@ bool append_subclass_appearance_refresh_notification(
         return false;
     }
     state::build_data::items::details::Definition detail{};
-    if (!state::build_data::find_configured_item_detail(
-            mutation.subclassDefinitionIndex, detail)
+    if (!state::build_data::find_configured_item_detail(mutation.subclassDefinitionIndex, detail)
         || detail.definitionIndex != mutation.subclassDefinitionIndex
         || detail.definitionHash != mutation.subclassDefinitionHash
         || !detail.equipmentSlot.has_value() || *detail.equipmentSlot < 0
@@ -460,14 +459,14 @@ bool append_socket_roster_refresh_notification(Scratch& scratch,
 }
 
 /** Appends the Family-3 character-only refresh owed by a subclass selection. */
-bool append_subclass_roster_refresh_notification(
-    Scratch& scratch,
-    const queuez::RosterAppearanceRefresh& refresh,
-    const state::PendingSubclassSelection& mutation,
-    std::span<const std::byte, state::kAesKeySize> key,
-    std::array<std::byte, state::kBapNonceSize>& nonce,
-    std::span<std::byte> response,
-    std::size_t& written) noexcept {
+bool append_subclass_roster_refresh_notification(Scratch& scratch,
+                                                 const queuez::RosterAppearanceRefresh& refresh,
+                                                 const state::PendingSubclassSelection& mutation,
+                                                 std::span<const std::byte, state::kAesKeySize> key,
+                                                 std::array<std::byte, state::kBapNonceSize>& nonce,
+                                                 std::span<std::byte> response,
+                                                 std::size_t& written) noexcept {
+    // Index of the subclass slot in the authored equipment array.
     constexpr std::size_t kSubclassSlot =
         static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass);
     if (!mutation.prepared || refresh.includeRoster

+ 1 - 2
Sunrise/src/server/bap/encrypted/push/queuez/queuez_select_character.cpp

@@ -180,8 +180,7 @@ bool append_subclass_selection_notification(Scratch& scratch,
     }
     const std::size_t objectCount = prepared.family.objects.size();
     const std::size_t beforeBytes = written;
-    if (objectCount != 1
-        || prepared.family.objects.front().id != selection.itemInstanceDefinitionId
+    if (objectCount != 1 || prepared.family.objects.front().id != selection.itemInstanceDefinitionId
         || prepared.family.objects.front().version != selection.subclassInstanceSoid
         || prepared.family.objects.front().encoding != middleware::queuez::Encoding::oodle
         || prepared.family.objects.front().payload.empty()

+ 11 - 28
Sunrise/src/server/bap/encrypted/push/snapshot/internal.h

@@ -103,15 +103,11 @@ inline constexpr std::size_t kSingleObjectCount = 1;
 
 /**
  * Builds the Family-4 character upsert for one prepared equipment swap.
- * @param scratch
- * Object and compression storage owned by the lock.
- * @param swap Checked queuez version
- * after-image and resident character definition.
- * @param mutation Checked State after-image that
- * is not committed yet.
+ * @param scratch Object and compression storage owned by the lock.
+ * @param swap Checked queuez version after-image and resident character definition.
+ * @param mutation Checked State after-image that is not committed yet.
  * @param prepared Gets the single character upsert descriptor.
- * @return
- * True when the after-image encodes and the complete object fits.
+ * @return True when the after-image encodes and the complete object fits.
  */
 [[nodiscard]] bool prepare_equipment_swap(Scratch& scratch,
                                           const queuez::EquipmentSwap& swap,
@@ -126,9 +122,7 @@ inline constexpr std::size_t kSingleObjectCount = 1;
 
 /**
  * Builds the Family-4 item-instance upsert for one prepared ordinary-socket selection.
- * The
- * character object is unchanged because item identity, placement, and mutation generation are
- *
+ * The character object is unchanged because item identity, placement and mutation generation are
  * preserved; the socket block lives entirely in the resident instance object.
  */
 [[nodiscard]] bool prepare_socket_plug(Scratch& scratch,
@@ -144,17 +138,11 @@ inline constexpr std::size_t kSingleObjectCount = 1;
 
 /**
  * Builds one Family-4 increment containing the newly resident item object followed by the
- *
  * changed character that references it.
- * @param scratch Object and compression storage owned by
- * the lock.
+ * @param scratch Object and compression storage owned by the lock.
  * @param acquisition Exact queuez after-image promised by the correlated response.
- *
  * @param mutation Checked State after-image that remains uncommitted while output is staged.
- *
- * @param prepared Gets the two complete upsert descriptors in item-then-character dependency
- *
- * order.
+ * @param prepared Gets the two upsert descriptors in item-then-character dependency order.
  * @return True when both after-image objects encode and fit atomically.
  */
 [[nodiscard]] bool prepare_item_acquisition(Scratch& scratch,
@@ -175,16 +163,11 @@ prepare_profile_item_acquisition(Scratch& scratch,
 
 /**
  * Builds one Family-4 increment containing the changed character and released item instance.
- *
  * @param scratch Object and compression storage owned by the lock.
- * @param dismantle Exact queuez
- * after-image promised by the correlated response.
- * @param mutation Checked State after-image
- * that remains uncommitted while output is staged.
- * @param prepared Gets the character upsert
- * followed by the empty release descriptor.
- * @return True when the character after-image and
- * complete two-operation update fit atomically.
+ * @param dismantle Exact queuez after-image promised by the correlated response.
+ * @param mutation Checked State after-image that remains uncommitted while output is staged.
+ * @param prepared Gets the character upsert followed by the empty release descriptor.
+ * @return True when the character after-image and two-operation update fit atomically.
  */
 [[nodiscard]] bool prepare_item_dismantle(Scratch& scratch,
                                           const queuez::ItemDismantle& dismantle,

+ 2 - 6
Sunrise/src/server/bap/encrypted/queuez/definition.h

@@ -19,12 +19,8 @@ inline constexpr std::uint32_t kAccountFamilyType = 4;
 inline constexpr std::int32_t kInitialFamilyVersion = 0;
 /**
  * Family four holds account, character, one id per character-owned item, and one id per
- *
- * resident-backed mod or shader stack. Profile currency rows deliberately carry no instance
- *
- * SOID; the fixed addition covers all 50 native rows in each supported action-source bucket.
- * It
- * matches the snapshot descriptor size, so a snapshot that builds always stages.
+ * resident-backed mod or shader stack. Profile currency rows carry no instance SOID; the fixed
+ * addition covers all 50 native rows in each supported action-source bucket.
  */
 inline constexpr std::size_t kResidentCapacity =
     2 + state::kCharacterCapacity * middleware::datagen::family4::loadout::kItemCapacity

+ 23 - 19
Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp

@@ -172,12 +172,9 @@ void report_repush(const char* stage, std::size_t bytes) noexcept {
 }
 
 /**
- * Re-derives the selected character's appearance and roster records once the ability-bucket
- * rebuild owed by a subclass selection has had time to land.
- * The refresh sent inline with the opcode-801 response can still carry stale or empty ability
- * buckets, because that rebuild runs asynchronously off the Client content-extraction pump. This
- * reuses the same generic resync builders the cross-peer refresh uses, since both just need to
- * re-derive the two records from whatever State holds right now.
+ * Re-derives the selected character's appearance and roster once the ability-bucket rebuild owed
+ * by a subclass selection has landed. The refresh sent inline with the opcode-801 response can
+ * still carry empty buckets, because that rebuild runs off the Client content-extraction pump.
  * @param session Auth, nonce and queuez state owned by the connection.
  * @param scratch Transform buffers owned by the lock.
  * @param response Whole-frame storage owned by the caller.
@@ -193,7 +190,11 @@ void report_repush(const char* stage, std::size_t bytes) noexcept {
     if (!session.abilityRefreshArmed || GetTickCount64() < session.abilityRefreshDueTick) {
         return false;
     }
-    session.abilityRefreshArmed = false;
+    // Nothing is owed until a family that reads abilities is subscribed. The arm stays set, the
+    // same way the banner re-push below keeps its own.
+    if (!session.queuez.family0Active && !session.queuez.family3Active) {
+        return false;
+    }
     touchesScratch = true;
 
     auto nextSendNonce = session.sendNonce;
@@ -203,12 +204,12 @@ void report_repush(const char* stage, std::size_t bytes) noexcept {
     if (current.family0Active) {
         queuez::SessionState appearanceAfter{};
         if (push::append_account_resync_appearance_notification(scratch,
-                                                                 current,
-                                                                 state::bap().sessionKey,
-                                                                 nextSendNonce,
-                                                                 scratch.framed,
-                                                                 framedSize,
-                                                                 appearanceAfter)) {
+                                                                current,
+                                                                state::bap().sessionKey,
+                                                                nextSendNonce,
+                                                                scratch.framed,
+                                                                framedSize,
+                                                                appearanceAfter)) {
             current = appearanceAfter;
             wrote = true;
         }
@@ -216,12 +217,12 @@ void report_repush(const char* stage, std::size_t bytes) noexcept {
     if (current.family3Active) {
         queuez::SessionState rosterAfter{};
         if (push::append_account_resync_roster_notification(scratch,
-                                                             current,
-                                                             state::bap().sessionKey,
-                                                             nextSendNonce,
-                                                             scratch.framed,
-                                                             framedSize,
-                                                             rosterAfter)) {
+                                                            current,
+                                                            state::bap().sessionKey,
+                                                            nextSendNonce,
+                                                            scratch.framed,
+                                                            framedSize,
+                                                            rosterAfter)) {
             current = rosterAfter;
             wrote = true;
         }
@@ -236,6 +237,9 @@ void report_repush(const char* stage, std::size_t bytes) noexcept {
     written = framedSize;
     session.sendNonce = nextSendNonce;
     session.queuez = current;
+    // The frame is committed here, so the arm is committed with it. Disarming any earlier drops
+    // the owed refresh on a transient encode failure.
+    session.abilityRefreshArmed = false;
     report_repush("ability_refresh", framedSize);
     return true;
 }

+ 11 - 20
Sunrise/src/server/bap/encrypted/queuez/queuez_outcome_staging.cpp

@@ -83,9 +83,8 @@ bool stage_service_outcome(Scratch& scratch,
             armsAbilityRefresh = true;
         }
         // Family four drives inventory placement, while Family zero owns the rendered appearance
-        // consumed by the open cosmetic panels and world player. Its resident character record is
-        // updated in place: releasing and re-adding the same key tears down the ship/banner
-        // binding.
+        // the cosmetic panels and world player consume. Its resident character record is updated
+        // in place: releasing and re-adding the same key tears down the ship/banner binding.
         if (after.family0Active) {
             CharacterAppearanceRefresh refresh{};
             if (!stage_character_appearance_refresh(
@@ -144,10 +143,9 @@ bool stage_service_outcome(Scratch& scratch,
         middleware::secure_channel::advance_nonce(nonce);
         after = update.after;
     } else if (subclassSelection != nullptr) {
-        // Body processing already staged the exact +1 revision promised by opcode 801. Publish
-        // the resident subclass instance upsert, then the character-summary appearance and
-        // roster refreshes so gameplay's ability read picks up the new selection immediately
-        // instead of waiting on the next unrelated poll.
+        // Body processing already staged the exact +1 revision opcode 801 promised. The instance
+        // upsert goes first, then the appearance and roster refreshes, so gameplay reads the new
+        // selection now rather than on the next unrelated poll.
         const SubclassSelection& selection = subclassSelection->update;
         bool preservedManifest =
             selection.after.family4ResidentCount == before.family4ResidentCount;
@@ -165,18 +163,12 @@ bool stage_service_outcome(Scratch& scratch,
         if (!valid(selection.after) || !preservedManifest || targetMatches != 1
             || selection.accountSoid != subclassSelection->pending.accountSoid
             || selection.characterSoid != subclassSelection->pending.characterSoid
-            || selection.subclassInstanceSoid
-                   != subclassSelection->pending.subclassInstanceSoid
+            || selection.subclassInstanceSoid != subclassSelection->pending.subclassInstanceSoid
             || selection.after.family4RootSoid != before.family4RootSoid
             || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
             || selection.after.family4Version != before.family4Version + 1
-            || !push::append_subclass_selection_notification(scratch,
-                                                              selection,
-                                                              subclassSelection->pending,
-                                                              key,
-                                                              nonce,
-                                                              response,
-                                                              written)) {
+            || !push::append_subclass_selection_notification(
+                scratch, selection, subclassSelection->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=subclass_select result=fail");
@@ -184,10 +176,9 @@ bool stage_service_outcome(Scratch& scratch,
         }
         middleware::secure_channel::advance_nonce(nonce);
         after = selection.after;
-        // The rebuild that repopulates the invalidated ability buckets runs asynchronously, off
-        // the Client content-extraction pump, so the two refreshes below can still race it and
-        // carry stale or empty buckets. A delayed re-derivation is owed regardless of whether they
-        // do.
+        // The ability-bucket rebuild runs off the Client content-extraction pump, so the two
+        // refreshes below can race it and carry empty buckets. The delayed re-derivation is owed
+        // either way.
         armsAbilityRefresh = true;
         // A subclass is always equipped, so both the appearance and roster ability reads are
         // always owed a refresh once one is active.

+ 1 - 3
Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h

@@ -160,9 +160,7 @@ namespace sunrise::server::bap::encrypted::queuez {
 /**
  * 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,
- * or is appended exactly once when Collections creates the row. Currency/material rows keep a
- * zero SOID and preserve the manifest.
- *
+ * or is appended once when Collections creates it. Currency rows keep a zero SOID.
  * @param before Current active peer state.
  * @param accountSoid Account root receiving the profile stack.
  * @param acquiredInstanceSoid Profile action-source key, or zero for a non-actionable stack.

+ 5 - 6
Sunrise/src/server/bap/encrypted/queuez/staging/queuez_character_staging.cpp

@@ -310,9 +310,8 @@ bool stage_subclass_selection(const SessionState& before,
     std::uint32_t characterDefinitionId = 0;
     std::uint32_t itemInstanceDefinitionId = 0;
     if (!valid(before) || !before.family4Active || before.family4RootSoid == 0 || accountSoid == 0
-        || accountSoid != before.family4RootSoid || characterSoid == 0
-        || subclassInstanceSoid == 0 || characterSoid == subclassInstanceSoid
-        || before.family4ResidentCount == 0
+        || accountSoid != before.family4RootSoid || characterSoid == 0 || subclassInstanceSoid == 0
+        || characterSoid == subclassInstanceSoid || before.family4ResidentCount == 0
         || before.family4ResidentCount > before.family4Residents.size()
         || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
         || !middleware::datagen::object_id(
@@ -333,9 +332,9 @@ bool stage_subclass_selection(const SessionState& before,
                                                    && object.definitionId == accountDefinitionId);
         characterMatches += static_cast<std::size_t>(
             object.objectSoid == characterSoid && object.definitionId == characterDefinitionId);
-        targetMatches += static_cast<std::size_t>(
-            object.objectSoid == subclassInstanceSoid
-            && object.definitionId == itemInstanceDefinitionId);
+        targetMatches +=
+            static_cast<std::size_t>(object.objectSoid == subclassInstanceSoid
+                                     && object.definitionId == itemInstanceDefinitionId);
     }
     if (accountMatches != 1 || characterMatches != 1 || targetMatches != 1) {
         return false;

+ 5 - 6
Sunrise/src/server/bap/encrypted/transactions/service_outcome_commit.cpp

@@ -182,9 +182,9 @@ bool commit(ServiceOutcome& outcome, Publication& publication) noexcept {
         return state::matchmaking::commit(*mutation);
     }
     if (auto* transaction = transaction_if<EquipmentSwapTransaction>(outcome)) {
-        const bool isSubclassSlot = transaction->pending.equipmentSlotIndex
-                                    == static_cast<std::size_t>(
-                                        state::account::inventory::EquipmentSlot::subclass);
+        const bool isSubclassSlot =
+            transaction->pending.equipmentSlotIndex
+            == static_cast<std::size_t>(state::account::inventory::EquipmentSlot::subclass);
         const bool committed = state::commit_equipment_swap(transaction->pending);
         core::log::write(core::log::Channel::server,
                          committed ? core::log::Level::debug : core::log::Level::warn,
@@ -192,9 +192,8 @@ bool commit(ServiceOutcome& outcome, Publication& publication) noexcept {
                                    : "ev=equip stage=transaction_commit result=fail");
         if (committed && isSubclassSlot) {
             // The equipped subclass just changed, which makes the published ability buckets
-            // stale the same way an ability-entry pick does; wake the investment worker so the
-            // character screen does not keep showing the previous subclass's resolution until
-            // some unrelated pump happens to refresh it.
+            // stale the same way an ability-entry pick does. Wake the investment worker so the
+            // character screen stops showing the previous subclass's resolution.
             client::content::investment::worker::request_slice();
         }
         return committed;

+ 5 - 9
Sunrise/src/server/bap/internal.h

@@ -121,9 +121,8 @@ struct Session {
     std::uint64_t activityRosterDueTick{};
     /**
      * Binding generation whose membership body this link has already delivered.
-     * The client sets its own membership flag once and never clears it, and never acknowledges a
-     * body on a public-target link, so a send condition has to be a one-shot per binding rather
-     * than a revision or acknowledgement gate. Latched on delivery, never on encode.
+     * The client sets its membership flag once and never clears it, and never acknowledges a body
+     * on a public-target link, so this is a one-shot per binding. Latched on delivery, not encode.
      */
     std::uint64_t activityMembershipSentGeneration{};
     /**
@@ -174,12 +173,9 @@ struct Session {
     /** True while another peer's account mutation still needs a full local refresh. */
     bool accountResyncArmed{};
     /**
-     * Tick count after which the owed ability-icon refresh may go out.
-     * A subclass selection invalidates the published ability buckets; the rebuild that restores
-     * them runs asynchronously off the Client content-extraction pump, so the appearance and
-     * roster refresh sent inline with the selection response can still carry the stale or empty
-     * buckets. This second, delayed refresh re-derives both records from committed State once the
-     * rebuild has had time to land.
+     * Tick count after which the owed ability-icon refresh may go out. A subclass selection
+     * invalidates the published ability buckets and the rebuild runs off the Client
+     * content-extraction pump, so the inline refresh can carry empty ones; this one re-derives.
      */
     std::uint64_t abilityRefreshDueTick{};
     /** True while one ability-icon refresh is still owed to this peer. */

+ 67 - 78
Sunrise/src/server/gameplay/peer/peer_transport.cpp

@@ -23,6 +23,7 @@ namespace sunrise::server::gameplay::peer {
 
 namespace {
 
+namespace gp = state::gameplay;
 namespace wire = middleware::gameplay::peer;
 namespace bits = middleware::encoding::bits;
 
@@ -33,7 +34,7 @@ namespace bits = middleware::encoding::bits;
  * @param payload Payload bytes.
  * @return True when one of the two carried it.
  */
-[[nodiscard]] bool send_transport(const state::gameplay::Endpoint& to,
+[[nodiscard]] bool send_transport(const gp::Endpoint& to,
                                   std::span<const std::byte> payload) noexcept {
     return dtls::send_payload(to, payload) || association::send_payload(to, payload);
 }
@@ -49,7 +50,7 @@ constexpr std::uint32_t kByteMask = 0xFF;
 /** Delay sentinel used until a round trip has been measured. */
 constexpr std::uint16_t kDelaySentinel = 1023;
 /** Packet sequences are published as ten bits. */
-constexpr std::uint16_t kPacketSequenceModulus = state::gameplay::kPacketSequenceModulus;
+constexpr std::uint16_t kPacketSequenceModulus = gp::kPacketSequenceModulus;
 /** Sequence the first packet to a peer carries, because the head advances before it is written. */
 constexpr std::uint16_t kFirstPacketSequence = 1;
 /** Smallest head-minus-cursor the peer accepts. This host keeps at most one packet in flight. */
@@ -63,22 +64,19 @@ constexpr std::size_t kMessageReportCapacity = 8;
 constexpr std::uint64_t kResendInterval = 250;
 
 SRWLOCK g_lock{SRWLOCK_INIT};
-std::array<state::gameplay::PeerLink, state::gameplay::kAssociationCapacity> g_peers;
+std::array<gp::PeerLink, gp::kAssociationCapacity> g_peers;
 /** Channel ids this host hands out. The peer refuses one that does not increase. */
 std::uint32_t g_channelId{0};
 
 /** @return True when both endpoints name the same address and port. */
-[[nodiscard]] bool same_endpoint(const state::gameplay::Endpoint& left,
-                                 const state::gameplay::Endpoint& right) noexcept {
+[[nodiscard]] bool same_endpoint(const gp::Endpoint& left, const gp::Endpoint& right) noexcept {
     return left.address == right.address && left.port == right.port;
 }
 
 /** @return Peer for one endpoint, or null. Callers already hold the lock. */
-[[nodiscard]] state::gameplay::PeerLink*
-find_locked(const state::gameplay::Endpoint& from) noexcept {
-    for (state::gameplay::PeerLink& peer : g_peers) {
-        if (peer.stage != state::gameplay::PeerStage::absent
-            && same_endpoint(peer.endpoint, from)) {
+[[nodiscard]] gp::PeerLink* find_locked(const gp::Endpoint& from) noexcept {
+    for (gp::PeerLink& peer : g_peers) {
+        if (peer.stage != gp::PeerStage::absent && same_endpoint(peer.endpoint, from)) {
             return &peer;
         }
     }
@@ -86,8 +84,7 @@ find_locked(const state::gameplay::Endpoint& from) noexcept {
 }
 
 /** @return True when the link carries one group session. Callers hold the lock. */
-[[nodiscard]] bool carries_locked(const state::gameplay::PeerLink& peer,
-                                  std::uint64_t sessionId) noexcept {
+[[nodiscard]] bool carries_locked(const gp::PeerLink& peer, std::uint64_t sessionId) noexcept {
     for (const std::uint64_t held : peer.sessions) {
         if (held == sessionId) {
             return true;
@@ -97,12 +94,12 @@ find_locked(const state::gameplay::Endpoint& from) noexcept {
 }
 
 /** @return Link carrying one group session, or null. Callers hold the lock. */
-[[nodiscard]] state::gameplay::PeerLink* find_session_locked(std::uint64_t sessionId) noexcept {
+[[nodiscard]] gp::PeerLink* find_session_locked(std::uint64_t sessionId) noexcept {
     if (sessionId == 0) {
         return nullptr;
     }
-    for (state::gameplay::PeerLink& peer : g_peers) {
-        if (peer.stage != state::gameplay::PeerStage::absent && carries_locked(peer, sessionId)) {
+    for (gp::PeerLink& peer : g_peers) {
+        if (peer.stage != gp::PeerStage::absent && carries_locked(peer, sessionId)) {
             return &peer;
         }
     }
@@ -115,7 +112,7 @@ find_locked(const state::gameplay::Endpoint& from) noexcept {
  * @param peer Link the message arrived on.
  * @return The session id, or zero when the link carries none or several.
  */
-[[nodiscard]] std::uint64_t sole_session_locked(const state::gameplay::PeerLink& peer) noexcept {
+[[nodiscard]] std::uint64_t sole_session_locked(const gp::PeerLink& peer) noexcept {
     std::uint64_t only = 0;
     for (const std::uint64_t held : peer.sessions) {
         if (held == 0) {
@@ -134,18 +131,18 @@ find_locked(const state::gameplay::Endpoint& from) noexcept {
  * @param from Peer endpoint.
  * @return The session id, or zero when it cannot be resolved.
  */
-[[nodiscard]] std::uint64_t session_for_endpoint(const state::gameplay::Endpoint& from) noexcept {
+[[nodiscard]] std::uint64_t session_for_endpoint(const gp::Endpoint& from) noexcept {
     AcquireSRWLockShared(&g_lock);
-    const state::gameplay::PeerLink* const peer = find_locked(from);
+    const gp::PeerLink* const peer = find_locked(from);
     const std::uint64_t only = peer == nullptr ? 0 : sole_session_locked(*peer);
     ReleaseSRWLockShared(&g_lock);
     return only;
 }
 
 /** @return A free peer slot, or null. Callers already hold the lock. */
-[[nodiscard]] state::gameplay::PeerLink* allocate_locked() noexcept {
-    for (state::gameplay::PeerLink& peer : g_peers) {
-        if (peer.stage == state::gameplay::PeerStage::absent) {
+[[nodiscard]] gp::PeerLink* allocate_locked() noexcept {
+    for (gp::PeerLink& peer : g_peers) {
+        if (peer.stage == gp::PeerStage::absent) {
             return &peer;
         }
     }
@@ -154,7 +151,7 @@ find_locked(const state::gameplay::Endpoint& from) noexcept {
 
 /** Fills the address blob that names this host on the direct path. */
 void local_address(std::array<std::byte, wire::kAddressBlobSize>& output) noexcept {
-    const state::gameplay::Endpoint advertised = endpoint::advertised();
+    const gp::Endpoint advertised = endpoint::advertised();
     middleware::gameplay::descriptor::write_net_addr(advertised.address, advertised.port, output);
 }
 
@@ -178,7 +175,7 @@ void local_address(std::array<std::byte, wire::kAddressBlobSize>& output) noexce
  * @param remoteChannelId Channel id the request carried. The link must still hold it.
  * @param body Both channel ids.
  */
-void answer_establish(const state::gameplay::Endpoint& to,
+void answer_establish(const gp::Endpoint& to,
                       std::uint32_t remoteChannelId,
                       const wire::ConnectEstablish& body) noexcept {
     std::array<std::byte, kReplyCapacity> buffer{};
@@ -190,7 +187,7 @@ void answer_establish(const state::gameplay::Endpoint& to,
     }
     AcquireSRWLockExclusive(&g_lock);
     // The endpoint's link. A channel the peer has retired has no link of its own to answer on.
-    state::gameplay::PeerLink* peer = find_locked(to);
+    gp::PeerLink* peer = find_locked(to);
     const bool queued =
         peer != nullptr && peer->remoteConnectionSequence == remoteChannelId
         && wire::enqueue_message(peer->outbound,
@@ -216,7 +213,7 @@ void answer_establish(const state::gameplay::Endpoint& to,
  * @param request Decoded request body.
  * @param now Monotonic tick count.
  */
-void answer_connect(const state::gameplay::Endpoint& from,
+void answer_connect(const gp::Endpoint& from,
                     const wire::ConnectRequest& request,
                     std::uint64_t now) noexcept {
     wire::ConnectResponse response{};
@@ -228,7 +225,7 @@ void answer_connect(const state::gameplay::Endpoint& from,
     AcquireSRWLockExclusive(&g_lock);
     // Keyed by endpoint. The client holds one channel per host peer, so a second link would stamp
     // packets with a channel id the client has already retired.
-    state::gameplay::PeerLink* peer = find_locked(from);
+    gp::PeerLink* peer = find_locked(from);
     // A repeat of the same request is a retransmission and leaves the link alone. A different
     // channel or sequence is a new incarnation the peer built without announcing the teardown.
     const bool rebuilt = peer != nullptr
@@ -237,15 +234,13 @@ void answer_connect(const state::gameplay::Endpoint& from,
     if (peer == nullptr) {
         peer = allocate_locked();
     }
-    const bool fresh =
-        peer != nullptr && (peer->stage == state::gameplay::PeerStage::absent || rebuilt);
+    const bool fresh = peer != nullptr && (peer->stage == gp::PeerStage::absent || rebuilt);
     if (fresh) {
         // The sessions outlive the channel. The client rebuilds one channel under every group
         // session it holds and rejoins none of them, so dropping them here strands each one.
-        const std::array<std::uint64_t, state::gameplay::kSessionsPerLink> held =
-            peer->stage == state::gameplay::PeerStage::absent
-                ? std::array<std::uint64_t, state::gameplay::kSessionsPerLink>{}
-                : peer->sessions;
+        const std::array<std::uint64_t, gp::kSessionsPerLink> held =
+            peer->stage == gp::PeerStage::absent ? std::array<std::uint64_t, gp::kSessionsPerLink>{}
+                                                 : peer->sessions;
         *peer = {};
         peer->sessions = held;
         peer->endpoint = from;
@@ -267,7 +262,7 @@ void answer_connect(const state::gameplay::Endpoint& from,
         peer->remoteAddressPresent = true;
         // A retransmission must not move an established link back a stage.
         if (fresh) {
-            peer->stage = state::gameplay::PeerStage::connecting;
+            peer->stage = gp::PeerStage::connecting;
         }
         peer->lastTick = now;
         response.channelId = peer->localConnectionSequence;
@@ -311,15 +306,14 @@ void answer_connect(const state::gameplay::Endpoint& from,
  * @param sessionId Session the join request named.
  * @return True when a link now carries that session.
  */
-[[nodiscard]] bool bind_session(const state::gameplay::Endpoint& from,
-                                std::uint64_t sessionId) noexcept {
+[[nodiscard]] bool bind_session(const gp::Endpoint& from, std::uint64_t sessionId) noexcept {
     if (sessionId == 0) {
         return false;
     }
     AcquireSRWLockExclusive(&g_lock);
     // The endpoint's link, whatever it already carries. A join for a second region arrives on the
     // same channel as the first, and out of band when that channel is still being rebuilt.
-    state::gameplay::PeerLink* const peer = find_locked(from);
+    gp::PeerLink* const peer = find_locked(from);
     const char* result = "nolink";
     bool bound = false;
     std::uint32_t channel = 0;
@@ -356,7 +350,7 @@ void answer_connect(const state::gameplay::Endpoint& from,
  * @param from Requesting endpoint.
  * @param request Decoded admission prefix.
  */
-void answer_join(const state::gameplay::Endpoint& from, const wire::JoinRequest& request) noexcept {
+void answer_join(const gp::Endpoint& from, const wire::JoinRequest& request) noexcept {
     const std::uint64_t hostSession = endpoint::identity().onlineSessionId;
     wire::RefuseReason reason = wire::RefuseReason::notFound;
     if (wire::admit(request, hostSession, reason)) {
@@ -425,8 +419,7 @@ void answer_join(const state::gameplay::Endpoint& from, const wire::JoinRequest&
  * @param reader Reader positioned at the ping body.
  * @return True when the body read, whether or not the reply left the endpoint.
  */
-[[nodiscard]] bool answer_ping(const state::gameplay::Endpoint& from,
-                               bits::Reader& reader) noexcept {
+[[nodiscard]] bool answer_ping(const gp::Endpoint& from, bits::Reader& reader) noexcept {
     wire::PingBody ping{};
     if (!wire::read_ping(reader, ping)) {
         return false;
@@ -452,7 +445,7 @@ void answer_join(const state::gameplay::Endpoint& from, const wire::JoinRequest&
  * @param payload Whole decrypted payload.
  * @param now Monotonic tick count.
  */
-void consume_container(const state::gameplay::Endpoint& from,
+void consume_container(const gp::Endpoint& from,
                        std::span<const std::byte> payload,
                        std::uint64_t now) noexcept {
     bits::Reader reader(payload);
@@ -541,7 +534,7 @@ void consume_container(const state::gameplay::Endpoint& from,
  * @param peer Peer receiving the packet.
  * @param sequence Sequence the packet published.
  */
-void record_sequence(state::gameplay::PeerLink& peer, std::uint16_t sequence) noexcept {
+void record_sequence(gp::PeerLink& peer, std::uint16_t sequence) noexcept {
     if (!peer.ringInitialized) {
         peer.ringInitialized = true;
         peer.receiveHead = sequence;
@@ -550,13 +543,12 @@ void record_sequence(state::gameplay::PeerLink& peer, std::uint16_t sequence) no
     }
     // Add the modulus before subtracting. A bare difference is signed and goes negative on a wrap.
     const std::uint16_t advance = static_cast<std::uint16_t>(
-        (sequence + state::gameplay::kPacketSequenceModulus - peer.receiveHead)
-        % state::gameplay::kPacketSequenceModulus);
-    if (advance == 0 || advance >= state::gameplay::kPacketSequenceHalf) {
+        (sequence + gp::kPacketSequenceModulus - peer.receiveHead) % gp::kPacketSequenceModulus);
+    if (advance == 0 || advance >= gp::kPacketSequenceHalf) {
         // A repeat or an older packet leaves the published history alone.
         return;
     }
-    std::array<bool, state::gameplay::kAckHistory> shifted{};
+    std::array<bool, gp::kAckHistory> shifted{};
     for (std::size_t index = 0; index < shifted.size(); ++index) {
         // Entry `index` is the packet `index + 1` before the new head, so the old head lands at
         // `advance - 1`. Anything newer than the old head and older than this packet was skipped.
@@ -579,12 +571,11 @@ void record_sequence(state::gameplay::PeerLink& peer, std::uint16_t sequence) no
  * @param peer Peer that sent it, held under the lock.
  * @param message Reassembled message and its inner header.
  */
-void apply_message(state::gameplay::PeerLink& peer,
-                   const wire::AssembledMessage& message) noexcept {
+void apply_message(gp::PeerLink& peer, const wire::AssembledMessage& message) noexcept {
     if (message.id == static_cast<std::uint8_t>(wire::ConnectId::establish)
-        && peer.stage == state::gameplay::PeerStage::connecting) {
+        && peer.stage == gp::PeerStage::connecting) {
         // The reliable establish is what moves a connected peer past the out-of-band pair.
-        peer.stage = state::gameplay::PeerStage::connected;
+        peer.stage = gp::PeerStage::connected;
     }
 }
 
@@ -594,14 +585,14 @@ void apply_message(state::gameplay::PeerLink& peer,
  * @param ack Acknowledgement state the packet published.
  * @return True when this acknowledgement emptied the queue.
  */
-bool apply_acknowledgement(state::gameplay::PeerLink& peer, const wire::AckState& ack) noexcept {
+bool apply_acknowledgement(gp::PeerLink& peer, const wire::AckState& ack) noexcept {
     if (!peer.outbound.awaitingAcknowledgement
         || !wire::acknowledgement_covers(ack, peer.outbound.sentInPacket)) {
         return false;
     }
     // The peer has the packet, so every fragment in it is delivered. The next sequence is kept
     // because message sequences continue across messages.
-    for (state::gameplay::OutboundFragment& fragment : peer.outbound.fragments) {
+    for (gp::OutboundFragment& fragment : peer.outbound.fragments) {
         fragment = {};
     }
     peer.outbound.count = 0;
@@ -615,7 +606,7 @@ bool apply_acknowledgement(state::gameplay::PeerLink& peer, const wire::AckState
  * @param payload Whole decrypted payload.
  * @param now Monotonic tick count.
  */
-void consume_established(const state::gameplay::Endpoint& from,
+void consume_established(const gp::Endpoint& from,
                          std::span<const std::byte> payload,
                          std::uint64_t now) noexcept {
     wire::EstablishedPacket packet{};
@@ -638,7 +629,7 @@ void consume_established(const state::gameplay::Endpoint& from,
     bool guardAccepted = false;
     std::uint8_t expectedGuard = 0;
     AcquireSRWLockExclusive(&g_lock);
-    state::gameplay::PeerLink* peer = find_locked(from);
+    gp::PeerLink* peer = find_locked(from);
     std::array<wire::AssembledMessage, kMessageReportCapacity> bodies{};
     if (peer != nullptr) {
         peerFound = true;
@@ -700,7 +691,7 @@ void consume_established(const state::gameplay::Endpoint& from,
             continue;
         }
         // Group handling runs outside the lock because answering takes it again.
-        bits::Reader reader({body.bytes.data(), state::gameplay::kReassemblyCapacity});
+        bits::Reader reader({body.bytes.data(), gp::kReassemblyCapacity});
         if (reader.skip(body.bodyBitOffset)
             && !group::consume(from, sessionId, body.id, reader, now)) {
             report(core::log::Level::debug,
@@ -733,7 +724,7 @@ void consume_established(const state::gameplay::Endpoint& from,
  * @param peer Peer state copied under the lock before the send.
  * @return True when the packet left the endpoint.
  */
-[[nodiscard]] bool send_acknowledgement(const state::gameplay::PeerLink& peer) noexcept {
+[[nodiscard]] bool send_acknowledgement(const gp::PeerLink& peer) noexcept {
     wire::AckState ack{};
     ack.outboundHead = peer.outboundHead;
     ack.outboundHeadPresent = peer.outboundHeadPresent;
@@ -762,7 +753,7 @@ void consume_established(const state::gameplay::Endpoint& from,
 } // namespace
 
 /** Consumes one decrypted transport payload. */
-void deliver(const state::gameplay::Endpoint& from,
+void deliver(const gp::Endpoint& from,
              std::span<const std::byte> payload,
              std::uint64_t now) noexcept {
     if (payload.empty()) {
@@ -776,7 +767,7 @@ void deliver(const state::gameplay::Endpoint& from,
 }
 
 /** Sends one already-encoded out-of-band body in its own container. */
-bool send_container(const state::gameplay::Endpoint& to,
+bool send_container(const gp::Endpoint& to,
                     std::uint8_t id,
                     std::uint32_t declaredSize,
                     std::span<const std::byte> body,
@@ -811,7 +802,7 @@ bool enqueue_reliable(std::uint64_t sessionId,
                       std::span<const std::byte> body,
                       std::size_t bodyBits) noexcept {
     AcquireSRWLockExclusive(&g_lock);
-    state::gameplay::PeerLink* peer = find_session_locked(sessionId);
+    gp::PeerLink* peer = find_session_locked(sessionId);
     const bool queued =
         peer != nullptr && wire::enqueue_message(peer->outbound, id, declaredSize, body, bodyBits);
     if (queued) {
@@ -826,9 +817,9 @@ bool enqueue_reliable(std::uint64_t sessionId,
 
 /** Reports the NetAddr one peer sent in its own connect request. */
 bool remote_address(std::uint64_t sessionId,
-                    std::array<std::byte, state::gameplay::kNetAddrBlobSize>& output) noexcept {
+                    std::array<std::byte, gp::kNetAddrBlobSize>& output) noexcept {
     AcquireSRWLockShared(&g_lock);
-    const state::gameplay::PeerLink* peer = find_session_locked(sessionId);
+    const gp::PeerLink* peer = find_session_locked(sessionId);
     const bool present = peer != nullptr && peer->remoteAddressPresent;
     if (present) {
         output = peer->remoteAddress;
@@ -838,12 +829,11 @@ bool remote_address(std::uint64_t sessionId,
 }
 
 /** Binds one peer's view signature. */
-void bind_view(const state::gameplay::Endpoint& from,
-               const state::gameplay::ViewSignature& signature) noexcept {
+void bind_view(const gp::Endpoint& from, const gp::ViewSignature& signature) noexcept {
     AcquireSRWLockExclusive(&g_lock);
     // Keyed by endpoint, not by session: the view body carries no session id, and a link holding
     // both a current and a target region resolves no sole session to key it by.
-    state::gameplay::PeerLink* peer = find_locked(from);
+    gp::PeerLink* peer = find_locked(from);
     if (peer != nullptr) {
         peer->view = signature;
     }
@@ -853,20 +843,20 @@ void bind_view(const state::gameplay::Endpoint& from,
 /** Reports whether the link carrying one session holds a bound view and is established. */
 bool view_bound(std::uint64_t sessionId) noexcept {
     AcquireSRWLockShared(&g_lock);
-    const state::gameplay::PeerLink* peer = find_session_locked(sessionId);
+    const gp::PeerLink* peer = find_session_locked(sessionId);
     // A bound body alone is not readiness. The link also has to be past its connect exchange, or
     // the view belongs to a channel the peer has already rebuilt.
     const bool ready =
-        peer != nullptr && peer->view.bound && peer->stage == state::gameplay::PeerStage::connected;
+        peer != nullptr && peer->view.bound && peer->stage == gp::PeerStage::connected;
     ReleaseSRWLockShared(&g_lock);
     return ready;
 }
 
 /** Reports how far the link carrying one group session has got. */
-bool link_stage(std::uint64_t sessionId, state::gameplay::PeerStage& stage) noexcept {
-    stage = state::gameplay::PeerStage::absent;
+bool link_stage(std::uint64_t sessionId, gp::PeerStage& stage) noexcept {
+    stage = gp::PeerStage::absent;
     AcquireSRWLockShared(&g_lock);
-    const state::gameplay::PeerLink* peer = find_session_locked(sessionId);
+    const gp::PeerLink* peer = find_session_locked(sessionId);
     const bool present = peer != nullptr;
     if (present) {
         stage = peer->stage;
@@ -879,7 +869,7 @@ bool link_stage(std::uint64_t sessionId, state::gameplay::PeerStage& stage) noex
 bool link_identity(std::uint64_t sessionId, LinkIdentity& output) noexcept {
     output = {};
     AcquireSRWLockShared(&g_lock);
-    const state::gameplay::PeerLink* peer = find_session_locked(sessionId);
+    const gp::PeerLink* peer = find_session_locked(sessionId);
     const bool present = peer != nullptr;
     if (present) {
         output.localConnectionSequence = peer->localConnectionSequence;
@@ -891,15 +881,15 @@ bool link_identity(std::uint64_t sessionId, LinkIdentity& output) noexcept {
 
 /** Sends any owed acknowledgement. */
 void service(std::uint64_t now) noexcept {
-    std::array<state::gameplay::PeerLink, state::gameplay::kAssociationCapacity> owed{};
+    std::array<gp::PeerLink, gp::kAssociationCapacity> owed{};
     std::size_t count = 0;
     AcquireSRWLockExclusive(&g_lock);
-    for (state::gameplay::PeerLink& peer : g_peers) {
+    for (gp::PeerLink& peer : g_peers) {
         // An unacknowledged send queue keeps the packet going out until the peer confirms it.
         // Every packet burns one sequence, so the resend is paced.
         const bool resendDue = peer.outbound.count != 0 && now - peer.lastSend >= kResendInterval;
         const bool due = peer.acknowledgementOwed || resendDue;
-        if (peer.stage == state::gameplay::PeerStage::absent || !due) {
+        if (peer.stage == gp::PeerStage::absent || !due) {
             continue;
         }
         peer.acknowledgementOwed = false;
@@ -930,7 +920,7 @@ void service(std::uint64_t now) noexcept {
 /** Drops one group session, leaving the link and its other sessions alone. */
 void drop(std::uint64_t sessionId) noexcept {
     AcquireSRWLockExclusive(&g_lock);
-    state::gameplay::PeerLink* const peer = find_session_locked(sessionId);
+    gp::PeerLink* const peer = find_session_locked(sessionId);
     if (peer != nullptr) {
         // The channel outlives the session. A leave names one region, and the client keeps playing
         // the other over the same channel.
@@ -944,11 +934,10 @@ void drop(std::uint64_t sessionId) noexcept {
 }
 
 /** Drops every link at one endpoint, which is what a connect-closed names. */
-void drop_endpoint(const state::gameplay::Endpoint& endpoint) noexcept {
+void drop_endpoint(const gp::Endpoint& endpoint) noexcept {
     AcquireSRWLockExclusive(&g_lock);
-    for (state::gameplay::PeerLink& peer : g_peers) {
-        if (peer.stage != state::gameplay::PeerStage::absent
-            && same_endpoint(peer.endpoint, endpoint)) {
+    for (gp::PeerLink& peer : g_peers) {
+        if (peer.stage != gp::PeerStage::absent && same_endpoint(peer.endpoint, endpoint)) {
             peer = {};
         }
     }
@@ -958,7 +947,7 @@ void drop_endpoint(const state::gameplay::Endpoint& endpoint) noexcept {
 /** Drops every peer. */
 void reset() noexcept {
     AcquireSRWLockExclusive(&g_lock);
-    for (state::gameplay::PeerLink& peer : g_peers) {
+    for (gp::PeerLink& peer : g_peers) {
         peer = {};
     }
     ReleaseSRWLockExclusive(&g_lock);

+ 1 - 1
Sunrise/src/server/gameplay/physics/host/physics_session.cpp

@@ -29,7 +29,7 @@ constexpr std::size_t kSessionCapacity = kWorldCapacity;
 constexpr std::size_t kAdmittedCapacity = 8;
 /** Host-session rows one snapshot reads. */
 constexpr std::size_t kHostRowCapacity = 8;
-/** `RE/49` runs the world at 30 Hz, so one tick is due every 33 ms. */
+/** The world runs at 30 Hz, so one tick is due every 33 ms. */
 constexpr std::uint64_t kTickIntervalMs = 33;
 /** Empty scene scale. Nothing reads it until an actor carries a transform. */
 constexpr float kMillimetersPerUnit = 1000.0F;

+ 18 - 17
Sunrise/src/server/web_service/web_service_actions.cpp

@@ -158,20 +158,21 @@ void report_subclass_selection_response(const middleware::web_service::Message&
                                         const state::PendingSubclassSelection& mutation,
                                         std::span<const std::byte> response) noexcept {
     std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=subclass_select stage=response result=ok opcode=%u transaction=%u "
-        "family_version=%d instance=0x%llX entry=%u bytes=%zu hex=",
-        static_cast<unsigned>(message.opcode),
-        static_cast<unsigned>(message.transactionId),
-        family4Version,
-        static_cast<unsigned long long>(mutation.subclassInstanceSoid),
-        static_cast<unsigned>(mutation.requestedEntry),
-        response.size());
+    const int prefix =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=subclass_select stage=response result=ok opcode=%u transaction=%u "
+                      "family_version=%d instance=0x%llX entry=%u bytes=%zu hex=",
+                      static_cast<unsigned>(message.opcode),
+                      static_cast<unsigned>(message.transactionId),
+                      family4Version,
+                      static_cast<unsigned long long>(mutation.subclassInstanceSoid),
+                      static_cast<unsigned>(mutation.requestedEntry),
+                      response.size());
     if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
         return;
     }
+    // Upper-case hex, which is the form the rest of the log lines use.
     constexpr char kHex[] = "0123456789ABCDEF";
     std::size_t length = static_cast<std::size_t>(prefix);
     for (const std::byte byte : response) {
@@ -304,12 +305,12 @@ void mutate_subclass_selection(const middleware::web_service::Message& message,
     middleware::web_service::messages::opcode801::Request request{};
     if (!middleware::web_service::messages::opcode801::parse_request(message, request)) {
         std::array<char, 128> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=ws801 stage=parse result=fail transaction=%u payload_bytes=%zu",
-            static_cast<unsigned>(message.transactionId),
-            message.payload.size());
+        const int count =
+            std::snprintf(line.data(),
+                          line.size(),
+                          "ev=ws801 stage=parse result=fail transaction=%u payload_bytes=%zu",
+                          static_cast<unsigned>(message.transactionId),
+                          message.payload.size());
         if (count > 0) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,

+ 2 - 4
Sunrise/src/server/web_service/web_service_runtime.cpp

@@ -271,10 +271,8 @@ bool consume(std::span<const std::byte> request,
         && middleware::web_service::messages::opcode206::parse_request(message, subscription);
 
     // The action runs before its reply is encoded, because the reply reports whether it worked.
-    // Each action fills the outcome only once it has prepared its whole transition, so an outcome
-    // still empty after one ran is that action refusing the request. Nothing is published here:
-    // both the prepared mutation and the subscription are handed back for the caller to publish
-    // once the whole response is framed.
+    // An action fills the outcome only once it has prepared its whole transition, so an outcome
+    // still empty afterwards is that action refusing the request. Nothing is published here.
     bool dispatched = true;
     if (message.opcode == middleware::web_service::messages::opcode504::kOpcode) {
         select_character(message, outcome);

+ 3 - 5
Sunrise/src/state/account/account_state.h

@@ -98,8 +98,7 @@ inline constexpr std::uint8_t kDefaultClassAbilityEntry = 2;
 /**
  * Semantic ability-bucket destinations shared by the wire encoder and the selection logic that
  * routes a clicked socket entry to a character field. A subclass entry's authored selector chain,
- * not its table position, decides which of these it reaches; a bundled pick (an Attunement, for
- * example) can freely mix members across them.
+ * not its table position, decides which it reaches; a bundled pick can mix members across them.
  */
 inline constexpr std::uint8_t kGrenadeAbilityBucket = 0;
 inline constexpr std::uint8_t kSuperAbilityBucket = 1;
@@ -147,9 +146,8 @@ struct CharacterState {
     bool contentBypass{};
     /**
      * Runtime-only socket entries the player has selected at least once. Selected entries still
-     * publish active; this mask keeps a later inactive entry acquired instead of new/unclaimed.
-     * EXPERIMENT: defaulted to all-set so every ready entry reads as acquired instead of new, to
-     * test whether the client only allows clicking an already-acquired node.
+     * publish active; this mask keeps a later inactive entry acquired instead of new. Unverified:
+     * defaulted all-set, assuming the Client only allows clicking an already-acquired node.
      */
     std::uint64_t acquiredSubclassAbilityMask{~std::uint64_t{0}};
     /** Authored loadout keyed only by stable semantic equipment slots. */

+ 5 - 8
Sunrise/src/state/account/inventory/inventory_state.h

@@ -85,20 +85,17 @@ struct Item {
     std::int32_t quantity{};
     /**
      * Rising per-character generation assigned whenever this item changes inventory rows. The
-     * Client also uses it as the stable ordering token inside a bucket's grid, so an item's cell
-     * follows its serial rather than its row index. Equip swaps therefore hand the displaced item
-     * the clicked item's prior serial so it takes the clicked cell.
+     * Client also orders a bucket's grid by it, so an equip swap hands the displaced item the
+     * clicked item's prior serial to keep it in the clicked cell.
      */
     std::int32_t mutationSerial{};
     /** Native accumulated item-state bits such as the finisher favorite marker. */
     std::uint32_t flags{};
     Sockets sockets;
     /**
-     * Selected ability-node socket entries. Only meaningful when this item is a subclass; every
-     * other item carries these unused. Kept on the item rather than the character so each owned
-     * subclass remembers its own picks independently instead of sharing one set across all of
-     * them. Defaults match state::kDefault*AbilityEntry (the first option of each group, where
-     * every shipped subclass starts) but are literal here to avoid a circular include.
+     * Selected ability-node socket entries. Only meaningful on a subclass. Kept on the item, not
+     * the character, so each owned subclass remembers its own picks. Defaults match
+     * state::kDefault*AbilityEntry, literal here to avoid a circular include.
      */
     std::uint8_t movementAbilityEntry{4};
     std::uint8_t grenadeAbilityEntry{7};

+ 4 - 9
Sunrise/src/state/build_data/items/socket_plugs/socket_plug_build_data_runtime.cpp

@@ -81,11 +81,8 @@ bool is_socket_plug_pooled(std::uint16_t plugDefinitionIndex) noexcept {
 
 /**
  * Answers whether applying one plug spends a stack the account has to hold.
- *
- * Only a shader is spent: an ornament stays owned once applied. The plug also has to be
- * one an account can come to own, which means Collections can grant it. A socket's default plug
- * is not in Collections and belongs to no stack, so clearing a socket back to it costs nothing
- * and is always available, whatever the item's own factory plug happens to be.
+ * Only a shader is spent; an ornament stays owned. The plug must also be one Collections can
+ * grant, so clearing a socket back to its default costs nothing and is always available.
  */
 bool is_consumed_on_apply(std::uint16_t itemDefinitionIndex, std::uint8_t bucketId) noexcept {
     return bucketId == kShaderBucketId && collectibles::grants_item(itemDefinitionIndex);
@@ -93,10 +90,8 @@ bool is_consumed_on_apply(std::uint16_t itemDefinitionIndex, std::uint8_t bucket
 
 /**
  * Answers whether one installed profile row is a materializable socket action source.
- *
- * Only ornaments and shaders qualify. Mods are bucket 37 and sit outside this model on purpose:
- * they are permanent unlocks rather than stacks, and nothing in the account records which are
- * unlocked, so no answer here could say whether it holds one.
+ * Only ornaments and shaders qualify. Mods are bucket 37 and sit outside this model: they are
+ * permanent unlocks, and nothing in the account records which are unlocked.
  */
 bool is_profile_action_source(std::uint16_t itemDefinitionIndex, std::uint8_t bucketId) noexcept {
     items::details::Definition detail{};

+ 9 - 17
Sunrise/src/state/build_data/runtime.h

@@ -87,12 +87,9 @@ publish_item_definitions(std::span<const items::Definition> definitions) noexcep
 
 /**
  * Finds one installed item by the native dense definition index used by Collections requests.
-
- * * @param definitionIndex Native item-definition row index.
- * @param definition Receives the
- * exact installed mapping.
- * @return True when the complete table is ready and contains the
- * requested row.
+ * @param definitionIndex Native item-definition row index.
+ * @param definition Receives the exact installed mapping.
+ * @return True when the complete table is ready and contains the requested row.
  */
 [[nodiscard]] bool find_item_definition_index(std::uint16_t definitionIndex,
                                               items::Definition& definition) noexcept;
@@ -263,19 +260,15 @@ void invalidate_ability_buckets() noexcept;
 
 /**
  * @return True when at least one socket-entry list's resolved bucket destinations are published.
- * Unlike the ability buckets above, this is never part of the on-disk content cache: it is a
- * small derived table, cheap to recompute, so a warm boot that skips re-extraction (because the
- * ability buckets it gates alongside are already cached) must not leave it permanently empty for
- * the whole session. package_item_rows.cpp checks this independently of ability_buckets_ready()
- * so a warm cache still triggers the one extraction pass this table needs.
+ * Never part of the on-disk content cache: it is small and cheap to recompute, so a warm boot that
+ * skips re-extraction must not leave it empty for the whole session.
  */
 [[nodiscard]] bool socket_entry_buckets_ready() noexcept;
 
 /**
  * Publishes every socket-entry list's resolved per-entry ability-bucket destinations.
- * Purely a derived cache of static content (which of the 12 semantic ability buckets each entry's
- * selector chain reaches), so unlike the ability buckets above it never needs invalidating: a
- * subclass's entry table does not change after content extraction.
+ * A derived cache of static content, so unlike the ability buckets above it never needs
+ * invalidating: a subclass's entry table does not change after content extraction.
  * @param definitions Complete rows, one per socket-entry list that carries a super lane.
  * @return True when the rows pass the checks.
  */
@@ -375,9 +368,8 @@ inline constexpr std::size_t kSubclassGroupSize = 3;
 
 /**
  * Finds the 2 other subclasses that share one character class with a known member.
- * The installed manifest lists every subclass item (any item carrying a socket-entry-list)
- * together as one dense run per class, in native definition-index order, so the run holding a
- * known member gives every other member with no per-class identity of its own to look up.
+ * The installed manifest lists every subclass item as one dense run per class, in native
+ * definition-index order, so the run holding a known member gives every other member.
  * @param memberDefinitionIndex Native item-definition index of one subclass in the class.
  * @param group Receives the 3 member indices, in native definition-index order.
  * @return True when every subclass item was found and `memberDefinitionIndex` is one of them.

+ 1 - 2
Sunrise/src/state/build_data/runtime/build_data_catalog_runtime.cpp

@@ -253,8 +253,7 @@ bool publish_ability_buckets(std::span<const abilities::Definition> definitions)
     }
     // The disk cache already froze every domain at boot, so the transaction above refuses to run.
     // Ability buckets track the player's live subclass selection rather than installed content, so
-    // a later in-session pick still has to update this one domain in memory; it just no longer
-    // takes part in the one-time disk snapshot.
+    // a later in-session pick still updates this one domain in memory.
     if (!abilities::replace(definitions)) {
         return false;
     }

+ 2 - 4
Sunrise/src/state/build_data/socket_entry_buckets/definition.h

@@ -15,10 +15,8 @@ inline constexpr std::uint8_t kNoDestinationBucket = 0xFF;
 
 /**
  * One socket-entry list's resolved ability-bucket destination per entry.
- * A pick's socket-entry-table position does not say which ability slot it fills; only its
- * selector chain does. This is computed once from the installed pool data during content
- * extraction, so runtime code can route a click to the right character field without re-reading
- * raw content.
+ * A pick's table position does not say which slot it fills; only its selector chain does. Computed
+ * once during content extraction so runtime code can route a click without re-reading content.
  */
 struct Definition {
     std::uint16_t socketEntryListIndex{};

+ 7 - 1
Sunrise/src/state/gameplay/physics/physics_state_runtime.cpp

@@ -16,6 +16,8 @@ constexpr std::uint8_t kWireIncarnationMask = kWireIncarnationCount - 1;
 /** The first process-local handle generation follows the cleared zero sentinel. */
 constexpr std::uint64_t kFirstHandleGeneration = 1;
 
+// No member initializers below: MSVC exhausts its heap on the 4 MB g_storage aggregate.
+
 /** One fixed allocator record. Exact owners live in context or peer ticket tables. */
 struct SlotRecord final {
     std::uint64_t actorId;
@@ -78,7 +80,11 @@ struct Storage final {
     bool ready;
 };
 
-Storage g_storage{SRWLOCK_INIT, {}, kFirstHandleGeneration, false};
+// Designated so a reordered member cannot silently shift these values.
+Storage g_storage{.lock = SRWLOCK_INIT,
+                  .contexts = {},
+                  .nextHandleGeneration = kFirstHandleGeneration,
+                  .ready = false};
 
 /** Clears all records but preserves the process-local handle sequence and Windows lock. */
 void clear_storage() noexcept {

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

@@ -16,11 +16,10 @@ namespace sunrise::state {
 [[nodiscard]] bool ensure_profile_item_identities() noexcept;
 
 /**
- * Grants each character the other 2 subclasses of the class its equipped subclass belongs to,
- * placing any missing ones into unequipped inventory with native socket defaults. Idempotent:
- * a subclass already equipped or already sitting in inventory is left alone.
- * @return True when every character with an equipped subclass ends up holding its whole class,
- * or there was nothing to check (no account, or no character carries a subclass yet).
+ * Grants each character the other 2 subclasses of its equipped subclass's class, placing missing
+ * ones into unequipped inventory with native socket defaults. Idempotent: one already equipped or
+ * already in inventory is left alone.
+ * @return True when every such character holds its whole class, or there was nothing to check.
  */
 [[nodiscard]] bool ensure_character_subclasses() noexcept;
 
@@ -45,8 +44,7 @@ struct PendingSubclassSelection {
 /**
  * Prepares one opcode-801 selection against the selected character's exact equipped subclass.
  * The installed socket-entry table maps the request to whichever of the character's 5 authored
- * ability picks currently competes in the same group; no class-specific node indices are
- * authored in State.
+ * picks competes in the same group; no class-specific node indices are authored in State.
  */
 [[nodiscard]] bool prepare_subclass_selection(std::uint64_t subclassInstanceSoid,
                                               std::uint8_t requestedEntry,
@@ -289,12 +287,9 @@ void shutdown() noexcept;
 /**
  * Prepares an equip operation for one unequipped instance on the selected character.
  * An occupied slot is swapped; an empty semantic slot receives the requested item directly.
- *
  * @param requestedInstanceSoid Unequipped item instance selected by the Client.
- * @param mutation
- * Gets the checked after-image without changing account State.
- * @return True when the instance is
- * owned, unequipped, and maps to one supported native equipment slot.
+ * @param mutation Gets the checked after-image without changing account State.
+ * @return True when the instance is owned, unequipped, and maps to one native equipment slot.
  */
 [[nodiscard]] bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid,
                                           PendingEquipmentSwap& mutation) noexcept;
@@ -386,14 +381,11 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 
 /**
  * Prepares removal of one unequipped instance from the selected character.
- *
  * The authored inventory prefix is compacted. Any surviving item whose installed native row
  * changes receives a fresh mutation generation. Equipped items are never accepted.
- *
  * @param instanceSoid Unequipped item-instance key selected by the Client.
  * @param mutation Gets checked before/after images without changing account State.
- * @return True when the instance is uniquely owned by the selected character and both loadouts
- * resolve completely.
+ * @return True when the selected character uniquely owns it and both loadouts resolve.
  */
 [[nodiscard]] bool prepare_item_dismantle(std::uint64_t instanceSoid,
                                           PendingItemDismantle& mutation) noexcept;
@@ -413,11 +405,8 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 
 /**
  * Prepares one exact opcode-903 ordinary-socket selection on a selected-character item.
- *
- * The target may be equipped or unequipped. Native defaults are first materialized into a complete
- * authored socket block, then only the requested lane changes. Item identity, native row,
- * quantity, level, and mutation generation remain byte-for-byte stable.
- *
+ * The target may be equipped or unequipped. Native defaults are materialized into a complete
+ * authored socket block, then only the requested lane changes; everything else stays byte-stable.
  * @param targetInstanceSoid Selected-character item-instance key named by the Client.
  * @param socketLane Zero-based ordinary socket lane.
  * @param plugDefinitionIndex Installed plug-definition row selected by the Client.
@@ -431,18 +420,13 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 
 /**
  * Prepares one ordinary-socket selection for an exact character-screen item selector.
- *
- * The resolved selected-character instance is passed through the same checked transition as an
- * instance-addressed action, so acquired and unequipped items do not depend on a coincidental
- * menu-row ordinal.
- *
+ * The resolved instance runs through the same checked transition as an instance-addressed action,
+ * so acquired and unequipped items do not depend on a coincidental menu-row ordinal.
  * @param instanceIdentityToken Item-instance identity decoded from the opcode-1901 selector.
- * @param requestedSocketLane Native socket action lane; the installed compatibility relation
- * resolves the target's exact physical lane.
+ * @param requestedSocketLane Native socket action lane; compatibility resolves the physical lane.
  * @param plugDefinitionIndex Installed plug-definition row selected by the Client.
  * @param mutation Gets the checked before/after images without changing account State.
- * @return True when the location has one matching item, the plug resolves to exactly the
- * requested compatible ordinary socket lane, and the socket transition is valid.
+ * @return True when one item matches, the plug resolves to that lane, and the transition is valid.
  */
 [[nodiscard]] bool prepare_character_selector_socket_plug(std::uint64_t instanceIdentityToken,
                                                           std::uint8_t requestedSocketLane,

+ 16 - 18
Sunrise/src/state/runtime/state_account_dismantle_staging.cpp

@@ -1,4 +1,4 @@
-/** Dismantle staging: the payout it credits and the after-image it is committed against. */
+/** Dismantle staging: the payout it credits and the after-image it is committed against. */
 
 #include <Windows.h>
 
@@ -148,7 +148,9 @@ void report_dismantle_reward_dropped(std::string_view reason,
 
 /** Per-stat-row tally of one lane's pool, enough to recognise a masterwork tier ladder. */
 struct LadderTally {
+    /** A stat row is one byte, so this covers every row a definition can name. */
     static constexpr std::size_t kRowCount = 256;
+    /** Distinct values one row can track, which is the width of the seen mask. */
     static constexpr std::size_t kValueBits = 64;
     std::array<std::uint16_t, kRowCount> members{};
     std::array<std::int32_t, kRowCount> greatest{};
@@ -179,18 +181,15 @@ bool tally_member(void* context, std::uint16_t plugIndex) noexcept {
 }
 
 /**
- * @return True when the plug in one lane sits high enough on that lane's masterwork ladder.
- *
- * A masterwork lane's pool is a ladder: its tier plugs all carry the same stat row, each with
- * a different value, one per tier. Any lane whose pool has such a row for the plug's stats is a
- * ladder; a mod pool is not, because many mods repeat the same value. A weapon counts only at
- * the top of its ladder; armor counts from halfway up, which is where the service started
- * refunding materials.
+ * @return True when the plug sits high enough on its lane's masterwork ladder, a pool whose
+ *         plugs share one stat row with a distinct value each. Assumed: the weapon-at-top and
+ *         armor-at-halfway cut-offs come from service behaviour, not from the Client.
  */
 [[nodiscard]] bool on_masterwork_ladder(const item_details::Definition& target,
                                         std::uint8_t lane,
                                         const item_details::Definition& plug,
                                         bool weapon) noexcept {
+    // Below three rungs a pool cannot be told apart from a mod pool that happens not to repeat.
     constexpr std::uint16_t kMinimumLadderRungs = 3;
     if (plug.statCount == 0 || plug.statCount > plug.stats.size()) {
         return false;
@@ -268,7 +267,10 @@ bool tally_member(void* context, std::uint16_t plugIndex) noexcept {
                                   std::uint8_t tier,
                                   std::uint8_t gearClass,
                                   bool isMasterworked) noexcept {
-    if (policy.tierMask != 0 && (policy.tierMask & (1U << tier)) == 0) {
+    // The mask is eight bits wide, so a manifest tier past it cannot be selected. Tested before
+    // the shift, which is undefined once the tier reaches the width of the shifted type.
+    constexpr std::uint8_t kTierBits = 8;
+    if (policy.tierMask != 0 && (tier >= kTierBits || (policy.tierMask & (1U << tier)) == 0)) {
         return false;
     }
     if (policy.classMask != 0 && (policy.classMask & gearClass) == 0) {
@@ -286,12 +288,8 @@ bool tally_member(void* context, std::uint16_t plugIndex) noexcept {
 
 /**
  * Credits the supported client's ordinary weapon/armor dismantle payout.
- *
- * Every policy row whose rarity, gear-class and masterwork filters match the dismantled item is
- * summed per material first, so one material lands as one credited row. Capped stacks lose only
- * the overflowing part, matching normal profile-inventory behavior; a stack already at its
- * native cap drops that row's payout and says so in the log. Every credited row receives a new
- * mutation serial so the account observer can display it.
+ * Matching policy rows are summed per material first, so one material lands as one credited row.
+ * A capped stack loses only the overflow and says so in the log. Each credited row gets a serial.
  */
 [[nodiscard]] bool
 apply_dismantle_rewards(const AccountState& before,
@@ -390,9 +388,9 @@ apply_dismantle_rewards(const AccountState& before,
         }
 
         const bool appended = profileIndex == after.profileItemCount;
-        if ((appended && after.profileItemCount >= after.profileItems.size())
-            || greatestMutationSerial == (std::numeric_limits<std::int32_t>::max)()) {
-            report_dismantle_reward_dropped(appended ? "profile_full" : "serial_exhausted",
+        const bool profileFull = appended && after.profileItemCount >= after.profileItems.size();
+        if (profileFull || greatestMutationSerial == (std::numeric_limits<std::int32_t>::max)()) {
+            report_dismantle_reward_dropped(profileFull ? "profile_full" : "serial_exhausted",
                                             policy.definitionHash,
                                             policy.quantity,
                                             0,

+ 2 - 5
Sunrise/src/state/runtime/state_account_equipment_runtime.cpp

@@ -158,11 +158,8 @@ namespace family4_loadout = middleware::datagen::family4::loadout;
 
 /**
  * Applies canonical mutation generations after one shape-only equipment transition.
- *
- * Every surviving instance must preserve its native bucket. A generation advances exactly when
- * its published native row or equipped marker changes, and a second resolution proves that the
- * stamped after-image retained the staged placement. Callers that need a moved item to keep an
- * older grid cell (equip swaps) rewrite that item's serial afterwards and re-validate.
+ * Every surviving instance keeps its native bucket, and a generation advances exactly when its
+ * published row or equipped marker changes. An equip swap rewrites the moved serial afterwards.
  */
 [[nodiscard]] bool
 finalize_equipment_transition(const AccountState& account,

+ 2 - 3
Sunrise/src/state/runtime/state_account_item_action_runtime.cpp

@@ -138,9 +138,8 @@ bool prepare_character_selector_socket_plug(std::uint64_t instanceIdentityToken,
         && targetDetail.ordinarySocketState == item_details::OrdinarySocketState::present
         && targetDetail.ordinarySocketCount <= authored_inventory::kPlugCapacity) {
         // Most action kinds are the physical ordinary-socket lane. Prefer that exact lane when
-        // its installed pool accepts the plug; this disambiguates armour items whose two mod
-        // sockets intentionally expose the same pool. Some action kinds are semantic categories
-        // instead (notably shaders), so retain the unique-compatible-lane fallback for those.
+        // its pool accepts the plug, which disambiguates armour with two mod sockets on one pool.
+        // Some kinds are semantic instead (shaders), so keep the unique-compatible-lane fallback.
         if (requestedSocketLane < targetDetail.ordinarySocketCount
             && build_data::is_socket_plug_allowed(
                 targetDefinition.definitionIndex, requestedSocketLane, plugDefinitionIndex)) {

+ 28 - 42
Sunrise/src/state/runtime/state_account_runtime.cpp

@@ -108,10 +108,8 @@ void report_acquisition(std::string_view stage,
 
 /**
  * Prepares a subclass ability-entry transition without publishing account State.
- * The requested entry must currently compete (share a socket-entry group) with exactly one of the
- * character's 5 authored ability picks; that pick is the one the transition updates. This mirrors
- * how `resolve_socket_states` decides which entries a selection makes active, so the entry a
- * request names always maps back to the same field that selection would have set.
+ * The requested entry must share a socket-entry group with exactly one of the character's 5
+ * authored picks; that pick is updated, mirroring how `resolve_socket_states` reads a selection.
  */
 [[nodiscard]] bool stage_subclass_selection(const AccountState& snapshot,
                                             std::size_t characterIndex,
@@ -127,6 +125,7 @@ void report_acquisition(std::string_view stage,
     if (!before.selected || before.soid == 0) {
         return false;
     }
+    // Index of the subclass slot in the authored equipment array.
     constexpr std::size_t kSubclassSlot =
         static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass);
     const auto& subclass = before.equipment.slots[kSubclassSlot];
@@ -147,10 +146,9 @@ void report_acquisition(std::string_view stage,
         return false;
     }
 
-    // A clicked entry's table position does not say which ability slot it fills; only its
-    // resolved destination bucket does. A bundled pick (an Attunement, for example) can mix its
-    // members freely across slots, so every member in the clicked entry's bundle is checked, not
-    // just the one clicked.
+    // A clicked entry's table position does not say which ability slot it fills; only its resolved
+    // destination bucket does. A bundled pick can mix members across slots, so every member of the
+    // clicked entry's bundle is checked, not just the one clicked.
     CharacterState after = before;
     // The picks belong to the equipped subclass item itself, not the character, so each owned
     // subclass remembers its own selection independently instead of sharing one set across all
@@ -162,17 +160,19 @@ void report_acquisition(std::string_view stage,
         std::uint8_t defaultEntry;
     };
     const std::array<Route, 5> routes{{
-        {kMovementAbilityBucket, &afterSubclass->movementAbilityEntry, kDefaultMovementAbilityEntry},
+        {kMovementAbilityBucket,
+         &afterSubclass->movementAbilityEntry,
+         kDefaultMovementAbilityEntry},
         {kGrenadeAbilityBucket, &afterSubclass->grenadeAbilityEntry, kDefaultGrenadeAbilityEntry},
         {kSuperAbilityBucket, &afterSubclass->superAbilityEntry, kDefaultSuperAbilityEntry},
         {kMeleeAbilityBucket, &afterSubclass->meleeAbilityEntry, kDefaultMeleeAbilityEntry},
-        {class_ability_bucket(after.characterClass), &afterSubclass->classAbilityEntry,
+        {class_ability_bucket(after.characterClass),
+         &afterSubclass->classAbilityEntry,
          kDefaultClassAbilityEntry},
     }};
     const auto bucket_of = [&](std::uint8_t entryIndex) noexcept {
         std::uint8_t bucket = build_data::socket_entry_buckets::kNoDestinationBucket;
-        (void)build_data::find_socket_entry_bucket(
-            detail.socketEntryListIndex, entryIndex, bucket);
+        (void)build_data::find_socket_entry_bucket(detail.socketEntryListIndex, entryIndex, bucket);
         return bucket;
     };
     const auto route_entry = [&](std::uint8_t entryIndex) noexcept {
@@ -184,13 +184,9 @@ void report_acquisition(std::string_view stage,
             }
         }
     };
-    // A click can land on any member of a bundle, not only the routable one: the diamond's other
-    // 3 quadrants are passive nodes with no destination bucket of their own (see the group-3
-    // dump: only one member of each 4-node group resolves to melee, or to super and melee both).
-    // The requested entry is only ever the whole bundle's anchor when it happens to be its lowest
-    // index, so the bundle's true start is found by scanning backward first, then every member is
-    // routed from there. Members share the anchor's group only while a wide group (a bundle, not
-    // a simple set of alternatives) is in play; see resolve_socket_states for the same threshold.
+    // A click can land on any member of a bundle, not only the routable one: the other quadrants
+    // are passive nodes with no destination bucket. The bundle's start is found by scanning
+    // backward, then every member is routed from there, and only while the group is wide.
     std::size_t groupPopulation = 0;
     for (std::size_t index = 0; index < entries.entries.size(); ++index) {
         if (entries.entries[index].group == requested.group) {
@@ -200,15 +196,9 @@ void report_acquisition(std::string_view stage,
     if (groupPopulation <= kMaxAttunementBundleSize) {
         route_entry(requestedEntry);
     } else {
-        // A wide group is several same-sized bundles competing for one pick, not several
-        // independent alternatives, so only one bundle's fields stay set at a time. A bundle that
-        // does not touch every field this group can reach (the top and bottom Attunement options
-        // here do not touch super, only the middle one does) must not leave an earlier bundle's
-        // value behind in the field it left alone: super stuck on a prior Attunement's pick while
-        // melee moves to a different one is a combination the game never produces on its own, and
-        // it stops accepting further picks once state reaches it. Every bucket this whole group
-        // can ever reach is reset to its ordinary default first, and only then does the picked
-        // bundle's own members overwrite the ones it actually claims.
+        // A wide group is several same-sized bundles competing for one pick, so only one bundle's
+        // fields stay set. A bundle that does not touch every field the group reaches must not
+        // leave an earlier bundle's value behind, so every bucket is reset before the pick writes.
         for (std::size_t index = 0; index < entries.entries.size(); ++index) {
             if (entries.entries[index].group != requested.group) {
                 continue;
@@ -225,8 +215,8 @@ void report_acquisition(std::string_view stage,
                && entries.entries[blockStart - 1].group == requested.group) {
             --blockStart;
         }
-        for (std::size_t offset = 0; offset < kMaxAttunementBundleSize
-             && blockStart + offset < entries.entries.size()
+        for (std::size_t offset = 0;
+             offset < kMaxAttunementBundleSize && blockStart + offset < entries.entries.size()
              && entries.entries[blockStart + offset].group == requested.group;
              ++offset) {
             route_entry(static_cast<std::uint8_t>(blockStart + offset));
@@ -402,12 +392,9 @@ bool prepare_equipment_swap(std::uint64_t requestedInstanceSoid,
     }
 
     if (previousInstanceSoid != 0) {
-        // The serial on an unequipped row is also the Client's stable ordering token for that
-        // bucket.  Giving the displaced item a fresh, greatest serial makes the Client rebuild it
-        // in the first grid cell even though the character object places it in the selected row.
-        // Transfer the selected row's prior token along with the row instead: the newly equipped
-        // item still has a fresh generation, while the displaced item occupies the grid cell the
-        // player clicked.
+        // The serial on an unequipped row is also the Client's ordering token for that bucket. A
+        // fresh greatest serial would move the displaced item to the first cell, so transfer the
+        // selected row's prior token instead and it keeps the cell the player clicked.
         authored_inventory::Item& displaced = after.inventory.values[inventoryIndex];
         if (displaced.instanceSoid != previousInstanceSoid) {
             return false;
@@ -631,10 +618,9 @@ bool commit_equipment_swap(PendingEquipmentSwap& mutation) noexcept {
     runtime::storage::g_state.account = candidate;
     ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
 
-    // The published ability buckets are resolved against whichever subclass is currently
-    // equipped; swapping that item away makes the domain stale the same way an ability-entry
-    // pick does, so it needs the same invalidation or the character screen keeps showing
-    // whatever the previous subclass resolved to until something else happens to refresh it.
+    // The published ability buckets resolve against whichever subclass is equipped, so swapping
+    // that item away makes the domain stale the same way an ability-entry pick does. It needs the
+    // same invalidation or the character screen keeps showing the previous resolution.
     if (prepared.equipmentSlotIndex
         == static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass)) {
         build_data::invalidate_ability_buckets();
@@ -665,6 +651,7 @@ AccountState account_snapshot() noexcept {
 
 /** Grants each character the other 2 subclasses of its equipped subclass's class. */
 bool ensure_character_subclasses() noexcept {
+    // Index of the subclass slot in the authored equipment array.
     constexpr std::size_t kSubclassSlot =
         static_cast<std::size_t>(authored_inventory::EquipmentSlot::subclass);
     AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
@@ -677,8 +664,7 @@ bool ensure_character_subclasses() noexcept {
     bool haveNextSoid = false;
     bool changed = false;
     bool failed = false;
-    for (std::size_t characterIndex = 0;
-         characterIndex < candidate.characterCount && !failed;
+    for (std::size_t characterIndex = 0; characterIndex < candidate.characterCount && !failed;
          ++characterIndex) {
         CharacterState& character = candidate.characters[characterIndex];
         const std::optional<authored_inventory::Item>& equipped =

+ 6 - 12
Sunrise/src/state/runtime/state_account_socket_runtime.cpp

@@ -160,11 +160,9 @@ void report_socket_plug(std::string_view stage,
         return fail("definition_or_compatibility");
     }
 
-    // Ownership is only meaningful where the plug is a finite supply the account draws down. A
-    // shader is one: it is pulled from Collections into a profile stack and spent by applying it.
-    // An ornament is a permanent unlock the account holds once earned, not a stack it draws
-    // down, which is why the Client offers every valid one for a socket. Requiring a stack for
-    // one would refuse a plug the account already has.
+    // Ownership only matters where the plug is a finite supply the account draws down. A shader is
+    // one: pulled from Collections into a profile stack and spent on apply. An ornament is a
+    // permanent unlock, so requiring a stack for one would refuse a plug the account already has.
     const bool consumesStack =
         build_data::is_profile_action_source(plugDefinitionIndex, plugDefinition.bucketId)
         && build_data::is_consumed_on_apply(plugDefinitionIndex, plugDefinition.bucketId)
@@ -185,13 +183,9 @@ void report_socket_plug(std::string_view stage,
         return fail("materials");
     }
 
-    // Applying spends the stack the plug came from. The insertion cost above is a separate
-    // authored charge that leaves the plug itself untouched, so the unit is taken here.
-    //
-    // The authored-cost path cannot do this. It refuses any row carrying an instance key, because
-    // it exists for the non-instanced currency and material stacks, and an action source always
-    // carries one. Spending one is therefore its own transition: the row keeps its identity while
-    // any unit remains, and releases it with the row once the last unit goes.
+    // Applying spends the stack the plug came from; the insertion cost above is a separate charge.
+    // The authored-cost path refuses any row carrying an instance key, and an action source always
+    // has one, so the row keeps its identity until the last unit goes and is released with it.
     if (consumesStack && !spend_plug_source(chargedAccount, plugDefinition.definitionHash)) {
         return fail("plug_stack");
     }

+ 4 - 0
Sunrise/src/state/runtime/state_rolled_socket_plugs.cpp

@@ -15,6 +15,7 @@ namespace items = build_data::items;
 
 /** Result plugs of one category, in installed-table order. */
 struct ResultSet {
+    /** Most result plugs any one category carries in the installed table. */
     static constexpr std::size_t kCapacity = 64;
     std::array<items::Definition, kCapacity> results{};
     std::size_t count{};
@@ -162,6 +163,7 @@ resolve_link(const items::Definition& result, std::uint8_t lane, RolledPlug& rol
 
 } // namespace
 
+/** Classifies one requested plug for one target lane. */
 RolledPlugAction classify_rolled_plug(const items::Definition& requested,
                                       const items::Definition& target,
                                       std::uint8_t lane) noexcept {
@@ -183,6 +185,7 @@ bool is_rolled_result(std::uint32_t plugHash) noexcept {
            && definition.definitionHash == plugHash && items::rolled_result(definition);
 }
 
+/** Rolls one result plug for a target item. */
 bool roll_socket_plug(const items::Definition& requested,
                       const item_details::Definition& target,
                       std::uint8_t characterClass,
@@ -238,6 +241,7 @@ bool roll_socket_plug(const items::Definition& requested,
     return resolve_link(result, pickLanes[chosen], rolled);
 }
 
+/** Re-derives the linked perk for a result an earlier staging rolled. */
 bool pin_rolled_plug(std::uint32_t plugHash,
                      const item_details::Definition& target,
                      RolledPlug& rolled) noexcept {

+ 6 - 18
Sunrise/src/state/runtime/state_rolled_socket_plugs.h

@@ -9,16 +9,9 @@
 namespace sunrise::state::runtime::detail {
 
 /**
- * Some sockets offer only action plugs: an "apply" and a "re-roll" that carry no effect of their
- * own. The live service answered them by socketing one plug out of a set it rolled from, and
- * those result plugs are never in the socket's own pool. The installed data still marks them:
- * every result plug names the roll set that grants it, and shares its plug category with the
- * action plugs of the same socket. Year-1 weapon masterworks (one stat bonus each) and Year-1
- * armor masterworks (each standing for one of the piece's own stat perks) are the two families
- * this build carries.
- *
- * Everything below is derived from the installed item table at call time; nothing names a
- * particular plug.
+ * Some sockets offer only action plugs, which carry no effect of their own. The service answered
+ * them with a result plug from a roll set: result plugs sit outside the socket's own pool but each
+ * names its set and shares the action plugs' category. All of it is derived from the item table.
  */
 
 /** How one requested plug should be socketed. */
@@ -57,14 +50,9 @@ struct RolledPlug {
 };
 
 /**
- * Rolls one result plug for a target item.
- *
- * Results that carry stats (weapon masterworks) are eligible when every stat they contribute
- * is one the target declares. Results that stand for another plug (armor masterworks) are
- * eligible when the target's socket pools offer one of the perks of their group, a group being
- * the results authored together for one class and slot; a target offering none takes the
- * delegate-less result for the character's class. The current plug is never rolled again, so a
- * re-roll changes something whenever more than one result is eligible.
+ * Rolls one result plug for a target item. A stat-carrying result needs every stat it contributes
+ * declared by the target; a delegating result needs the target's pools to offer a perk of its
+ * group, else the delegate-less result for the class. The current plug is never rolled again.
  * @param requested The action plug, which names the roll set through its category.
  * @param target Installed detail of the item being socketed.
  * @param characterClass Wire class of the owning character.