Procházet zdrojové kódy

refactor(inventory): isolate action transaction pipelines

Split acquisition, dismantle, socket, item-state, and web-service actions out of the monolithic state and protocol runtimes. Represent service mutations as an exclusive transaction variant so staging and commit cannot accidentally publish overlapping actions.

Move Family-4 inventory updates into their own snapshot unit, centralize full-family refresh staging, and resync other authenticated peers after a shared account mutation so Family 4, appearance, and roster views do not remain stale.

Replace fixed dismantle material constants with a validated version-4 settings policy, keeping payout definitions and quantities configuration-driven across build variants.
Thomas Shields před 2 měsíci
rodič
revize
820557727e
35 změnil soubory, kde provedl 3773 přidání a 2973 odebrání
  1. 8 0
      Sunrise/Sunrise.vcxproj
  2. 10 0
      Sunrise/resources/default_settings.json
  3. 2 0
      Sunrise/src/core/settings/parser.h
  4. 1 1
      Sunrise/src/core/settings/settings.h
  5. 65 0
      Sunrise/src/core/settings/state/account_rows_parser.cpp
  6. 6 0
      Sunrise/src/core/settings/state_settings.cpp
  7. 41 0
      Sunrise/src/server/bap/bap_route.cpp
  8. 8 8
      Sunrise/src/server/bap/encrypted/bap_connection_publication.cpp
  9. 106 85
      Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp
  10. 71 38
      Sunrise/src/server/bap/encrypted/encrypted_runtime.cpp
  11. 91 31
      Sunrise/src/server/bap/encrypted/internal.h
  12. 78 0
      Sunrise/src/server/bap/encrypted/push/queuez/queuez_banner_push.cpp
  13. 39 0
      Sunrise/src/server/bap/encrypted/push/queuez/queuez_subscription.cpp
  14. 635 0
      Sunrise/src/server/bap/encrypted/push/snapshot/family4_inventory_updates.cpp
  15. 0 616
      Sunrise/src/server/bap/encrypted/push/snapshot/family4_selection_move.cpp
  16. 20 0
      Sunrise/src/server/bap/encrypted/push/snapshot/initial_snapshot.cpp
  17. 10 0
      Sunrise/src/server/bap/encrypted/push/snapshot/snapshot.h
  18. 80 0
      Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp
  19. 55 57
      Sunrise/src/server/bap/encrypted/queuez/queuez_outcome_staging.cpp
  20. 5 0
      Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h
  21. 48 0
      Sunrise/src/server/bap/encrypted/queuez/staging/queuez_family_refresh_staging.cpp
  22. 23 37
      Sunrise/src/server/bap/encrypted/transactions/service_outcome_commit.cpp
  23. 8 0
      Sunrise/src/server/bap/internal.h
  24. 861 0
      Sunrise/src/server/web_service/web_service_actions.cpp
  25. 21 0
      Sunrise/src/server/web_service/web_service_actions.h
  26. 1 862
      Sunrise/src/server/web_service/web_service_runtime.cpp
  27. 16 18
      Sunrise/src/server/web_service/web_service_runtime.h
  28. 37 3
      Sunrise/src/state/account/account_state.cpp
  29. 11 0
      Sunrise/src/state/account/account_state.h
  30. 2 2
      Sunrise/src/state/runtime/runtime.h
  31. 582 0
      Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp
  32. 128 0
      Sunrise/src/state/runtime/state_account_dismantle_runtime.cpp
  33. 517 0
      Sunrise/src/state/runtime/state_account_item_action_runtime.cpp
  34. 24 1215
      Sunrise/src/state/runtime/state_account_runtime.cpp
  35. 163 0
      Sunrise/src/state/runtime/state_account_transaction_helpers.h

+ 8 - 0
Sunrise/Sunrise.vcxproj

@@ -128,6 +128,9 @@
     <ClCompile Include="src\middleware\web_service\messages\opcode903_codec.cpp" />
     <ClCompile Include="src\middleware\web_service\messages\opcode1901_codec.cpp" />
     <ClCompile Include="src\state\runtime\state_account_runtime.cpp" />
+    <ClCompile Include="src\state\runtime\state_account_acquisition_runtime.cpp" />
+    <ClCompile Include="src\state\runtime\state_account_dismantle_runtime.cpp" />
+    <ClCompile Include="src\state\runtime\state_account_item_action_runtime.cpp" />
     <ClCompile Include="src\core\ui\busy\ui_busy_overlay.cpp" />
     <ClCompile Include="src\core\ui\busy\ui_busy_state.cpp" />
     <ClCompile Include="src\core\ui\notice\ui_notice_overlay.cpp" />
@@ -461,6 +464,7 @@
     <ClCompile Include="src\server\bap\encrypted\transactions\service_outcome_commit.cpp" />
     <ClCompile Include="src\server\bap\encrypted\queuez\queuez_state_validation.cpp" />
     <ClCompile Include="src\server\bap\encrypted\queuez\staging\queuez_family_staging.cpp" />
+    <ClCompile Include="src\server\bap\encrypted\queuez\staging\queuez_family_refresh_staging.cpp" />
     <ClCompile Include="src\server\bap\encrypted\queuez\staging\queuez_character_staging.cpp" />
     <ClCompile Include="src\server\bap\encrypted\queuez\queuez_deferred_push.cpp" />
     <ClCompile Include="src\server\bap\encrypted\queuez\queuez_outcome_staging.cpp" />
@@ -482,12 +486,14 @@
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\family4_selection_resolver.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\family4_snapshot_preparer.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\family4_selection_move.cpp" />
+    <ClCompile Include="src\server\bap\encrypted\push\snapshot\family4_inventory_updates.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\initial_snapshot.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\banner_snapshot.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\roster_snapshot.cpp" />
     <ClCompile Include="src\server\bap\encrypted\push\snapshot\snapshot_storage.cpp" />
     <ClCompile Include="src\server\web_service\opcode_routes.cpp" />
     <ClCompile Include="src\server\web_service\web_service_runtime.cpp" />
+    <ClCompile Include="src\server\web_service\web_service_actions.cpp" />
     <ClCompile Include="src\server\http\server_http.cpp" />
     <ClCompile Include="src\middleware\runtime\middleware_runtime.cpp" />
     <ClCompile Include="src\middleware\protobuf\protobuf_measure.cpp" />
@@ -774,6 +780,7 @@
     <ClInclude Include="src\state\unlocks\definition.h" />
     <ClInclude Include="src\state\unlocks\unlocks_runtime.h" />
     <ClInclude Include="src\state\runtime\runtime.h" />
+    <ClInclude Include="src\state\runtime\state_account_transaction_helpers.h" />
     <ClInclude Include="src\state\runtime\state.h" />
     <ClInclude Include="src\state\runtime\storage\internal.h" />
     <ClInclude Include="src\state\activity\definition.h" />
@@ -889,6 +896,7 @@
     <ClInclude Include="src\server\bap\encrypted\push\snapshot\snapshot_storage.h" />
     <ClInclude Include="src\server\web_service\opcode_routes.h" />
     <ClInclude Include="src\server\web_service\web_service_runtime.h" />
+    <ClInclude Include="src\server\web_service\web_service_actions.h" />
     <ClInclude Include="src\server\http\server_http.h" />
     <ClInclude Include="src\middleware\runtime\middleware_runtime.h" />
     <ClInclude Include="src\middleware\protobuf\codec.h" />

+ 10 - 0
Sunrise/resources/default_settings.json

@@ -129,6 +129,16 @@
     },
     "account": {
       "primary_soid": "0x9EAA300100100100",
+      "dismantle_rewards": [
+        {
+          "definition_hash": "0x3CF2E8E2",
+          "quantity": 4
+        },
+        {
+          "definition_hash": "0x28D6AC07",
+          "quantity": 3
+        }
+      ],
       "profile_items": [
         {
           "definition_hash": "0xBC53E66E",

+ 2 - 0
Sunrise/src/core/settings/parser.h

@@ -146,6 +146,8 @@ private:
      * @return True when every entry is complete and the array fits its size.
      */
     [[nodiscard]] bool profile_items(state::AccountState& output) noexcept;
+    /** Parses the server-authored, definition-driven ordinary-gear dismantle payout. */
+    [[nodiscard]] bool dismantle_rewards(state::AccountState& output) noexcept;
     /**
      * Parses one authored character identity.
      * @param output Receives one complete authored character row.

+ 1 - 1
Sunrise/src/core/settings/settings.h

@@ -18,7 +18,7 @@ namespace sunrise::core::settings {
  * Raise it when a key is renamed, removed, or changes meaning. Adding a key needs no raise,
  * because a missing key already takes its default. An older file is upgraded in place at load.
  */
-inline constexpr std::uint32_t kSettingsVersion = 3;
+inline constexpr std::uint32_t kSettingsVersion = 4;
 
 /** Parsed read-only process settings. */
 struct Settings {

+ 65 - 0
Sunrise/src/core/settings/state/account_rows_parser.cpp

@@ -12,6 +12,71 @@ constexpr std::uint64_t kMaximumDestinationHash = (std::numeric_limits<std::uint
 
 } // namespace
 
+/** Parses the definition hashes and quantities credited by ordinary gear dismantles. */
+bool Parser::dismantle_rewards(state::AccountState& output) noexcept {
+    output.dismantleRewards = {};
+    output.dismantleRewardCount = 0;
+    if (!consume('[')) {
+        return false;
+    }
+    if (consume(']')) {
+        return true;
+    }
+    for (;;) {
+        if (output.dismantleRewardCount >= output.dismantleRewards.size() || !consume('{')) {
+            return false;
+        }
+        state::DismantleRewardPolicy reward{};
+        bool hasHash = false;
+        bool hasQuantity = false;
+        for (;;) {
+            std::string_view key;
+            if (!string(key) || !consume(':')) {
+                return false;
+            }
+            std::uint64_t value = 0;
+            if (key == "definition_hash") {
+                if (hasHash || !unsigned_value(value) || value == 0
+                    || value > (std::numeric_limits<std::uint32_t>::max)()) {
+                    return false;
+                }
+                reward.definitionHash = static_cast<std::uint32_t>(value);
+                hasHash = true;
+            } else if (key == "quantity") {
+                if (hasQuantity || !unsigned_integer(value) || value == 0
+                    || value > (std::numeric_limits<std::int32_t>::max)()) {
+                    return false;
+                }
+                reward.quantity = static_cast<std::int32_t>(value);
+                hasQuantity = true;
+            } else if (!skip_value(0)) {
+                return false;
+            }
+            if (consume('}')) {
+                break;
+            }
+            if (!consume(',')) {
+                return false;
+            }
+        }
+        for (std::size_t index = 0; index < output.dismantleRewardCount; ++index) {
+            if (output.dismantleRewards[index].definitionHash == reward.definitionHash) {
+                return false;
+            }
+        }
+        if (!hasHash || !hasQuantity) {
+            return false;
+        }
+        output.dismantleRewards[output.dismantleRewardCount++] = reward;
+        if (consume(']')) {
+            return true;
+        }
+        if (!consume(',')) {
+            return false;
+        }
+    }
+}
+
 /** Parses the authored account-wide item array. */
 bool Parser::profile_items(state::AccountState& output) noexcept {
     namespace inventory = state::account::inventory;

+ 6 - 0
Sunrise/src/core/settings/state_settings.cpp

@@ -210,6 +210,7 @@ bool Parser::account(state::AccountState& output) noexcept {
     }
     bool hasPrimarySoid = false;
     bool hasSettings = false;
+    bool hasDismantleRewards = false;
     if (consume('}')) {
         return false;
     }
@@ -232,6 +233,11 @@ bool Parser::account(state::AccountState& output) noexcept {
             if (!profile_items(output)) {
                 return false;
             }
+        } else if (key == "dismantle_rewards") {
+            if (hasDismantleRewards || !dismantle_rewards(output)) {
+                return false;
+            }
+            hasDismantleRewards = true;
         } else if (!skip_value(0)) {
             return false;
         }

+ 41 - 0
Sunrise/src/server/bap/bap_route.cpp

@@ -2,6 +2,8 @@
 
 #include <array>
 #include <atomic>
+#include <cstdio>
+#include <limits>
 
 #include "../../core/logging/log.h"
 #include "../../state/matchmaking/matchmaking_state.h"
@@ -14,6 +16,40 @@ namespace {
 SRWLOCK g_lock{SRWLOCK_INIT};
 std::array<Session, kSessionCount> g_sessions{};
 Scratch g_scratch{};
+std::uint64_t g_accountGeneration{};
+
+/** Arms every other active peer after one shared-account transaction is published. */
+void publish_account_mutation(Session& origin) noexcept {
+    origin.accountMutationPublished = false;
+    g_accountGeneration = g_accountGeneration == (std::numeric_limits<std::uint64_t>::max)()
+                              ? 1
+                              : g_accountGeneration + 1;
+    origin.accountGeneration = g_accountGeneration;
+    origin.accountResyncGeneration = g_accountGeneration;
+    origin.accountResyncArmed = false;
+    std::size_t armed = 0;
+    for (auto& peer : g_sessions) {
+        if (&peer == &origin || peer.id == 0 || !peer.authenticated || !peer.queuez.family4Active) {
+            continue;
+        }
+        peer.accountResyncGeneration = g_accountGeneration;
+        peer.accountResyncArmed = true;
+        ++armed;
+    }
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count = std::snprintf(line.data(),
+                                    line.size(),
+                                    "ev=queuez stage=peer_resync_arm result=ok generation=%llu "
+                                    "origin=%u peers=%zu",
+                                    static_cast<unsigned long long>(g_accountGeneration),
+                                    origin.id,
+                                    armed);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::debug,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
 
 /** @param id Nonzero connection id. @return Matching open session, or null. */
 [[nodiscard]] Session* session_for(std::uint32_t id) noexcept {
@@ -98,6 +134,10 @@ void clear_session(Session& session) noexcept {
     if (!handled) {
         return false;
     }
+    if (frame.frameType == middleware::bap::FrameType::encrypted
+        && session->accountMutationPublished) {
+        publish_account_mutation(*session);
+    }
     // A frame response can carry one already-due push in the same bounded socket write.
     bool touchesScratch = true;
     std::size_t deferred = 0;
@@ -178,6 +218,7 @@ void shutdown() noexcept {
     }
     SecureZeroMemory(g_sessions.data(), sizeof g_sessions);
     SecureZeroMemory(&g_scratch, sizeof g_scratch);
+    g_accountGeneration = 0;
     ReleaseSRWLockExclusive(&g_lock);
 }
 

+ 8 - 8
Sunrise/src/server/bap/encrypted/bap_connection_publication.cpp

@@ -20,20 +20,20 @@ constexpr std::uint64_t kTransitionWindowMs = 15'000;
 /** Captures the connection fields one service outcome carries. */
 ConnectionFields connection_fields(const ServiceOutcome& outcome) noexcept {
     ConnectionFields fields{};
-    if (!outcome.hasActivityTransaction) {
+    const auto* plan = transaction_if<activity_message::ActivityPlan>(outcome);
+    if (plan == nullptr) {
         return fields;
     }
-    const auto& plan = outcome.activityPlan;
-    if (plan.delivery == activity_message::Delivery::joinNotifications) {
-        fields.joinMemberKey = plan.entitySlotMutation.memberKey;
-        fields.joinCharacterSoid = plan.joinCharacterSoid;
+    if (plan->delivery == activity_message::Delivery::joinNotifications) {
+        fields.joinMemberKey = plan->entitySlotMutation.memberKey;
+        fields.joinCharacterSoid = plan->joinCharacterSoid;
         fields.joinsActivity = true;
     }
     // The initial load is a transition too, and its token does not arrive for several seconds.
     fields.opensTransitionWindow =
-        plan.delivery == activity_message::Delivery::joinNotifications || plan.transitionStarted;
-    if (plan.mutationDomain == activity_message::MutationDomain::patchEpoch) {
-        fields.patchEpoch = plan.patchEpoch;
+        plan->delivery == activity_message::Delivery::joinNotifications || plan->transitionStarted;
+    if (plan->mutationDomain == activity_message::MutationDomain::patchEpoch) {
+        fields.patchEpoch = plan->patchEpoch;
         fields.retainsPatchEpoch = true;
     }
     return fields;

+ 106 - 85
Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp

@@ -47,16 +47,27 @@ bool process(const ServiceRoute& route,
         return middleware::bap::account_translation::encode_response(
             requestBody, account.primarySoid, output, written);
     }
-    case BodyCodec::activityHostManagerResponse:
-        return activity_host_manager::encode_response(requestBody,
-                                                      output,
-                                                      written,
-                                                      outcome.activitySessionAllocation,
-                                                      outcome.hasActivitySessionAllocation);
-    case BodyCodec::activityMessageRequest:
+    case BodyCodec::activityHostManagerResponse: {
+        state::activity::PendingAllocation allocation{};
+        bool hasAllocation = false;
+        const bool encoded = activity_host_manager::encode_response(
+            requestBody, output, written, allocation, hasAllocation);
+        if (encoded && hasAllocation) {
+            outcome.transaction = allocation;
+        }
+        return encoded;
+    }
+    case BodyCodec::activityMessageRequest: {
         written = 0;
-        return activity_message::process(
-            activitySessionId, requestBody, outcome.activityPlan, outcome.hasActivityTransaction);
+        activity_message::ActivityPlan plan{};
+        bool hasTransaction = false;
+        const bool processed =
+            activity_message::process(activitySessionId, requestBody, plan, hasTransaction);
+        if (processed && hasTransaction) {
+            outcome.transaction = plan;
+        }
+        return processed;
+    }
     case BodyCodec::activityHostResponse: {
         const state::SignOnState& signOn = state::sign_on();
         return middleware::bap::activity_host::encode_response(
@@ -75,13 +86,16 @@ bool process(const ServiceRoute& route,
             middleware::bap::family_unsubscription::parse(requestBody, outcome.unsubscription);
         return outcome.hasUnsubscription;
     }
-    case BodyCodec::matchmakingResponse:
-        return matchmaking::encode_response(matchmakingContext,
-                                            requestBody,
-                                            output,
-                                            written,
-                                            outcome.matchmakingMutation,
-                                            outcome.hasMatchmakingMutation);
+    case BodyCodec::matchmakingResponse: {
+        state::matchmaking::PendingMutation mutation{};
+        bool hasMutation = false;
+        const bool encoded = matchmaking::encode_response(
+            matchmakingContext, requestBody, output, written, mutation, hasMutation);
+        if (encoded && hasMutation) {
+            outcome.transaction = mutation;
+        }
+        return encoded;
+    }
     case BodyCodec::steamCertificate:
         return middleware::bap::certificate::encode_response(requestBody, output, written);
     case BodyCodec::userMessageResponse:
@@ -116,23 +130,33 @@ bool process(const ServiceRoute& route,
         }
         outcome.hasSubscription = webOutcome.hasSubscription;
         outcome.subscription = webOutcome.subscription;
-        if (webOutcome.hasEquipmentSwap) {
+        const auto* equipmentSwap =
+            web_service::mutation_if<state::PendingEquipmentSwap>(webOutcome);
+        const auto* socketPlug = web_service::mutation_if<state::PendingSocketPlug>(webOutcome);
+        const auto* itemState = web_service::mutation_if<state::PendingItemState>(webOutcome);
+        const auto* itemAcquisition =
+            web_service::mutation_if<state::PendingItemAcquisition>(webOutcome);
+        const auto* profileItemAcquisition =
+            web_service::mutation_if<state::PendingProfileItemAcquisition>(webOutcome);
+        const auto* itemDismantle =
+            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.
-            if (!queuez::stage_equipment_swap(queuezState,
-                                              webOutcome.equipmentSwap.characterSoid,
-                                              outcome.equipmentSwapUpdate)) {
+            auto& transaction = outcome.transaction.emplace<EquipmentSwapTransaction>();
+            if (!queuez::stage_equipment_swap(
+                    queuezState, equipmentSwap->characterSoid, transaction.update)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=ws403 stage=queuez_preflight result=fail");
                 // Keep the already-encoded sentinel response and publish no mutation, matching
                 // the change-character failure contract instead of dropping the correlated task.
-                outcome.equipmentSwapUpdate = {};
+                outcome.transaction = std::monostate{};
             } else {
                 middleware::web_service::StatusResponse status{};
-                status.value = outcome.equipmentSwapUpdate.after.family4Version;
+                status.value = transaction.update.after.family4Version;
                 if (!middleware::web_service::encode_response(
                         message,
                         middleware::web_service::ResponseShape::statusPair,
@@ -145,26 +169,26 @@ bool process(const ServiceRoute& route,
                     return false;
                 }
                 web_service::report_equip_response(message, status.value, output.first(written));
-                outcome.hasEquipmentSwap = true;
-                outcome.equipmentSwap = webOutcome.equipmentSwap;
+                transaction.pending = *equipmentSwap;
             }
         }
-        if (webOutcome.hasSocketPlug) {
+        if (socketPlug != nullptr) {
             // Opcode 903 completes at the exact Family-4 revision carrying the changed resident
             // item instance. The resident manifest and character placement remain unchanged.
+            auto& transaction = outcome.transaction.emplace<SocketPlugTransaction>();
             if (!queuez::stage_socket_plug(queuezState,
-                                           webOutcome.socketPlug.accountSoid,
-                                           webOutcome.socketPlug.characterSoid,
-                                           webOutcome.socketPlug.targetInstanceSoid,
-                                           webOutcome.socketPlug.profileChanged,
-                                           outcome.socketPlugUpdate)) {
+                                           socketPlug->accountSoid,
+                                           socketPlug->characterSoid,
+                                           socketPlug->targetInstanceSoid,
+                                           socketPlug->profileChanged,
+                                           transaction.update)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=socket_plug stage=queuez_preflight result=fail");
-                outcome.socketPlugUpdate = {};
+                outcome.transaction = std::monostate{};
             } else {
                 middleware::web_service::StatusResponse status{};
-                status.value = outcome.socketPlugUpdate.after.family4Version;
+                status.value = transaction.update.after.family4Version;
                 if (!middleware::web_service::encode_response(
                         message,
                         middleware::web_service::ResponseShape::statusPair,
@@ -178,26 +202,26 @@ bool process(const ServiceRoute& route,
                 }
                 web_service::report_socket_plug_response(message,
                                                          status.value,
-                                                         webOutcome.socketPlug.targetInstanceSoid,
-                                                         webOutcome.socketPlug.socketLane,
-                                                         webOutcome.socketPlug.plugDefinitionIndex,
+                                                         socketPlug->targetInstanceSoid,
+                                                         socketPlug->socketLane,
+                                                         socketPlug->plugDefinitionIndex,
                                                          output.first(written));
-                outcome.hasSocketPlug = true;
-                outcome.socketPlug = webOutcome.socketPlug;
+                transaction.pending = *socketPlug;
             }
         }
-        if (webOutcome.hasItemState) {
+        if (itemState != nullptr) {
             // Opcode 406 completes at the exact Family-4 revision carrying the changed inventory
             // row flags. Placement and every resident item-instance body remain unchanged.
+            auto& transaction = outcome.transaction.emplace<ItemStateTransaction>();
             if (!queuez::stage_equipment_swap(
-                    queuezState, webOutcome.itemState.characterSoid, outcome.itemStateUpdate)) {
+                    queuezState, itemState->characterSoid, transaction.update)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=item_state stage=queuez_preflight result=fail");
-                outcome.itemStateUpdate = {};
+                outcome.transaction = std::monostate{};
             } else {
                 middleware::web_service::StatusResponse status{};
-                status.value = outcome.itemStateUpdate.after.family4Version;
+                status.value = transaction.update.after.family4Version;
                 if (!middleware::web_service::encode_response(
                         message,
                         middleware::web_service::ResponseShape::statusPair,
@@ -209,27 +233,27 @@ bool process(const ServiceRoute& route,
                                      "ev=item_state stage=response result=fail");
                     return false;
                 }
-                outcome.hasItemState = true;
-                outcome.itemState = webOutcome.itemState;
+                transaction.pending = *itemState;
             }
         }
-        if (webOutcome.hasItemAcquisition) {
+        if (itemAcquisition != nullptr) {
             // A Collections pull is complete only at the exact Family-4 revision that adds both
             // the inventory row and its newly resident instance object. Stage that revision before
             // re-encoding the correlated status pair, just like an equipment swap.
+            auto& transaction = outcome.transaction.emplace<ItemAcquisitionTransaction>();
             if (!queuez::stage_item_acquisition(queuezState,
-                                                webOutcome.itemAcquisition.accountSoid,
-                                                webOutcome.itemAcquisition.characterSoid,
-                                                webOutcome.itemAcquisition.acquiredInstanceSoid,
-                                                webOutcome.itemAcquisition.profileChanged,
-                                                outcome.itemAcquisitionUpdate)) {
+                                                itemAcquisition->accountSoid,
+                                                itemAcquisition->characterSoid,
+                                                itemAcquisition->acquiredInstanceSoid,
+                                                itemAcquisition->profileChanged,
+                                                transaction.update)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=acquire stage=queuez_preflight result=fail");
-                outcome.itemAcquisitionUpdate = {};
+                outcome.transaction = std::monostate{};
             } else {
                 middleware::web_service::StatusResponse status{};
-                status.value = outcome.itemAcquisitionUpdate.after.family4Version;
+                status.value = transaction.update.after.family4Version;
                 if (!middleware::web_service::encode_response(
                         message,
                         middleware::web_service::ResponseShape::statusPair,
@@ -241,33 +265,32 @@ bool process(const ServiceRoute& route,
                                      "ev=acquire stage=response result=fail");
                     return false;
                 }
-                web_service::report_item_acquisition_response(
-                    message,
-                    status.value,
-                    webOutcome.itemAcquisition.acquiredInstanceSoid,
-                    output.first(written));
-                outcome.hasItemAcquisition = true;
-                outcome.itemAcquisition = webOutcome.itemAcquisition;
+                web_service::report_item_acquisition_response(message,
+                                                              status.value,
+                                                              itemAcquisition->acquiredInstanceSoid,
+                                                              output.first(written));
+                transaction.pending = *itemAcquisition;
             }
         }
-        if (webOutcome.hasProfileItemAcquisition) {
+        if (profileItemAcquisition != nullptr) {
             // Profile stacks live in the account body. Actionable shaders/modifications also name
             // a Family-4 item resident: an existing stack must already own it, while a newly
             // appended row adds it atomically at this exact +1 revision.
+            auto& transaction = outcome.transaction.emplace<ProfileItemAcquisitionTransaction>();
             if (!queuez::stage_profile_item_acquisition(
                     queuezState,
-                    webOutcome.profileItemAcquisition.accountSoid,
-                    webOutcome.profileItemAcquisition.acquiredInstanceSoid,
-                    webOutcome.profileItemAcquisition.actionSource,
-                    webOutcome.profileItemAcquisition.appended,
-                    outcome.profileItemAcquisitionUpdate)) {
+                    profileItemAcquisition->accountSoid,
+                    profileItemAcquisition->acquiredInstanceSoid,
+                    profileItemAcquisition->actionSource,
+                    profileItemAcquisition->appended,
+                    transaction.update)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=profile_acquire stage=queuez_preflight result=fail");
-                outcome.profileItemAcquisitionUpdate = {};
+                outcome.transaction = std::monostate{};
             } else {
                 middleware::web_service::StatusResponse status{};
-                status.value = outcome.profileItemAcquisitionUpdate.after.family4Version;
+                status.value = transaction.update.after.family4Version;
                 if (!middleware::web_service::encode_response(
                         message,
                         middleware::web_service::ResponseShape::statusPair,
@@ -282,31 +305,31 @@ bool process(const ServiceRoute& route,
                 web_service::report_profile_item_acquisition_response(
                     message,
                     status.value,
-                    webOutcome.profileItemAcquisition.acquiredDefinitionHash,
-                    webOutcome.profileItemAcquisition.acquiredQuantity,
+                    profileItemAcquisition->acquiredDefinitionHash,
+                    profileItemAcquisition->acquiredQuantity,
                     output.first(written));
-                outcome.hasProfileItemAcquisition = true;
-                outcome.profileItemAcquisition = webOutcome.profileItemAcquisition;
+                transaction.pending = *profileItemAcquisition;
             }
         }
-        if (webOutcome.hasItemDismantle) {
+        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.
+            auto& transaction = outcome.transaction.emplace<ItemDismantleTransaction>();
             if (!queuez::stage_item_dismantle(queuezState,
-                                              webOutcome.itemDismantle.accountSoid,
-                                              webOutcome.itemDismantle.characterSoid,
-                                              webOutcome.itemDismantle.dismantledInstanceSoid,
-                                              webOutcome.itemDismantle.profileChanged,
-                                              outcome.itemDismantleUpdate)) {
+                                              itemDismantle->accountSoid,
+                                              itemDismantle->characterSoid,
+                                              itemDismantle->dismantledInstanceSoid,
+                                              itemDismantle->profileChanged,
+                                              transaction.update)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=dismantle stage=queuez_preflight result=fail");
-                outcome.itemDismantleUpdate = {};
+                outcome.transaction = std::monostate{};
             } else {
                 middleware::web_service::StatusResponse status{};
-                status.value = outcome.itemDismantleUpdate.after.family4Version;
+                status.value = transaction.update.after.family4Version;
                 if (!middleware::web_service::encode_response(
                         message,
                         middleware::web_service::ResponseShape::statusPair,
@@ -318,13 +341,11 @@ bool process(const ServiceRoute& route,
                                      "ev=dismantle stage=response result=fail");
                     return false;
                 }
-                web_service::report_item_dismantle_response(
-                    message,
-                    status.value,
-                    webOutcome.itemDismantle.dismantledInstanceSoid,
-                    output.first(written));
-                outcome.hasItemDismantle = true;
-                outcome.itemDismantle = webOutcome.itemDismantle;
+                web_service::report_item_dismantle_response(message,
+                                                            status.value,
+                                                            itemDismantle->dismantledInstanceSoid,
+                                                            output.first(written));
+                transaction.pending = *itemDismantle;
             }
         }
         // A pick that names the resident character moves nothing, so staging refuses it and the

+ 71 - 38
Sunrise/src/server/bap/encrypted/encrypted_runtime.cpp

@@ -43,6 +43,7 @@ bool consume(Session& session,
              std::span<std::byte> response,
              std::size_t& written) noexcept {
     written = 0;
+    session.accountMutationPublished = false;
     if (!session.authenticated) {
         // Staying silent here looks the same as a decode fault, and both look like a dead link.
         core::log::write(core::log::Channel::server,
@@ -108,7 +109,7 @@ bool consume(Session& session,
         // good and every later reply is rejected, which is worse than a thin reply.
         clear_prefix(scratch.responseBody, responseBodySize);
         responseBodySize = 0;
-        SecureZeroMemory(&outcome, sizeof outcome);
+        outcome = {};
         handled = sendsReply;
     }
     if (handled && sendsReply) {
@@ -146,20 +147,53 @@ bool consume(Session& session,
             diagnostics::report_failure(frame.messageId, "stage");
         }
     }
-    if (handled && outcome.hasActivityTransaction) {
-        const auto& activityPlan = outcome.activityPlan;
-        handled = route.responseMode == ResponseMode::uncorrelatedPush
-                  && activity_transaction::stage_notifications(session,
-                                                               scratch,
-                                                               activityPlan,
-                                                               bapState.sessionKey,
-                                                               nextSendNonce,
-                                                               scratch.framed,
-                                                               framedSize);
-        if (!handled) {
-            diagnostics::report_failure(frame.messageId, "notify");
+    if (handled) {
+        const auto* activityPlan = transaction_if<activity_message::ActivityPlan>(outcome);
+        if (activityPlan != nullptr) {
+            handled = route.responseMode == ResponseMode::uncorrelatedPush
+                      && activity_transaction::stage_notifications(session,
+                                                                   scratch,
+                                                                   *activityPlan,
+                                                                   bapState.sessionKey,
+                                                                   nextSendNonce,
+                                                                   scratch.framed,
+                                                                   framedSize);
+            if (!handled) {
+                diagnostics::report_failure(frame.messageId, "notify");
+            }
         }
     }
+    const bool mutatesAccount =
+        outcome.hasChangeCharacter || outcome.hasSelectCharacter
+        || transaction_if<EquipmentSwapTransaction>(outcome) != nullptr
+        || transaction_if<SocketPlugTransaction>(outcome) != nullptr
+        || transaction_if<ItemStateTransaction>(outcome) != nullptr
+        || transaction_if<ItemAcquisitionTransaction>(outcome) != nullptr
+        || transaction_if<ProfileItemAcquisitionTransaction>(outcome) != nullptr
+        || transaction_if<ItemDismantleTransaction>(outcome) != nullptr;
+    // State commits consume and clear their pending payloads. Retain only the small diagnostic
+    // fields needed after publication; QueueZ after-images stay owned by the transaction variant.
+    const auto* stagedSocket = transaction_if<SocketPlugTransaction>(outcome);
+    const std::uint8_t socketLane = stagedSocket == nullptr ? 0 : stagedSocket->pending.socketLane;
+    const std::uint16_t socketPlugDefinition =
+        stagedSocket == nullptr ? 0 : stagedSocket->pending.plugDefinitionIndex;
+    const std::uint8_t socketTargetBucket =
+        stagedSocket == nullptr ? 0 : stagedSocket->pending.targetBucketId;
+    const std::uint8_t socketPlugBucket =
+        stagedSocket == nullptr ? 0 : stagedSocket->pending.plugBucketId;
+    const auto* stagedItemState = transaction_if<ItemStateTransaction>(outcome);
+    const std::uint64_t itemStateInstance =
+        stagedItemState == nullptr ? 0 : stagedItemState->pending.targetInstanceSoid;
+    const std::uint32_t itemStateFlags =
+        stagedItemState == nullptr ? 0 : stagedItemState->pending.afterFlags;
+    const auto* stagedProfile = transaction_if<ProfileItemAcquisitionTransaction>(outcome);
+    const std::uint32_t profileDefinitionHash =
+        stagedProfile == nullptr ? 0 : stagedProfile->pending.acquiredDefinitionHash;
+    const std::int32_t profileQuantity =
+        stagedProfile == nullptr ? 0 : stagedProfile->pending.acquiredQuantity;
+    const bool profileActionSource =
+        stagedProfile != nullptr && stagedProfile->pending.actionSource;
+    const bool profileAppended = stagedProfile != nullptr && stagedProfile->pending.appended;
     // Committing the transaction clears the mutation the member key lives in, so the connection
     // fields are captured before the commit and published after it.
     const ConnectionFields connection = connection_fields(outcome);
@@ -179,7 +213,8 @@ bool consume(Session& session,
             }
             arm_repushes(session, queuezPublication);
             publish_connection_fields(session, publication, connection);
-            if (outcome.hasEquipmentSwap) {
+            session.accountMutationPublished = mutatesAccount;
+            if (transaction_if<EquipmentSwapTransaction>(outcome) != nullptr) {
                 std::array<char, core::log::kLineCapacity> line{};
                 const int count = std::snprintf(
                     line.data(),
@@ -197,7 +232,7 @@ bool consume(Session& session,
                                      {line.data(), static_cast<std::size_t>(count)});
                 }
             }
-            if (outcome.hasSocketPlug) {
+            if (const auto* transaction = transaction_if<SocketPlugTransaction>(outcome)) {
                 std::array<char, core::log::kLineCapacity> line{};
                 const int count = std::snprintf(
                     line.data(),
@@ -211,18 +246,18 @@ bool consume(Session& session,
                     session.queuez.family4Version,
                     session.queuez.family0Version,
                     session.queuez.family3Version,
-                    static_cast<unsigned long long>(outcome.socketPlug.targetInstanceSoid),
-                    static_cast<unsigned>(outcome.socketPlug.socketLane),
-                    static_cast<unsigned>(outcome.socketPlug.plugDefinitionIndex),
-                    static_cast<unsigned>(outcome.socketPlug.targetBucketId),
-                    static_cast<unsigned>(outcome.socketPlug.plugBucketId));
+                    static_cast<unsigned long long>(transaction->update.targetInstanceSoid),
+                    static_cast<unsigned>(socketLane),
+                    static_cast<unsigned>(socketPlugDefinition),
+                    static_cast<unsigned>(socketTargetBucket),
+                    static_cast<unsigned>(socketPlugBucket));
                 if (count > 0) {
                     core::log::write(core::log::Channel::server,
                                      core::log::Level::debug,
                                      {line.data(), static_cast<std::size_t>(count)});
                 }
             }
-            if (outcome.hasItemState) {
+            if (const auto* transaction = transaction_if<ItemStateTransaction>(outcome)) {
                 std::array<char, core::log::kLineCapacity> line{};
                 const int count = std::snprintf(
                     line.data(),
@@ -232,15 +267,15 @@ bool consume(Session& session,
                     framedSize,
                     static_cast<unsigned>(publishesQueuez),
                     session.queuez.family4Version,
-                    static_cast<unsigned long long>(outcome.itemState.targetInstanceSoid),
-                    outcome.itemState.afterFlags);
+                    static_cast<unsigned long long>(itemStateInstance),
+                    itemStateFlags);
                 if (count > 0) {
                     core::log::write(core::log::Channel::server,
                                      core::log::Level::debug,
                                      {line.data(), static_cast<std::size_t>(count)});
                 }
             }
-            if (outcome.hasItemAcquisition) {
+            if (const auto* transaction = transaction_if<ItemAcquisitionTransaction>(outcome)) {
                 std::array<char, core::log::kLineCapacity> line{};
                 const int count = std::snprintf(
                     line.data(),
@@ -251,15 +286,15 @@ bool consume(Session& session,
                     static_cast<unsigned>(publishesQueuez),
                     session.queuez.family4Version,
                     static_cast<unsigned>(session.queuez.family4ResidentCount),
-                    static_cast<unsigned long long>(
-                        outcome.itemAcquisitionUpdate.acquiredInstanceSoid));
+                    static_cast<unsigned long long>(transaction->update.acquiredInstanceSoid));
                 if (count > 0) {
                     core::log::write(core::log::Channel::server,
                                      core::log::Level::debug,
                                      {line.data(), static_cast<std::size_t>(count)});
                 }
             }
-            if (outcome.hasProfileItemAcquisition) {
+            if (const auto* transaction =
+                    transaction_if<ProfileItemAcquisitionTransaction>(outcome)) {
                 std::array<char, core::log::kLineCapacity> line{};
                 const int count = std::snprintf(
                     line.data(),
@@ -272,20 +307,19 @@ bool consume(Session& session,
                     static_cast<unsigned>(publishesQueuez),
                     session.queuez.family4Version,
                     static_cast<unsigned>(session.queuez.family4ResidentCount),
-                    outcome.profileItemAcquisition.acquiredDefinitionHash,
-                    outcome.profileItemAcquisition.acquiredQuantity,
-                    static_cast<unsigned long long>(
-                        outcome.profileItemAcquisition.acquiredInstanceSoid),
-                    static_cast<unsigned>(outcome.profileItemAcquisition.actionSource),
-                    static_cast<unsigned>(outcome.profileItemAcquisition.appended),
-                    static_cast<unsigned>(outcome.profileItemAcquisitionUpdate.appendedResident));
+                    profileDefinitionHash,
+                    profileQuantity,
+                    static_cast<unsigned long long>(transaction->update.acquiredInstanceSoid),
+                    static_cast<unsigned>(profileActionSource),
+                    static_cast<unsigned>(profileAppended),
+                    static_cast<unsigned>(transaction->update.appendedResident));
                 if (count > 0) {
                     core::log::write(core::log::Channel::server,
                                      core::log::Level::debug,
                                      {line.data(), static_cast<std::size_t>(count)});
                 }
             }
-            if (outcome.hasItemDismantle) {
+            if (const auto* transaction = transaction_if<ItemDismantleTransaction>(outcome)) {
                 std::array<char, core::log::kLineCapacity> line{};
                 const int count = std::snprintf(
                     line.data(),
@@ -296,8 +330,7 @@ bool consume(Session& session,
                     static_cast<unsigned>(publishesQueuez),
                     session.queuez.family4Version,
                     static_cast<unsigned>(session.queuez.family4ResidentCount),
-                    static_cast<unsigned long long>(
-                        outcome.itemDismantleUpdate.dismantledInstanceSoid));
+                    static_cast<unsigned long long>(transaction->update.dismantledInstanceSoid));
                 if (count > 0) {
                     core::log::write(core::log::Channel::server,
                                      core::log::Level::debug,
@@ -309,7 +342,7 @@ bool consume(Session& session,
     clear_prefix(scratch.plaintext, plaintextSize);
     clear_prefix(scratch.responseBody, responseBodySize);
     clear_prefix(scratch.framed, framedSize);
-    SecureZeroMemory(&outcome, sizeof outcome);
+    outcome = {};
     SecureZeroMemory(&publication, sizeof publication);
     SecureZeroMemory(&queuezPublication, sizeof queuezPublication);
     if (handled) {

+ 91 - 31
Sunrise/src/server/bap/encrypted/internal.h

@@ -5,6 +5,7 @@
 #include <cstdint>
 #include <span>
 #include <string_view>
+#include <variant>
 
 #include "../../../middleware/bap/family_unsubscription.h"
 #include "../../../middleware/bap/frame.h"
@@ -33,6 +34,42 @@ enum class BodyCodec : std::uint8_t {
     webService,
 };
 
+/** Equipment mutation and the exact QueueZ after-image promised by its response. */
+struct EquipmentSwapTransaction {
+    state::PendingEquipmentSwap pending{};
+    queuez::EquipmentSwap update{};
+};
+
+/** Socket mutation and the exact QueueZ after-image promised by its response. */
+struct SocketPlugTransaction {
+    state::PendingSocketPlug pending{};
+    queuez::SocketPlug update{};
+};
+
+/** Item-state mutation and the exact QueueZ character after-image promised by its response. */
+struct ItemStateTransaction {
+    state::PendingItemState pending{};
+    queuez::EquipmentSwap update{};
+};
+
+/** Character acquisition and its exact QueueZ after-image. */
+struct ItemAcquisitionTransaction {
+    state::PendingItemAcquisition pending{};
+    queuez::ItemAcquisition update{};
+};
+
+/** Profile acquisition and its exact account/resident QueueZ after-image. */
+struct ProfileItemAcquisitionTransaction {
+    state::PendingProfileItemAcquisition pending{};
+    queuez::ProfileItemAcquisition update{};
+};
+
+/** Dismantle mutation and its exact QueueZ after-image. */
+struct ItemDismantleTransaction {
+    state::PendingItemDismantle pending{};
+    queuez::ItemDismantle update{};
+};
+
 /** Optional side effect produced while decoding one authenticated service body. */
 struct ServiceOutcome {
     bool hasSubscription{};
@@ -43,39 +80,32 @@ struct ServiceOutcome {
     queuez::ChangeCharacter changeCharacter{};
     bool hasSelectCharacter{};
     queuez::SelectCharacter selectCharacter{};
-    bool hasEquipmentSwap{};
-    state::PendingEquipmentSwap equipmentSwap{};
-    /** Exact Queuez after-image promised by the opcode-403 status value. */
-    queuez::EquipmentSwap equipmentSwapUpdate{};
-    bool hasSocketPlug{};
-    state::PendingSocketPlug socketPlug{};
-    /** Exact Queuez item-instance revision promised by the opcode-903 status value. */
-    queuez::SocketPlug socketPlugUpdate{};
-    bool hasItemState{};
-    state::PendingItemState itemState{};
-    /** Exact Queuez character revision promised by opcode 406. */
-    queuez::EquipmentSwap itemStateUpdate{};
-    bool hasItemAcquisition{};
-    state::PendingItemAcquisition itemAcquisition{};
-    /** Exact Queuez after-image promised by the item-creation status value. */
-    queuez::ItemAcquisition itemAcquisitionUpdate{};
-    bool hasProfileItemAcquisition{};
-    state::PendingProfileItemAcquisition profileItemAcquisition{};
-    /** Exact Queuez account revision and optional resident append promised by profile acquisition.
-     */
-    queuez::ProfileItemAcquisition profileItemAcquisitionUpdate{};
-    bool hasItemDismantle{};
-    state::PendingItemDismantle itemDismantle{};
-    /** Exact Queuez after-image promised by the dismantle status value. */
-    queuez::ItemDismantle itemDismantleUpdate{};
-    bool hasActivitySessionAllocation{};
-    state::activity::PendingAllocation activitySessionAllocation{};
-    bool hasActivityTransaction{};
-    activity_message::ActivityPlan activityPlan{};
-    bool hasMatchmakingMutation{};
-    state::matchmaking::PendingMutation matchmakingMutation{};
+    /** One service owns at most one independently versioned transaction. */
+    using Transaction = std::variant<std::monostate,
+                                     state::activity::PendingAllocation,
+                                     activity_message::ActivityPlan,
+                                     state::matchmaking::PendingMutation,
+                                     EquipmentSwapTransaction,
+                                     SocketPlugTransaction,
+                                     ItemStateTransaction,
+                                     ItemAcquisitionTransaction,
+                                     ProfileItemAcquisitionTransaction,
+                                     ItemDismantleTransaction>;
+    Transaction transaction{};
 };
 
+/** @return The service transaction of the requested type, or null for another route. */
+template <typename Transaction>
+[[nodiscard]] Transaction* transaction_if(ServiceOutcome& outcome) noexcept {
+    return std::get_if<Transaction>(&outcome.transaction);
+}
+
+/** @return The service transaction of the requested type, or null for another route. */
+template <typename Transaction>
+[[nodiscard]] const Transaction* transaction_if(const ServiceOutcome& outcome) noexcept {
+    return std::get_if<Transaction>(&outcome.transaction);
+}
+
 /** Outbound delivery behavior picked for one authenticated request service. */
 enum class ResponseMode : std::uint8_t {
     none,
@@ -183,6 +213,16 @@ void append_queuez_notification(Scratch& scratch,
                                 queuez::SessionState& after,
                                 bool& armsRepush) noexcept;
 
+/** Appends one next-version full Family-4 snapshot used to resynchronize another peer. */
+[[nodiscard]] bool
+append_account_resync_notification(Scratch& scratch,
+                                   const queuez::SessionState& before,
+                                   std::span<const std::byte, state::kAesKeySize> key,
+                                   std::array<std::byte, state::kBapNonceSize>& nonce,
+                                   std::span<std::byte> response,
+                                   std::size_t& written,
+                                   queuez::SessionState& after) noexcept;
+
 /**
  * Appends the family-zero banner pair as its own notification.
  * Sent twice per boot at the same version, which is what survives the state-1 DECLARED race.
@@ -328,6 +368,26 @@ append_socket_roster_refresh_notification(Scratch& scratch,
                                           std::span<std::byte> response,
                                           std::size_t& written) noexcept;
 
+/** Refreshes the selected character's complete Family-0 appearance from committed State. */
+[[nodiscard]] bool
+append_account_resync_appearance_notification(Scratch& scratch,
+                                              const queuez::SessionState& before,
+                                              std::span<const std::byte, state::kAesKeySize> key,
+                                              std::array<std::byte, state::kBapNonceSize>& nonce,
+                                              std::span<std::byte> response,
+                                              std::size_t& written,
+                                              queuez::SessionState& after) noexcept;
+
+/** Refreshes the selected character and account roster from committed State. */
+[[nodiscard]] bool
+append_account_resync_roster_notification(Scratch& scratch,
+                                          const queuez::SessionState& before,
+                                          std::span<const std::byte, state::kAesKeySize> key,
+                                          std::array<std::byte, state::kBapNonceSize>& nonce,
+                                          std::span<std::byte> response,
+                                          std::size_t& written,
+                                          queuez::SessionState& after) noexcept;
+
 /** Appends the opcode-903 Family-4 item-instance upsert exposing one socket selection. */
 [[nodiscard]] bool
 append_socket_plug_notification(Scratch& scratch,

+ 78 - 0
Sunrise/src/server/bap/encrypted/push/queuez/queuez_banner_push.cpp

@@ -396,4 +396,82 @@ bool append_socket_roster_refresh_notification(Scratch& scratch,
         scratch, refresh, prepared, "socket_roster", key, nonce, response, written);
 }
 
+/** Refreshes the selected character's complete Family-0 appearance from committed State. */
+bool append_account_resync_appearance_notification(
+    Scratch& scratch,
+    const queuez::SessionState& before,
+    std::span<const std::byte, state::kAesKeySize> key,
+    std::array<std::byte, state::kBapNonceSize>& nonce,
+    std::span<std::byte> response,
+    std::size_t& written,
+    queuez::SessionState& after) noexcept {
+    after = before;
+    if (!before.family0Active) {
+        return true;
+    }
+    const state::AccountState account = state::account_snapshot();
+    const std::uint64_t selected = state::account::selected_character_soid(account);
+    if (selected == 0) {
+        return false;
+    }
+    if (before.family0Character != selected) {
+        return append_banner_move_notification(
+            scratch, before, selected, key, nonce, response, written, after);
+    }
+    std::size_t characterIndex = account.characterCount;
+    for (std::size_t index = 0; index < account.characterCount; ++index) {
+        if (account.characters[index].soid == selected) {
+            characterIndex = index;
+            break;
+        }
+    }
+    queuez::CharacterAppearanceRefresh refresh{};
+    snapshot::Prepared prepared{};
+    if (characterIndex >= account.characterCount
+        || !queuez::stage_character_appearance_refresh(before, selected, refresh)
+        || !snapshot::prepare_character_appearance_refresh(
+            scratch, refresh, account.characters[characterIndex], characterIndex, 0, true, prepared)
+        || !append_appearance_frame(
+            scratch, refresh, prepared, "peer_resync_appearance", key, nonce, response, written)) {
+        return false;
+    }
+    after = refresh.after;
+    return true;
+}
+
+/** Refreshes the selected character and its account roster from committed State. */
+bool append_account_resync_roster_notification(Scratch& scratch,
+                                               const queuez::SessionState& before,
+                                               std::span<const std::byte, state::kAesKeySize> key,
+                                               std::array<std::byte, state::kBapNonceSize>& nonce,
+                                               std::span<std::byte> response,
+                                               std::size_t& written,
+                                               queuez::SessionState& after) noexcept {
+    after = before;
+    if (!before.family3Active) {
+        return true;
+    }
+    const state::AccountState account = state::account_snapshot();
+    const std::uint64_t selected = state::account::selected_character_soid(account);
+    std::size_t characterIndex = account.characterCount;
+    for (std::size_t index = 0; index < account.characterCount; ++index) {
+        if (account.characters[index].soid == selected) {
+            characterIndex = index;
+            break;
+        }
+    }
+    queuez::RosterAppearanceRefresh refresh{};
+    snapshot::Prepared prepared{};
+    if (selected == 0 || characterIndex >= account.characterCount
+        || !queuez::stage_roster_appearance_refresh(before, selected, true, refresh)
+        || !snapshot::prepare_roster_appearance_refresh(
+            scratch, refresh, account.characters[characterIndex], characterIndex, prepared)
+        || !append_roster_appearance_frame(
+            scratch, refresh, prepared, "peer_resync_roster", key, nonce, response, written)) {
+        return false;
+    }
+    after = refresh.after;
+    return true;
+}
+
 } // namespace sunrise::server::bap::encrypted::push

+ 39 - 0
Sunrise/src/server/bap/encrypted/push/queuez/queuez_subscription.cpp

@@ -1,4 +1,5 @@
 #include <array>
+#include <limits>
 
 #include "../../../../../core/logging/log.h"
 #include "../../../../../middleware/datagen/definitions.h"
@@ -80,6 +81,44 @@ namespace {
 
 } // namespace
 
+/** Appends one current full account snapshot at the peer's next Family-4 version. */
+bool append_account_resync_notification(Scratch& scratch,
+                                        const queuez::SessionState& before,
+                                        std::span<const std::byte, state::kAesKeySize> key,
+                                        std::array<std::byte, state::kBapNonceSize>& nonce,
+                                        std::span<std::byte> response,
+                                        std::size_t& written,
+                                        queuez::SessionState& after) noexcept {
+    after = before;
+    if (!queuez::valid(before) || !before.family4Active || before.family4RootSoid == 0
+        || before.family4Version == (std::numeric_limits<std::int32_t>::max)()) {
+        return false;
+    }
+    snapshot::Prepared prepared{};
+    if (!snapshot::prepare_family4_refresh(
+            scratch, before.family4RootSoid, before.family4Version + 1, prepared)
+        || !queuez::stage_family4_refresh(before, prepared.family, after)) {
+        return false;
+    }
+    const std::size_t objectCount = prepared.family.objects.size();
+    const std::size_t beforeBytes = written;
+    if (!queuez_frame::append(scratch,
+                              prepared.family,
+                              prepared.rawClearSize,
+                              prepared.compressedClearSize,
+                              key,
+                              nonce,
+                              response,
+                              written)) {
+        after = before;
+        return false;
+    }
+    middleware::secure_channel::advance_nonce(nonce);
+    queuez_report::push(
+        "peer_resync", queuez::kAccountFamilyType, objectCount, written - beforeBytes, 1);
+    return true;
+}
+
 /**
  * Stages the snapshots one subscription needs.
  * Every step reports and continues. The subscribe is answered whether or not a frame is built.

+ 635 - 0
Sunrise/src/server/bap/encrypted/push/snapshot/family4_inventory_updates.cpp

@@ -0,0 +1,635 @@
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdio>
+#include <optional>
+#include <span>
+
+#include "../../../../../core/logging/log.h"
+#include "../../../../../middleware/datagen/family4/account/account_encoder.h"
+#include "../../../../../middleware/datagen/family4/account/layout.h"
+#include "../../../../../middleware/datagen/family4/character/character_encoder.h"
+#include "../../../../../middleware/datagen/family4/character/layout.h"
+#include "../../../../../middleware/datagen/family4/instance/instance_encoder.h"
+#include "../../../../../middleware/datagen/family4/instance/layout.h"
+#include "../../../../../state/runtime/runtime.h"
+#include "internal.h"
+#include "snapshot_storage.h"
+
+namespace sunrise::server::bap::encrypted::push::snapshot {
+
+namespace family4_datagen = middleware::datagen::family4;
+
+/** Builds a single full account-object upsert from an uncommitted profile-stack after-image. */
+bool prepare_profile_item_acquisition(Scratch& scratch,
+                                      const queuez::ProfileItemAcquisition& acquisition,
+                                      const state::PendingProfileItemAcquisition& mutation,
+                                      Prepared& prepared) noexcept {
+    const Reservation reservation = reserve_prior(scratch, prepared);
+    if (reservation.rawWriteOffset > scratch.plaintext.size()
+        || reservation.compressedWriteOffset > scratch.sealed.size()) {
+        return report_failure("profile_acquire_reservation");
+    }
+    if (!mutation.prepared || mutation.accountSoid == 0
+        || mutation.accountSoid != acquisition.accountSoid
+        || mutation.acquiredInstanceSoid != acquisition.acquiredInstanceSoid
+        || mutation.actionSource != acquisition.actionSource
+        || acquisition.appendedResident != (mutation.appended && mutation.actionSource)
+        || acquisition.accountSoid != acquisition.after.family4RootSoid
+        || acquisition.accountDefinitionId == 0
+        || (acquisition.appendedResident && acquisition.itemInstanceDefinitionId == 0)) {
+        return report_failure("profile_acquire_mutation");
+    }
+    state::AccountState account{};
+    if (!state::preview_profile_item_acquisition(mutation, account)
+        || account.primarySoid != acquisition.accountSoid || !state::account::valid(account)) {
+        return report_failure("profile_acquire_account");
+    }
+
+    const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
+    if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
+        return report_failure("profile_acquire_account_storage");
+    }
+    const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
+    if (!family4_datagen::account::encode(account, accountBytes)) {
+        return report_failure("profile_acquire_account_encode");
+    }
+
+    // The native account observer compares profile quantities but only emits pickup feedback when
+    // the changed row's mutation serial also appears in this transient 16-record bank at 0x6978.
+    // Keep the descriptor local to this one incremental upsert; ordinary snapshots encode an empty
+    // bank, while the row's rising mutation serial remains persistent State.
+    constexpr std::uint16_t kAcquisitionChangeSequence = 0;
+    constexpr std::uint16_t kAcquisitionChangeNextWriteSlot = 1;
+    constexpr std::uint16_t kAcquisitionChangeNextSequence = 1;
+    constexpr std::uint8_t kAcquisitionChangeKind = 1;
+    constexpr std::uint16_t kAcquisitionChangeFlags = 0;
+    auto& accountObject =
+        *reinterpret_cast<family4_datagen::account::layout::Object*>(accountBytes.data());
+    std::size_t acquiredRow = accountObject.profileItems.size();
+    for (std::size_t row = 0; row < accountObject.profileItems.size(); ++row) {
+        const auto& inventoryRow = accountObject.profileItems[row];
+        if (inventoryRow.mutationSerial != mutation.acquiredMutationSerial) {
+            continue;
+        }
+        if (acquiredRow != accountObject.profileItems.size()) {
+            clear_after(scratch, reservation);
+            return report_failure("profile_acquire_row_duplicate");
+        }
+        acquiredRow = row;
+    }
+    const auto recordIsZero =
+        [](const family4_datagen::account::layout::ProfileInventoryChangeRecord& record) noexcept {
+            return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
+                   && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
+        };
+    const bool recordsAreZero = std::all_of(accountObject.profileInventoryChanges.records.cbegin(),
+                                            accountObject.profileInventoryChanges.records.cend(),
+                                            recordIsZero);
+    if (acquiredRow >= accountObject.profileItems.size()
+        || accountObject.profileItems[acquiredRow].quantity != mutation.acquiredQuantity
+        || accountObject.profileInventoryChanges.writeSlot != 0
+        || accountObject.profileInventoryChanges.nextSequence != 0 || !recordsAreZero) {
+        clear_after(scratch, reservation);
+        return report_failure("profile_acquire_inventory_change_state");
+    }
+    accountObject.profileInventoryChanges.writeSlot = kAcquisitionChangeNextWriteSlot;
+    accountObject.profileInventoryChanges.nextSequence = kAcquisitionChangeNextSequence;
+    auto& acquisitionChange = accountObject.profileInventoryChanges.records.front();
+    acquisitionChange.sequence = kAcquisitionChangeSequence;
+    acquisitionChange.mutationSerial = mutation.acquiredMutationSerial;
+    acquisitionChange.kind = kAcquisitionChangeKind;
+    acquisitionChange.flags = kAcquisitionChangeFlags;
+
+    Prepared staged{};
+    staged.rawClearSize =
+        (std::max)(reservation.rawClearSize,
+                   reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
+    std::size_t compressedExtent = reservation.compressedWriteOffset;
+    const std::size_t accountObjectIndex = acquisition.appendedResident ? 1U : 0U;
+    if (!append_object(scratch,
+                       accountBytes,
+                       acquisition.accountDefinitionId,
+                       acquisition.accountSoid,
+                       staged.objects[accountObjectIndex],
+                       compressedExtent)) {
+        return report_failure("profile_acquire_account_object");
+    }
+    std::size_t objectCount = 1;
+    if (acquisition.appendedResident) {
+        if (mutation.profileIndex >= account.profileItemCount) {
+            return report_failure("profile_acquire_instance_index");
+        }
+        family4_datagen::instance::ResolvedInstance instance{};
+        const state::account::inventory::ProfileItem& profileItem =
+            account.profileItems[mutation.profileIndex];
+        const auto instanceBytes = rawStorage.first(family4_datagen::instance::layout::kObjectSize);
+        if (!resolve_profile_item_instance(profileItem, instance)
+            || instance.instanceSoid != acquisition.acquiredInstanceSoid
+            || !family4_datagen::instance::encode(instance, instanceBytes)
+            || !append_object(scratch,
+                              instanceBytes,
+                              acquisition.itemInstanceDefinitionId,
+                              acquisition.acquiredInstanceSoid,
+                              staged.objects.front(),
+                              compressedExtent)) {
+            return report_failure("profile_acquire_instance_object");
+        }
+        objectCount = 2;
+    }
+    staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
+    staged.family = middleware::queuez::Family{
+        kAccountFamilyType,
+        acquisition.after.family4RootSoid,
+        acquisition.after.family4Version,
+        0,
+        std::span(staged.objects).first(objectCount),
+    };
+    if (!commit(staged, prepared)) {
+        clear_after(scratch, reservation);
+        return report_failure("profile_acquire_commit");
+    }
+
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=profile_acquire stage=account_object result=ok family_version=%d "
+                      "account=0x%llX definition=%u item_count=%zu definition_hash=%u quantity=%d "
+                      "native_row=%zu mutation_serial=%d change_slot=%u change_next_sequence=%u "
+                      "change_kind=%u account_payload_bytes=%zu objects=%zu object_order=%s",
+                      acquisition.after.family4Version,
+                      static_cast<unsigned long long>(acquisition.accountSoid),
+                      acquisition.accountDefinitionId,
+                      mutation.afterItemCount,
+                      mutation.acquiredDefinitionHash,
+                      mutation.acquiredQuantity,
+                      acquiredRow,
+                      mutation.acquiredMutationSerial,
+                      static_cast<unsigned>(kAcquisitionChangeNextWriteSlot),
+                      static_cast<unsigned>(kAcquisitionChangeNextSequence),
+                      static_cast<unsigned>(kAcquisitionChangeKind),
+                      prepared.family.objects[accountObjectIndex].payload.size(),
+                      objectCount,
+                      acquisition.appendedResident ? "item-account" : "account");
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::debug,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+    return true;
+}
+
+/** Builds a new item-instance upsert before its character after-image. */
+bool prepare_item_acquisition(Scratch& scratch,
+                              const queuez::ItemAcquisition& acquisition,
+                              const state::PendingItemAcquisition& mutation,
+                              Prepared& prepared) noexcept {
+    const Reservation reservation = reserve_prior(scratch, prepared);
+    if (reservation.rawWriteOffset > scratch.plaintext.size()
+        || reservation.compressedWriteOffset > scratch.sealed.size()) {
+        return report_failure("acquire_reservation");
+    }
+
+    state::AccountState account{};
+    if (!mutation.prepared || mutation.characterSoid == 0 || mutation.acquiredInstanceSoid == 0
+        || mutation.accountSoid == 0 || mutation.accountSoid != acquisition.accountSoid
+        || mutation.characterSoid != acquisition.characterSoid
+        || mutation.acquiredInstanceSoid != acquisition.acquiredInstanceSoid
+        || mutation.profileChanged != acquisition.updatesAccount
+        || acquisition.accountSoid != acquisition.after.family4RootSoid
+        || acquisition.accountDefinitionId == 0 || acquisition.after.family4ResidentCount == 0
+        || acquisition.after.family4Residents[acquisition.after.family4ResidentCount - 1U]
+                   .objectSoid
+               != mutation.acquiredInstanceSoid
+        || acquisition.after.family4Residents[acquisition.after.family4ResidentCount - 1U]
+                   .definitionId
+               != acquisition.itemInstanceDefinitionId) {
+        return report_failure("acquire_mutation");
+    }
+    if (!state::preview_item_acquisition(mutation, account)
+        || mutation.characterIndex >= account.characterCount
+        || account.primarySoid != acquisition.accountSoid
+        || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
+        return report_failure("acquire_account");
+    }
+
+    Resolved selected{};
+    const std::optional<std::size_t> selectedIndex = find_character_index(account);
+    if (!state::account::valid(account) || !selectedIndex.has_value()
+        || *selectedIndex != mutation.characterIndex
+        || !resolve(account, mutation.characterIndex, selected)
+        || selected.characterObjectId != acquisition.characterDefinitionId
+        || selected.itemInstanceObjectId != acquisition.itemInstanceDefinitionId) {
+        return report_failure("acquire_selection");
+    }
+
+    family4_datagen::loadout::ResolvedInstances acquired{};
+    std::int32_t acquiredMutationSerial = -1;
+    for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
+        const family4_datagen::loadout::ResolvedItem& item = selected.loadout.items[index];
+        if (item.instance.instanceSoid != mutation.acquiredInstanceSoid) {
+            continue;
+        }
+        if (acquired.itemCount != 0 || item.equipped || item.inventoryRow != mutation.inventoryRow
+            || item.equipmentSlot != mutation.equipmentSlot) {
+            return report_failure("acquire_item_row");
+        }
+        acquired.items[0] = {item.equipmentSlot, item.instance};
+        acquired.itemCount = 1;
+        acquiredMutationSerial = item.mutationSerial;
+    }
+    if (acquired.itemCount != 1 || acquiredMutationSerial < 0) {
+        return report_failure("acquire_item_missing");
+    }
+
+    const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
+    if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
+        return report_failure("acquire_character_storage");
+    }
+    const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
+    if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
+                                            selected.loadout,
+                                            selected.lightEvaluation,
+                                            characterBytes)) {
+        return report_failure("acquire_character_object");
+    }
+
+    // The native character-object observer requires a transient inventory-change record whose
+    // serial matches the newly filled row. Without it the row is accepted, but the observer never
+    // queues the acquisition feedback. Keep this record local to this one acquisition push; the
+    // canonical encoder leaves the bank empty on every later snapshot.
+    constexpr std::size_t kBitsPerFlagByte = 8;
+    constexpr std::int32_t kEncodedOccupiedRowWatermark = 1;
+    constexpr std::uint16_t kAcquisitionChangeSequence = 0;
+    constexpr std::uint16_t kAcquisitionChangeNextWriteSlot = 1;
+    constexpr std::uint16_t kAcquisitionChangeNextSequence = 1;
+    constexpr std::uint8_t kAcquisitionChangeKind = 1;
+    constexpr std::uint16_t kAcquisitionChangeFlags = 0;
+    auto& characterObject =
+        *reinterpret_cast<family4_datagen::character::layout::Object*>(characterBytes.data());
+    const std::size_t acquiredRow = mutation.inventoryRow;
+    if (acquiredRow >= characterObject.inventoryItems.size()
+        || acquiredRow >= characterObject.instanceProgressWatermarks.size()) {
+        clear_after(scratch, reservation);
+        return report_failure("acquire_new_item_row");
+    }
+    const std::size_t newItemFlagIndex = acquiredRow / kBitsPerFlagByte;
+    const std::byte newItemFlagMask = std::byte{1U} << (acquiredRow % kBitsPerFlagByte);
+    const auto recordIsZero =
+        [](const family4_datagen::character::layout::InventoryChangeRecord& record) noexcept {
+            return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
+                   && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
+        };
+    const bool unknownIsZero =
+        std::all_of(characterObject.inventoryChangeUnknown.cbegin(),
+                    characterObject.inventoryChangeUnknown.cend(),
+                    [](std::byte value) noexcept { return value == std::byte{}; });
+    const bool recordsAreZero = std::all_of(characterObject.inventoryChanges.records.cbegin(),
+                                            characterObject.inventoryChanges.records.cend(),
+                                            recordIsZero);
+    const auto& acquiredInventoryRow = characterObject.inventoryItems[acquiredRow];
+    if (newItemFlagIndex >= characterObject.newItemFlags.size()
+        || acquiredInventoryRow.instanceSoid != mutation.acquiredInstanceSoid
+        || acquiredInventoryRow.mutationSerial != acquiredMutationSerial
+        || (characterObject.newItemFlags[newItemFlagIndex] & newItemFlagMask) != newItemFlagMask
+        || characterObject.instanceProgressWatermarks[acquiredRow] != kEncodedOccupiedRowWatermark
+        || !unknownIsZero || characterObject.inventoryChanges.writeSlot != 0
+        || characterObject.inventoryChanges.nextSequence != 0 || !recordsAreZero) {
+        clear_after(scratch, reservation);
+        return report_failure("acquire_inventory_change_state");
+    }
+    characterObject.inventoryChanges.writeSlot = kAcquisitionChangeNextWriteSlot;
+    characterObject.inventoryChanges.nextSequence = kAcquisitionChangeNextSequence;
+    auto& acquisitionChange = characterObject.inventoryChanges.records.front();
+    acquisitionChange.sequence = kAcquisitionChangeSequence;
+    acquisitionChange.mutationSerial = acquiredMutationSerial;
+    acquisitionChange.kind = kAcquisitionChangeKind;
+    acquisitionChange.flags = kAcquisitionChangeFlags;
+    if (!std::all_of(characterObject.inventoryChanges.records.cbegin() + 1,
+                     characterObject.inventoryChanges.records.cend(),
+                     recordIsZero)) {
+        clear_after(scratch, reservation);
+        return report_failure("acquire_inventory_change_records");
+    }
+
+    Prepared staged{};
+    staged.rawClearSize =
+        (std::max)(reservation.rawClearSize,
+                   reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
+    std::size_t compressedExtent = reservation.compressedWriteOffset;
+    if (!append_object(scratch,
+                       characterBytes,
+                       acquisition.characterDefinitionId,
+                       acquisition.characterSoid,
+                       staged.objects[0],
+                       compressedExtent)) {
+        return report_failure("acquire_character_object");
+    }
+
+    std::size_t itemCursor = 0;
+    if (!append_items(scratch,
+                      rawStorage,
+                      acquisition.itemInstanceDefinitionId,
+                      acquired,
+                      1,
+                      staged,
+                      itemCursor,
+                      compressedExtent)
+        || itemCursor != 1) {
+        clear_after(scratch, reservation);
+        return report_failure("acquire_item_object");
+    }
+
+    const auto& acquiredObject =
+        *reinterpret_cast<const family4_datagen::instance::layout::Object*>(rawStorage.data());
+    if (acquiredObject.instanceSoid != mutation.acquiredInstanceSoid
+        || acquiredObject.roll.progress
+               != family4_datagen::instance::layout::kInitialInstanceProgress) {
+        clear_after(scratch, reservation);
+        return report_failure("acquire_item_progress");
+    }
+    const std::int32_t acquiredInstanceProgress = acquiredObject.roll.progress;
+
+    std::size_t objectCount = 2;
+    if (acquisition.updatesAccount) {
+        if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
+            clear_after(scratch, reservation);
+            return report_failure("acquire_account_storage");
+        }
+        const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
+        if (!family4_datagen::account::encode(account, accountBytes)
+            || !append_object(scratch,
+                              accountBytes,
+                              acquisition.accountDefinitionId,
+                              acquisition.accountSoid,
+                              staged.objects[2],
+                              compressedExtent)) {
+            clear_after(scratch, reservation);
+            return report_failure("acquire_account_object");
+        }
+        staged.rawClearSize =
+            (std::max)(staged.rawClearSize,
+                       reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
+        objectCount = 3;
+    }
+
+    // Creation increments publish the dependency before the reference to it. Compression order is
+    // irrelevant because each descriptor already owns its sealed span, so exchange only the wire
+    // descriptors: new item first, then the character after-image. Dismantle deliberately uses the
+    // inverse dependency order (drop the character reference, then release the item).
+    std::swap(staged.objects[0], staged.objects[1]);
+
+    staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
+    staged.family = middleware::queuez::Family{
+        kAccountFamilyType,
+        acquisition.after.family4RootSoid,
+        acquisition.after.family4Version,
+        0,
+        std::span(staged.objects).first(objectCount),
+    };
+    if (!commit(staged, prepared)) {
+        clear_after(scratch, reservation);
+        return report_failure("acquire_commit");
+    }
+
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=acquire stage=family4_objects result=ok family_version=%d root=0x%llX "
+        "character=0x%llX character_definition=%u instance=0x%llX item_definition=%u "
+        "definition_hash=%u inventory_row=%u equipment_slot=%u next_serial=%u objects=%zu "
+        "order=%s new_item_flag=1 watermark=1 acquired_row_serial=%d "
+        "inventory_change_write_slot=%u inventory_change_next_sequence=%u "
+        "inventory_change_record=0 inventory_change_sequence=%u "
+        "inventory_change_serial=%d inventory_change_kind=%u inventory_change_flags=%u "
+        "instance_progress=%d",
+        acquisition.after.family4Version,
+        static_cast<unsigned long long>(acquisition.after.family4RootSoid),
+        static_cast<unsigned long long>(acquisition.characterSoid),
+        acquisition.characterDefinitionId,
+        static_cast<unsigned long long>(acquisition.acquiredInstanceSoid),
+        acquisition.itemInstanceDefinitionId,
+        mutation.acquiredDefinitionHash,
+        static_cast<unsigned>(mutation.inventoryRow),
+        static_cast<unsigned>(mutation.equipmentSlot),
+        mutation.afterCharacter.nextInventorySerial,
+        objectCount,
+        acquisition.updatesAccount ? "item_character_account" : "item_character",
+        acquiredMutationSerial,
+        static_cast<unsigned>(kAcquisitionChangeNextWriteSlot),
+        static_cast<unsigned>(kAcquisitionChangeNextSequence),
+        static_cast<unsigned>(kAcquisitionChangeSequence),
+        acquiredMutationSerial,
+        static_cast<unsigned>(kAcquisitionChangeKind),
+        static_cast<unsigned>(kAcquisitionChangeFlags),
+        acquiredInstanceProgress);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::debug,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+    return true;
+}
+
+/** Builds character removal, instance release, and any profile-reward pickup descriptors. */
+bool prepare_item_dismantle(Scratch& scratch,
+                            const queuez::ItemDismantle& dismantle,
+                            const state::PendingItemDismantle& mutation,
+                            Prepared& prepared) noexcept {
+    const Reservation reservation = reserve_prior(scratch, prepared);
+    if (reservation.rawWriteOffset > scratch.plaintext.size()
+        || reservation.compressedWriteOffset > scratch.sealed.size()) {
+        return report_failure("dismantle_reservation");
+    }
+
+    state::AccountState account{};
+    if (!mutation.prepared || mutation.characterSoid == 0 || mutation.dismantledInstanceSoid == 0
+        || mutation.dismantledItem.instanceSoid != mutation.dismantledInstanceSoid
+        || mutation.accountSoid != dismantle.accountSoid
+        || mutation.characterSoid != dismantle.characterSoid
+        || mutation.dismantledInstanceSoid != dismantle.dismantledInstanceSoid
+        || mutation.profileChanged != dismantle.updatesAccount
+        || mutation.rewardCount > state::kDismantleRewardCapacity
+        || mutation.profileChanged != (mutation.rewardCount != 0)
+        || dismantle.accountDefinitionId == 0 || dismantle.characterDefinitionId == 0
+        || dismantle.itemInstanceDefinitionId == 0
+        || !state::preview_item_dismantle(mutation, account)
+        || mutation.characterIndex >= account.characterCount
+        || account.primarySoid != dismantle.accountSoid
+        || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
+        return report_failure("dismantle_mutation");
+    }
+
+    Resolved selected{};
+    const std::optional<std::size_t> selectedIndex = find_character_index(account);
+    if (!state::account::valid(account) || !selectedIndex.has_value()
+        || *selectedIndex != mutation.characterIndex
+        || !resolve(account, mutation.characterIndex, selected)
+        || selected.characterObjectId != dismantle.characterDefinitionId
+        || selected.itemInstanceObjectId != dismantle.itemInstanceDefinitionId) {
+        return report_failure("dismantle_selection");
+    }
+    for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
+        if (selected.loadout.items[index].instance.instanceSoid
+            == mutation.dismantledInstanceSoid) {
+            return report_failure("dismantle_item_present");
+        }
+    }
+
+    const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
+    if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
+        return report_failure("dismantle_character_storage");
+    }
+    const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
+    if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
+                                            selected.loadout,
+                                            selected.lightEvaluation,
+                                            characterBytes)) {
+        return report_failure("dismantle_character_object");
+    }
+
+    Prepared staged{};
+    staged.rawClearSize =
+        (std::max)(reservation.rawClearSize,
+                   reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
+    std::size_t compressedExtent = reservation.compressedWriteOffset;
+    if (!append_object(scratch,
+                       characterBytes,
+                       dismantle.characterDefinitionId,
+                       dismantle.characterSoid,
+                       staged.objects[0],
+                       compressedExtent)) {
+        return report_failure("dismantle_character_object");
+    }
+    // Queuez represents a release with the ordinary object key and an empty payload. The
+    // encoding selector is not read for empty descriptors; oodle matches the surrounding objects.
+    staged.objects[1] = middleware::queuez::Object{
+        dismantle.itemInstanceDefinitionId,
+        dismantle.dismantledInstanceSoid,
+        middleware::queuez::Encoding::oodle,
+        {},
+    };
+
+    std::size_t objectCount = 2;
+    if (dismantle.updatesAccount) {
+        if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
+            clear_after(scratch, reservation);
+            return report_failure("dismantle_account_storage");
+        }
+        const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
+        if (!family4_datagen::account::encode(account, accountBytes)) {
+            clear_after(scratch, reservation);
+            return report_failure("dismantle_account_encode");
+        }
+
+        auto& accountObject =
+            *reinterpret_cast<family4_datagen::account::layout::Object*>(accountBytes.data());
+        const auto recordIsZero =
+            [](const family4_datagen::account::layout::ProfileInventoryChangeRecord&
+                   record) noexcept {
+                return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
+                       && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
+            };
+        if (accountObject.profileInventoryChanges.writeSlot != 0
+            || accountObject.profileInventoryChanges.nextSequence != 0
+            || !std::all_of(accountObject.profileInventoryChanges.records.cbegin(),
+                            accountObject.profileInventoryChanges.records.cend(),
+                            recordIsZero)
+            || mutation.rewardCount == 0
+            || mutation.rewardCount > accountObject.profileInventoryChanges.records.size()) {
+            clear_after(scratch, reservation);
+            return report_failure("dismantle_account_change_state");
+        }
+
+        constexpr std::uint8_t kRewardChangeKind = 1;
+        constexpr std::uint16_t kRewardChangeFlags = 0;
+        for (std::size_t rewardIndex = 0; rewardIndex < mutation.rewardCount; ++rewardIndex) {
+            const state::DismantleReward& reward = mutation.rewards[rewardIndex];
+            std::size_t matchedRows = 0;
+            for (const auto& row : accountObject.profileItems) {
+                if (row.mutationSerial != reward.mutationSerial) {
+                    continue;
+                }
+                if (row.quantity != reward.afterQuantity) {
+                    clear_after(scratch, reservation);
+                    return report_failure("dismantle_reward_quantity");
+                }
+                ++matchedRows;
+            }
+            if (matchedRows != 1) {
+                clear_after(scratch, reservation);
+                return report_failure("dismantle_reward_row");
+            }
+            auto& change = accountObject.profileInventoryChanges.records[rewardIndex];
+            change.sequence = static_cast<std::uint16_t>(rewardIndex);
+            change.mutationSerial = reward.mutationSerial;
+            change.kind = kRewardChangeKind;
+            change.flags = kRewardChangeFlags;
+        }
+        accountObject.profileInventoryChanges.writeSlot =
+            static_cast<std::uint16_t>(mutation.rewardCount);
+        accountObject.profileInventoryChanges.nextSequence =
+            static_cast<std::uint16_t>(mutation.rewardCount);
+
+        if (!append_object(scratch,
+                           accountBytes,
+                           dismantle.accountDefinitionId,
+                           dismantle.accountSoid,
+                           staged.objects[2],
+                           compressedExtent)) {
+            clear_after(scratch, reservation);
+            return report_failure("dismantle_account_object");
+        }
+        staged.rawClearSize =
+            (std::max)(staged.rawClearSize,
+                       reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
+        objectCount = 3;
+    }
+    staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
+    staged.family = middleware::queuez::Family{
+        kAccountFamilyType,
+        dismantle.after.family4RootSoid,
+        dismantle.after.family4Version,
+        0,
+        std::span(staged.objects).first(objectCount),
+    };
+    if (!commit(staged, prepared)) {
+        clear_after(scratch, reservation);
+        return report_failure("dismantle_commit");
+    }
+
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=dismantle stage=family4_objects result=ok family_version=%d root=0x%llX "
+                      "character=0x%llX character_definition=%u instance=0x%llX item_definition=%u "
+                      "definition_hash=%u inventory_index=%zu inventory_row=%u equipment_slot=%u "
+                      "moved_items=%zu items_after=%zu next_serial=%u rewards=%zu objects=%zu "
+                      "order=%s",
+                      dismantle.after.family4Version,
+                      static_cast<unsigned long long>(dismantle.after.family4RootSoid),
+                      static_cast<unsigned long long>(dismantle.characterSoid),
+                      dismantle.characterDefinitionId,
+                      static_cast<unsigned long long>(dismantle.dismantledInstanceSoid),
+                      dismantle.itemInstanceDefinitionId,
+                      mutation.dismantledItem.definitionHash,
+                      mutation.inventoryIndex,
+                      static_cast<unsigned>(mutation.inventoryRow),
+                      static_cast<unsigned>(mutation.equipmentSlot),
+                      mutation.movedInventoryItemCount,
+                      mutation.afterCharacter.inventory.count,
+                      mutation.afterCharacter.nextInventorySerial,
+                      mutation.rewardCount,
+                      objectCount,
+                      dismantle.updatesAccount ? "character_release_account" : "character_release");
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::debug,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+    return true;
+}
+} // namespace sunrise::server::bap::encrypted::push::snapshot

+ 0 - 616
Sunrise/src/server/bap/encrypted/push/snapshot/family4_selection_move.cpp

@@ -517,620 +517,4 @@ bool prepare_socket_plug(Scratch& scratch,
     return true;
 }
 
-/** Builds a single full account-object upsert from an uncommitted profile-stack after-image. */
-bool prepare_profile_item_acquisition(Scratch& scratch,
-                                      const queuez::ProfileItemAcquisition& acquisition,
-                                      const state::PendingProfileItemAcquisition& mutation,
-                                      Prepared& prepared) noexcept {
-    const Reservation reservation = reserve_prior(scratch, prepared);
-    if (reservation.rawWriteOffset > scratch.plaintext.size()
-        || reservation.compressedWriteOffset > scratch.sealed.size()) {
-        return report_failure("profile_acquire_reservation");
-    }
-    if (!mutation.prepared || mutation.accountSoid == 0
-        || mutation.accountSoid != acquisition.accountSoid
-        || mutation.acquiredInstanceSoid != acquisition.acquiredInstanceSoid
-        || mutation.actionSource != acquisition.actionSource
-        || acquisition.appendedResident != (mutation.appended && mutation.actionSource)
-        || acquisition.accountSoid != acquisition.after.family4RootSoid
-        || acquisition.accountDefinitionId == 0
-        || (acquisition.appendedResident && acquisition.itemInstanceDefinitionId == 0)) {
-        return report_failure("profile_acquire_mutation");
-    }
-    state::AccountState account{};
-    if (!state::preview_profile_item_acquisition(mutation, account)
-        || account.primarySoid != acquisition.accountSoid || !state::account::valid(account)) {
-        return report_failure("profile_acquire_account");
-    }
-
-    const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
-    if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
-        return report_failure("profile_acquire_account_storage");
-    }
-    const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
-    if (!family4_datagen::account::encode(account, accountBytes)) {
-        return report_failure("profile_acquire_account_encode");
-    }
-
-    // The native account observer compares profile quantities but only emits pickup feedback when
-    // the changed row's mutation serial also appears in this transient 16-record bank at 0x6978.
-    // Keep the descriptor local to this one incremental upsert; ordinary snapshots encode an empty
-    // bank, while the row's rising mutation serial remains persistent State.
-    constexpr std::uint16_t kAcquisitionChangeSequence = 0;
-    constexpr std::uint16_t kAcquisitionChangeNextWriteSlot = 1;
-    constexpr std::uint16_t kAcquisitionChangeNextSequence = 1;
-    constexpr std::uint8_t kAcquisitionChangeKind = 1;
-    constexpr std::uint16_t kAcquisitionChangeFlags = 0;
-    auto& accountObject =
-        *reinterpret_cast<family4_datagen::account::layout::Object*>(accountBytes.data());
-    std::size_t acquiredRow = accountObject.profileItems.size();
-    for (std::size_t row = 0; row < accountObject.profileItems.size(); ++row) {
-        const auto& inventoryRow = accountObject.profileItems[row];
-        if (inventoryRow.mutationSerial != mutation.acquiredMutationSerial) {
-            continue;
-        }
-        if (acquiredRow != accountObject.profileItems.size()) {
-            clear_after(scratch, reservation);
-            return report_failure("profile_acquire_row_duplicate");
-        }
-        acquiredRow = row;
-    }
-    const auto recordIsZero =
-        [](const family4_datagen::account::layout::ProfileInventoryChangeRecord& record) noexcept {
-            return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
-                   && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
-        };
-    const bool recordsAreZero = std::all_of(accountObject.profileInventoryChanges.records.cbegin(),
-                                            accountObject.profileInventoryChanges.records.cend(),
-                                            recordIsZero);
-    if (acquiredRow >= accountObject.profileItems.size()
-        || accountObject.profileItems[acquiredRow].quantity != mutation.acquiredQuantity
-        || accountObject.profileInventoryChanges.writeSlot != 0
-        || accountObject.profileInventoryChanges.nextSequence != 0 || !recordsAreZero) {
-        clear_after(scratch, reservation);
-        return report_failure("profile_acquire_inventory_change_state");
-    }
-    accountObject.profileInventoryChanges.writeSlot = kAcquisitionChangeNextWriteSlot;
-    accountObject.profileInventoryChanges.nextSequence = kAcquisitionChangeNextSequence;
-    auto& acquisitionChange = accountObject.profileInventoryChanges.records.front();
-    acquisitionChange.sequence = kAcquisitionChangeSequence;
-    acquisitionChange.mutationSerial = mutation.acquiredMutationSerial;
-    acquisitionChange.kind = kAcquisitionChangeKind;
-    acquisitionChange.flags = kAcquisitionChangeFlags;
-
-    Prepared staged{};
-    staged.rawClearSize =
-        (std::max)(reservation.rawClearSize,
-                   reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
-    std::size_t compressedExtent = reservation.compressedWriteOffset;
-    const std::size_t accountObjectIndex = acquisition.appendedResident ? 1U : 0U;
-    if (!append_object(scratch,
-                       accountBytes,
-                       acquisition.accountDefinitionId,
-                       acquisition.accountSoid,
-                       staged.objects[accountObjectIndex],
-                       compressedExtent)) {
-        return report_failure("profile_acquire_account_object");
-    }
-    std::size_t objectCount = 1;
-    if (acquisition.appendedResident) {
-        if (mutation.profileIndex >= account.profileItemCount) {
-            return report_failure("profile_acquire_instance_index");
-        }
-        family4_datagen::instance::ResolvedInstance instance{};
-        const state::account::inventory::ProfileItem& profileItem =
-            account.profileItems[mutation.profileIndex];
-        const auto instanceBytes = rawStorage.first(family4_datagen::instance::layout::kObjectSize);
-        if (!resolve_profile_item_instance(profileItem, instance)
-            || instance.instanceSoid != acquisition.acquiredInstanceSoid
-            || !family4_datagen::instance::encode(instance, instanceBytes)
-            || !append_object(scratch,
-                              instanceBytes,
-                              acquisition.itemInstanceDefinitionId,
-                              acquisition.acquiredInstanceSoid,
-                              staged.objects.front(),
-                              compressedExtent)) {
-            return report_failure("profile_acquire_instance_object");
-        }
-        objectCount = 2;
-    }
-    staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
-    staged.family = middleware::queuez::Family{
-        kAccountFamilyType,
-        acquisition.after.family4RootSoid,
-        acquisition.after.family4Version,
-        0,
-        std::span(staged.objects).first(objectCount),
-    };
-    if (!commit(staged, prepared)) {
-        clear_after(scratch, reservation);
-        return report_failure("profile_acquire_commit");
-    }
-
-    std::array<char, core::log::kLineCapacity> line{};
-    const int count =
-        std::snprintf(line.data(),
-                      line.size(),
-                      "ev=profile_acquire stage=account_object result=ok family_version=%d "
-                      "account=0x%llX definition=%u item_count=%zu definition_hash=%u quantity=%d "
-                      "native_row=%zu mutation_serial=%d change_slot=%u change_next_sequence=%u "
-                      "change_kind=%u account_payload_bytes=%zu objects=%zu object_order=%s",
-                      acquisition.after.family4Version,
-                      static_cast<unsigned long long>(acquisition.accountSoid),
-                      acquisition.accountDefinitionId,
-                      mutation.afterItemCount,
-                      mutation.acquiredDefinitionHash,
-                      mutation.acquiredQuantity,
-                      acquiredRow,
-                      mutation.acquiredMutationSerial,
-                      static_cast<unsigned>(kAcquisitionChangeNextWriteSlot),
-                      static_cast<unsigned>(kAcquisitionChangeNextSequence),
-                      static_cast<unsigned>(kAcquisitionChangeKind),
-                      prepared.family.objects[accountObjectIndex].payload.size(),
-                      objectCount,
-                      acquisition.appendedResident ? "item-account" : "account");
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-    return true;
-}
-
-/** Builds a new item-instance upsert before its character after-image. */
-bool prepare_item_acquisition(Scratch& scratch,
-                              const queuez::ItemAcquisition& acquisition,
-                              const state::PendingItemAcquisition& mutation,
-                              Prepared& prepared) noexcept {
-    const Reservation reservation = reserve_prior(scratch, prepared);
-    if (reservation.rawWriteOffset > scratch.plaintext.size()
-        || reservation.compressedWriteOffset > scratch.sealed.size()) {
-        return report_failure("acquire_reservation");
-    }
-
-    state::AccountState account{};
-    if (!mutation.prepared || mutation.characterSoid == 0 || mutation.acquiredInstanceSoid == 0
-        || mutation.accountSoid == 0 || mutation.accountSoid != acquisition.accountSoid
-        || mutation.characterSoid != acquisition.characterSoid
-        || mutation.acquiredInstanceSoid != acquisition.acquiredInstanceSoid
-        || mutation.profileChanged != acquisition.updatesAccount
-        || acquisition.accountSoid != acquisition.after.family4RootSoid
-        || acquisition.accountDefinitionId == 0 || acquisition.after.family4ResidentCount == 0
-        || acquisition.after.family4Residents[acquisition.after.family4ResidentCount - 1U]
-                   .objectSoid
-               != mutation.acquiredInstanceSoid
-        || acquisition.after.family4Residents[acquisition.after.family4ResidentCount - 1U]
-                   .definitionId
-               != acquisition.itemInstanceDefinitionId) {
-        return report_failure("acquire_mutation");
-    }
-    if (!state::preview_item_acquisition(mutation, account)
-        || mutation.characterIndex >= account.characterCount
-        || account.primarySoid != acquisition.accountSoid
-        || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
-        return report_failure("acquire_account");
-    }
-
-    Resolved selected{};
-    const std::optional<std::size_t> selectedIndex = find_character_index(account);
-    if (!state::account::valid(account) || !selectedIndex.has_value()
-        || *selectedIndex != mutation.characterIndex
-        || !resolve(account, mutation.characterIndex, selected)
-        || selected.characterObjectId != acquisition.characterDefinitionId
-        || selected.itemInstanceObjectId != acquisition.itemInstanceDefinitionId) {
-        return report_failure("acquire_selection");
-    }
-
-    family4_datagen::loadout::ResolvedInstances acquired{};
-    std::int32_t acquiredMutationSerial = -1;
-    for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
-        const family4_datagen::loadout::ResolvedItem& item = selected.loadout.items[index];
-        if (item.instance.instanceSoid != mutation.acquiredInstanceSoid) {
-            continue;
-        }
-        if (acquired.itemCount != 0 || item.equipped || item.inventoryRow != mutation.inventoryRow
-            || item.equipmentSlot != mutation.equipmentSlot) {
-            return report_failure("acquire_item_row");
-        }
-        acquired.items[0] = {item.equipmentSlot, item.instance};
-        acquired.itemCount = 1;
-        acquiredMutationSerial = item.mutationSerial;
-    }
-    if (acquired.itemCount != 1 || acquiredMutationSerial < 0) {
-        return report_failure("acquire_item_missing");
-    }
-
-    const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
-    if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
-        return report_failure("acquire_character_storage");
-    }
-    const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
-    if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
-                                            selected.loadout,
-                                            selected.lightEvaluation,
-                                            characterBytes)) {
-        return report_failure("acquire_character_object");
-    }
-
-    // The native character-object observer requires a transient inventory-change record whose
-    // serial matches the newly filled row. Without it the row is accepted, but the observer never
-    // queues the acquisition feedback. Keep this record local to this one acquisition push; the
-    // canonical encoder leaves the bank empty on every later snapshot.
-    constexpr std::size_t kBitsPerFlagByte = 8;
-    constexpr std::int32_t kEncodedOccupiedRowWatermark = 1;
-    constexpr std::uint16_t kAcquisitionChangeSequence = 0;
-    constexpr std::uint16_t kAcquisitionChangeNextWriteSlot = 1;
-    constexpr std::uint16_t kAcquisitionChangeNextSequence = 1;
-    constexpr std::uint8_t kAcquisitionChangeKind = 1;
-    constexpr std::uint16_t kAcquisitionChangeFlags = 0;
-    auto& characterObject =
-        *reinterpret_cast<family4_datagen::character::layout::Object*>(characterBytes.data());
-    const std::size_t acquiredRow = mutation.inventoryRow;
-    if (acquiredRow >= characterObject.inventoryItems.size()
-        || acquiredRow >= characterObject.instanceProgressWatermarks.size()) {
-        clear_after(scratch, reservation);
-        return report_failure("acquire_new_item_row");
-    }
-    const std::size_t newItemFlagIndex = acquiredRow / kBitsPerFlagByte;
-    const std::byte newItemFlagMask = std::byte{1U} << (acquiredRow % kBitsPerFlagByte);
-    const auto recordIsZero =
-        [](const family4_datagen::character::layout::InventoryChangeRecord& record) noexcept {
-            return record.sequence == 0 && record.reserved == 0 && record.mutationSerial == 0
-                   && record.kind == 0 && record.reservedKind == 0 && record.flags == 0;
-        };
-    const bool unknownIsZero =
-        std::all_of(characterObject.inventoryChangeUnknown.cbegin(),
-                    characterObject.inventoryChangeUnknown.cend(),
-                    [](std::byte value) noexcept { return value == std::byte{}; });
-    const bool recordsAreZero = std::all_of(characterObject.inventoryChanges.records.cbegin(),
-                                            characterObject.inventoryChanges.records.cend(),
-                                            recordIsZero);
-    const auto& acquiredInventoryRow = characterObject.inventoryItems[acquiredRow];
-    if (newItemFlagIndex >= characterObject.newItemFlags.size()
-        || acquiredInventoryRow.instanceSoid != mutation.acquiredInstanceSoid
-        || acquiredInventoryRow.mutationSerial != acquiredMutationSerial
-        || (characterObject.newItemFlags[newItemFlagIndex] & newItemFlagMask) != newItemFlagMask
-        || characterObject.instanceProgressWatermarks[acquiredRow] != kEncodedOccupiedRowWatermark
-        || !unknownIsZero || characterObject.inventoryChanges.writeSlot != 0
-        || characterObject.inventoryChanges.nextSequence != 0 || !recordsAreZero) {
-        clear_after(scratch, reservation);
-        return report_failure("acquire_inventory_change_state");
-    }
-    characterObject.inventoryChanges.writeSlot = kAcquisitionChangeNextWriteSlot;
-    characterObject.inventoryChanges.nextSequence = kAcquisitionChangeNextSequence;
-    auto& acquisitionChange = characterObject.inventoryChanges.records.front();
-    acquisitionChange.sequence = kAcquisitionChangeSequence;
-    acquisitionChange.mutationSerial = acquiredMutationSerial;
-    acquisitionChange.kind = kAcquisitionChangeKind;
-    acquisitionChange.flags = kAcquisitionChangeFlags;
-    if (!std::all_of(characterObject.inventoryChanges.records.cbegin() + 1,
-                     characterObject.inventoryChanges.records.cend(),
-                     recordIsZero)) {
-        clear_after(scratch, reservation);
-        return report_failure("acquire_inventory_change_records");
-    }
-
-    Prepared staged{};
-    staged.rawClearSize =
-        (std::max)(reservation.rawClearSize,
-                   reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
-    std::size_t compressedExtent = reservation.compressedWriteOffset;
-    if (!append_object(scratch,
-                       characterBytes,
-                       acquisition.characterDefinitionId,
-                       acquisition.characterSoid,
-                       staged.objects[0],
-                       compressedExtent)) {
-        return report_failure("acquire_character_object");
-    }
-
-    std::size_t itemCursor = 0;
-    if (!append_items(scratch,
-                      rawStorage,
-                      acquisition.itemInstanceDefinitionId,
-                      acquired,
-                      1,
-                      staged,
-                      itemCursor,
-                      compressedExtent)
-        || itemCursor != 1) {
-        clear_after(scratch, reservation);
-        return report_failure("acquire_item_object");
-    }
-
-    const auto& acquiredObject =
-        *reinterpret_cast<const family4_datagen::instance::layout::Object*>(rawStorage.data());
-    if (acquiredObject.instanceSoid != mutation.acquiredInstanceSoid
-        || acquiredObject.roll.progress
-               != family4_datagen::instance::layout::kInitialInstanceProgress) {
-        clear_after(scratch, reservation);
-        return report_failure("acquire_item_progress");
-    }
-    const std::int32_t acquiredInstanceProgress = acquiredObject.roll.progress;
-
-    std::size_t objectCount = 2;
-    if (acquisition.updatesAccount) {
-        if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
-            clear_after(scratch, reservation);
-            return report_failure("acquire_account_storage");
-        }
-        const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
-        if (!family4_datagen::account::encode(account, accountBytes)
-            || !append_object(scratch,
-                              accountBytes,
-                              acquisition.accountDefinitionId,
-                              acquisition.accountSoid,
-                              staged.objects[2],
-                              compressedExtent)) {
-            clear_after(scratch, reservation);
-            return report_failure("acquire_account_object");
-        }
-        staged.rawClearSize =
-            (std::max)(staged.rawClearSize,
-                       reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
-        objectCount = 3;
-    }
-
-    // Creation increments publish the dependency before the reference to it. Compression order is
-    // irrelevant because each descriptor already owns its sealed span, so exchange only the wire
-    // descriptors: new item first, then the character after-image. Dismantle deliberately uses the
-    // inverse dependency order (drop the character reference, then release the item).
-    std::swap(staged.objects[0], staged.objects[1]);
-
-    staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
-    staged.family = middleware::queuez::Family{
-        kAccountFamilyType,
-        acquisition.after.family4RootSoid,
-        acquisition.after.family4Version,
-        0,
-        std::span(staged.objects).first(objectCount),
-    };
-    if (!commit(staged, prepared)) {
-        clear_after(scratch, reservation);
-        return report_failure("acquire_commit");
-    }
-
-    std::array<char, core::log::kLineCapacity> line{};
-    const int count = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=acquire stage=family4_objects result=ok family_version=%d root=0x%llX "
-        "character=0x%llX character_definition=%u instance=0x%llX item_definition=%u "
-        "definition_hash=%u inventory_row=%u equipment_slot=%u next_serial=%u objects=%zu "
-        "order=%s new_item_flag=1 watermark=1 acquired_row_serial=%d "
-        "inventory_change_write_slot=%u inventory_change_next_sequence=%u "
-        "inventory_change_record=0 inventory_change_sequence=%u "
-        "inventory_change_serial=%d inventory_change_kind=%u inventory_change_flags=%u "
-        "instance_progress=%d",
-        acquisition.after.family4Version,
-        static_cast<unsigned long long>(acquisition.after.family4RootSoid),
-        static_cast<unsigned long long>(acquisition.characterSoid),
-        acquisition.characterDefinitionId,
-        static_cast<unsigned long long>(acquisition.acquiredInstanceSoid),
-        acquisition.itemInstanceDefinitionId,
-        mutation.acquiredDefinitionHash,
-        static_cast<unsigned>(mutation.inventoryRow),
-        static_cast<unsigned>(mutation.equipmentSlot),
-        mutation.afterCharacter.nextInventorySerial,
-        objectCount,
-        acquisition.updatesAccount ? "item_character_account" : "item_character",
-        acquiredMutationSerial,
-        static_cast<unsigned>(kAcquisitionChangeNextWriteSlot),
-        static_cast<unsigned>(kAcquisitionChangeNextSequence),
-        static_cast<unsigned>(kAcquisitionChangeSequence),
-        acquiredMutationSerial,
-        static_cast<unsigned>(kAcquisitionChangeKind),
-        static_cast<unsigned>(kAcquisitionChangeFlags),
-        acquiredInstanceProgress);
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-    return true;
-}
-
-/** Builds character removal, instance release, and any profile-reward pickup descriptors. */
-bool prepare_item_dismantle(Scratch& scratch,
-                            const queuez::ItemDismantle& dismantle,
-                            const state::PendingItemDismantle& mutation,
-                            Prepared& prepared) noexcept {
-    const Reservation reservation = reserve_prior(scratch, prepared);
-    if (reservation.rawWriteOffset > scratch.plaintext.size()
-        || reservation.compressedWriteOffset > scratch.sealed.size()) {
-        return report_failure("dismantle_reservation");
-    }
-
-    state::AccountState account{};
-    if (!mutation.prepared || mutation.characterSoid == 0 || mutation.dismantledInstanceSoid == 0
-        || mutation.dismantledItem.instanceSoid != mutation.dismantledInstanceSoid
-        || mutation.accountSoid != dismantle.accountSoid
-        || mutation.characterSoid != dismantle.characterSoid
-        || mutation.dismantledInstanceSoid != dismantle.dismantledInstanceSoid
-        || mutation.profileChanged != dismantle.updatesAccount
-        || mutation.rewardCount > state::kDismantleRewardCapacity
-        || mutation.profileChanged != (mutation.rewardCount != 0)
-        || dismantle.accountDefinitionId == 0 || dismantle.characterDefinitionId == 0
-        || dismantle.itemInstanceDefinitionId == 0
-        || !state::preview_item_dismantle(mutation, account)
-        || mutation.characterIndex >= account.characterCount
-        || account.primarySoid != dismantle.accountSoid
-        || account.characters[mutation.characterIndex].soid != mutation.characterSoid) {
-        return report_failure("dismantle_mutation");
-    }
-
-    Resolved selected{};
-    const std::optional<std::size_t> selectedIndex = find_character_index(account);
-    if (!state::account::valid(account) || !selectedIndex.has_value()
-        || *selectedIndex != mutation.characterIndex
-        || !resolve(account, mutation.characterIndex, selected)
-        || selected.characterObjectId != dismantle.characterDefinitionId
-        || selected.itemInstanceObjectId != dismantle.itemInstanceDefinitionId) {
-        return report_failure("dismantle_selection");
-    }
-    for (std::size_t index = 0; index < selected.loadout.itemCount; ++index) {
-        if (selected.loadout.items[index].instance.instanceSoid
-            == mutation.dismantledInstanceSoid) {
-            return report_failure("dismantle_item_present");
-        }
-    }
-
-    const auto rawStorage = std::span(scratch.plaintext).subspan(reservation.rawWriteOffset);
-    if (family4_datagen::character::layout::kObjectSize > rawStorage.size()) {
-        return report_failure("dismantle_character_storage");
-    }
-    const auto characterBytes = rawStorage.first(family4_datagen::character::layout::kObjectSize);
-    if (!family4_datagen::character::encode(account.characters[mutation.characterIndex],
-                                            selected.loadout,
-                                            selected.lightEvaluation,
-                                            characterBytes)) {
-        return report_failure("dismantle_character_object");
-    }
-
-    Prepared staged{};
-    staged.rawClearSize =
-        (std::max)(reservation.rawClearSize,
-                   reservation.rawWriteOffset + family4_datagen::character::layout::kObjectSize);
-    std::size_t compressedExtent = reservation.compressedWriteOffset;
-    if (!append_object(scratch,
-                       characterBytes,
-                       dismantle.characterDefinitionId,
-                       dismantle.characterSoid,
-                       staged.objects[0],
-                       compressedExtent)) {
-        return report_failure("dismantle_character_object");
-    }
-    // Queuez represents a release with the ordinary object key and an empty payload. The
-    // encoding selector is not read for empty descriptors; oodle matches the surrounding objects.
-    staged.objects[1] = middleware::queuez::Object{
-        dismantle.itemInstanceDefinitionId,
-        dismantle.dismantledInstanceSoid,
-        middleware::queuez::Encoding::oodle,
-        {},
-    };
-
-    std::size_t objectCount = 2;
-    if (dismantle.updatesAccount) {
-        if (family4_datagen::account::layout::kObjectSize > rawStorage.size()) {
-            clear_after(scratch, reservation);
-            return report_failure("dismantle_account_storage");
-        }
-        const auto accountBytes = rawStorage.first(family4_datagen::account::layout::kObjectSize);
-        if (!family4_datagen::account::encode(account, accountBytes)) {
-            clear_after(scratch, reservation);
-            return report_failure("dismantle_account_encode");
-        }
-
-        auto& accountObject =
-            *reinterpret_cast<family4_datagen::account::layout::Object*>(accountBytes.data());
-        const auto recordIsZero =
-            [](const family4_datagen::account::layout::ProfileInventoryChangeRecord& record)
-            noexcept {
-                return record.sequence == 0 && record.reserved == 0
-                       && record.mutationSerial == 0 && record.kind == 0
-                       && record.reservedKind == 0 && record.flags == 0;
-            };
-        if (accountObject.profileInventoryChanges.writeSlot != 0
-            || accountObject.profileInventoryChanges.nextSequence != 0
-            || !std::all_of(accountObject.profileInventoryChanges.records.cbegin(),
-                            accountObject.profileInventoryChanges.records.cend(),
-                            recordIsZero)
-            || mutation.rewardCount == 0
-            || mutation.rewardCount
-                   > accountObject.profileInventoryChanges.records.size()) {
-            clear_after(scratch, reservation);
-            return report_failure("dismantle_account_change_state");
-        }
-
-        constexpr std::uint8_t kRewardChangeKind = 1;
-        constexpr std::uint16_t kRewardChangeFlags = 0;
-        for (std::size_t rewardIndex = 0; rewardIndex < mutation.rewardCount; ++rewardIndex) {
-            const state::DismantleReward& reward = mutation.rewards[rewardIndex];
-            std::size_t matchedRows = 0;
-            for (const auto& row : accountObject.profileItems) {
-                if (row.mutationSerial != reward.mutationSerial) {
-                    continue;
-                }
-                if (row.quantity != reward.afterQuantity) {
-                    clear_after(scratch, reservation);
-                    return report_failure("dismantle_reward_quantity");
-                }
-                ++matchedRows;
-            }
-            if (matchedRows != 1) {
-                clear_after(scratch, reservation);
-                return report_failure("dismantle_reward_row");
-            }
-            auto& change = accountObject.profileInventoryChanges.records[rewardIndex];
-            change.sequence = static_cast<std::uint16_t>(rewardIndex);
-            change.mutationSerial = reward.mutationSerial;
-            change.kind = kRewardChangeKind;
-            change.flags = kRewardChangeFlags;
-        }
-        accountObject.profileInventoryChanges.writeSlot =
-            static_cast<std::uint16_t>(mutation.rewardCount);
-        accountObject.profileInventoryChanges.nextSequence =
-            static_cast<std::uint16_t>(mutation.rewardCount);
-
-        if (!append_object(scratch,
-                           accountBytes,
-                           dismantle.accountDefinitionId,
-                           dismantle.accountSoid,
-                           staged.objects[2],
-                           compressedExtent)) {
-            clear_after(scratch, reservation);
-            return report_failure("dismantle_account_object");
-        }
-        staged.rawClearSize =
-            (std::max)(staged.rawClearSize,
-                       reservation.rawWriteOffset + family4_datagen::account::layout::kObjectSize);
-        objectCount = 3;
-    }
-    staged.compressedClearSize = (std::max)(reservation.compressedClearSize, compressedExtent);
-    staged.family = middleware::queuez::Family{
-        kAccountFamilyType,
-        dismantle.after.family4RootSoid,
-        dismantle.after.family4Version,
-        0,
-        std::span(staged.objects).first(objectCount),
-    };
-    if (!commit(staged, prepared)) {
-        clear_after(scratch, reservation);
-        return report_failure("dismantle_commit");
-    }
-
-    std::array<char, core::log::kLineCapacity> line{};
-    const int count =
-        std::snprintf(line.data(),
-                      line.size(),
-                      "ev=dismantle stage=family4_objects result=ok family_version=%d root=0x%llX "
-                      "character=0x%llX character_definition=%u instance=0x%llX item_definition=%u "
-                      "definition_hash=%u inventory_index=%zu inventory_row=%u equipment_slot=%u "
-                      "moved_items=%zu items_after=%zu next_serial=%u rewards=%zu objects=%zu "
-                      "order=%s",
-                      dismantle.after.family4Version,
-                      static_cast<unsigned long long>(dismantle.after.family4RootSoid),
-                      static_cast<unsigned long long>(dismantle.characterSoid),
-                      dismantle.characterDefinitionId,
-                      static_cast<unsigned long long>(dismantle.dismantledInstanceSoid),
-                      dismantle.itemInstanceDefinitionId,
-                      mutation.dismantledItem.definitionHash,
-                      mutation.inventoryIndex,
-                      static_cast<unsigned>(mutation.inventoryRow),
-                      static_cast<unsigned>(mutation.equipmentSlot),
-                      mutation.movedInventoryItemCount,
-                      mutation.afterCharacter.inventory.count,
-                      mutation.afterCharacter.nextInventorySerial,
-                      mutation.rewardCount,
-                      objectCount,
-                      dismantle.updatesAccount ? "character_release_account"
-                                               : "character_release");
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::debug,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-    return true;
-}
-
 } // namespace sunrise::server::bap::encrypted::push::snapshot

+ 20 - 0
Sunrise/src/server/bap/encrypted/push/snapshot/initial_snapshot.cpp

@@ -50,4 +50,24 @@ bool prepare_initial(Scratch& scratch,
     return true;
 }
 
+/** Rebuilds the account family at an explicitly staged nonzero version. */
+bool prepare_family4_refresh(Scratch& scratch,
+                             std::uint64_t familyRootSoid,
+                             std::int32_t version,
+                             Prepared& prepared) noexcept {
+    if (familyRootSoid == 0 || version <= kInitialFamilyVersion) {
+        return false;
+    }
+    middleware::queuez::Subscription subscription{};
+    subscription.familyType = kAccountFamilyType;
+    subscription.familyRootSoid = familyRootSoid;
+    if (!prepare_initial(scratch, subscription, prepared) || prepared.family.objects.empty()
+        || prepared.family.type != kAccountFamilyType || prepared.family.rootSoid != familyRootSoid
+        || prepared.family.flags != middleware::queuez::kFullSnapshotFlag) {
+        return false;
+    }
+    prepared.family.version = version;
+    return true;
+}
+
 } // namespace sunrise::server::bap::encrypted::push::snapshot

+ 10 - 0
Sunrise/src/server/bap/encrypted/push/snapshot/snapshot.h

@@ -53,6 +53,16 @@ struct Prepared {
                                    const middleware::queuez::Subscription& subscription,
                                    Prepared& prepared) noexcept;
 
+/**
+ * Rebuilds the active account family as a full snapshot at the peer's next version.
+ * This is
+ * used only to repair another authenticated peer after shared State changes.
+ */
+[[nodiscard]] bool prepare_family4_refresh(Scratch& scratch,
+                                           std::uint64_t familyRootSoid,
+                                           std::int32_t version,
+                                           Prepared& prepared) noexcept;
+
 /**
  * Builds the family-zero banner anchor and the record for the character it names.
  * @param scratch Raw object storage owned by the lock.

+ 80 - 0
Sunrise/src/server/bap/encrypted/queuez/queuez_deferred_push.cpp

@@ -32,6 +32,79 @@ void report_repush(const char* stage, std::size_t bytes) noexcept {
     }
 }
 
+/** Publishes the current account graph to a peer invalidated by another connection. */
+[[nodiscard]] bool consume_account_resync(Session& session,
+                                          Scratch& scratch,
+                                          std::span<std::byte> response,
+                                          std::size_t& written,
+                                          bool& touchesScratch) noexcept {
+    if (!session.accountResyncArmed || session.accountResyncGeneration == 0) {
+        return false;
+    }
+    touchesScratch = true;
+    auto nextSendNonce = session.sendNonce;
+    std::size_t framedSize = 0;
+    queuez::SessionState currentQueuez{};
+    if (!push::append_account_resync_notification(scratch,
+                                                  session.queuez,
+                                                  state::bap().sessionKey,
+                                                  nextSendNonce,
+                                                  scratch.framed,
+                                                  framedSize,
+                                                  currentQueuez)) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::warn,
+                         "ev=queuez stage=peer_resync result=fail reason=family4");
+        return false;
+    }
+    if (currentQueuez.family0Active) {
+        queuez::SessionState appearanceAfter{};
+        if (!push::append_account_resync_appearance_notification(scratch,
+                                                                 currentQueuez,
+                                                                 state::bap().sessionKey,
+                                                                 nextSendNonce,
+                                                                 scratch.framed,
+                                                                 framedSize,
+                                                                 appearanceAfter)) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             "ev=queuez stage=peer_resync result=fail reason=family0");
+            return false;
+        }
+        currentQueuez = appearanceAfter;
+    }
+    if (currentQueuez.family3Active) {
+        queuez::SessionState rosterAfter{};
+        if (!push::append_account_resync_roster_notification(scratch,
+                                                             currentQueuez,
+                                                             state::bap().sessionKey,
+                                                             nextSendNonce,
+                                                             scratch.framed,
+                                                             framedSize,
+                                                             rosterAfter)) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             "ev=queuez stage=peer_resync result=fail reason=family3");
+            return false;
+        }
+        currentQueuez = rosterAfter;
+    }
+    if (framedSize == 0 || framedSize > response.size() || !queuez::valid(currentQueuez)) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::warn,
+                         "ev=queuez stage=peer_resync result=fail reason=output");
+        return false;
+    }
+    std::copy_n(scratch.framed.begin(), framedSize, response.begin());
+    written = framedSize;
+    session.sendNonce = nextSendNonce;
+    session.queuez = currentQueuez;
+    session.accountGeneration = session.accountResyncGeneration;
+    session.accountResyncArmed = false;
+    report_repush("peer_resync", framedSize);
+    return true;
+}
+
 /**
  * Sends the owed banner re-push once its delay has passed.
  * The banner has no subscribe of its own, so the timer is its only second chance.
@@ -107,6 +180,13 @@ bool consume_deferred(Session& session,
     if (!session.authenticated) {
         return false;
     }
+    if (consume_account_resync(session, scratch, response, written, touchesScratch)) {
+        return true;
+    }
+    // A failed resync remains armed and blocks unrelated deferred output until it can be retried.
+    if (session.accountResyncArmed) {
+        return false;
+    }
     if (!session.family4RepushArmed || session.family4RepushRoot == 0
         || GetTickCount64() < session.family4RepushDueTick) {
         return consume_banner_repush(session, scratch, response, written, touchesScratch)

+ 55 - 57
Sunrise/src/server/bap/encrypted/queuez/queuez_outcome_staging.cpp

@@ -20,6 +20,12 @@ bool stage_service_outcome(Scratch& scratch,
     publication = {};
     SessionState after{};
     bool armsRepush = false;
+    const auto* equipment = transaction_if<EquipmentSwapTransaction>(outcome);
+    const auto* itemState = transaction_if<ItemStateTransaction>(outcome);
+    const auto* socket = transaction_if<SocketPlugTransaction>(outcome);
+    const auto* itemAcquisition = transaction_if<ItemAcquisitionTransaction>(outcome);
+    const auto* profileAcquisition = transaction_if<ProfileItemAcquisitionTransaction>(outcome);
+    const auto* itemDismantle = transaction_if<ItemDismantleTransaction>(outcome);
     if (outcome.hasSubscription) {
         push::append_queuez_notification(scratch,
                                          before,
@@ -44,17 +50,17 @@ bool stage_service_outcome(Scratch& scratch,
         }
         middleware::secure_channel::advance_nonce(nonce);
         after = outcome.changeCharacter.after;
-    } else if (outcome.hasEquipmentSwap) {
+    } else if (equipment != nullptr) {
         // Body processing already staged this exact after-image so the correlated opcode-403
         // response could promise its version. Reuse it here; staging a second revision would make
         // the response and pushed Family-4 ladder disagree.
-        const EquipmentSwap& swap = outcome.equipmentSwapUpdate;
-        if (!valid(swap.after) || swap.characterSoid != outcome.equipmentSwap.characterSoid
+        const EquipmentSwap& swap = equipment->update;
+        if (!valid(swap.after) || swap.characterSoid != equipment->pending.characterSoid
             || swap.after.family4RootSoid != before.family4RootSoid
             || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
             || swap.after.family4Version != before.family4Version + 1
             || !push::append_equipment_swap_notification(
-                scratch, swap, outcome.equipmentSwap, key, nonce, response, written)) {
+                scratch, swap, equipment->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=equip result=fail");
@@ -70,9 +76,9 @@ bool stage_service_outcome(Scratch& scratch,
         if (after.family0Active) {
             CharacterAppearanceRefresh refresh{};
             if (!stage_character_appearance_refresh(
-                    after, outcome.equipmentSwap.characterSoid, refresh)
+                    after, equipment->pending.characterSoid, refresh)
                 || !push::append_equipment_appearance_refresh_notification(
-                    scratch, refresh, outcome.equipmentSwap, key, nonce, response, written)) {
+                    scratch, refresh, equipment->pending, key, nonce, response, written)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=queuez stage=equip_appearance result=fail");
@@ -88,9 +94,9 @@ bool stage_service_outcome(Scratch& scratch,
         if (after.family3Active) {
             RosterAppearanceRefresh refresh{};
             if (!stage_roster_appearance_refresh(
-                    after, outcome.equipmentSwap.characterSoid, true, refresh)
+                    after, equipment->pending.characterSoid, true, refresh)
                 || !push::append_equipment_roster_refresh_notification(
-                    scratch, refresh, outcome.equipmentSwap, key, nonce, response, written)) {
+                    scratch, refresh, equipment->pending, key, nonce, response, written)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=queuez stage=equip_roster result=fail");
@@ -98,10 +104,10 @@ bool stage_service_outcome(Scratch& scratch,
             }
             after = refresh.after;
         }
-    } else if (outcome.hasItemState) {
+    } else if (itemState != nullptr) {
         // Item-state bits live in the selected-character inventory row. Publish only that
         // resident character body; item-instance, appearance, roster and manifest are unchanged.
-        const EquipmentSwap& update = outcome.itemStateUpdate;
+        const EquipmentSwap& update = itemState->update;
         bool preservedManifest = update.after.family4ResidentCount == before.family4ResidentCount;
         for (std::size_t index = 0; preservedManifest && index < before.family4ResidentCount;
              ++index) {
@@ -111,12 +117,12 @@ bool stage_service_outcome(Scratch& scratch,
                                        == before.family4Residents[index].definitionId;
         }
         if (!valid(update.after) || !preservedManifest
-            || update.characterSoid != outcome.itemState.characterSoid
+            || update.characterSoid != itemState->pending.characterSoid
             || update.after.family4RootSoid != before.family4RootSoid
             || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
             || update.after.family4Version != before.family4Version + 1
             || !push::append_item_state_notification(
-                scratch, update, outcome.itemState, key, nonce, response, written)) {
+                scratch, update, itemState->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=item_state result=fail");
@@ -124,10 +130,10 @@ bool stage_service_outcome(Scratch& scratch,
         }
         middleware::secure_channel::advance_nonce(nonce);
         after = update.after;
-    } else if (outcome.hasSocketPlug) {
+    } else if (socket != nullptr) {
         // Body processing staged this exact +1 revision before encoding opcode 903's status pair.
         // A socket selection changes only one already-resident item-instance body.
-        const SocketPlug& socketPlug = outcome.socketPlugUpdate;
+        const SocketPlug& socketPlug = socket->update;
         bool preservedManifest =
             socketPlug.after.family4ResidentCount == before.family4ResidentCount;
         std::size_t accountMatches = 0;
@@ -146,15 +152,15 @@ bool stage_service_outcome(Scratch& scratch,
                                                               == socketPlug.accountDefinitionId);
         }
         if (!valid(socketPlug.after) || !preservedManifest || accountMatches != 1
-            || targetMatches != 1 || socketPlug.accountSoid != outcome.socketPlug.accountSoid
-            || socketPlug.characterSoid != outcome.socketPlug.characterSoid
-            || socketPlug.targetInstanceSoid != outcome.socketPlug.targetInstanceSoid
-            || socketPlug.updatesAccount != outcome.socketPlug.profileChanged
+            || targetMatches != 1 || socketPlug.accountSoid != socket->pending.accountSoid
+            || socketPlug.characterSoid != socket->pending.characterSoid
+            || socketPlug.targetInstanceSoid != socket->pending.targetInstanceSoid
+            || socketPlug.updatesAccount != socket->pending.profileChanged
             || socketPlug.after.family4RootSoid != before.family4RootSoid
             || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
             || socketPlug.after.family4Version != before.family4Version + 1
             || !push::append_socket_plug_notification(
-                scratch, socketPlug, outcome.socketPlug, key, nonce, response, written)) {
+                scratch, socketPlug, socket->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=socket_plug result=fail");
@@ -164,12 +170,11 @@ bool stage_service_outcome(Scratch& scratch,
         after = socketPlug.after;
         // Equipped plugs feed Family zero's material, overflow-hash and sandbox-perk banks. An
         // inventory-only socket change has no rendered character record to refresh.
-        if (outcome.socketPlug.targetEquipped && after.family0Active) {
+        if (socket->pending.targetEquipped && after.family0Active) {
             CharacterAppearanceRefresh refresh{};
-            if (!stage_character_appearance_refresh(
-                    after, outcome.socketPlug.characterSoid, refresh)
+            if (!stage_character_appearance_refresh(after, socket->pending.characterSoid, refresh)
                 || !push::append_socket_appearance_refresh_notification(
-                    scratch, refresh, outcome.socketPlug, key, nonce, response, written)) {
+                    scratch, refresh, socket->pending, key, nonce, response, written)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=queuez stage=socket_appearance result=fail");
@@ -179,12 +184,12 @@ bool stage_service_outcome(Scratch& scratch,
         }
         // A socket change can alter the rendered shader/perk banks in Family three, but it does not
         // change the roster's base-definition references.  Only the character record is owed.
-        if (outcome.socketPlug.targetEquipped && after.family3Active) {
+        if (socket->pending.targetEquipped && after.family3Active) {
             RosterAppearanceRefresh refresh{};
             if (!stage_roster_appearance_refresh(
-                    after, outcome.socketPlug.characterSoid, false, refresh)
+                    after, socket->pending.characterSoid, false, refresh)
                 || !push::append_socket_roster_refresh_notification(
-                    scratch, refresh, outcome.socketPlug, key, nonce, response, written)) {
+                    scratch, refresh, socket->pending, key, nonce, response, written)) {
                 core::log::write(core::log::Channel::server,
                                  core::log::Level::warn,
                                  "ev=queuez stage=socket_roster result=fail");
@@ -192,10 +197,10 @@ bool stage_service_outcome(Scratch& scratch,
             }
             after = refresh.after;
         }
-    } else if (outcome.hasItemAcquisition) {
+    } else if (itemAcquisition != nullptr) {
         // Body processing staged this exact manifest append before encoding the response version.
         // The character and new item objects must both fit or the State insertion is not committed.
-        const ItemAcquisition& acquisition = outcome.itemAcquisitionUpdate;
+        const ItemAcquisition& acquisition = itemAcquisition->update;
         const std::size_t appendedIndex = before.family4ResidentCount;
         bool preservedManifest = acquisition.after.family4ResidentCount == appendedIndex + 1U;
         for (std::size_t index = 0; preservedManifest && index < appendedIndex; ++index) {
@@ -205,10 +210,10 @@ bool stage_service_outcome(Scratch& scratch,
                                        == before.family4Residents[index].definitionId;
         }
         if (!valid(acquisition.after) || !preservedManifest
-            || acquisition.accountSoid != outcome.itemAcquisition.accountSoid
-            || acquisition.characterSoid != outcome.itemAcquisition.characterSoid
-            || acquisition.acquiredInstanceSoid != outcome.itemAcquisition.acquiredInstanceSoid
-            || acquisition.updatesAccount != outcome.itemAcquisition.profileChanged
+            || acquisition.accountSoid != itemAcquisition->pending.accountSoid
+            || acquisition.characterSoid != itemAcquisition->pending.characterSoid
+            || acquisition.acquiredInstanceSoid != itemAcquisition->pending.acquiredInstanceSoid
+            || acquisition.updatesAccount != itemAcquisition->pending.profileChanged
             || acquisition.accountSoid != before.family4RootSoid
             || acquisition.after.family4RootSoid != before.family4RootSoid
             || before.family4ResidentCount >= before.family4Residents.size()
@@ -219,7 +224,7 @@ bool stage_service_outcome(Scratch& scratch,
             || acquisition.after.family4Residents[appendedIndex].definitionId
                    != acquisition.itemInstanceDefinitionId
             || !push::append_item_acquisition_notification(
-                scratch, acquisition, outcome.itemAcquisition, key, nonce, response, written)) {
+                scratch, acquisition, itemAcquisition->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=acquire result=fail");
@@ -227,10 +232,10 @@ bool stage_service_outcome(Scratch& scratch,
         }
         middleware::secure_channel::advance_nonce(nonce);
         after = acquisition.after;
-    } else if (outcome.hasProfileItemAcquisition) {
+    } else if (profileAcquisition != nullptr) {
         // A source-backed profile append creates one dependency before the account starts naming
         // it. Existing stacks and non-actionable currency rows preserve the complete manifest.
-        const ProfileItemAcquisition& acquisition = outcome.profileItemAcquisitionUpdate;
+        const ProfileItemAcquisition& acquisition = profileAcquisition->update;
         const std::size_t priorResidentCount = before.family4ResidentCount;
         const std::size_t expectedResidentCount =
             priorResidentCount + static_cast<std::size_t>(acquisition.appendedResident);
@@ -266,25 +271,19 @@ bool stage_service_outcome(Scratch& scratch,
                     : acquisition.itemInstanceDefinitionId == 0 && !acquisition.appendedResident
                           && priorProfileResidentMatches == 0);
         if (!valid(acquisition.after) || !validManifest || !sourceIdentityValid
-            || acquisition.accountSoid != outcome.profileItemAcquisition.accountSoid
-            || acquisition.acquiredInstanceSoid
-                   != outcome.profileItemAcquisition.acquiredInstanceSoid
-            || acquisition.actionSource != outcome.profileItemAcquisition.actionSource
+            || acquisition.accountSoid != profileAcquisition->pending.accountSoid
+            || acquisition.acquiredInstanceSoid != profileAcquisition->pending.acquiredInstanceSoid
+            || acquisition.actionSource != profileAcquisition->pending.actionSource
             || acquisition.appendedResident
-                   != (outcome.profileItemAcquisition.appended
-                       && outcome.profileItemAcquisition.actionSource)
+                   != (profileAcquisition->pending.appended
+                       && profileAcquisition->pending.actionSource)
             || acquisition.accountSoid != before.family4RootSoid || before.family4ResidentCount == 0
             || acquisition.accountDefinitionId != before.family4Residents.front().definitionId
             || acquisition.after.family4RootSoid != before.family4RootSoid
             || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
             || acquisition.after.family4Version != before.family4Version + 1
-            || !push::append_profile_item_acquisition_notification(scratch,
-                                                                   acquisition,
-                                                                   outcome.profileItemAcquisition,
-                                                                   key,
-                                                                   nonce,
-                                                                   response,
-                                                                   written)) {
+            || !push::append_profile_item_acquisition_notification(
+                scratch, acquisition, profileAcquisition->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=profile_acquire result=fail");
@@ -292,11 +291,11 @@ bool stage_service_outcome(Scratch& scratch,
         }
         middleware::secure_channel::advance_nonce(nonce);
         after = acquisition.after;
-    } else if (outcome.hasItemDismantle) {
+    } else if (itemDismantle != nullptr) {
         // A dismantle removes exactly one resident while preserving the relative order of every
         // survivor. The character after-image and empty release descriptor must fit together or
         // the State removal is not committed.
-        const ItemDismantle& dismantle = outcome.itemDismantleUpdate;
+        const ItemDismantle& dismantle = itemDismantle->update;
         bool compactedManifest =
             before.family4ResidentCount != 0
             && dismantle.after.family4ResidentCount + 1U == before.family4ResidentCount;
@@ -323,18 +322,17 @@ bool stage_service_outcome(Scratch& scratch,
                             && afterIndex == dismantle.after.family4ResidentCount;
 
         if (!valid(dismantle.after) || !compactedManifest
-            || dismantle.accountSoid != outcome.itemDismantle.accountSoid
-            || dismantle.characterSoid != outcome.itemDismantle.characterSoid
-            || dismantle.dismantledInstanceSoid != outcome.itemDismantle.dismantledInstanceSoid
-            || dismantle.updatesAccount != outcome.itemDismantle.profileChanged
-            || dismantle.accountSoid != before.family4RootSoid
-            || before.family4ResidentCount == 0
+            || dismantle.accountSoid != itemDismantle->pending.accountSoid
+            || dismantle.characterSoid != itemDismantle->pending.characterSoid
+            || dismantle.dismantledInstanceSoid != itemDismantle->pending.dismantledInstanceSoid
+            || dismantle.updatesAccount != itemDismantle->pending.profileChanged
+            || dismantle.accountSoid != before.family4RootSoid || before.family4ResidentCount == 0
             || dismantle.accountDefinitionId != before.family4Residents.front().definitionId
             || dismantle.after.family4RootSoid != before.family4RootSoid
             || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
             || dismantle.after.family4Version != before.family4Version + 1
             || !push::append_item_dismantle_notification(
-                scratch, dismantle, outcome.itemDismantle, key, nonce, response, written)) {
+                scratch, dismantle, itemDismantle->pending, key, nonce, response, written)) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
                              "ev=queuez stage=dismantle result=fail");

+ 5 - 0
Sunrise/src/server/bap/encrypted/queuez/queuez_state_validation.h

@@ -22,6 +22,11 @@ namespace sunrise::server::bap::encrypted::queuez {
                                           const middleware::queuez::Family& family,
                                           SessionState& after) noexcept;
 
+/** Replaces an active peer's Family-4 manifest with a full next-version snapshot. */
+[[nodiscard]] bool stage_family4_refresh(const SessionState& before,
+                                         const middleware::queuez::Family& family,
+                                         SessionState& after) noexcept;
+
 /**
  * Decides whether one family-zero subscription publishes, and as which kind of frame.
  * Retail sets the full-snapshot flag once per family and adds one to every later push, so a

+ 48 - 0
Sunrise/src/server/bap/encrypted/queuez/staging/queuez_family_refresh_staging.cpp

@@ -0,0 +1,48 @@
+#include <cstddef>
+#include <limits>
+
+#include "../queuez_state_validation.h"
+
+namespace sunrise::server::bap::encrypted::queuez {
+
+/** Replaces an active Family-4 manifest at exactly the next peer-local version. */
+bool stage_family4_refresh(const SessionState& before,
+                           const middleware::queuez::Family& family,
+                           SessionState& after) noexcept {
+    after = before;
+    if (!valid(before) || !before.family4Active || before.family4RootSoid == 0
+        || before.family4Version == (std::numeric_limits<std::int32_t>::max)()
+        || family.type != kAccountFamilyType || family.rootSoid != before.family4RootSoid
+        || family.version != before.family4Version + 1
+        || family.flags != middleware::queuez::kFullSnapshotFlag || family.objects.empty()
+        || family.objects.size() > kResidentCapacity
+        || family.objects.size()
+               > static_cast<std::size_t>((std::numeric_limits<std::uint16_t>::max)())) {
+        return false;
+    }
+
+    SessionState candidate = before;
+    candidate.family4Residents = {};
+    candidate.family4Version = family.version;
+    candidate.family4ResidentCount = static_cast<std::uint16_t>(family.objects.size());
+    for (std::size_t index = 0; index < family.objects.size(); ++index) {
+        const middleware::queuez::Object& object = family.objects[index];
+        if (object.id == 0 || object.version == 0) {
+            return false;
+        }
+        for (std::size_t prior = 0; prior < index; ++prior) {
+            if (family.objects[prior].version == object.version) {
+                return false;
+            }
+        }
+        candidate.family4Residents[index] = ResidentObject{object.version, object.id};
+    }
+    if (candidate.family4Residents.front().objectSoid != before.family4RootSoid
+        || !valid(candidate)) {
+        return false;
+    }
+    after = candidate;
+    return true;
+}
+
+} // namespace sunrise::server::bap::encrypted::queuez

+ 23 - 37
Sunrise/src/server/bap/encrypted/transactions/service_outcome_commit.cpp

@@ -16,85 +16,71 @@ namespace sunrise::server::bap::encrypted::transactions {
  */
 bool commit(ServiceOutcome& outcome, Publication& publication) noexcept {
     publication = {};
-    const unsigned mutationCount = static_cast<unsigned>(outcome.hasActivitySessionAllocation)
-                                   + static_cast<unsigned>(outcome.hasActivityTransaction)
-                                   + static_cast<unsigned>(outcome.hasMatchmakingMutation)
-                                   + static_cast<unsigned>(outcome.hasEquipmentSwap)
-                                   + static_cast<unsigned>(outcome.hasSocketPlug)
-                                   + static_cast<unsigned>(outcome.hasItemState)
-                                   + static_cast<unsigned>(outcome.hasItemAcquisition)
-                                   + static_cast<unsigned>(outcome.hasProfileItemAcquisition)
-                                   + static_cast<unsigned>(outcome.hasItemDismantle);
-    // A service route may never combine independently versioned State transactions.
-    if (mutationCount > 1U) {
-        return false;
-    }
-    if (outcome.hasActivitySessionAllocation) {
-        const std::uint64_t sessionId = outcome.activitySessionAllocation.sessionId;
+    if (auto* allocation = transaction_if<state::activity::PendingAllocation>(outcome)) {
+        const std::uint64_t sessionId = allocation->sessionId;
         if (sessionId == state::activity::kAbsentSessionId
-            || !state::activity::commit(outcome.activitySessionAllocation)) {
+            || !state::activity::commit(*allocation)) {
             return false;
         }
         publication.activitySessionId = sessionId;
         publication.hasActivitySessionBinding = true;
         return true;
     }
-    if (outcome.hasActivityTransaction) {
-        if (outcome.activityPlan.mutationDomain == activity_message::MutationDomain::entitySlots) {
-            return state::activity::entity_slots::commit(outcome.activityPlan.entitySlotMutation);
+    if (auto* plan = transaction_if<activity_message::ActivityPlan>(outcome)) {
+        if (plan->mutationDomain == activity_message::MutationDomain::entitySlots) {
+            return state::activity::entity_slots::commit(plan->entitySlotMutation);
         }
-        if (outcome.activityPlan.mutationDomain == activity_message::MutationDomain::membership) {
-            return state::activity::membership::commit(outcome.activityPlan.membershipMutation);
+        if (plan->mutationDomain == activity_message::MutationDomain::membership) {
+            return state::activity::membership::commit(plan->membershipMutation);
         }
         // The retained patch epoch is connection state, so it commits nothing here.
-        return outcome.activityPlan.mutationDomain == activity_message::MutationDomain::patchEpoch;
+        return plan->mutationDomain == activity_message::MutationDomain::patchEpoch;
     }
-    if (outcome.hasMatchmakingMutation) {
-        return state::matchmaking::commit(outcome.matchmakingMutation);
+    if (auto* mutation = transaction_if<state::matchmaking::PendingMutation>(outcome)) {
+        return state::matchmaking::commit(*mutation);
     }
-    if (outcome.hasEquipmentSwap) {
-        const bool committed = state::commit_equipment_swap(outcome.equipmentSwap);
+    if (auto* transaction = transaction_if<EquipmentSwapTransaction>(outcome)) {
+        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,
                          committed ? "ev=equip stage=transaction_commit result=ok"
                                    : "ev=equip stage=transaction_commit result=fail");
         return committed;
     }
-    if (outcome.hasItemAcquisition) {
-        const bool committed = state::commit_item_acquisition(outcome.itemAcquisition);
+    if (auto* transaction = transaction_if<ItemAcquisitionTransaction>(outcome)) {
+        const bool committed = state::commit_item_acquisition(transaction->pending);
         core::log::write(core::log::Channel::server,
                          committed ? core::log::Level::debug : core::log::Level::warn,
                          committed ? "ev=acquire stage=transaction_commit result=ok"
                                    : "ev=acquire stage=transaction_commit result=fail");
         return committed;
     }
-    if (outcome.hasSocketPlug) {
-        const bool committed = state::commit_socket_plug(outcome.socketPlug);
+    if (auto* transaction = transaction_if<SocketPlugTransaction>(outcome)) {
+        const bool committed = state::commit_socket_plug(transaction->pending);
         core::log::write(core::log::Channel::server,
                          committed ? core::log::Level::debug : core::log::Level::warn,
                          committed ? "ev=socket_plug stage=transaction_commit result=ok"
                                    : "ev=socket_plug stage=transaction_commit result=fail");
         return committed;
     }
-    if (outcome.hasItemState) {
-        const bool committed = state::commit_item_state(outcome.itemState);
+    if (auto* transaction = transaction_if<ItemStateTransaction>(outcome)) {
+        const bool committed = state::commit_item_state(transaction->pending);
         core::log::write(core::log::Channel::server,
                          committed ? core::log::Level::debug : core::log::Level::warn,
                          committed ? "ev=item_state stage=transaction_commit result=ok"
                                    : "ev=item_state stage=transaction_commit result=fail");
         return committed;
     }
-    if (outcome.hasProfileItemAcquisition) {
-        const bool committed =
-            state::commit_profile_item_acquisition(outcome.profileItemAcquisition);
+    if (auto* transaction = transaction_if<ProfileItemAcquisitionTransaction>(outcome)) {
+        const bool committed = state::commit_profile_item_acquisition(transaction->pending);
         core::log::write(core::log::Channel::server,
                          committed ? core::log::Level::debug : core::log::Level::warn,
                          committed ? "ev=profile_acquire stage=transaction_commit result=ok"
                                    : "ev=profile_acquire stage=transaction_commit result=fail");
         return committed;
     }
-    if (outcome.hasItemDismantle) {
-        const bool committed = state::commit_item_dismantle(outcome.itemDismantle);
+    if (auto* transaction = transaction_if<ItemDismantleTransaction>(outcome)) {
+        const bool committed = state::commit_item_dismantle(transaction->pending);
         core::log::write(core::log::Channel::server,
                          committed ? core::log::Level::debug : core::log::Level::warn,
                          committed ? "ev=dismantle stage=transaction_commit result=ok"

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

@@ -87,6 +87,14 @@ struct Session {
     std::uint64_t bannerRepushRoot{};
     /** True while one banner re-push is still owed to this peer. */
     bool bannerRepushArmed{};
+    /** Latest shared-account generation this peer has received. */
+    std::uint64_t accountGeneration{};
+    /** Newest shared-account generation owed as a full cross-peer refresh. */
+    std::uint64_t accountResyncGeneration{};
+    /** Set by encrypted processing only after one account mutation commits and is copied out. */
+    bool accountMutationPublished{};
+    /** True while another peer's account mutation still needs a full local refresh. */
+    bool accountResyncArmed{};
 };
 
 namespace plaintext {

+ 861 - 0
Sunrise/src/server/web_service/web_service_actions.cpp

@@ -0,0 +1,861 @@
+#include "web_service_actions.h"
+
+#include <array>
+#include <cstdio>
+#include <limits>
+#include <string_view>
+
+#include "../../core/logging/log.h"
+#include "../../middleware/encoding/bit_reader.h"
+#include "../../middleware/encoding/byte_order.h"
+#include "../../middleware/web_service/messages/opcode1901.h"
+#include "../../middleware/web_service/messages/opcode504.h"
+#include "../../middleware/web_service/messages/opcode903.h"
+#include "../../state/account/account_state.h"
+#include "../../state/build_data/runtime.h"
+#include "../../state/runtime/runtime.h"
+
+namespace sunrise::server::web_service {
+
+namespace {
+
+constexpr std::size_t kEquipmentActionPayloadSize = middleware::encoding::kU64Size + 1U;
+constexpr std::size_t kItemStatePayloadSize = 15;
+constexpr std::uint8_t kItemStateDefinitionIndexWidth = 15;
+constexpr std::uint8_t kItemStateValueWidth = 32;
+constexpr std::uint8_t kItemStatePaddingWidth = 7;
+constexpr std::uint64_t kItemStateValueBias = 0x80000000ULL;
+constexpr std::size_t kItemDismantlePayloadSize = 16;
+constexpr std::uint8_t kItemDismantleInstanceWidth = 64;
+constexpr std::uint8_t kItemDismantleDefinitionIndexWidth = 15;
+constexpr std::uint8_t kItemDismantleQuantityWidth = 32;
+constexpr std::uint32_t kItemDismantleSingleQuantityWire = 0x80000001U;
+constexpr std::uint8_t kItemDismantleRequiredFlagWidth = 1;
+constexpr std::uint8_t kItemDismantleNestedPaddingWidth = 6;
+constexpr std::uint8_t kItemDismantleOuterTrailerWidth = 2;
+constexpr std::uint8_t kItemDismantleFinalPaddingWidth = 6;
+constexpr std::uint64_t kEquipmentSelectorStride = 4;
+constexpr std::uint8_t kEquippedShaderModelSocketKind = 0;
+constexpr std::size_t kItemAcquisitionPayloadSize = 3;
+constexpr std::uint8_t kItemAcquisitionPresenceWidth = 1;
+constexpr std::uint8_t kItemAcquisitionCollectibleIndexWidth = 15;
+constexpr std::uint8_t kItemAcquisitionPaddingWidth = 8;
+constexpr std::uint32_t kUnavailableDefinitionIndex = (std::numeric_limits<std::uint16_t>::max)();
+} // namespace
+
+/** Logs one exact correlated equipment response after its Queuez update is staged. */
+void report_equip_response(const middleware::web_service::Message& message,
+                           std::int32_t family4Version,
+                           std::span<const std::byte> response) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int prefix = std::snprintf(line.data(),
+                                     line.size(),
+                                     "ev=equipment stage=response opcode=%u transaction=%u "
+                                     "family_version=%d bytes=%zu hex=",
+                                     static_cast<unsigned>(message.opcode),
+                                     static_cast<unsigned>(message.transactionId),
+                                     family4Version,
+                                     response.size());
+    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
+        return;
+    }
+    constexpr char kHex[] = "0123456789ABCDEF";
+    std::size_t length = static_cast<std::size_t>(prefix);
+    for (const std::byte byte : response) {
+        if (length + 2 >= line.size()) {
+            break;
+        }
+        const unsigned value = std::to_integer<unsigned>(byte);
+        line[length++] = kHex[(value >> 4U) & 0xFU];
+        line[length++] = kHex[value & 0xFU];
+    }
+    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
+}
+
+/** Logs the final item-creation status pair and the exact Family-4 revision it promises. */
+void report_item_acquisition_response(const middleware::web_service::Message& message,
+                                      std::int32_t family4Version,
+                                      std::uint64_t acquiredInstanceSoid,
+                                      std::span<const std::byte> response) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int prefix = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=acquire stage=response result=ok opcode=%u transaction=%u family_version=%d "
+        "instance=0x%llX bytes=%zu hex=",
+        static_cast<unsigned>(message.opcode),
+        static_cast<unsigned>(message.transactionId),
+        family4Version,
+        static_cast<unsigned long long>(acquiredInstanceSoid),
+        response.size());
+    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
+        return;
+    }
+    constexpr char kHex[] = "0123456789ABCDEF";
+    std::size_t length = static_cast<std::size_t>(prefix);
+    for (const std::byte byte : response) {
+        if (length + 2 >= line.size()) {
+            break;
+        }
+        const unsigned value = std::to_integer<unsigned>(byte);
+        line[length++] = kHex[(value >> 4U) & 0xFU];
+        line[length++] = kHex[value & 0xFU];
+    }
+    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
+}
+
+/** Logs the final profile-stack status pair and the exact Family-4 account revision it promises. */
+void report_profile_item_acquisition_response(const middleware::web_service::Message& message,
+                                              std::int32_t family4Version,
+                                              std::uint32_t definitionHash,
+                                              std::int32_t quantity,
+                                              std::span<const std::byte> response) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int prefix =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=profile_acquire stage=response result=ok opcode=%u transaction=%u "
+                      "family_version=%d definition_hash=%u quantity=%d bytes=%zu hex=",
+                      static_cast<unsigned>(message.opcode),
+                      static_cast<unsigned>(message.transactionId),
+                      family4Version,
+                      definitionHash,
+                      quantity,
+                      response.size());
+    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
+        return;
+    }
+    constexpr char kHex[] = "0123456789ABCDEF";
+    std::size_t length = static_cast<std::size_t>(prefix);
+    for (const std::byte byte : response) {
+        if (length + 2 >= line.size()) {
+            break;
+        }
+        const unsigned value = std::to_integer<unsigned>(byte);
+        line[length++] = kHex[(value >> 4U) & 0xFU];
+        line[length++] = kHex[value & 0xFU];
+    }
+    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
+}
+
+/** Logs the final dismantle status pair and the exact Family-4 revision it promises. */
+void report_item_dismantle_response(const middleware::web_service::Message& message,
+                                    std::int32_t family4Version,
+                                    std::uint64_t dismantledInstanceSoid,
+                                    std::span<const std::byte> response) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int prefix = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=dismantle stage=response result=ok opcode=%u transaction=%u family_version=%d "
+        "instance=0x%llX bytes=%zu hex=",
+        static_cast<unsigned>(message.opcode),
+        static_cast<unsigned>(message.transactionId),
+        family4Version,
+        static_cast<unsigned long long>(dismantledInstanceSoid),
+        response.size());
+    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
+        return;
+    }
+    constexpr char kHex[] = "0123456789ABCDEF";
+    std::size_t length = static_cast<std::size_t>(prefix);
+    for (const std::byte byte : response) {
+        if (length + 2 >= line.size()) {
+            break;
+        }
+        const unsigned value = std::to_integer<unsigned>(byte);
+        line[length++] = kHex[(value >> 4U) & 0xFU];
+        line[length++] = kHex[value & 0xFU];
+    }
+    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
+}
+
+/** Logs the exact opcode-903 status pair and the item-instance revision it promises. */
+void report_socket_plug_response(const middleware::web_service::Message& message,
+                                 std::int32_t family4Version,
+                                 std::uint64_t targetInstanceSoid,
+                                 std::uint8_t socketLane,
+                                 std::uint16_t plugDefinitionIndex,
+                                 std::span<const std::byte> response) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int prefix = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=socket_plug stage=response result=ok opcode=%u transaction=%u family_version=%d "
+        "instance=0x%llX lane=%u plug_definition=%u bytes=%zu hex=",
+        static_cast<unsigned>(message.opcode),
+        static_cast<unsigned>(message.transactionId),
+        family4Version,
+        static_cast<unsigned long long>(targetInstanceSoid),
+        static_cast<unsigned>(socketLane),
+        static_cast<unsigned>(plugDefinitionIndex),
+        response.size());
+    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
+        return;
+    }
+    constexpr char kHex[] = "0123456789ABCDEF";
+    std::size_t length = static_cast<std::size_t>(prefix);
+    for (const std::byte byte : response) {
+        if (length + 2 >= line.size()) {
+            break;
+        }
+        const unsigned value = std::to_integer<unsigned>(byte);
+        line[length++] = kHex[(value >> 4U) & 0xFU];
+        line[length++] = kHex[value & 0xFU];
+    }
+    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
+}
+
+/** One line carries the picked id and whether the selection moved. */
+constexpr std::size_t kSelectLineCapacity = 96;
+
+/**
+ * Records the player's character pick, which arrives nowhere else.
+ * A bad or unknown id leaves the selection alone. The reply is the status pair either way. The
+ * Family-4 object move follows this call, and the family-zero pair after it.
+ * @param message Parsed select-character request.
+ * @param outcome Gets the picked key once the selection has moved in State.
+ */
+void select_character(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
+    middleware::web_service::messages::opcode504::Request picked;
+    if (!middleware::web_service::messages::opcode504::parse_request(message, picked)) {
+        core::log::write(
+            core::log::Channel::server, core::log::Level::warn, "ev=ws504 stage=parse result=fail");
+        return;
+    }
+    bool changed = false;
+    if (!state::set_selected_character(picked.characterSoid, changed)) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::warn,
+                         "ev=ws504 stage=select result=unknown");
+        return;
+    }
+    outcome.hasSelectedCharacter = true;
+    outcome.selectedCharacterSoid = picked.characterSoid;
+
+    std::array<char, kSelectLineCapacity> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=ws504 stage=select result=ok soid=0x%llX changed=%u",
+                                      static_cast<unsigned long long>(picked.characterSoid),
+                                      static_cast<unsigned>(changed));
+    if (written > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(written)});
+    }
+}
+
+/** Parses the exact shared opcode-403/404 SOID descriptor. */
+[[nodiscard]] bool parse_equipment_instance(const middleware::web_service::Message& message,
+                                            std::uint64_t& instanceSoid) noexcept {
+    instanceSoid = 0;
+    if (message.payload.size() != kEquipmentActionPayloadSize
+        || message.payload[middleware::encoding::kU64Size] != std::byte{}) {
+        return false;
+    }
+    instanceSoid = middleware::encoding::read_u64_be(
+        std::span<const std::byte, middleware::encoding::kU64Size>{message.payload.data(),
+                                                                   middleware::encoding::kU64Size});
+    return instanceSoid != 0;
+}
+
+/** Prepares one opcode-403/404 equipment mutation without publishing State early. */
+void mutate_equipment(const middleware::web_service::Message& message,
+                      bool unequip,
+                      Outcome& outcome) noexcept {
+    std::uint64_t requestedInstanceSoid = 0;
+    if (!parse_equipment_instance(message, requestedInstanceSoid)) {
+        std::array<char, 112> line{};
+        const int count = std::snprintf(line.data(),
+                                        line.size(),
+                                        "ev=equipment stage=parse result=fail opcode=%u "
+                                        "payload_bytes=%zu",
+                                        static_cast<unsigned>(message.opcode),
+                                        message.payload.size());
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+
+    state::PendingEquipmentSwap mutation;
+    const bool prepared = unequip
+                              ? state::prepare_equipment_unequip(requestedInstanceSoid, mutation)
+                              : state::prepare_equipment_swap(requestedInstanceSoid, mutation);
+    if (!prepared) {
+        std::array<char, 144> line{};
+        const int count = std::snprintf(
+            line.data(),
+            line.size(),
+            "ev=equipment stage=prepare result=fail opcode=%u action=%s requested=0x%llX",
+            static_cast<unsigned>(message.opcode),
+            unequip ? "unequip" : "equip",
+            static_cast<unsigned long long>(requestedInstanceSoid));
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+    outcome.mutation = mutation;
+
+    std::array<char, 224> line{};
+    const int count =
+        std::snprintf(line.data(),
+                      line.size(),
+                      "ev=equipment stage=prepare result=ok opcode=%u action=%s character=0x%llX "
+                      "previous=0x%llX requested=0x%llX native_slot=%u moved_items=%zu",
+                      static_cast<unsigned>(message.opcode),
+                      unequip ? "unequip" : "equip",
+                      static_cast<unsigned long long>(mutation.characterSoid),
+                      static_cast<unsigned long long>(mutation.previousInstanceSoid),
+                      static_cast<unsigned long long>(mutation.requestedInstanceSoid),
+                      static_cast<unsigned>(mutation.nativeEquipmentSlot),
+                      mutation.movedItemCount);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
+/** Parses and prepares one exact selected-character opcode-903 socket selection. */
+void mutate_socket_plug(const middleware::web_service::Message& message,
+                        Outcome& outcome) noexcept {
+    middleware::web_service::messages::opcode903::Request request{};
+    if (!middleware::web_service::messages::opcode903::parse_request(message, request)
+        || !request.hasInstance || request.instanceSoid == 0 || request.hasTargetDefinition
+        || !request.hasPlugDefinition
+        || request.socketIndex >= state::account::inventory::kPlugCapacity) {
+        std::array<char, 192> line{};
+        const int count = std::snprintf(
+            line.data(),
+            line.size(),
+            "ev=ws903 stage=parse result=fail transaction=%u payload_bytes=%zu has_instance=%u "
+            "instance=0x%llX has_target_definition=%u socket=%u has_plug_definition=%u",
+            static_cast<unsigned>(message.transactionId),
+            message.payload.size(),
+            static_cast<unsigned>(request.hasInstance),
+            static_cast<unsigned long long>(request.instanceSoid),
+            static_cast<unsigned>(request.hasTargetDefinition),
+            request.socketIndex,
+            static_cast<unsigned>(request.hasPlugDefinition));
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+
+    state::PendingSocketPlug mutation{};
+    if (!state::prepare_socket_plug(request.instanceSoid,
+                                    static_cast<std::uint8_t>(request.socketIndex),
+                                    request.plugDefinitionIndex,
+                                    mutation)) {
+        std::array<char, 192> line{};
+        const int count = std::snprintf(
+            line.data(),
+            line.size(),
+            "ev=ws903 stage=prepare result=fail transaction=%u instance=0x%llX lane=%u "
+            "plug_definition=%u",
+            static_cast<unsigned>(message.transactionId),
+            static_cast<unsigned long long>(request.instanceSoid),
+            request.socketIndex,
+            static_cast<unsigned>(request.plugDefinitionIndex));
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+
+    outcome.mutation = mutation;
+    std::array<char, 240> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=ws903 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
+        "target_definition=%u target_bucket=%u lane=%u plug_definition=%u plug_bucket=%u "
+        "equipped=%u item_index=%zu",
+        static_cast<unsigned>(message.transactionId),
+        static_cast<unsigned long long>(mutation.characterSoid),
+        static_cast<unsigned long long>(mutation.targetInstanceSoid),
+        static_cast<unsigned>(mutation.targetDefinitionIndex),
+        static_cast<unsigned>(mutation.targetBucketId),
+        static_cast<unsigned>(mutation.socketLane),
+        static_cast<unsigned>(mutation.plugDefinitionIndex),
+        static_cast<unsigned>(mutation.plugBucketId),
+        static_cast<unsigned>(mutation.targetEquipped),
+        mutation.itemIndex);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
+/** Parses and prepares one character-location opcode-1901 socket selection. */
+void mutate_equipped_socket_plug(const middleware::web_service::Message& message,
+                                 Outcome& outcome) noexcept {
+    middleware::web_service::messages::opcode1901::Request request{};
+    if (!middleware::web_service::messages::opcode1901::parse_request(message, request)
+        || request.canonicalSocketKind != request.socketIndex
+        || request.modelSocketKind != kEquippedShaderModelSocketKind || request.auxiliary != 0
+        || request.socketIndex >= state::account::inventory::kPlugCapacity
+        || request.equipmentSelector == 0
+        || request.equipmentSelector % kEquipmentSelectorStride != 0) {
+        std::array<char, 256> line{};
+        const int count = std::snprintf(
+            line.data(),
+            line.size(),
+            "ev=ws1901 stage=parse result=fail transaction=%u payload_bytes=%zu "
+            "plug_definition=%u canonical_kind=%u model_kind=%u socket=%u auxiliary=0x%llX "
+            "equipment_selector=%llu",
+            static_cast<unsigned>(message.transactionId),
+            message.payload.size(),
+            static_cast<unsigned>(request.plugDefinitionIndex),
+            static_cast<unsigned>(request.canonicalSocketKind),
+            static_cast<unsigned>(request.modelSocketKind),
+            request.socketIndex,
+            static_cast<unsigned long long>(request.auxiliary),
+            static_cast<unsigned long long>(request.equipmentSelector));
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+
+    const std::uint64_t identityToken = request.equipmentSelector / kEquipmentSelectorStride;
+    state::PendingSocketPlug mutation{};
+    if (!state::prepare_character_selector_socket_plug(
+            request.equipmentSelector,
+            static_cast<std::uint8_t>(request.socketIndex),
+            request.plugDefinitionIndex,
+            mutation)) {
+        std::array<char, 224> line{};
+        const int count = std::snprintf(
+            line.data(),
+            line.size(),
+            "ev=ws1901 stage=prepare result=fail transaction=%u equipment_selector=%llu "
+            "identity_token=%llu lane=%u plug_definition=%u canonical_kind=%u model_kind=%u "
+            "auxiliary=0x%llX",
+            static_cast<unsigned>(message.transactionId),
+            static_cast<unsigned long long>(request.equipmentSelector),
+            static_cast<unsigned long long>(identityToken),
+            request.socketIndex,
+            static_cast<unsigned>(request.plugDefinitionIndex),
+            static_cast<unsigned>(request.canonicalSocketKind),
+            static_cast<unsigned>(request.modelSocketKind),
+            static_cast<unsigned long long>(request.auxiliary));
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+
+    outcome.mutation = mutation;
+    std::array<char, 288> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=ws1901 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
+        "equipment_selector=%llu identity_token=%llu target_definition=%u target_bucket=%u "
+        "lane=%u plug_definition=%u plug_bucket=%u canonical_kind=%u model_kind=%u "
+        "auxiliary=0x%llX",
+        static_cast<unsigned>(message.transactionId),
+        static_cast<unsigned long long>(mutation.characterSoid),
+        static_cast<unsigned long long>(mutation.targetInstanceSoid),
+        static_cast<unsigned long long>(request.equipmentSelector),
+        static_cast<unsigned long long>(identityToken),
+        static_cast<unsigned>(mutation.targetDefinitionIndex),
+        static_cast<unsigned>(mutation.targetBucketId),
+        static_cast<unsigned>(mutation.socketLane),
+        static_cast<unsigned>(mutation.plugDefinitionIndex),
+        static_cast<unsigned>(mutation.plugBucketId),
+        static_cast<unsigned>(request.canonicalSocketKind),
+        static_cast<unsigned>(request.modelSocketKind),
+        static_cast<unsigned long long>(request.auxiliary));
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
+/** Parses and prepares one complete accumulated item-state value from opcode 406. */
+void mutate_item_state(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
+    middleware::encoding::bits::Reader reader(message.payload);
+    std::uint64_t instancePresent = 0;
+    std::uint64_t instanceSoid = 0;
+    std::uint64_t definitionPresent = 0;
+    std::uint64_t definitionIndex = 0;
+    std::uint64_t encodedFlags = 0;
+    std::uint64_t padding = 0;
+    if (message.payload.size() != kItemStatePayloadSize || !reader.read(1, instancePresent)
+        || !reader.read(64, instanceSoid) || !reader.read(1, definitionPresent)
+        || !reader.read(kItemStateDefinitionIndexWidth, definitionIndex)
+        || !reader.read(kItemStateValueWidth, encodedFlags)
+        || !reader.read(kItemStatePaddingWidth, padding) || reader.remaining_bits() != 0
+        || instancePresent == 0 || instanceSoid == 0 || definitionPresent == 0
+        || definitionIndex > (std::numeric_limits<std::uint16_t>::max)()
+        || encodedFlags < kItemStateValueBias || padding != 0
+        || encodedFlags - kItemStateValueBias > 0x3U) {
+        std::array<char, 224> line{};
+        const int count = std::snprintf(
+            line.data(),
+            line.size(),
+            "ev=ws406 stage=parse result=fail transaction=%u payload_bytes=%zu instance=0x%llX "
+            "definition=%llu flags_wire=0x%llX padding=0x%llX",
+            static_cast<unsigned>(message.transactionId),
+            message.payload.size(),
+            static_cast<unsigned long long>(instanceSoid),
+            static_cast<unsigned long long>(definitionIndex),
+            static_cast<unsigned long long>(encodedFlags),
+            static_cast<unsigned long long>(padding));
+        if (count > 0) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             {line.data(), static_cast<std::size_t>(count)});
+        }
+        return;
+    }
+
+    const std::uint32_t flags = static_cast<std::uint32_t>(encodedFlags - kItemStateValueBias);
+    state::PendingItemState mutation{};
+    if (!state::prepare_item_state(
+            instanceSoid, static_cast<std::uint16_t>(definitionIndex), flags, mutation)) {
+        return;
+    }
+    outcome.mutation = mutation;
+    std::array<char, 224> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=ws406 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
+        "definition=%u flags_before=0x%X flags_after=0x%X equipped=%u item_index=%zu",
+        static_cast<unsigned>(message.transactionId),
+        static_cast<unsigned long long>(mutation.characterSoid),
+        static_cast<unsigned long long>(mutation.targetInstanceSoid),
+        static_cast<unsigned>(mutation.targetDefinitionIndex),
+        mutation.beforeFlags,
+        mutation.afterFlags,
+        mutation.targetEquipped ? 1U : 0U,
+        mutation.itemIndex);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
+/** Records strict opcode-402 parsing, identity checks, and State preparation outcomes. */
+void report_item_dismantle(const middleware::web_service::Message& message,
+                           std::string_view result,
+                           std::string_view reason,
+                           std::uint64_t instanceSoid,
+                           std::uint32_t definitionIndex,
+                           std::uint32_t definitionHash,
+                           std::uint32_t quantity) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=ws402 stage=prepare result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
+        "instance=0x%llX definition_index=%u definition_hash=%u quantity=%u",
+        static_cast<int>(result.size()),
+        result.data(),
+        static_cast<int>(reason.size()),
+        reason.data(),
+        static_cast<unsigned>(message.transactionId),
+        message.payload.size(),
+        static_cast<unsigned long long>(instanceSoid),
+        definitionIndex,
+        definitionHash,
+        quantity);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         result == "ok" ? core::log::Level::info : core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
+/** Prepares the exact fixed-width opcode-402 Character-inventory removal request. */
+void dismantle_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
+    if (message.payload.size() != kItemDismantlePayloadSize) {
+        report_item_dismantle(
+            message, "fail", "payload_size", 0, kUnavailableDefinitionIndex, 0, 0);
+        return;
+    }
+
+    middleware::encoding::bits::Reader reader(message.payload);
+    std::uint64_t instancePresent = 0;
+    std::uint64_t instanceSoid = 0;
+    std::uint64_t definitionPresent = 0;
+    std::uint64_t encodedDefinitionIndex = 0;
+    std::uint64_t encodedQuantity = 0;
+    std::uint64_t requiredFlag = 0;
+    std::uint64_t nestedPadding = 0;
+    std::uint64_t outerTrailers = 0;
+    std::uint64_t finalPadding = 0;
+    if (!reader.read(1, instancePresent) || !reader.read(kItemDismantleInstanceWidth, instanceSoid)
+        || !reader.read(1, definitionPresent)
+        || !reader.read(kItemDismantleDefinitionIndexWidth, encodedDefinitionIndex)
+        || !reader.read(kItemDismantleQuantityWidth, encodedQuantity)
+        || !reader.read(kItemDismantleRequiredFlagWidth, requiredFlag)
+        || !reader.read(kItemDismantleNestedPaddingWidth, nestedPadding)
+        || !reader.read(kItemDismantleOuterTrailerWidth, outerTrailers)
+        || !reader.read(kItemDismantleFinalPaddingWidth, finalPadding)
+        || reader.remaining_bits() != 0) {
+        report_item_dismantle(
+            message, "fail", "payload_bits", 0, kUnavailableDefinitionIndex, 0, 0);
+        return;
+    }
+    const auto definitionIndex = static_cast<std::uint16_t>(encodedDefinitionIndex);
+    const auto quantityWire = static_cast<std::uint32_t>(encodedQuantity);
+    constexpr std::uint32_t kSingleQuantity = 1;
+    if (instancePresent == 0 || definitionPresent == 0 || instanceSoid == 0) {
+        report_item_dismantle(
+            message, "fail", "required_field", instanceSoid, definitionIndex, 0, 0);
+        return;
+    }
+    if (nestedPadding != 0 || outerTrailers != 0 || finalPadding != 0) {
+        report_item_dismantle(
+            message, "fail", "padding_or_trailer", instanceSoid, definitionIndex, 0, 0);
+        return;
+    }
+    if (quantityWire != kItemDismantleSingleQuantityWire || requiredFlag != 1) {
+        report_item_dismantle(
+            message, "fail", "quantity_or_flag", instanceSoid, definitionIndex, 0, 0);
+        return;
+    }
+
+    state::build_data::items::Definition definition{};
+    if (!state::build_data::find_item_definition_index(definitionIndex, definition)) {
+        report_item_dismantle(
+            message, "fail", "definition", instanceSoid, definitionIndex, 0, kSingleQuantity);
+        return;
+    }
+    state::PendingItemDismantle mutation{};
+    if (!state::prepare_item_dismantle(instanceSoid, mutation)) {
+        report_item_dismantle(message,
+                              "fail",
+                              "state",
+                              instanceSoid,
+                              definitionIndex,
+                              definition.definitionHash,
+                              kSingleQuantity);
+        return;
+    }
+    if (mutation.dismantledItem.definitionHash != definition.definitionHash
+        || mutation.dismantledItem.quantity != static_cast<std::int32_t>(kSingleQuantity)) {
+        report_item_dismantle(message,
+                              "fail",
+                              "identity",
+                              instanceSoid,
+                              definitionIndex,
+                              definition.definitionHash,
+                              kSingleQuantity);
+        return;
+    }
+    outcome.mutation = mutation;
+    report_item_dismantle(message,
+                          "ok",
+                          "ready",
+                          instanceSoid,
+                          definitionIndex,
+                          definition.definitionHash,
+                          kSingleQuantity);
+}
+
+/** Records strict opcode-1820 parsing, installed mapping, and State preparation outcomes. */
+void report_item_acquisition(const middleware::web_service::Message& message,
+                             std::string_view result,
+                             std::string_view reason,
+                             std::uint32_t collectibleIndex,
+                             std::uint32_t itemDefinitionIndex,
+                             std::uint32_t definitionHash,
+                             std::uint64_t instanceSoid) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=ws1820 stage=prepare result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
+        "collectible_index=%u item_definition_index=%u definition_hash=%u instance=0x%llX",
+        static_cast<int>(result.size()),
+        result.data(),
+        static_cast<int>(reason.size()),
+        reason.data(),
+        static_cast<unsigned>(message.transactionId),
+        message.payload.size(),
+        collectibleIndex,
+        itemDefinitionIndex,
+        definitionHash,
+        static_cast<unsigned long long>(instanceSoid));
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         result == "ok" ? core::log::Level::info : core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
+/** Prepares the exact three-byte opcode-1820 Collections item request. */
+void acquire_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
+    if (message.payload.size() != kItemAcquisitionPayloadSize) {
+        report_item_acquisition(message,
+                                "fail",
+                                "payload_size",
+                                kUnavailableDefinitionIndex,
+                                kUnavailableDefinitionIndex,
+                                0,
+                                0);
+        return;
+    }
+
+    middleware::encoding::bits::Reader reader(message.payload);
+    std::uint64_t present = 0;
+    std::uint64_t encodedCollectibleIndex = 0;
+    std::uint64_t padding = 0;
+    if (!reader.read(kItemAcquisitionPresenceWidth, present)
+        || !reader.read(kItemAcquisitionCollectibleIndexWidth, encodedCollectibleIndex)
+        || !reader.read(kItemAcquisitionPaddingWidth, padding) || reader.remaining_bits() != 0) {
+        report_item_acquisition(message,
+                                "fail",
+                                "payload_bits",
+                                kUnavailableDefinitionIndex,
+                                kUnavailableDefinitionIndex,
+                                0,
+                                0);
+        return;
+    }
+    const auto collectibleIndex = static_cast<std::uint16_t>(encodedCollectibleIndex);
+    if (present == 0) {
+        report_item_acquisition(message,
+                                "fail",
+                                "collectible_absent",
+                                collectibleIndex,
+                                kUnavailableDefinitionIndex,
+                                0,
+                                0);
+        return;
+    }
+    if (padding != 0) {
+        report_item_acquisition(
+            message, "fail", "padding", collectibleIndex, kUnavailableDefinitionIndex, 0, 0);
+        return;
+    }
+
+    std::uint16_t itemDefinitionIndex = 0;
+    if (!state::build_data::find_collectible_item_definition_index(collectibleIndex,
+                                                                   itemDefinitionIndex)) {
+        report_item_acquisition(message,
+                                "fail",
+                                "collectible_definition",
+                                collectibleIndex,
+                                kUnavailableDefinitionIndex,
+                                0,
+                                0);
+        return;
+    }
+
+    state::build_data::items::Definition definition{};
+    if (!state::build_data::find_item_definition_index(itemDefinitionIndex, definition)) {
+        report_item_acquisition(
+            message, "fail", "item_definition", collectibleIndex, itemDefinitionIndex, 0, 0);
+        return;
+    }
+
+    state::build_data::items::details::Definition detail{};
+    state::build_data::inventory::buckets::Descriptor bucket{};
+    if (!state::build_data::find_configured_item_detail(itemDefinitionIndex, detail)
+        || detail.definitionIndex != itemDefinitionIndex
+        || detail.definitionHash != definition.definitionHash
+        || detail.bucketId != definition.bucketId
+        || !state::build_data::find_inventory_bucket_descriptor(detail.bucketId, bucket)) {
+        report_item_acquisition(message,
+                                "fail",
+                                "item_detail_or_bucket",
+                                collectibleIndex,
+                                itemDefinitionIndex,
+                                definition.definitionHash,
+                                0);
+        return;
+    }
+
+    namespace bucket_domain = state::build_data::inventory::buckets;
+    namespace detail_domain = state::build_data::items::details;
+    if (bucket.arraySelector == bucket_domain::ArraySelector::profile) {
+        if (detail.instancedDefinitionState != detail_domain::InstancedDefinitionState::stackable) {
+            report_item_acquisition(message,
+                                    "fail",
+                                    "profile_item_instanced",
+                                    collectibleIndex,
+                                    itemDefinitionIndex,
+                                    definition.definitionHash,
+                                    0);
+            return;
+        }
+        state::PendingProfileItemAcquisition mutation{};
+        if (!state::prepare_profile_item_acquisition(
+                collectibleIndex, definition.definitionHash, mutation)) {
+            report_item_acquisition(message,
+                                    "fail",
+                                    "profile_state",
+                                    collectibleIndex,
+                                    itemDefinitionIndex,
+                                    definition.definitionHash,
+                                    0);
+            return;
+        }
+        outcome.mutation = mutation;
+        report_item_acquisition(message,
+                                "ok",
+                                "profile_ready",
+                                collectibleIndex,
+                                itemDefinitionIndex,
+                                definition.definitionHash,
+                                0);
+        return;
+    }
+    if (bucket.arraySelector != bucket_domain::ArraySelector::character) {
+        report_item_acquisition(message,
+                                "fail",
+                                "unsupported_inventory_array",
+                                collectibleIndex,
+                                itemDefinitionIndex,
+                                definition.definitionHash,
+                                0);
+        return;
+    }
+
+    state::PendingItemAcquisition mutation{};
+    if (!state::prepare_item_acquisition(collectibleIndex, definition.definitionHash, mutation)) {
+        report_item_acquisition(message,
+                                "fail",
+                                "state",
+                                collectibleIndex,
+                                itemDefinitionIndex,
+                                definition.definitionHash,
+                                0);
+        return;
+    }
+    outcome.mutation = mutation;
+    report_item_acquisition(message,
+                            "ok",
+                            "ready",
+                            collectibleIndex,
+                            itemDefinitionIndex,
+                            definition.definitionHash,
+                            mutation.acquiredInstanceSoid);
+}
+
+} // namespace sunrise::server::web_service

+ 21 - 0
Sunrise/src/server/web_service/web_service_actions.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../../middleware/web_service/web_service_envelope.h"
+#include "web_service_runtime.h"
+
+namespace sunrise::server::web_service {
+
+void select_character(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
+void mutate_equipment(const middleware::web_service::Message& message,
+                      bool unequip,
+                      Outcome& outcome) noexcept;
+void mutate_socket_plug(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
+void mutate_equipped_socket_plug(const middleware::web_service::Message& message,
+                                 Outcome& outcome) noexcept;
+void mutate_item_state(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
+void dismantle_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
+void acquire_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
+
+} // namespace sunrise::server::web_service

+ 1 - 862
Sunrise/src/server/web_service/web_service_runtime.cpp

@@ -22,6 +22,7 @@
 #include "../../state/build_data/runtime.h"
 #include "../../state/runtime/runtime.h"
 #include "opcode_routes.h"
+#include "web_service_actions.h"
 
 namespace sunrise::server::web_service {
 
@@ -35,50 +36,10 @@ constexpr std::uint16_t kEquipOpcode = 403;
 constexpr std::uint16_t kUnequipOpcode = 404;
 /** Web Service opcode used by item-state actions such as finisher Favorite. */
 constexpr std::uint16_t kItemStateOpcode = 406;
-/** Opcode 406 carries an item identity, one biased 32-bit state value, and seven zero bits. */
-constexpr std::size_t kItemStatePayloadSize = 15;
-constexpr std::uint8_t kItemStateDefinitionIndexWidth = 15;
-constexpr std::uint8_t kItemStateValueWidth = 32;
-constexpr std::uint8_t kItemStatePaddingWidth = 7;
-constexpr std::uint64_t kItemStateValueBias = 0x80000000ULL;
-/** Opcodes 403 and 404 carry one SOID followed by one zero envelope trailer byte. */
-constexpr std::size_t kEquipmentActionPayloadSize = middleware::encoding::kU64Size + 1U;
 /** Web Service opcode used by the Character screen's Dismantle action. */
 constexpr std::uint16_t kItemDismantleOpcode = 402;
-/** Opcode 402 occupies one fixed 128-bit request descriptor. */
-constexpr std::size_t kItemDismantlePayloadSize = 16;
-/** The dismantled item instance is an optional 64-bit descriptor field. */
-constexpr std::uint8_t kItemDismantleInstanceWidth = 64;
-/** Installed item-definition rows fit the opcode's optional 15-bit descriptor field. */
-constexpr std::uint8_t kItemDismantleDefinitionIndexWidth = 15;
-/** Dismantle quantity is one signed 32-bit descriptor biased from INT32_MIN. */
-constexpr std::uint8_t kItemDismantleQuantityWidth = 32;
-/** Logical quantity one after applying the signed descriptor's minimum-int bias. */
-constexpr std::uint32_t kItemDismantleSingleQuantityWire = 0x80000001U;
-/** One required true flag follows the quantity in an instanced-item dismantle request. */
-constexpr std::uint8_t kItemDismantleRequiredFlagWidth = 1;
-/** Nested item-action alignment before the two absent outer-envelope trailers. */
-constexpr std::uint8_t kItemDismantleNestedPaddingWidth = 6;
-/** Two absent optional outer fields terminate the item-action request. */
-constexpr std::uint8_t kItemDismantleOuterTrailerWidth = 2;
-/** The fixed 128-bit request storage ends with six zero alignment bits. */
-constexpr std::uint8_t kItemDismantleFinalPaddingWidth = 6;
 /** Web Service opcode used by Collections to create one item instance. */
 constexpr std::uint16_t kItemAcquisitionOpcode = 1820;
-/** Opcode 1901 numbers flat equipment then native-inventory locations in one-based groups of 4. */
-constexpr std::uint64_t kEquipmentSelectorStride = 4;
-/** Captured equipped shader requests use the ordinary instanced-item socket descriptor arm. */
-constexpr std::uint8_t kEquippedShaderModelSocketKind = 0;
-/** Opcode 1820 carries one presence bit, a 15-bit collectible index, and one padding byte. */
-constexpr std::size_t kItemAcquisitionPayloadSize = 3;
-/** The optional collectible-index field starts with one presence bit. */
-constexpr std::uint8_t kItemAcquisitionPresenceWidth = 1;
-/** The installed collectible table is addressed by a 15-bit native index. */
-constexpr std::uint8_t kItemAcquisitionCollectibleIndexWidth = 15;
-/** One zero byte pads the request after its meaningful 16 bits. */
-constexpr std::uint8_t kItemAcquisitionPaddingWidth = 8;
-/** Failed parsing and mapping report the ordinary absent native-index sentinel. */
-constexpr std::uint32_t kUnavailableDefinitionIndex = (std::numeric_limits<std::uint16_t>::max)();
 
 /**
  * Logs which Web Service opcode arrived and a bounded payload trace. One svc-10 frame looks like
@@ -124,828 +85,6 @@ void report_request(const middleware::web_service::Message& message) noexcept {
     }
 }
 
-/** Logs one exact correlated equipment response after its Queuez update is staged. */
-void report_equip_response(const middleware::web_service::Message& message,
-                           std::int32_t family4Version,
-                           std::span<const std::byte> response) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(line.data(),
-                                     line.size(),
-                                     "ev=equipment stage=response opcode=%u transaction=%u "
-                                     "family_version=%d bytes=%zu hex=",
-                                     static_cast<unsigned>(message.opcode),
-                                     static_cast<unsigned>(message.transactionId),
-                                     family4Version,
-                                     response.size());
-    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
-        return;
-    }
-    constexpr char kHex[] = "0123456789ABCDEF";
-    std::size_t length = static_cast<std::size_t>(prefix);
-    for (const std::byte byte : response) {
-        if (length + 2 >= line.size()) {
-            break;
-        }
-        const unsigned value = std::to_integer<unsigned>(byte);
-        line[length++] = kHex[(value >> 4U) & 0xFU];
-        line[length++] = kHex[value & 0xFU];
-    }
-    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
-}
-
-/** Logs the final item-creation status pair and the exact Family-4 revision it promises. */
-void report_item_acquisition_response(const middleware::web_service::Message& message,
-                                      std::int32_t family4Version,
-                                      std::uint64_t acquiredInstanceSoid,
-                                      std::span<const std::byte> response) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=acquire stage=response result=ok opcode=%u transaction=%u family_version=%d "
-        "instance=0x%llX bytes=%zu hex=",
-        static_cast<unsigned>(message.opcode),
-        static_cast<unsigned>(message.transactionId),
-        family4Version,
-        static_cast<unsigned long long>(acquiredInstanceSoid),
-        response.size());
-    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
-        return;
-    }
-    constexpr char kHex[] = "0123456789ABCDEF";
-    std::size_t length = static_cast<std::size_t>(prefix);
-    for (const std::byte byte : response) {
-        if (length + 2 >= line.size()) {
-            break;
-        }
-        const unsigned value = std::to_integer<unsigned>(byte);
-        line[length++] = kHex[(value >> 4U) & 0xFU];
-        line[length++] = kHex[value & 0xFU];
-    }
-    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
-}
-
-/** Logs the final profile-stack status pair and the exact Family-4 account revision it promises. */
-void report_profile_item_acquisition_response(const middleware::web_service::Message& message,
-                                              std::int32_t family4Version,
-                                              std::uint32_t definitionHash,
-                                              std::int32_t quantity,
-                                              std::span<const std::byte> response) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix =
-        std::snprintf(line.data(),
-                      line.size(),
-                      "ev=profile_acquire stage=response result=ok opcode=%u transaction=%u "
-                      "family_version=%d definition_hash=%u quantity=%d bytes=%zu hex=",
-                      static_cast<unsigned>(message.opcode),
-                      static_cast<unsigned>(message.transactionId),
-                      family4Version,
-                      definitionHash,
-                      quantity,
-                      response.size());
-    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
-        return;
-    }
-    constexpr char kHex[] = "0123456789ABCDEF";
-    std::size_t length = static_cast<std::size_t>(prefix);
-    for (const std::byte byte : response) {
-        if (length + 2 >= line.size()) {
-            break;
-        }
-        const unsigned value = std::to_integer<unsigned>(byte);
-        line[length++] = kHex[(value >> 4U) & 0xFU];
-        line[length++] = kHex[value & 0xFU];
-    }
-    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
-}
-
-/** Logs the final dismantle status pair and the exact Family-4 revision it promises. */
-void report_item_dismantle_response(const middleware::web_service::Message& message,
-                                    std::int32_t family4Version,
-                                    std::uint64_t dismantledInstanceSoid,
-                                    std::span<const std::byte> response) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=dismantle stage=response result=ok opcode=%u transaction=%u family_version=%d "
-        "instance=0x%llX bytes=%zu hex=",
-        static_cast<unsigned>(message.opcode),
-        static_cast<unsigned>(message.transactionId),
-        family4Version,
-        static_cast<unsigned long long>(dismantledInstanceSoid),
-        response.size());
-    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
-        return;
-    }
-    constexpr char kHex[] = "0123456789ABCDEF";
-    std::size_t length = static_cast<std::size_t>(prefix);
-    for (const std::byte byte : response) {
-        if (length + 2 >= line.size()) {
-            break;
-        }
-        const unsigned value = std::to_integer<unsigned>(byte);
-        line[length++] = kHex[(value >> 4U) & 0xFU];
-        line[length++] = kHex[value & 0xFU];
-    }
-    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
-}
-
-/** Logs the exact opcode-903 status pair and the item-instance revision it promises. */
-void report_socket_plug_response(const middleware::web_service::Message& message,
-                                 std::int32_t family4Version,
-                                 std::uint64_t targetInstanceSoid,
-                                 std::uint8_t socketLane,
-                                 std::uint16_t plugDefinitionIndex,
-                                 std::span<const std::byte> response) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int prefix = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=socket_plug stage=response result=ok opcode=%u transaction=%u family_version=%d "
-        "instance=0x%llX lane=%u plug_definition=%u bytes=%zu hex=",
-        static_cast<unsigned>(message.opcode),
-        static_cast<unsigned>(message.transactionId),
-        family4Version,
-        static_cast<unsigned long long>(targetInstanceSoid),
-        static_cast<unsigned>(socketLane),
-        static_cast<unsigned>(plugDefinitionIndex),
-        response.size());
-    if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
-        return;
-    }
-    constexpr char kHex[] = "0123456789ABCDEF";
-    std::size_t length = static_cast<std::size_t>(prefix);
-    for (const std::byte byte : response) {
-        if (length + 2 >= line.size()) {
-            break;
-        }
-        const unsigned value = std::to_integer<unsigned>(byte);
-        line[length++] = kHex[(value >> 4U) & 0xFU];
-        line[length++] = kHex[value & 0xFU];
-    }
-    core::log::write(core::log::Channel::server, core::log::Level::debug, {line.data(), length});
-}
-
-/** One line carries the picked id and whether the selection moved. */
-constexpr std::size_t kSelectLineCapacity = 96;
-
-/**
- * Records the player's character pick, which arrives nowhere else.
- * A bad or unknown id leaves the selection alone. The reply is the status pair either way. The
- * Family-4 object move follows this call, and the family-zero pair after it.
- * @param message Parsed select-character request.
- * @param outcome Gets the picked key once the selection has moved in State.
- */
-void select_character(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    middleware::web_service::messages::opcode504::Request picked;
-    if (!middleware::web_service::messages::opcode504::parse_request(message, picked)) {
-        core::log::write(
-            core::log::Channel::server, core::log::Level::warn, "ev=ws504 stage=parse result=fail");
-        return;
-    }
-    bool changed = false;
-    if (!state::set_selected_character(picked.characterSoid, changed)) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::warn,
-                         "ev=ws504 stage=select result=unknown");
-        return;
-    }
-    outcome.hasSelectedCharacter = true;
-    outcome.selectedCharacterSoid = picked.characterSoid;
-
-    std::array<char, kSelectLineCapacity> line{};
-    const int written = std::snprintf(line.data(),
-                                      line.size(),
-                                      "ev=ws504 stage=select result=ok soid=0x%llX changed=%u",
-                                      static_cast<unsigned long long>(picked.characterSoid),
-                                      static_cast<unsigned>(changed));
-    if (written > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(written)});
-    }
-}
-
-/** Parses the exact shared opcode-403/404 SOID descriptor. */
-[[nodiscard]] bool parse_equipment_instance(const middleware::web_service::Message& message,
-                                            std::uint64_t& instanceSoid) noexcept {
-    instanceSoid = 0;
-    if (message.payload.size() != kEquipmentActionPayloadSize
-        || message.payload[middleware::encoding::kU64Size] != std::byte{}) {
-        return false;
-    }
-    instanceSoid = middleware::encoding::read_u64_be(
-        std::span<const std::byte, middleware::encoding::kU64Size>{message.payload.data(),
-                                                                   middleware::encoding::kU64Size});
-    return instanceSoid != 0;
-}
-
-/** Prepares one opcode-403/404 equipment mutation without publishing State early. */
-void mutate_equipment(const middleware::web_service::Message& message,
-                      bool unequip,
-                      Outcome& outcome) noexcept {
-    std::uint64_t requestedInstanceSoid = 0;
-    if (!parse_equipment_instance(message, requestedInstanceSoid)) {
-        std::array<char, 112> line{};
-        const int count = std::snprintf(line.data(),
-                                        line.size(),
-                                        "ev=equipment stage=parse result=fail opcode=%u "
-                                        "payload_bytes=%zu",
-                                        static_cast<unsigned>(message.opcode),
-                                        message.payload.size());
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-
-    state::PendingEquipmentSwap mutation;
-    const bool prepared = unequip
-                              ? state::prepare_equipment_unequip(requestedInstanceSoid, mutation)
-                              : state::prepare_equipment_swap(requestedInstanceSoid, mutation);
-    if (!prepared) {
-        std::array<char, 144> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=equipment stage=prepare result=fail opcode=%u action=%s requested=0x%llX",
-            static_cast<unsigned>(message.opcode),
-            unequip ? "unequip" : "equip",
-            static_cast<unsigned long long>(requestedInstanceSoid));
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-    outcome.hasEquipmentSwap = true;
-    outcome.equipmentSwap = mutation;
-
-    std::array<char, 224> line{};
-    const int count =
-        std::snprintf(line.data(),
-                      line.size(),
-                      "ev=equipment stage=prepare result=ok opcode=%u action=%s character=0x%llX "
-                      "previous=0x%llX requested=0x%llX native_slot=%u moved_items=%zu",
-                      static_cast<unsigned>(message.opcode),
-                      unequip ? "unequip" : "equip",
-                      static_cast<unsigned long long>(mutation.characterSoid),
-                      static_cast<unsigned long long>(mutation.previousInstanceSoid),
-                      static_cast<unsigned long long>(mutation.requestedInstanceSoid),
-                      static_cast<unsigned>(mutation.nativeEquipmentSlot),
-                      mutation.movedItemCount);
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-}
-
-/** Parses and prepares one exact selected-character opcode-903 socket selection. */
-void mutate_socket_plug(const middleware::web_service::Message& message,
-                        Outcome& outcome) noexcept {
-    middleware::web_service::messages::opcode903::Request request{};
-    if (!middleware::web_service::messages::opcode903::parse_request(message, request)
-        || !request.hasInstance || request.instanceSoid == 0 || request.hasTargetDefinition
-        || !request.hasPlugDefinition
-        || request.socketIndex >= state::account::inventory::kPlugCapacity) {
-        std::array<char, 192> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=ws903 stage=parse result=fail transaction=%u payload_bytes=%zu has_instance=%u "
-            "instance=0x%llX has_target_definition=%u socket=%u has_plug_definition=%u",
-            static_cast<unsigned>(message.transactionId),
-            message.payload.size(),
-            static_cast<unsigned>(request.hasInstance),
-            static_cast<unsigned long long>(request.instanceSoid),
-            static_cast<unsigned>(request.hasTargetDefinition),
-            request.socketIndex,
-            static_cast<unsigned>(request.hasPlugDefinition));
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-
-    state::PendingSocketPlug mutation{};
-    if (!state::prepare_socket_plug(request.instanceSoid,
-                                    static_cast<std::uint8_t>(request.socketIndex),
-                                    request.plugDefinitionIndex,
-                                    mutation)) {
-        std::array<char, 192> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=ws903 stage=prepare result=fail transaction=%u instance=0x%llX lane=%u "
-            "plug_definition=%u",
-            static_cast<unsigned>(message.transactionId),
-            static_cast<unsigned long long>(request.instanceSoid),
-            request.socketIndex,
-            static_cast<unsigned>(request.plugDefinitionIndex));
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-
-    outcome.hasSocketPlug = true;
-    outcome.socketPlug = mutation;
-    std::array<char, 240> line{};
-    const int count = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=ws903 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
-        "target_definition=%u target_bucket=%u lane=%u plug_definition=%u plug_bucket=%u "
-        "equipped=%u item_index=%zu",
-        static_cast<unsigned>(message.transactionId),
-        static_cast<unsigned long long>(mutation.characterSoid),
-        static_cast<unsigned long long>(mutation.targetInstanceSoid),
-        static_cast<unsigned>(mutation.targetDefinitionIndex),
-        static_cast<unsigned>(mutation.targetBucketId),
-        static_cast<unsigned>(mutation.socketLane),
-        static_cast<unsigned>(mutation.plugDefinitionIndex),
-        static_cast<unsigned>(mutation.plugBucketId),
-        static_cast<unsigned>(mutation.targetEquipped),
-        mutation.itemIndex);
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-}
-
-/** Parses and prepares one character-location opcode-1901 socket selection. */
-void mutate_equipped_socket_plug(const middleware::web_service::Message& message,
-                                 Outcome& outcome) noexcept {
-    middleware::web_service::messages::opcode1901::Request request{};
-    if (!middleware::web_service::messages::opcode1901::parse_request(message, request)
-        || request.canonicalSocketKind != request.socketIndex
-        || request.modelSocketKind != kEquippedShaderModelSocketKind || request.auxiliary != 0
-        || request.socketIndex >= state::account::inventory::kPlugCapacity
-        || request.equipmentSelector == 0
-        || request.equipmentSelector % kEquipmentSelectorStride != 0) {
-        std::array<char, 256> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=ws1901 stage=parse result=fail transaction=%u payload_bytes=%zu "
-            "plug_definition=%u canonical_kind=%u model_kind=%u socket=%u auxiliary=0x%llX "
-            "equipment_selector=%llu",
-            static_cast<unsigned>(message.transactionId),
-            message.payload.size(),
-            static_cast<unsigned>(request.plugDefinitionIndex),
-            static_cast<unsigned>(request.canonicalSocketKind),
-            static_cast<unsigned>(request.modelSocketKind),
-            request.socketIndex,
-            static_cast<unsigned long long>(request.auxiliary),
-            static_cast<unsigned long long>(request.equipmentSelector));
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-
-    const std::uint64_t identityToken = request.equipmentSelector / kEquipmentSelectorStride;
-    state::PendingSocketPlug mutation{};
-    if (!state::prepare_character_selector_socket_plug(
-            request.equipmentSelector,
-            static_cast<std::uint8_t>(request.socketIndex),
-            request.plugDefinitionIndex,
-            mutation)) {
-        std::array<char, 224> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=ws1901 stage=prepare result=fail transaction=%u equipment_selector=%llu "
-            "identity_token=%llu lane=%u plug_definition=%u canonical_kind=%u model_kind=%u "
-            "auxiliary=0x%llX",
-            static_cast<unsigned>(message.transactionId),
-            static_cast<unsigned long long>(request.equipmentSelector),
-            static_cast<unsigned long long>(identityToken),
-            request.socketIndex,
-            static_cast<unsigned>(request.plugDefinitionIndex),
-            static_cast<unsigned>(request.canonicalSocketKind),
-            static_cast<unsigned>(request.modelSocketKind),
-            static_cast<unsigned long long>(request.auxiliary));
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-
-    outcome.hasSocketPlug = true;
-    outcome.socketPlug = mutation;
-    std::array<char, 288> line{};
-    const int count = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=ws1901 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
-        "equipment_selector=%llu identity_token=%llu target_definition=%u target_bucket=%u "
-        "lane=%u plug_definition=%u plug_bucket=%u canonical_kind=%u model_kind=%u "
-        "auxiliary=0x%llX",
-        static_cast<unsigned>(message.transactionId),
-        static_cast<unsigned long long>(mutation.characterSoid),
-        static_cast<unsigned long long>(mutation.targetInstanceSoid),
-        static_cast<unsigned long long>(request.equipmentSelector),
-        static_cast<unsigned long long>(identityToken),
-        static_cast<unsigned>(mutation.targetDefinitionIndex),
-        static_cast<unsigned>(mutation.targetBucketId),
-        static_cast<unsigned>(mutation.socketLane),
-        static_cast<unsigned>(mutation.plugDefinitionIndex),
-        static_cast<unsigned>(mutation.plugBucketId),
-        static_cast<unsigned>(request.canonicalSocketKind),
-        static_cast<unsigned>(request.modelSocketKind),
-        static_cast<unsigned long long>(request.auxiliary));
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-}
-
-/** Parses and prepares one complete accumulated item-state value from opcode 406. */
-void mutate_item_state(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    middleware::encoding::bits::Reader reader(message.payload);
-    std::uint64_t instancePresent = 0;
-    std::uint64_t instanceSoid = 0;
-    std::uint64_t definitionPresent = 0;
-    std::uint64_t definitionIndex = 0;
-    std::uint64_t encodedFlags = 0;
-    std::uint64_t padding = 0;
-    if (message.payload.size() != kItemStatePayloadSize || !reader.read(1, instancePresent)
-        || !reader.read(64, instanceSoid) || !reader.read(1, definitionPresent)
-        || !reader.read(kItemStateDefinitionIndexWidth, definitionIndex)
-        || !reader.read(kItemStateValueWidth, encodedFlags)
-        || !reader.read(kItemStatePaddingWidth, padding) || reader.remaining_bits() != 0
-        || instancePresent == 0 || instanceSoid == 0 || definitionPresent == 0
-        || definitionIndex > (std::numeric_limits<std::uint16_t>::max)()
-        || encodedFlags < kItemStateValueBias || padding != 0
-        || encodedFlags - kItemStateValueBias > 0x3U) {
-        std::array<char, 224> line{};
-        const int count = std::snprintf(
-            line.data(),
-            line.size(),
-            "ev=ws406 stage=parse result=fail transaction=%u payload_bytes=%zu instance=0x%llX "
-            "definition=%llu flags_wire=0x%llX padding=0x%llX",
-            static_cast<unsigned>(message.transactionId),
-            message.payload.size(),
-            static_cast<unsigned long long>(instanceSoid),
-            static_cast<unsigned long long>(definitionIndex),
-            static_cast<unsigned long long>(encodedFlags),
-            static_cast<unsigned long long>(padding));
-        if (count > 0) {
-            core::log::write(core::log::Channel::server,
-                             core::log::Level::warn,
-                             {line.data(), static_cast<std::size_t>(count)});
-        }
-        return;
-    }
-
-    const std::uint32_t flags = static_cast<std::uint32_t>(encodedFlags - kItemStateValueBias);
-    state::PendingItemState mutation{};
-    if (!state::prepare_item_state(
-            instanceSoid, static_cast<std::uint16_t>(definitionIndex), flags, mutation)) {
-        return;
-    }
-    outcome.hasItemState = true;
-    outcome.itemState = mutation;
-    std::array<char, 224> line{};
-    const int count = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=ws406 stage=prepare result=ok transaction=%u character=0x%llX instance=0x%llX "
-        "definition=%u flags_before=0x%X flags_after=0x%X equipped=%u item_index=%zu",
-        static_cast<unsigned>(message.transactionId),
-        static_cast<unsigned long long>(mutation.characterSoid),
-        static_cast<unsigned long long>(mutation.targetInstanceSoid),
-        static_cast<unsigned>(mutation.targetDefinitionIndex),
-        mutation.beforeFlags,
-        mutation.afterFlags,
-        mutation.targetEquipped ? 1U : 0U,
-        mutation.itemIndex);
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         core::log::Level::info,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-}
-
-/** Records strict opcode-402 parsing, identity checks, and State preparation outcomes. */
-void report_item_dismantle(const middleware::web_service::Message& message,
-                           std::string_view result,
-                           std::string_view reason,
-                           std::uint64_t instanceSoid,
-                           std::uint32_t definitionIndex,
-                           std::uint32_t definitionHash,
-                           std::uint32_t quantity) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int count = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=ws402 stage=prepare result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
-        "instance=0x%llX definition_index=%u definition_hash=%u quantity=%u",
-        static_cast<int>(result.size()),
-        result.data(),
-        static_cast<int>(reason.size()),
-        reason.data(),
-        static_cast<unsigned>(message.transactionId),
-        message.payload.size(),
-        static_cast<unsigned long long>(instanceSoid),
-        definitionIndex,
-        definitionHash,
-        quantity);
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         result == "ok" ? core::log::Level::info : core::log::Level::warn,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-}
-
-/** Prepares the exact fixed-width opcode-402 Character-inventory removal request. */
-void dismantle_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    if (message.payload.size() != kItemDismantlePayloadSize) {
-        report_item_dismantle(
-            message, "fail", "payload_size", 0, kUnavailableDefinitionIndex, 0, 0);
-        return;
-    }
-
-    middleware::encoding::bits::Reader reader(message.payload);
-    std::uint64_t instancePresent = 0;
-    std::uint64_t instanceSoid = 0;
-    std::uint64_t definitionPresent = 0;
-    std::uint64_t encodedDefinitionIndex = 0;
-    std::uint64_t encodedQuantity = 0;
-    std::uint64_t requiredFlag = 0;
-    std::uint64_t nestedPadding = 0;
-    std::uint64_t outerTrailers = 0;
-    std::uint64_t finalPadding = 0;
-    if (!reader.read(1, instancePresent) || !reader.read(kItemDismantleInstanceWidth, instanceSoid)
-        || !reader.read(1, definitionPresent)
-        || !reader.read(kItemDismantleDefinitionIndexWidth, encodedDefinitionIndex)
-        || !reader.read(kItemDismantleQuantityWidth, encodedQuantity)
-        || !reader.read(kItemDismantleRequiredFlagWidth, requiredFlag)
-        || !reader.read(kItemDismantleNestedPaddingWidth, nestedPadding)
-        || !reader.read(kItemDismantleOuterTrailerWidth, outerTrailers)
-        || !reader.read(kItemDismantleFinalPaddingWidth, finalPadding)
-        || reader.remaining_bits() != 0) {
-        report_item_dismantle(
-            message, "fail", "payload_bits", 0, kUnavailableDefinitionIndex, 0, 0);
-        return;
-    }
-    const auto definitionIndex = static_cast<std::uint16_t>(encodedDefinitionIndex);
-    const auto quantityWire = static_cast<std::uint32_t>(encodedQuantity);
-    constexpr std::uint32_t kSingleQuantity = 1;
-    if (instancePresent == 0 || definitionPresent == 0 || instanceSoid == 0) {
-        report_item_dismantle(
-            message, "fail", "required_field", instanceSoid, definitionIndex, 0, 0);
-        return;
-    }
-    if (nestedPadding != 0 || outerTrailers != 0 || finalPadding != 0) {
-        report_item_dismantle(
-            message, "fail", "padding_or_trailer", instanceSoid, definitionIndex, 0, 0);
-        return;
-    }
-    if (quantityWire != kItemDismantleSingleQuantityWire || requiredFlag != 1) {
-        report_item_dismantle(
-            message, "fail", "quantity_or_flag", instanceSoid, definitionIndex, 0, 0);
-        return;
-    }
-
-    state::build_data::items::Definition definition{};
-    if (!state::build_data::find_item_definition_index(definitionIndex, definition)) {
-        report_item_dismantle(
-            message, "fail", "definition", instanceSoid, definitionIndex, 0, kSingleQuantity);
-        return;
-    }
-    state::PendingItemDismantle mutation{};
-    if (!state::prepare_item_dismantle(instanceSoid, mutation)) {
-        report_item_dismantle(message,
-                              "fail",
-                              "state",
-                              instanceSoid,
-                              definitionIndex,
-                              definition.definitionHash,
-                              kSingleQuantity);
-        return;
-    }
-    if (mutation.dismantledItem.definitionHash != definition.definitionHash
-        || mutation.dismantledItem.quantity != static_cast<std::int32_t>(kSingleQuantity)) {
-        report_item_dismantle(message,
-                              "fail",
-                              "identity",
-                              instanceSoid,
-                              definitionIndex,
-                              definition.definitionHash,
-                              kSingleQuantity);
-        return;
-    }
-    outcome.hasItemDismantle = true;
-    outcome.itemDismantle = mutation;
-    report_item_dismantle(message,
-                          "ok",
-                          "ready",
-                          instanceSoid,
-                          definitionIndex,
-                          definition.definitionHash,
-                          kSingleQuantity);
-}
-
-/** Records strict opcode-1820 parsing, installed mapping, and State preparation outcomes. */
-void report_item_acquisition(const middleware::web_service::Message& message,
-                             std::string_view result,
-                             std::string_view reason,
-                             std::uint32_t collectibleIndex,
-                             std::uint32_t itemDefinitionIndex,
-                             std::uint32_t definitionHash,
-                             std::uint64_t instanceSoid) noexcept {
-    std::array<char, core::log::kLineCapacity> line{};
-    const int count = std::snprintf(
-        line.data(),
-        line.size(),
-        "ev=ws1820 stage=prepare result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
-        "collectible_index=%u item_definition_index=%u definition_hash=%u instance=0x%llX",
-        static_cast<int>(result.size()),
-        result.data(),
-        static_cast<int>(reason.size()),
-        reason.data(),
-        static_cast<unsigned>(message.transactionId),
-        message.payload.size(),
-        collectibleIndex,
-        itemDefinitionIndex,
-        definitionHash,
-        static_cast<unsigned long long>(instanceSoid));
-    if (count > 0) {
-        core::log::write(core::log::Channel::server,
-                         result == "ok" ? core::log::Level::info : core::log::Level::warn,
-                         {line.data(), static_cast<std::size_t>(count)});
-    }
-}
-
-/** Prepares the exact three-byte opcode-1820 Collections item request. */
-void acquire_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    if (message.payload.size() != kItemAcquisitionPayloadSize) {
-        report_item_acquisition(message,
-                                "fail",
-                                "payload_size",
-                                kUnavailableDefinitionIndex,
-                                kUnavailableDefinitionIndex,
-                                0,
-                                0);
-        return;
-    }
-
-    middleware::encoding::bits::Reader reader(message.payload);
-    std::uint64_t present = 0;
-    std::uint64_t encodedCollectibleIndex = 0;
-    std::uint64_t padding = 0;
-    if (!reader.read(kItemAcquisitionPresenceWidth, present)
-        || !reader.read(kItemAcquisitionCollectibleIndexWidth, encodedCollectibleIndex)
-        || !reader.read(kItemAcquisitionPaddingWidth, padding) || reader.remaining_bits() != 0) {
-        report_item_acquisition(message,
-                                "fail",
-                                "payload_bits",
-                                kUnavailableDefinitionIndex,
-                                kUnavailableDefinitionIndex,
-                                0,
-                                0);
-        return;
-    }
-    const auto collectibleIndex = static_cast<std::uint16_t>(encodedCollectibleIndex);
-    if (present == 0) {
-        report_item_acquisition(message,
-                                "fail",
-                                "collectible_absent",
-                                collectibleIndex,
-                                kUnavailableDefinitionIndex,
-                                0,
-                                0);
-        return;
-    }
-    if (padding != 0) {
-        report_item_acquisition(
-            message, "fail", "padding", collectibleIndex, kUnavailableDefinitionIndex, 0, 0);
-        return;
-    }
-
-    std::uint16_t itemDefinitionIndex = 0;
-    if (!state::build_data::find_collectible_item_definition_index(collectibleIndex,
-                                                                   itemDefinitionIndex)) {
-        report_item_acquisition(message,
-                                "fail",
-                                "collectible_definition",
-                                collectibleIndex,
-                                kUnavailableDefinitionIndex,
-                                0,
-                                0);
-        return;
-    }
-
-    state::build_data::items::Definition definition{};
-    if (!state::build_data::find_item_definition_index(itemDefinitionIndex, definition)) {
-        report_item_acquisition(
-            message, "fail", "item_definition", collectibleIndex, itemDefinitionIndex, 0, 0);
-        return;
-    }
-
-    state::build_data::items::details::Definition detail{};
-    state::build_data::inventory::buckets::Descriptor bucket{};
-    if (!state::build_data::find_configured_item_detail(itemDefinitionIndex, detail)
-        || detail.definitionIndex != itemDefinitionIndex
-        || detail.definitionHash != definition.definitionHash
-        || detail.bucketId != definition.bucketId
-        || !state::build_data::find_inventory_bucket_descriptor(detail.bucketId, bucket)) {
-        report_item_acquisition(message,
-                                "fail",
-                                "item_detail_or_bucket",
-                                collectibleIndex,
-                                itemDefinitionIndex,
-                                definition.definitionHash,
-                                0);
-        return;
-    }
-
-    namespace bucket_domain = state::build_data::inventory::buckets;
-    namespace detail_domain = state::build_data::items::details;
-    if (bucket.arraySelector == bucket_domain::ArraySelector::profile) {
-        if (detail.instancedDefinitionState != detail_domain::InstancedDefinitionState::stackable) {
-            report_item_acquisition(message,
-                                    "fail",
-                                    "profile_item_instanced",
-                                    collectibleIndex,
-                                    itemDefinitionIndex,
-                                    definition.definitionHash,
-                                    0);
-            return;
-        }
-        state::PendingProfileItemAcquisition mutation{};
-        if (!state::prepare_profile_item_acquisition(
-                collectibleIndex, definition.definitionHash, mutation)) {
-            report_item_acquisition(message,
-                                    "fail",
-                                    "profile_state",
-                                    collectibleIndex,
-                                    itemDefinitionIndex,
-                                    definition.definitionHash,
-                                    0);
-            return;
-        }
-        outcome.hasProfileItemAcquisition = true;
-        outcome.profileItemAcquisition = mutation;
-        report_item_acquisition(message,
-                                "ok",
-                                "profile_ready",
-                                collectibleIndex,
-                                itemDefinitionIndex,
-                                definition.definitionHash,
-                                0);
-        return;
-    }
-    if (bucket.arraySelector != bucket_domain::ArraySelector::character) {
-        report_item_acquisition(message,
-                                "fail",
-                                "unsupported_inventory_array",
-                                collectibleIndex,
-                                itemDefinitionIndex,
-                                definition.definitionHash,
-                                0);
-        return;
-    }
-
-    state::PendingItemAcquisition mutation{};
-    if (!state::prepare_item_acquisition(collectibleIndex, definition.definitionHash, mutation)) {
-        report_item_acquisition(message,
-                                "fail",
-                                "state",
-                                collectibleIndex,
-                                itemDefinitionIndex,
-                                definition.definitionHash,
-                                0);
-        return;
-    }
-    outcome.hasItemAcquisition = true;
-    outcome.itemAcquisition = mutation;
-    report_item_acquisition(message,
-                            "ok",
-                            "ready",
-                            collectibleIndex,
-                            itemDefinitionIndex,
-                            definition.definitionHash,
-                            mutation.acquiredInstanceSoid);
-}
-
 /**
  * Answers a request whose own codec refused with the bare correlated echo.
  * The Client matches on the echoed transaction id. A missing body is worse than a thin one. It

+ 16 - 18
Sunrise/src/server/web_service/web_service_runtime.h

@@ -3,6 +3,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <span>
+#include <variant>
 
 #include "../../middleware/web_service/messages/opcode206.h"
 #include "../../state/runtime/runtime.h"
@@ -16,26 +17,23 @@ struct Outcome {
     /** An opcode-504 pick moved the selection and its Family-4 object still has to follow. */
     bool hasSelectedCharacter{};
     std::uint64_t selectedCharacterSoid{};
-    /** Opcode 403/404 prepared one checked equipment mutation for the BAP transaction. */
-    bool hasEquipmentSwap{};
-    state::PendingEquipmentSwap equipmentSwap{};
-    /** An item-creation route prepared one checked selected-character inventory insertion. */
-    bool hasItemAcquisition{};
-    state::PendingItemAcquisition itemAcquisition{};
-    /** Opcode 1820 prepared one account-profile stack increment or insertion. */
-    bool hasProfileItemAcquisition{};
-    state::PendingProfileItemAcquisition profileItemAcquisition{};
-    /** Opcode 402 prepared one checked selected-character inventory removal. */
-    bool hasItemDismantle{};
-    state::PendingItemDismantle itemDismantle{};
-    /** Opcode 903 or 1901 prepared one exact selected-character ordinary-socket selection. */
-    bool hasSocketPlug{};
-    state::PendingSocketPlug socketPlug{};
-    /** Opcode 406 prepared one complete accumulated item-state value. */
-    bool hasItemState{};
-    state::PendingItemState itemState{};
+    /** A request prepares at most one State mutation; the alternative owns only that payload. */
+    using Mutation = std::variant<std::monostate,
+                                  state::PendingEquipmentSwap,
+                                  state::PendingItemAcquisition,
+                                  state::PendingProfileItemAcquisition,
+                                  state::PendingItemDismantle,
+                                  state::PendingSocketPlug,
+                                  state::PendingItemState>;
+    Mutation mutation{};
 };
 
+/** @return The prepared mutation of the requested type, or null when another route ran. */
+template <typename Mutation>
+[[nodiscard]] const Mutation* mutation_if(const Outcome& outcome) noexcept {
+    return std::get_if<Mutation>(&outcome.mutation);
+}
+
 /** Records the final opcode-403/404 reply after its paired Family-4 version is known. */
 void report_equip_response(const middleware::web_service::Message& message,
                            std::int32_t family4Version,

+ 37 - 3
Sunrise/src/state/account/account_state.cpp

@@ -18,6 +18,36 @@ inline constexpr std::size_t kIdentityCapacity =
            && item.mutationSerial == 0;
 }
 
+/** @return True when one unused dismantle policy row is canonical zero. */
+[[nodiscard]] bool empty_dismantle_reward(const DismantleRewardPolicy& reward) noexcept {
+    return reward.definitionHash == 0 && reward.quantity == 0;
+}
+
+/** Checks filled policy rows, uniqueness, and the zero tail. */
+[[nodiscard]] bool valid_dismantle_rewards(const AccountState& state) noexcept {
+    if (state.dismantleRewardCount > state.dismantleRewards.size()) {
+        return false;
+    }
+    for (std::size_t index = 0; index < state.dismantleRewards.size(); ++index) {
+        const DismantleRewardPolicy& reward = state.dismantleRewards[index];
+        if (index >= state.dismantleRewardCount) {
+            if (!empty_dismantle_reward(reward)) {
+                return false;
+            }
+            continue;
+        }
+        if (reward.definitionHash == inventory::kNoDefinitionHash || reward.quantity <= 0) {
+            return false;
+        }
+        for (std::size_t prior = 0; prior < index; ++prior) {
+            if (state.dismantleRewards[prior].definitionHash == reward.definitionHash) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
 /** Adds one nonzero globally unique key to a bounded identity set. */
 [[nodiscard]] bool append_identity(std::array<std::uint64_t, kIdentityCapacity>& identities,
                                    std::size_t& count,
@@ -40,14 +70,18 @@ inline constexpr std::size_t kIdentityCapacity =
         return false;
     }
     if (state.primarySoid == 0) {
-        if (state.profileItemCount != 0 || state.characterCount != 0 || state.settings.configured
+        if (state.profileItemCount != 0 || state.characterCount != 0
+            || state.dismantleRewardCount != 0 || state.settings.configured
             || state.settings.keyBindings.configured) {
             return false;
         }
         return std::all_of(
-            state.profileItems.cbegin(), state.profileItems.cend(), empty_profile_item);
+                   state.profileItems.cbegin(), state.profileItems.cend(), empty_profile_item)
+               && std::all_of(state.dismantleRewards.cbegin(),
+                              state.dismantleRewards.cend(),
+                              empty_dismantle_reward);
     }
-    if (!settings::valid(state.settings)) {
+    if (!settings::valid(state.settings) || !valid_dismantle_rewards(state)) {
         return false;
     }
 

+ 11 - 0
Sunrise/src/state/account/account_state.h

@@ -11,6 +11,14 @@ namespace sunrise::state {
 
 /** One account can own at most the 3 playable character slots. */
 inline constexpr std::size_t kCharacterCapacity = 3;
+/** A server-authored dismantle policy stays small but leaves room for build variants. */
+inline constexpr std::size_t kDismantleRewardPolicyCapacity = 8;
+
+/** One profile material credited when ordinary character gear is dismantled. */
+struct DismantleRewardPolicy {
+    std::uint32_t definitionHash{};
+    std::int32_t quantity{};
+};
 
 /** Stable character race values authored independently of package definition mappings. */
 enum class CharacterRace : std::uint8_t {
@@ -100,6 +108,9 @@ struct CharacterState {
 /** Account identity shared by backend object families. */
 struct AccountState {
     std::uint64_t primarySoid{};
+    /** Economy policy comes from configuration, never from item-specific runtime constants. */
+    std::array<DismantleRewardPolicy, kDismantleRewardPolicyCapacity> dismantleRewards{};
+    std::size_t dismantleRewardCount{};
     /** Account-wide currencies and materials, placed by bucket rather than by authored slot. */
     std::array<account::inventory::ProfileItem, account::inventory::kProfileItemCapacity>
         profileItems{};

+ 2 - 2
Sunrise/src/state/runtime/runtime.h

@@ -107,8 +107,8 @@ struct DismantleReward {
     std::int32_t mutationSerial{};
 };
 
-/** Dismantles currently publish the ordinary gear material pair in one native pickup bank. */
-inline constexpr std::size_t kDismantleRewardCapacity = 2;
+/** Dismantle feedback can publish every bounded server-authored policy row. */
+inline constexpr std::size_t kDismantleRewardCapacity = kDismantleRewardPolicyCapacity;
 
 /** Prepared selected-character inventory removal kept private until its reply and push fit. */
 struct PendingItemDismantle {

+ 582 - 0
Sunrise/src/state/runtime/state_account_acquisition_runtime.cpp

@@ -0,0 +1,582 @@
+#include <Windows.h>
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+
+#include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
+#include "../build_data/runtime.h"
+#include "runtime.h"
+#include "state_account_transaction_helpers.h"
+#include "storage/internal.h"
+
+namespace sunrise::state {
+
+using namespace runtime::detail;
+namespace authored_inventory = account::inventory;
+namespace item_details = build_data::items::details;
+namespace inventory_buckets = build_data::inventory::buckets;
+namespace family4_loadout = middleware::datagen::family4::loadout;
+
+/** Prepares one native-row-checked selected-character inventory insertion. */
+bool prepare_item_acquisition(std::uint16_t collectibleIndex,
+                              std::uint32_t definitionHash,
+                              PendingItemAcquisition& mutation) noexcept {
+    mutation = {};
+    const AccountState account = account_snapshot();
+    build_data::collectibles::Definition collectible{};
+    build_data::items::Definition grantedDefinition{};
+    if (definitionHash == authored_inventory::kNoDefinitionHash || !account::valid(account)
+        || !valid_profile_inventory(account)
+        || !build_data::find_collectible_definition(collectibleIndex, collectible)
+        || collectible.itemDefinitionIndex
+               == build_data::collectibles::kUnavailableItemDefinitionIndex
+        || !build_data::find_item_definition_index(collectible.itemDefinitionIndex,
+                                                   grantedDefinition)
+        || grantedDefinition.definitionHash != definitionHash) {
+        report_acquisition("prepare", "fail", "input", definitionHash, 0, 0, 0, 0, 0, 0);
+        return false;
+    }
+
+    AccountState chargedAccount{};
+    bool profileChanged = false;
+    if (!apply_collection_materials(account, collectible, chargedAccount, profileChanged)) {
+        report_acquisition("prepare", "fail", "materials", definitionHash, 0, 0, 0, 0, 0, 0);
+        return false;
+    }
+
+    std::size_t characterIndex = account.characterCount;
+    for (std::size_t index = 0; index < account.characterCount; ++index) {
+        if (account.characters[index].selected) {
+            characterIndex = index;
+            break;
+        }
+    }
+    if (characterIndex == account.characterCount) {
+        report_acquisition("prepare", "fail", "selection", definitionHash, 0, 0, 0, 0, 0, 0);
+        return false;
+    }
+
+    const CharacterState& before = account.characters[characterIndex];
+    if (before.inventory.count >= before.inventory.values.size()
+        || before.nextInventorySerial
+               >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
+        report_acquisition("prepare",
+                           "fail",
+                           "state_capacity",
+                           definitionHash,
+                           before.soid,
+                           0,
+                           before.inventory.count,
+                           0,
+                           0,
+                           before.nextInventorySerial);
+        return false;
+    }
+
+    std::uint64_t instanceSoid = 0;
+    if (!next_item_instance_soid(account, instanceSoid)) {
+        report_acquisition("prepare",
+                           "fail",
+                           "soid",
+                           definitionHash,
+                           before.soid,
+                           0,
+                           before.inventory.count,
+                           0,
+                           0,
+                           before.nextInventorySerial);
+        return false;
+    }
+
+    CharacterState after = before;
+    const std::size_t inventoryIndex = after.inventory.count;
+    authored_inventory::Item acquired{};
+    acquired.instanceSoid = instanceSoid;
+    acquired.definitionHash = definitionHash;
+    acquired.level = acquisition_level(before);
+    acquired.quantity = 1;
+    acquired.mutationSerial = static_cast<std::int32_t>(after.nextInventorySerial++);
+    acquired.sockets.policy = authored_inventory::SocketPolicy::nativeDefaults;
+    after.inventory.values[inventoryIndex] = acquired;
+    ++after.inventory.count;
+
+    AccountState candidate = chargedAccount;
+    candidate.characters[characterIndex] = after;
+    family4_loadout::ResolvedLoadout resolved{};
+    std::uint16_t inventoryRow = 0;
+    std::uint8_t equipmentSlot = 0;
+    if (!account::valid(candidate) || identity_uses_soid(candidate, instanceSoid)
+        || !family4_loadout::resolve(candidate, characterIndex, resolved)
+        || !find_acquired_row(resolved, instanceSoid, inventoryRow, equipmentSlot)) {
+        report_acquisition("prepare",
+                           "fail",
+                           "resolve_or_bucket_full",
+                           definitionHash,
+                           before.soid,
+                           instanceSoid,
+                           inventoryIndex,
+                           0,
+                           0,
+                           after.nextInventorySerial);
+        return false;
+    }
+
+    mutation.beforeCharacter = before;
+    mutation.afterCharacter = after;
+    mutation.beforeProfileItems = account.profileItems;
+    mutation.afterProfileItems = chargedAccount.profileItems;
+    mutation.accountSoid = account.primarySoid;
+    mutation.characterSoid = before.soid;
+    mutation.acquiredInstanceSoid = instanceSoid;
+    mutation.acquiredDefinitionHash = definitionHash;
+    mutation.materialRequirementSetHash = collectible.materialRequirementSetHash;
+    mutation.expectedNextInventorySerial = before.nextInventorySerial;
+    mutation.characterIndex = characterIndex;
+    mutation.expectedInventoryCount = before.inventory.count;
+    mutation.expectedProfileItemCount = account.profileItemCount;
+    mutation.afterProfileItemCount = chargedAccount.profileItemCount;
+    mutation.inventoryIndex = inventoryIndex;
+    mutation.collectibleIndex = collectibleIndex;
+    mutation.inventoryRow = inventoryRow;
+    mutation.equipmentSlot = equipmentSlot;
+    mutation.materialRequirementCount = collectible.materialRequirementCount;
+    mutation.profileChanged = profileChanged;
+    mutation.prepared = true;
+    report_acquisition("prepare",
+                       "ok",
+                       "ready",
+                       definitionHash,
+                       before.soid,
+                       instanceSoid,
+                       inventoryIndex,
+                       inventoryRow,
+                       equipmentSlot,
+                       after.nextInventorySerial);
+    return true;
+}
+
+/** Produces the full account after-image while a prepared character pull remains current. */
+bool preview_item_acquisition(const PendingItemAcquisition& mutation,
+                              AccountState& after) noexcept {
+    after = {};
+    if (!mutation.prepared || mutation.accountSoid == 0 || mutation.characterSoid == 0
+        || mutation.acquiredInstanceSoid == 0 || mutation.characterIndex >= kCharacterCapacity
+        || mutation.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
+        || mutation.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
+        return false;
+    }
+    const AccountState current = account_snapshot();
+    if (mutation.characterIndex >= current.characterCount
+        || current.primarySoid != mutation.accountSoid
+        || !same_character(current.characters[mutation.characterIndex], mutation.beforeCharacter)
+        || !same_profile_inventory(
+            current, mutation.beforeProfileItems, mutation.expectedProfileItemCount)) {
+        return false;
+    }
+    after = current;
+    after.profileItems = mutation.afterProfileItems;
+    after.profileItemCount = mutation.afterProfileItemCount;
+    after.characters[mutation.characterIndex] = mutation.afterCharacter;
+    family4_loadout::ResolvedLoadout resolved{};
+    std::uint16_t checkedRow = 0;
+    std::uint8_t checkedSlot = 0;
+    return account::valid(after) && valid_profile_inventory(after)
+           && family4_loadout::resolve(after, mutation.characterIndex, resolved)
+           && find_acquired_row(resolved, mutation.acquiredInstanceSoid, checkedRow, checkedSlot)
+           && checkedRow == mutation.inventoryRow && checkedSlot == mutation.equipmentSlot;
+}
+
+/** Commits one prepared insertion only while its prepare-time loadout remains current. */
+bool commit_item_acquisition(PendingItemAcquisition& mutation) noexcept {
+    const PendingItemAcquisition prepared = mutation;
+    mutation = {};
+    const auto fail = [&prepared](std::string_view reason) noexcept {
+        report_acquisition("commit",
+                           "fail",
+                           reason,
+                           prepared.acquiredDefinitionHash,
+                           prepared.characterSoid,
+                           prepared.acquiredInstanceSoid,
+                           prepared.inventoryIndex,
+                           prepared.inventoryRow,
+                           prepared.equipmentSlot,
+                           prepared.afterCharacter.nextInventorySerial);
+        return false;
+    };
+    if (!prepared.prepared || prepared.characterSoid == 0 || prepared.acquiredInstanceSoid == 0
+        || prepared.accountSoid == 0
+        || prepared.acquiredDefinitionHash == authored_inventory::kNoDefinitionHash
+        || prepared.characterIndex >= kCharacterCapacity
+        || prepared.expectedInventoryCount >= authored_inventory::kCharacterItemCapacity
+        || prepared.inventoryIndex != prepared.expectedInventoryCount
+        || prepared.afterCharacter.inventory.count != prepared.expectedInventoryCount + 1U
+        || prepared.inventoryIndex >= prepared.afterCharacter.inventory.count
+        || prepared.afterCharacter.inventory.values[prepared.inventoryIndex].instanceSoid
+               != prepared.acquiredInstanceSoid
+        || prepared.afterCharacter.inventory.values[prepared.inventoryIndex].definitionHash
+               != prepared.acquiredDefinitionHash
+        || prepared.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
+        || prepared.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
+        return fail("mutation");
+    }
+
+    build_data::collectibles::Definition collectible{};
+    if (!build_data::find_collectible_definition(prepared.collectibleIndex, collectible)
+        || collectible.itemDefinitionIndex
+               == build_data::collectibles::kUnavailableItemDefinitionIndex
+        || collectible.materialRequirementSetHash != prepared.materialRequirementSetHash
+        || collectible.materialRequirementCount != prepared.materialRequirementCount) {
+        return fail("collectible");
+    }
+
+    report_acquisition("commit_begin",
+                       "ok",
+                       "ready",
+                       prepared.acquiredDefinitionHash,
+                       prepared.characterSoid,
+                       prepared.acquiredInstanceSoid,
+                       prepared.inventoryIndex,
+                       prepared.inventoryRow,
+                       prepared.equipmentSlot,
+                       prepared.afterCharacter.nextInventorySerial);
+
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate = runtime::storage::g_state.account;
+    if (prepared.characterIndex >= candidate.characterCount
+        || candidate.primarySoid != prepared.accountSoid
+        || !same_profile_inventory(
+            candidate, prepared.beforeProfileItems, prepared.expectedProfileItemCount)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("account");
+    }
+    CharacterState& character = candidate.characters[prepared.characterIndex];
+    if (!character.selected || character.soid != prepared.characterSoid
+        || !same_character(character, prepared.beforeCharacter)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("stale");
+    }
+
+    candidate.profileItems = prepared.afterProfileItems;
+    candidate.profileItemCount = prepared.afterProfileItemCount;
+    character = prepared.afterCharacter;
+    family4_loadout::ResolvedLoadout resolved{};
+    std::uint16_t checkedRow = 0;
+    std::uint8_t checkedSlot = 0;
+    if (!account::valid(candidate) || !valid_profile_inventory(candidate)
+        || identity_uses_soid(candidate, prepared.acquiredInstanceSoid)
+        || !family4_loadout::resolve(candidate, prepared.characterIndex, resolved)
+        || !find_acquired_row(resolved, prepared.acquiredInstanceSoid, checkedRow, checkedSlot)
+        || checkedRow != prepared.inventoryRow || checkedSlot != prepared.equipmentSlot) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("resolve");
+    }
+    runtime::storage::g_state.account = candidate;
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+    report_acquisition("commit_end",
+                       "ok",
+                       "published",
+                       prepared.acquiredDefinitionHash,
+                       prepared.characterSoid,
+                       prepared.acquiredInstanceSoid,
+                       prepared.inventoryIndex,
+                       prepared.inventoryRow,
+                       prepared.equipmentSlot,
+                       prepared.afterCharacter.nextInventorySerial);
+    return true;
+}
+
+/** Prepares one checked profile-stack increment or append for a Collections pull. */
+bool prepare_profile_item_acquisition(std::uint16_t collectibleIndex,
+                                      std::uint32_t definitionHash,
+                                      PendingProfileItemAcquisition& mutation) noexcept {
+    mutation = {};
+    const AccountState account = account_snapshot();
+    build_data::collectibles::Definition collectible{};
+    build_data::items::Definition item{};
+    item_details::Definition detail{};
+    inventory_buckets::Descriptor bucket{};
+    if (definitionHash == authored_inventory::kNoDefinitionHash || !account::valid(account)
+        || !valid_profile_inventory(account)
+        || !build_data::find_collectible_definition(collectibleIndex, collectible)
+        || collectible.itemDefinitionIndex
+               == build_data::collectibles::kUnavailableItemDefinitionIndex
+        || !build_data::find_item_definition_hash(definitionHash, item)
+        || item.definitionHash != definitionHash
+        || item.definitionIndex != collectible.itemDefinitionIndex
+        || !build_data::find_configured_item_detail(item.definitionIndex, detail)
+        || detail.definitionIndex != item.definitionIndex || detail.definitionHash != definitionHash
+        || detail.bucketId != item.bucketId
+        || detail.instancedDefinitionState != item_details::InstancedDefinitionState::stackable
+        || detail.maxStackSize <= 0
+        || !build_data::find_inventory_bucket_descriptor(detail.bucketId, bucket)
+        || bucket.arraySelector != inventory_buckets::ArraySelector::profile) {
+        report_profile_acquisition("prepare",
+                                   "fail",
+                                   "definition_or_profile",
+                                   definitionHash,
+                                   account.primarySoid,
+                                   0,
+                                   0,
+                                   0,
+                                   account.profileItemCount,
+                                   0,
+                                   0,
+                                   false);
+        return false;
+    }
+    AccountState chargedAccount{};
+    bool materialsChanged = false;
+    if (!apply_collection_materials(account, collectible, chargedAccount, materialsChanged)) {
+        report_profile_acquisition("prepare",
+                                   "fail",
+                                   "materials",
+                                   definitionHash,
+                                   account.primarySoid,
+                                   0,
+                                   detail.bucketId,
+                                   0,
+                                   account.profileItemCount,
+                                   0,
+                                   0,
+                                   false);
+        return false;
+    }
+    (void)materialsChanged;
+    const bool actionSource =
+        build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
+
+    std::size_t profileIndex = chargedAccount.profileItemCount;
+    std::int32_t previousQuantity = 0;
+    std::int32_t previousMutationSerial = 0;
+    std::int32_t greatestMutationSerial = 0;
+    bool appended = true;
+    for (std::size_t index = 0; index < chargedAccount.profileItemCount; ++index) {
+        greatestMutationSerial =
+            (std::max)(greatestMutationSerial, chargedAccount.profileItems[index].mutationSerial);
+    }
+    for (std::size_t index = 0; index < chargedAccount.profileItemCount; ++index) {
+        const authored_inventory::ProfileItem& existing = chargedAccount.profileItems[index];
+        if (existing.definitionHash != definitionHash) {
+            continue;
+        }
+        if (existing.quantity > detail.maxStackSize) {
+            report_profile_acquisition("prepare",
+                                       "fail",
+                                       "existing_quantity",
+                                       definitionHash,
+                                       account.primarySoid,
+                                       existing.instanceSoid,
+                                       detail.bucketId,
+                                       index,
+                                       chargedAccount.profileItemCount,
+                                       existing.quantity,
+                                       existing.quantity,
+                                       false);
+            return false;
+        }
+        if (appended && existing.quantity < detail.maxStackSize) {
+            profileIndex = index;
+            previousQuantity = existing.quantity;
+            previousMutationSerial = existing.mutationSerial;
+            appended = false;
+        }
+    }
+    if (greatestMutationSerial == (std::numeric_limits<std::int32_t>::max)()
+        || (appended && chargedAccount.profileItemCount >= chargedAccount.profileItems.size())) {
+        report_profile_acquisition(
+            "prepare",
+            "fail",
+            "state_capacity",
+            definitionHash,
+            account.primarySoid,
+            appended ? 0 : chargedAccount.profileItems[profileIndex].instanceSoid,
+            detail.bucketId,
+            profileIndex,
+            chargedAccount.profileItemCount,
+            0,
+            0,
+            true);
+        return false;
+    }
+
+    std::uint64_t acquiredInstanceSoid =
+        appended ? 0 : chargedAccount.profileItems[profileIndex].instanceSoid;
+    if (actionSource != (acquiredInstanceSoid != 0) && !appended) {
+        report_profile_acquisition("prepare",
+                                   "fail",
+                                   "identity_policy",
+                                   definitionHash,
+                                   account.primarySoid,
+                                   acquiredInstanceSoid,
+                                   detail.bucketId,
+                                   profileIndex,
+                                   chargedAccount.profileItemCount,
+                                   previousQuantity,
+                                   previousQuantity,
+                                   false);
+        return false;
+    }
+    if (appended && actionSource
+        && !next_profile_item_instance_soid(chargedAccount, acquiredInstanceSoid)) {
+        report_profile_acquisition("prepare",
+                                   "fail",
+                                   "identity_capacity",
+                                   definitionHash,
+                                   account.primarySoid,
+                                   acquiredInstanceSoid,
+                                   detail.bucketId,
+                                   profileIndex,
+                                   chargedAccount.profileItemCount,
+                                   0,
+                                   0,
+                                   true);
+        return false;
+    }
+
+    AccountState after = chargedAccount;
+    const std::int32_t acquiredMutationSerial = greatestMutationSerial + 1;
+    if (appended) {
+        after.profileItems[profileIndex] = {
+            acquiredInstanceSoid, definitionHash, 1, acquiredMutationSerial};
+        ++after.profileItemCount;
+    } else {
+        ++after.profileItems[profileIndex].quantity;
+        after.profileItems[profileIndex].mutationSerial = acquiredMutationSerial;
+    }
+    const std::int32_t acquiredQuantity = after.profileItems[profileIndex].quantity;
+    if (after.profileItems[profileIndex].instanceSoid != acquiredInstanceSoid
+        || acquiredQuantity <= previousQuantity || acquiredQuantity > detail.maxStackSize
+        || !account::valid(after) || !valid_profile_inventory(after)) {
+        report_profile_acquisition("prepare",
+                                   "fail",
+                                   "bucket_full_or_quantity",
+                                   definitionHash,
+                                   account.primarySoid,
+                                   acquiredInstanceSoid,
+                                   detail.bucketId,
+                                   profileIndex,
+                                   after.profileItemCount,
+                                   previousQuantity,
+                                   acquiredQuantity,
+                                   appended);
+        return false;
+    }
+
+    mutation.beforeItems = account.profileItems;
+    mutation.afterItems = after.profileItems;
+    mutation.accountSoid = account.primarySoid;
+    mutation.acquiredInstanceSoid = acquiredInstanceSoid;
+    mutation.acquiredDefinitionHash = definitionHash;
+    mutation.materialRequirementSetHash = collectible.materialRequirementSetHash;
+    mutation.expectedItemCount = account.profileItemCount;
+    mutation.afterItemCount = after.profileItemCount;
+    mutation.profileIndex = profileIndex;
+    mutation.previousQuantity = previousQuantity;
+    mutation.acquiredQuantity = acquiredQuantity;
+    mutation.previousMutationSerial = previousMutationSerial;
+    mutation.acquiredMutationSerial = acquiredMutationSerial;
+    mutation.collectibleIndex = collectibleIndex;
+    mutation.bucketId = detail.bucketId;
+    mutation.materialRequirementCount = collectible.materialRequirementCount;
+    mutation.actionSource = actionSource;
+    mutation.appended = appended;
+    mutation.prepared = true;
+    if (!valid_profile_mutation_shape(mutation)) {
+        mutation = {};
+        report_profile_acquisition("prepare",
+                                   "fail",
+                                   "mutation",
+                                   definitionHash,
+                                   account.primarySoid,
+                                   acquiredInstanceSoid,
+                                   detail.bucketId,
+                                   profileIndex,
+                                   after.profileItemCount,
+                                   previousQuantity,
+                                   acquiredQuantity,
+                                   appended);
+        return false;
+    }
+    report_profile_acquisition("prepare",
+                               "ok",
+                               "ready",
+                               definitionHash,
+                               account.primarySoid,
+                               acquiredInstanceSoid,
+                               detail.bucketId,
+                               profileIndex,
+                               after.profileItemCount,
+                               previousQuantity,
+                               acquiredQuantity,
+                               appended);
+    return true;
+}
+
+/** Produces the exact account after-image while the captured profile view is still current. */
+bool preview_profile_item_acquisition(const PendingProfileItemAcquisition& mutation,
+                                      AccountState& after) noexcept {
+    after = {};
+    const AccountState current = account_snapshot();
+    const bool ready = materialize_profile_acquisition(current, mutation, after);
+    report_profile_acquisition("preview",
+                               ready ? "ok" : "fail",
+                               ready ? "ready" : "stale_or_invalid",
+                               mutation.acquiredDefinitionHash,
+                               mutation.accountSoid,
+                               mutation.acquiredInstanceSoid,
+                               mutation.bucketId,
+                               mutation.profileIndex,
+                               mutation.afterItemCount,
+                               mutation.previousQuantity,
+                               mutation.acquiredQuantity,
+                               mutation.appended);
+    return ready;
+}
+
+/** Commits one profile-stack after-image only while its exact prepare-time view remains current. */
+bool commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcept {
+    const PendingProfileItemAcquisition prepared = mutation;
+    mutation = {};
+    if (!valid_profile_mutation_shape(prepared)) {
+        report_profile_acquisition("commit",
+                                   "fail",
+                                   "mutation",
+                                   prepared.acquiredDefinitionHash,
+                                   prepared.accountSoid,
+                                   prepared.acquiredInstanceSoid,
+                                   prepared.bucketId,
+                                   prepared.profileIndex,
+                                   prepared.afterItemCount,
+                                   prepared.previousQuantity,
+                                   prepared.acquiredQuantity,
+                                   prepared.appended);
+        return false;
+    }
+
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate{};
+    const bool ready =
+        materialize_profile_acquisition(runtime::storage::g_state.account, prepared, candidate);
+    if (ready) {
+        runtime::storage::g_state.account = candidate;
+    }
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+    report_profile_acquisition("commit",
+                               ready ? "ok" : "fail",
+                               ready ? "published" : "stale_or_invalid",
+                               prepared.acquiredDefinitionHash,
+                               prepared.accountSoid,
+                               prepared.acquiredInstanceSoid,
+                               prepared.bucketId,
+                               prepared.profileIndex,
+                               prepared.afterItemCount,
+                               prepared.previousQuantity,
+                               prepared.acquiredQuantity,
+                               prepared.appended);
+    return ready;
+}
+
+} // namespace sunrise::state

+ 128 - 0
Sunrise/src/state/runtime/state_account_dismantle_runtime.cpp

@@ -0,0 +1,128 @@
+#include <Windows.h>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+
+#include "runtime.h"
+#include "state_account_transaction_helpers.h"
+#include "storage/internal.h"
+
+namespace sunrise::state {
+
+using namespace runtime::detail;
+
+/** Prepares one selected-character unequipped item removal without changing account State. */
+bool prepare_item_dismantle(std::uint64_t instanceSoid, PendingItemDismantle& mutation) noexcept {
+    mutation = {};
+    const AccountState account = account_snapshot();
+    if (instanceSoid == 0 || !account::valid(account)) {
+        report_dismantle("prepare", "fail", "input", 0, 0, instanceSoid, 0, 0, 0, 0, 0);
+        return false;
+    }
+
+    std::size_t characterIndex = account.characterCount;
+    for (std::size_t index = 0; index < account.characterCount; ++index) {
+        if (account.characters[index].selected) {
+            characterIndex = index;
+            break;
+        }
+    }
+    if (characterIndex >= account.characterCount
+        || !stage_item_dismantle(account, characterIndex, instanceSoid, mutation)) {
+        report_dismantle(
+            "prepare", "fail", "ownership_or_resolve", 0, 0, instanceSoid, 0, 0, 0, 0, 0);
+        return false;
+    }
+
+    report_dismantle("prepare",
+                     "ok",
+                     "ready",
+                     mutation.dismantledItem.definitionHash,
+                     mutation.characterSoid,
+                     mutation.dismantledInstanceSoid,
+                     mutation.inventoryIndex,
+                     mutation.inventoryRow,
+                     mutation.equipmentSlot,
+                     mutation.movedInventoryItemCount,
+                     mutation.afterCharacter.nextInventorySerial);
+    return true;
+}
+
+/** Produces the exact character-and-profile after-image while the captured views remain current. */
+bool preview_item_dismantle(const PendingItemDismantle& mutation, AccountState& after) noexcept {
+    const AccountState current = account_snapshot();
+    const bool ready = materialize_item_dismantle(current, mutation, after);
+    report_dismantle("preview",
+                     ready ? "ok" : "fail",
+                     ready ? "ready" : "stale_or_invalid",
+                     mutation.dismantledItem.definitionHash,
+                     mutation.characterSoid,
+                     mutation.dismantledInstanceSoid,
+                     mutation.inventoryIndex,
+                     mutation.inventoryRow,
+                     mutation.equipmentSlot,
+                     mutation.movedInventoryItemCount,
+                     mutation.afterCharacter.nextInventorySerial);
+    return ready;
+}
+
+/** Commits one prepared dismantle only while its complete account views are unchanged. */
+bool commit_item_dismantle(PendingItemDismantle& mutation) noexcept {
+    const PendingItemDismantle prepared = mutation;
+    mutation = {};
+    const auto fail = [&prepared](std::string_view reason) noexcept {
+        report_dismantle("commit",
+                         "fail",
+                         reason,
+                         prepared.dismantledItem.definitionHash,
+                         prepared.characterSoid,
+                         prepared.dismantledInstanceSoid,
+                         prepared.inventoryIndex,
+                         prepared.inventoryRow,
+                         prepared.equipmentSlot,
+                         prepared.movedInventoryItemCount,
+                         prepared.afterCharacter.nextInventorySerial);
+        return false;
+    };
+
+    report_dismantle("commit_begin",
+                     "ok",
+                     "ready",
+                     prepared.dismantledItem.definitionHash,
+                     prepared.characterSoid,
+                     prepared.dismantledInstanceSoid,
+                     prepared.inventoryIndex,
+                     prepared.inventoryRow,
+                     prepared.equipmentSlot,
+                     prepared.movedInventoryItemCount,
+                     prepared.afterCharacter.nextInventorySerial);
+
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate{};
+    const bool ready =
+        materialize_item_dismantle(runtime::storage::g_state.account, prepared, candidate);
+    if (ready) {
+        runtime::storage::g_state.account = candidate;
+    }
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+
+    if (!ready) {
+        return fail("stale_or_invalid");
+    }
+
+    report_dismantle("commit_end",
+                     "ok",
+                     "published",
+                     prepared.dismantledItem.definitionHash,
+                     prepared.characterSoid,
+                     prepared.dismantledInstanceSoid,
+                     prepared.inventoryIndex,
+                     prepared.inventoryRow,
+                     prepared.equipmentSlot,
+                     prepared.movedInventoryItemCount,
+                     prepared.afterCharacter.nextInventorySerial);
+    return true;
+}
+
+} // namespace sunrise::state

+ 517 - 0
Sunrise/src/state/runtime/state_account_item_action_runtime.cpp

@@ -0,0 +1,517 @@
+#include <Windows.h>
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+
+#include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
+#include "../build_data/runtime.h"
+#include "runtime.h"
+#include "state_account_transaction_helpers.h"
+#include "storage/internal.h"
+
+namespace sunrise::state {
+
+using namespace runtime::detail;
+namespace authored_inventory = account::inventory;
+namespace item_details = build_data::items::details;
+namespace family4_loadout = middleware::datagen::family4::loadout;
+
+/** Prepares one checked ordinary-socket selection without publishing account State. */
+bool prepare_socket_plug(std::uint64_t targetInstanceSoid,
+                         std::uint8_t socketLane,
+                         std::uint16_t plugDefinitionIndex,
+                         PendingSocketPlug& mutation) noexcept {
+    mutation = {};
+    const AccountState snapshot = account_snapshot();
+    std::size_t characterIndex = snapshot.characterCount;
+    if (targetInstanceSoid != 0 && account::valid(snapshot)) {
+        for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
+            if (snapshot.characters[index].selected) {
+                characterIndex = index;
+                break;
+            }
+        }
+    }
+    if (characterIndex >= snapshot.characterCount
+        || !stage_socket_plug(snapshot,
+                              characterIndex,
+                              targetInstanceSoid,
+                              socketLane,
+                              plugDefinitionIndex,
+                              mutation)) {
+        report_socket_plug("prepare",
+                           "fail",
+                           "ownership_definition_or_compatibility",
+                           0,
+                           targetInstanceSoid,
+                           0,
+                           socketLane,
+                           plugDefinitionIndex,
+                           0,
+                           0,
+                           false,
+                           0);
+        return false;
+    }
+    report_socket_plug("prepare",
+                       "ok",
+                       "ready",
+                       mutation.characterSoid,
+                       mutation.targetInstanceSoid,
+                       mutation.targetDefinitionIndex,
+                       mutation.socketLane,
+                       mutation.plugDefinitionIndex,
+                       mutation.targetBucketId,
+                       mutation.plugBucketId,
+                       mutation.targetEquipped,
+                       mutation.itemIndex);
+    return true;
+}
+
+/** Prepares a socket transition for the exact instance identity carried by opcode 1901. */
+bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
+                                            std::uint8_t requestedSocketLane,
+                                            std::uint16_t plugDefinitionIndex,
+                                            PendingSocketPlug& mutation) noexcept {
+    mutation = {};
+    const AccountState snapshot = account_snapshot();
+    std::size_t characterIndex = snapshot.characterCount;
+    if (account::valid(snapshot)) {
+        for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
+            if (snapshot.characters[index].selected) {
+                characterIndex = index;
+                break;
+            }
+        }
+    }
+
+    constexpr std::uint64_t kSelectorStride = 4;
+    constexpr std::uint64_t kInstanceIdentityMask = 0x3FFFFFFFFFFFFFFFULL;
+    if (characterIndex >= snapshot.characterCount || itemSelector == 0
+        || itemSelector % kSelectorStride != 0
+        || itemSelector / kSelectorStride > kInstanceIdentityMask
+        || requestedSocketLane >= authored_inventory::kPlugCapacity) {
+        report_socket_plug("prepare_location",
+                           "fail",
+                           "selection_or_slot",
+                           0,
+                           0,
+                           0,
+                           requestedSocketLane,
+                           plugDefinitionIndex,
+                           0,
+                           0,
+                           false,
+                           static_cast<std::size_t>(itemSelector / kSelectorStride));
+        return false;
+    }
+
+    family4_loadout::ResolvedLoadout resolvedLoadout{};
+    if (!family4_loadout::resolve(snapshot, characterIndex, resolvedLoadout)) {
+        return false;
+    }
+    const std::uint64_t identityToken = itemSelector / kSelectorStride;
+    const family4_loadout::ResolvedItem* resolvedTarget = nullptr;
+    for (std::size_t index = 0; index < resolvedLoadout.itemCount; ++index) {
+        const family4_loadout::ResolvedItem& item = resolvedLoadout.items[index];
+        if ((item.instance.instanceSoid & kInstanceIdentityMask) != identityToken) {
+            continue;
+        }
+        if (resolvedTarget != nullptr) {
+            return false;
+        }
+        resolvedTarget = &item;
+    }
+    const bool expectedEquipped = resolvedTarget != nullptr && resolvedTarget->equipped;
+
+    std::uint8_t resolvedSocketLane = static_cast<std::uint8_t>(authored_inventory::kPlugCapacity);
+    build_data::items::Definition targetDefinition{};
+    item_details::Definition targetDetail{};
+    bool ambiguousSocketLane = false;
+    if (resolvedTarget != nullptr
+        && build_data::find_item_definition_index(resolvedTarget->instance.baseDefinitionIndex,
+                                                  targetDefinition)
+        && targetDefinition.definitionIndex == resolvedTarget->instance.baseDefinitionIndex
+        && build_data::find_configured_item_detail(targetDefinition.definitionIndex, targetDetail)
+        && targetDetail.definitionIndex == targetDefinition.definitionIndex
+        && targetDetail.definitionHash == targetDefinition.definitionHash
+        && targetDetail.bucketId == targetDefinition.bucketId
+        && 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.
+        if (requestedSocketLane < targetDetail.ordinarySocketCount
+            && build_data::is_socket_plug_allowed(
+                targetDefinition.definitionIndex, requestedSocketLane, plugDefinitionIndex)) {
+            resolvedSocketLane = requestedSocketLane;
+        } else {
+            for (std::size_t lane = 0; lane < targetDetail.ordinarySocketCount; ++lane) {
+                if (!build_data::is_socket_plug_allowed(targetDefinition.definitionIndex,
+                                                        static_cast<std::uint8_t>(lane),
+                                                        plugDefinitionIndex)) {
+                    continue;
+                }
+                if (resolvedSocketLane < authored_inventory::kPlugCapacity) {
+                    ambiguousSocketLane = true;
+                    break;
+                }
+                resolvedSocketLane = static_cast<std::uint8_t>(lane);
+            }
+        }
+    }
+    if (resolvedTarget == nullptr || resolvedTarget->instance.instanceSoid == 0
+        || ambiguousSocketLane || resolvedSocketLane >= authored_inventory::kPlugCapacity
+        || !stage_socket_plug(snapshot,
+                              characterIndex,
+                              resolvedTarget->instance.instanceSoid,
+                              resolvedSocketLane,
+                              plugDefinitionIndex,
+                              mutation)
+        || mutation.targetEquipped != expectedEquipped) {
+        report_socket_plug("prepare_location",
+                           "fail",
+                           "equipment_or_compatibility",
+                           snapshot.characters[characterIndex].soid,
+                           resolvedTarget != nullptr ? resolvedTarget->instance.instanceSoid : 0,
+                           0,
+                           resolvedSocketLane,
+                           plugDefinitionIndex,
+                           0,
+                           0,
+                           expectedEquipped,
+                           static_cast<std::size_t>(identityToken));
+        mutation = {};
+        return false;
+    }
+
+    report_socket_plug("prepare_location",
+                       "ok",
+                       "ready",
+                       mutation.characterSoid,
+                       mutation.targetInstanceSoid,
+                       mutation.targetDefinitionIndex,
+                       mutation.socketLane,
+                       mutation.plugDefinitionIndex,
+                       mutation.targetBucketId,
+                       mutation.plugBucketId,
+                       mutation.targetEquipped,
+                       mutation.itemIndex);
+    return true;
+}
+
+/** Produces the complete account after-image while the prepared socket action remains current. */
+bool preview_socket_plug(const PendingSocketPlug& mutation, AccountState& after) noexcept {
+    after = {};
+    if (!mutation.prepared || mutation.accountSoid == 0 || mutation.characterSoid == 0
+        || mutation.targetInstanceSoid == 0 || mutation.characterIndex >= kCharacterCapacity
+        || mutation.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
+        || mutation.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
+        return false;
+    }
+    const AccountState current = account_snapshot();
+    if (mutation.characterIndex >= current.characterCount
+        || current.primarySoid != mutation.accountSoid
+        || !same_character(current.characters[mutation.characterIndex], mutation.beforeCharacter)
+        || !same_profile_inventory(
+            current, mutation.beforeProfileItems, mutation.expectedProfileItemCount)) {
+        return false;
+    }
+
+    PendingSocketPlug canonical{};
+    if (!stage_socket_plug(current,
+                           mutation.characterIndex,
+                           mutation.targetInstanceSoid,
+                           mutation.socketLane,
+                           mutation.plugDefinitionIndex,
+                           canonical)
+        || canonical.accountSoid != mutation.accountSoid
+        || canonical.characterSoid != mutation.characterSoid
+        || canonical.targetDefinitionHash != mutation.targetDefinitionHash
+        || canonical.plugDefinitionHash != mutation.plugDefinitionHash
+        || canonical.materialRequirementSetHash != mutation.materialRequirementSetHash
+        || canonical.characterIndex != mutation.characterIndex
+        || canonical.expectedProfileItemCount != mutation.expectedProfileItemCount
+        || canonical.afterProfileItemCount != mutation.afterProfileItemCount
+        || canonical.itemIndex != mutation.itemIndex
+        || canonical.targetDefinitionIndex != mutation.targetDefinitionIndex
+        || canonical.plugDefinitionIndex != mutation.plugDefinitionIndex
+        || canonical.materialRequirementSetIndex != mutation.materialRequirementSetIndex
+        || canonical.socketLane != mutation.socketLane
+        || canonical.targetBucketId != mutation.targetBucketId
+        || canonical.plugBucketId != mutation.plugBucketId
+        || canonical.materialRequirementCount != mutation.materialRequirementCount
+        || canonical.profileChanged != mutation.profileChanged
+        || canonical.targetEquipped != mutation.targetEquipped
+        || !same_character(canonical.beforeCharacter, mutation.beforeCharacter)
+        || !same_character(canonical.afterCharacter, mutation.afterCharacter)) {
+        return false;
+    }
+
+    after = current;
+    after.profileItems = canonical.afterProfileItems;
+    after.profileItemCount = canonical.afterProfileItemCount;
+    after.characters[mutation.characterIndex] = canonical.afterCharacter;
+    const bool balancesChanged = !same_profile_inventory(
+        after, mutation.beforeProfileItems, mutation.expectedProfileItemCount);
+    family4_loadout::ResolvedLoadout resolved{};
+    return balancesChanged == mutation.profileChanged
+           && same_profile_inventory(
+               after, mutation.afterProfileItems, mutation.afterProfileItemCount)
+           && account::valid(after) && valid_profile_inventory(after)
+           && family4_loadout::resolve(after, mutation.characterIndex, resolved);
+}
+
+/** Commits one prepared socket-and-material after-image behind exact staleness guards. */
+bool commit_socket_plug(PendingSocketPlug& mutation) noexcept {
+    const PendingSocketPlug prepared = mutation;
+    mutation = {};
+    const auto fail = [&prepared](std::string_view reason) noexcept {
+        report_socket_plug("commit",
+                           "fail",
+                           reason,
+                           prepared.characterSoid,
+                           prepared.targetInstanceSoid,
+                           prepared.targetDefinitionIndex,
+                           prepared.socketLane,
+                           prepared.plugDefinitionIndex,
+                           prepared.targetBucketId,
+                           prepared.plugBucketId,
+                           prepared.targetEquipped,
+                           prepared.itemIndex);
+        return false;
+    };
+    const std::size_t itemCapacity = prepared.targetEquipped
+                                         ? authored_inventory::kEquipmentSlotCount
+                                         : authored_inventory::kCharacterItemCapacity;
+    if (!prepared.prepared || prepared.accountSoid == 0 || prepared.characterSoid == 0
+        || prepared.targetInstanceSoid == 0
+        || prepared.targetDefinitionHash == authored_inventory::kNoDefinitionHash
+        || prepared.plugDefinitionHash == authored_inventory::kNoDefinitionHash
+        || prepared.characterIndex >= kCharacterCapacity || prepared.itemIndex >= itemCapacity
+        || prepared.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
+        || prepared.afterProfileItemCount > authored_inventory::kProfileItemCapacity
+        || prepared.socketLane >= authored_inventory::kPlugCapacity
+        || prepared.beforeCharacter.soid != prepared.characterSoid
+        || prepared.afterCharacter.soid != prepared.characterSoid) {
+        return fail("mutation");
+    }
+
+    report_socket_plug("commit_begin",
+                       "ok",
+                       "ready",
+                       prepared.characterSoid,
+                       prepared.targetInstanceSoid,
+                       prepared.targetDefinitionIndex,
+                       prepared.socketLane,
+                       prepared.plugDefinitionIndex,
+                       prepared.targetBucketId,
+                       prepared.plugBucketId,
+                       prepared.targetEquipped,
+                       prepared.itemIndex);
+
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate = runtime::storage::g_state.account;
+    if (prepared.characterIndex >= candidate.characterCount
+        || candidate.primarySoid != prepared.accountSoid
+        || !same_profile_inventory(
+            candidate, prepared.beforeProfileItems, prepared.expectedProfileItemCount)
+        || !same_character(candidate.characters[prepared.characterIndex],
+                           prepared.beforeCharacter)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("stale");
+    }
+
+    PendingSocketPlug canonical{};
+    if (!stage_socket_plug(candidate,
+                           prepared.characterIndex,
+                           prepared.targetInstanceSoid,
+                           prepared.socketLane,
+                           prepared.plugDefinitionIndex,
+                           canonical)
+        || canonical.characterSoid != prepared.characterSoid
+        || canonical.accountSoid != prepared.accountSoid
+        || canonical.targetDefinitionHash != prepared.targetDefinitionHash
+        || canonical.plugDefinitionHash != prepared.plugDefinitionHash
+        || canonical.materialRequirementSetHash != prepared.materialRequirementSetHash
+        || canonical.characterIndex != prepared.characterIndex
+        || canonical.expectedProfileItemCount != prepared.expectedProfileItemCount
+        || canonical.afterProfileItemCount != prepared.afterProfileItemCount
+        || canonical.itemIndex != prepared.itemIndex
+        || canonical.targetDefinitionIndex != prepared.targetDefinitionIndex
+        || canonical.plugDefinitionIndex != prepared.plugDefinitionIndex
+        || canonical.materialRequirementSetIndex != prepared.materialRequirementSetIndex
+        || canonical.socketLane != prepared.socketLane
+        || canonical.targetBucketId != prepared.targetBucketId
+        || canonical.plugBucketId != prepared.plugBucketId
+        || canonical.materialRequirementCount != prepared.materialRequirementCount
+        || canonical.profileChanged != prepared.profileChanged
+        || canonical.targetEquipped != prepared.targetEquipped
+        || !same_character(canonical.beforeCharacter, prepared.beforeCharacter)
+        || !same_character(canonical.afterCharacter, prepared.afterCharacter)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("transition");
+    }
+
+    candidate.profileItems = canonical.afterProfileItems;
+    candidate.profileItemCount = canonical.afterProfileItemCount;
+    if (!same_profile_inventory(
+            candidate, prepared.afterProfileItems, prepared.afterProfileItemCount)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("materials");
+    }
+    candidate.characters[prepared.characterIndex] = canonical.afterCharacter;
+    family4_loadout::ResolvedLoadout checked{};
+    if (!account::valid(candidate) || !valid_profile_inventory(candidate)
+        || !family4_loadout::resolve(candidate, prepared.characterIndex, checked)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("account_or_resolve");
+    }
+    runtime::storage::g_state.account = candidate;
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+
+    report_socket_plug("commit_end",
+                       "ok",
+                       "published",
+                       prepared.characterSoid,
+                       prepared.targetInstanceSoid,
+                       prepared.targetDefinitionIndex,
+                       prepared.socketLane,
+                       prepared.plugDefinitionIndex,
+                       prepared.targetBucketId,
+                       prepared.plugBucketId,
+                       prepared.targetEquipped,
+                       prepared.itemIndex);
+    return true;
+}
+
+/** Prepares one complete native item-state value for an owned selected-character instance. */
+bool prepare_item_state(std::uint64_t targetInstanceSoid,
+                        std::uint16_t targetDefinitionIndex,
+                        std::uint32_t flags,
+                        PendingItemState& mutation) noexcept {
+    mutation = {};
+    const AccountState snapshot = account_snapshot();
+    std::size_t characterIndex = snapshot.characterCount;
+    if (account::valid(snapshot)) {
+        for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
+            if (snapshot.characters[index].selected) {
+                characterIndex = index;
+                break;
+            }
+        }
+    }
+    if (characterIndex >= snapshot.characterCount
+        || !stage_item_state(
+            snapshot, characterIndex, targetInstanceSoid, targetDefinitionIndex, flags, mutation)) {
+        report_item_state(
+            "prepare",
+            "fail",
+            "selection_or_item",
+            characterIndex < snapshot.characterCount ? snapshot.characters[characterIndex].soid : 0,
+            targetInstanceSoid,
+            targetDefinitionIndex,
+            0,
+            flags,
+            false,
+            0);
+        mutation = {};
+        return false;
+    }
+    report_item_state("prepare",
+                      "ok",
+                      "ready",
+                      mutation.characterSoid,
+                      mutation.targetInstanceSoid,
+                      mutation.targetDefinitionIndex,
+                      mutation.beforeFlags,
+                      mutation.afterFlags,
+                      mutation.targetEquipped,
+                      mutation.itemIndex);
+    return true;
+}
+
+/** Commits one prepared item-state after-image behind an exact character staleness guard. */
+bool commit_item_state(PendingItemState& mutation) noexcept {
+    const PendingItemState prepared = mutation;
+    mutation = {};
+    const auto fail = [&prepared](std::string_view reason) noexcept {
+        report_item_state("commit",
+                          "fail",
+                          reason,
+                          prepared.characterSoid,
+                          prepared.targetInstanceSoid,
+                          prepared.targetDefinitionIndex,
+                          prepared.beforeFlags,
+                          prepared.afterFlags,
+                          prepared.targetEquipped,
+                          prepared.itemIndex);
+        return false;
+    };
+    const std::size_t capacity = prepared.targetEquipped
+                                     ? authored_inventory::kEquipmentSlotCount
+                                     : authored_inventory::kCharacterItemCapacity;
+    if (!prepared.prepared || prepared.characterSoid == 0 || prepared.targetInstanceSoid == 0
+        || prepared.characterIndex >= kCharacterCapacity || prepared.itemIndex >= capacity
+        || prepared.beforeCharacter.soid != prepared.characterSoid
+        || prepared.afterCharacter.soid != prepared.characterSoid
+        || prepared.beforeFlags == prepared.afterFlags) {
+        return fail("mutation");
+    }
+
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate = runtime::storage::g_state.account;
+    if (prepared.characterIndex >= candidate.characterCount
+        || !same_character(candidate.characters[prepared.characterIndex],
+                           prepared.beforeCharacter)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("stale");
+    }
+
+    PendingItemState canonical{};
+    if (!stage_item_state(candidate,
+                          prepared.characterIndex,
+                          prepared.targetInstanceSoid,
+                          prepared.targetDefinitionIndex,
+                          prepared.afterFlags,
+                          canonical)
+        || canonical.characterSoid != prepared.characterSoid
+        || canonical.characterIndex != prepared.characterIndex
+        || canonical.itemIndex != prepared.itemIndex
+        || canonical.targetDefinitionIndex != prepared.targetDefinitionIndex
+        || canonical.beforeFlags != prepared.beforeFlags
+        || canonical.afterFlags != prepared.afterFlags
+        || canonical.targetEquipped != prepared.targetEquipped
+        || !same_character(canonical.beforeCharacter, prepared.beforeCharacter)
+        || !same_character(canonical.afterCharacter, prepared.afterCharacter)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("transition");
+    }
+
+    candidate.characters[prepared.characterIndex] = prepared.afterCharacter;
+    family4_loadout::ResolvedLoadout checked{};
+    if (!account::valid(candidate)
+        || !family4_loadout::resolve(candidate, prepared.characterIndex, checked)) {
+        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+        return fail("account_or_resolve");
+    }
+    runtime::storage::g_state.account = candidate;
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+
+    report_item_state("commit",
+                      "ok",
+                      "published",
+                      prepared.characterSoid,
+                      prepared.targetInstanceSoid,
+                      prepared.targetDefinitionIndex,
+                      prepared.beforeFlags,
+                      prepared.afterFlags,
+                      prepared.targetEquipped,
+                      prepared.itemIndex);
+    return true;
+}
+
+} // namespace sunrise::state

+ 24 - 1215
Sunrise/src/state/runtime/state_account_runtime.cpp

@@ -14,10 +14,11 @@
 #include "../build_data/runtime.h"
 #include "runtime.h"
 #include "state.h"
+#include "state_account_transaction_helpers.h"
 #include "storage/internal.h"
 
 namespace sunrise::state {
-namespace {
+namespace runtime::detail {
 
 namespace authored_inventory = account::inventory;
 namespace item_details = build_data::items::details;
@@ -27,26 +28,9 @@ namespace family4_loadout = middleware::datagen::family4::loadout;
 /** First SOID reserved for item instances created by this local runtime. */
 constexpr std::uint64_t kFirstGeneratedItemSoid = 0x4000000000000001ULL;
 
-/** Historical profile definitions returned by ordinary gear dismantles. */
-constexpr std::uint32_t kLegendaryShardsDefinitionHash = 0x3CF2E8E2U;
-constexpr std::uint32_t kGunsmithMaterialsDefinitionHash = 0x28D6AC07U;
-/** Base gear payout for the supported pre-Beyond-Light client. */
-constexpr std::int32_t kLegendaryShardDismantleQuantity = 4;
-constexpr std::int32_t kGunsmithMaterialDismantleQuantity = 3;
 /** Equipment slots 0-2 are weapons and 3-7 are class-specific armor. */
 constexpr std::uint8_t kGearEquipmentSlotCount = 8;
 
-struct DismantleRewardPolicy {
-    std::uint32_t definitionHash{};
-    std::int32_t quantity{};
-};
-
-constexpr std::array<DismantleRewardPolicy, kDismantleRewardCapacity>
-    kGearDismantleRewards{{
-        {kLegendaryShardsDefinitionHash, kLegendaryShardDismantleQuantity},
-        {kGunsmithMaterialsDefinitionHash, kGunsmithMaterialDismantleQuantity},
-    }};
-
 /** Writes one exhaustive equipment-transaction checkpoint to the persistent diagnostic log. */
 void report_equipment(std::string_view stage,
                       std::string_view result,
@@ -196,11 +180,11 @@ same_profile_inventory(const AccountState& account,
 
 /** @return True when two fixed profile views, including their empty tails, are identical. */
 [[nodiscard]] bool same_profile_views(
-    const std::array<authored_inventory::ProfileItem,
-                     authored_inventory::kProfileItemCapacity>& left,
+    const std::array<authored_inventory::ProfileItem, authored_inventory::kProfileItemCapacity>&
+        left,
     std::size_t leftCount,
-    const std::array<authored_inventory::ProfileItem,
-                     authored_inventory::kProfileItemCapacity>& right,
+    const std::array<authored_inventory::ProfileItem, authored_inventory::kProfileItemCapacity>&
+        right,
     std::size_t rightCount) noexcept {
     if (leftCount != rightCount) {
         return false;
@@ -759,14 +743,6 @@ void report_socket_plug(std::string_view stage,
     return semanticIndex < authored_inventory::kEquipmentSlotCount;
 }
 
-/** One instance's unique native row and equipped marker in a resolved character loadout. */
-struct ResolvedPosition {
-    std::uint16_t inventoryRow{};
-    std::uint8_t equipmentSlot{};
-    bool equipped{};
-    std::int32_t mutationSerial{};
-};
-
 /** Finds one instance exactly once in a checked, row-sorted loadout. */
 [[nodiscard]] bool find_resolved_position(const family4_loadout::ResolvedLoadout& loadout,
                                           std::uint64_t instanceSoid,
@@ -1535,15 +1511,17 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
 /**
  * Credits the supported client's ordinary weapon/armor dismantle payout.
  *
- * Capped stacks lose only the overflowing part, matching normal profile-inventory behavior.
- * Every credited row receives a new mutation serial so the account observer can display it.
+ * Capped stacks
+ * lose only the overflowing part, matching normal profile-inventory behavior.
+ * Every credited row
+ * receives a new mutation serial so the account observer can display it.
  */
-[[nodiscard]] bool apply_dismantle_rewards(
-    const AccountState& before,
-    std::uint8_t equipmentSlot,
-    AccountState& after,
-    std::array<DismantleReward, kDismantleRewardCapacity>& rewards,
-    std::size_t& rewardCount) noexcept {
+[[nodiscard]] bool
+apply_dismantle_rewards(const AccountState& before,
+                        std::uint8_t equipmentSlot,
+                        AccountState& after,
+                        std::array<DismantleReward, kDismantleRewardCapacity>& rewards,
+                        std::size_t& rewardCount) noexcept {
     after = before;
     rewards = {};
     rewardCount = 0;
@@ -1560,7 +1538,8 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
             (std::max)(greatestMutationSerial, before.profileItems[index].mutationSerial);
     }
 
-    for (const DismantleRewardPolicy& policy : kGearDismantleRewards) {
+    for (std::size_t policyIndex = 0; policyIndex < before.dismantleRewardCount; ++policyIndex) {
+        const DismantleRewardPolicy& policy = before.dismantleRewards[policyIndex];
         build_data::items::Definition definition{};
         item_details::Definition detail{};
         inventory_buckets::Descriptor bucket{};
@@ -1752,8 +1731,7 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
 
     build_data::items::Definition dismantledDefinition{};
     item_details::Definition dismantledDetail{};
-    if (!build_data::find_item_definition_hash(dismantledItem.definitionHash,
-                                               dismantledDefinition)
+    if (!build_data::find_item_definition_hash(dismantledItem.definitionHash, dismantledDefinition)
         || dismantledDefinition.definitionHash != dismantledItem.definitionHash
         || !build_data::find_configured_item_detail(dismantledDefinition.definitionIndex,
                                                     dismantledDetail)
@@ -1819,8 +1797,7 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
         || left.inventoryIndex != right.inventoryIndex
         || left.movedInventoryItemCount != right.movedInventoryItemCount
         || left.rewardCount != right.rewardCount || left.inventoryRow != right.inventoryRow
-        || left.equipmentSlot != right.equipmentSlot
-        || left.profileChanged != right.profileChanged
+        || left.equipmentSlot != right.equipmentSlot || left.profileChanged != right.profileChanged
         || !same_stationary_item(left.dismantledItem, right.dismantledItem)
         || !same_character(left.beforeCharacter, right.beforeCharacter)
         || !same_character(left.afterCharacter, right.afterCharacter)
@@ -1851,8 +1828,7 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
         || mutation.dismantledInstanceSoid == 0
         || mutation.dismantledItem.instanceSoid != mutation.dismantledInstanceSoid
         || mutation.dismantledItem.definitionHash == authored_inventory::kNoDefinitionHash
-        || mutation.characterIndex >= current.characterCount
-        || mutation.expectedInventoryCount == 0
+        || mutation.characterIndex >= current.characterCount || mutation.expectedInventoryCount == 0
         || mutation.expectedInventoryCount > authored_inventory::kCharacterItemCapacity
         || mutation.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
         || mutation.afterProfileItemCount > authored_inventory::kProfileItemCapacity
@@ -1918,7 +1894,9 @@ character_item_at(CharacterState& character, const CharacterItemLocation& locati
            && !identity_uses_soid(after, mutation.dismantledInstanceSoid);
 }
 
-} // namespace
+} // namespace runtime::detail
+
+using namespace runtime::detail;
 
 /** Stores the active account key without publishing an incomplete account. */
 bool set_primary_soid(std::uint64_t primarySoid) noexcept {
@@ -2285,1175 +2263,6 @@ bool commit_equipment_swap(PendingEquipmentSwap& mutation) noexcept {
     return true;
 }
 
-/** Prepares one native-row-checked selected-character inventory insertion. */
-bool prepare_item_acquisition(std::uint16_t collectibleIndex,
-                              std::uint32_t definitionHash,
-                              PendingItemAcquisition& mutation) noexcept {
-    mutation = {};
-    const AccountState account = account_snapshot();
-    build_data::collectibles::Definition collectible{};
-    build_data::items::Definition grantedDefinition{};
-    if (definitionHash == authored_inventory::kNoDefinitionHash || !account::valid(account)
-        || !valid_profile_inventory(account)
-        || !build_data::find_collectible_definition(collectibleIndex, collectible)
-        || collectible.itemDefinitionIndex
-               == build_data::collectibles::kUnavailableItemDefinitionIndex
-        || !build_data::find_item_definition_index(collectible.itemDefinitionIndex,
-                                                   grantedDefinition)
-        || grantedDefinition.definitionHash != definitionHash) {
-        report_acquisition("prepare", "fail", "input", definitionHash, 0, 0, 0, 0, 0, 0);
-        return false;
-    }
-
-    AccountState chargedAccount{};
-    bool profileChanged = false;
-    if (!apply_collection_materials(account, collectible, chargedAccount, profileChanged)) {
-        report_acquisition("prepare", "fail", "materials", definitionHash, 0, 0, 0, 0, 0, 0);
-        return false;
-    }
-
-    std::size_t characterIndex = account.characterCount;
-    for (std::size_t index = 0; index < account.characterCount; ++index) {
-        if (account.characters[index].selected) {
-            characterIndex = index;
-            break;
-        }
-    }
-    if (characterIndex == account.characterCount) {
-        report_acquisition("prepare", "fail", "selection", definitionHash, 0, 0, 0, 0, 0, 0);
-        return false;
-    }
-
-    const CharacterState& before = account.characters[characterIndex];
-    if (before.inventory.count >= before.inventory.values.size()
-        || before.nextInventorySerial
-               >= static_cast<std::uint32_t>((std::numeric_limits<std::int32_t>::max)())) {
-        report_acquisition("prepare",
-                           "fail",
-                           "state_capacity",
-                           definitionHash,
-                           before.soid,
-                           0,
-                           before.inventory.count,
-                           0,
-                           0,
-                           before.nextInventorySerial);
-        return false;
-    }
-
-    std::uint64_t instanceSoid = 0;
-    if (!next_item_instance_soid(account, instanceSoid)) {
-        report_acquisition("prepare",
-                           "fail",
-                           "soid",
-                           definitionHash,
-                           before.soid,
-                           0,
-                           before.inventory.count,
-                           0,
-                           0,
-                           before.nextInventorySerial);
-        return false;
-    }
-
-    CharacterState after = before;
-    const std::size_t inventoryIndex = after.inventory.count;
-    authored_inventory::Item acquired{};
-    acquired.instanceSoid = instanceSoid;
-    acquired.definitionHash = definitionHash;
-    acquired.level = acquisition_level(before);
-    acquired.quantity = 1;
-    acquired.mutationSerial = static_cast<std::int32_t>(after.nextInventorySerial++);
-    acquired.sockets.policy = authored_inventory::SocketPolicy::nativeDefaults;
-    after.inventory.values[inventoryIndex] = acquired;
-    ++after.inventory.count;
-
-    AccountState candidate = chargedAccount;
-    candidate.characters[characterIndex] = after;
-    family4_loadout::ResolvedLoadout resolved{};
-    std::uint16_t inventoryRow = 0;
-    std::uint8_t equipmentSlot = 0;
-    if (!account::valid(candidate) || identity_uses_soid(candidate, instanceSoid)
-        || !family4_loadout::resolve(candidate, characterIndex, resolved)
-        || !find_acquired_row(resolved, instanceSoid, inventoryRow, equipmentSlot)) {
-        report_acquisition("prepare",
-                           "fail",
-                           "resolve_or_bucket_full",
-                           definitionHash,
-                           before.soid,
-                           instanceSoid,
-                           inventoryIndex,
-                           0,
-                           0,
-                           after.nextInventorySerial);
-        return false;
-    }
-
-    mutation.beforeCharacter = before;
-    mutation.afterCharacter = after;
-    mutation.beforeProfileItems = account.profileItems;
-    mutation.afterProfileItems = chargedAccount.profileItems;
-    mutation.accountSoid = account.primarySoid;
-    mutation.characterSoid = before.soid;
-    mutation.acquiredInstanceSoid = instanceSoid;
-    mutation.acquiredDefinitionHash = definitionHash;
-    mutation.materialRequirementSetHash = collectible.materialRequirementSetHash;
-    mutation.expectedNextInventorySerial = before.nextInventorySerial;
-    mutation.characterIndex = characterIndex;
-    mutation.expectedInventoryCount = before.inventory.count;
-    mutation.expectedProfileItemCount = account.profileItemCount;
-    mutation.afterProfileItemCount = chargedAccount.profileItemCount;
-    mutation.inventoryIndex = inventoryIndex;
-    mutation.collectibleIndex = collectibleIndex;
-    mutation.inventoryRow = inventoryRow;
-    mutation.equipmentSlot = equipmentSlot;
-    mutation.materialRequirementCount = collectible.materialRequirementCount;
-    mutation.profileChanged = profileChanged;
-    mutation.prepared = true;
-    report_acquisition("prepare",
-                       "ok",
-                       "ready",
-                       definitionHash,
-                       before.soid,
-                       instanceSoid,
-                       inventoryIndex,
-                       inventoryRow,
-                       equipmentSlot,
-                       after.nextInventorySerial);
-    return true;
-}
-
-/** Produces the full account after-image while a prepared character pull remains current. */
-bool preview_item_acquisition(const PendingItemAcquisition& mutation,
-                              AccountState& after) noexcept {
-    after = {};
-    if (!mutation.prepared || mutation.accountSoid == 0 || mutation.characterSoid == 0
-        || mutation.acquiredInstanceSoid == 0 || mutation.characterIndex >= kCharacterCapacity
-        || mutation.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
-        || mutation.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
-        return false;
-    }
-    const AccountState current = account_snapshot();
-    if (mutation.characterIndex >= current.characterCount
-        || current.primarySoid != mutation.accountSoid
-        || !same_character(current.characters[mutation.characterIndex], mutation.beforeCharacter)
-        || !same_profile_inventory(
-            current, mutation.beforeProfileItems, mutation.expectedProfileItemCount)) {
-        return false;
-    }
-    after = current;
-    after.profileItems = mutation.afterProfileItems;
-    after.profileItemCount = mutation.afterProfileItemCount;
-    after.characters[mutation.characterIndex] = mutation.afterCharacter;
-    family4_loadout::ResolvedLoadout resolved{};
-    std::uint16_t checkedRow = 0;
-    std::uint8_t checkedSlot = 0;
-    return account::valid(after) && valid_profile_inventory(after)
-           && family4_loadout::resolve(after, mutation.characterIndex, resolved)
-           && find_acquired_row(resolved, mutation.acquiredInstanceSoid, checkedRow, checkedSlot)
-           && checkedRow == mutation.inventoryRow && checkedSlot == mutation.equipmentSlot;
-}
-
-/** Commits one prepared insertion only while its prepare-time loadout remains current. */
-bool commit_item_acquisition(PendingItemAcquisition& mutation) noexcept {
-    const PendingItemAcquisition prepared = mutation;
-    mutation = {};
-    const auto fail = [&prepared](std::string_view reason) noexcept {
-        report_acquisition("commit",
-                           "fail",
-                           reason,
-                           prepared.acquiredDefinitionHash,
-                           prepared.characterSoid,
-                           prepared.acquiredInstanceSoid,
-                           prepared.inventoryIndex,
-                           prepared.inventoryRow,
-                           prepared.equipmentSlot,
-                           prepared.afterCharacter.nextInventorySerial);
-        return false;
-    };
-    if (!prepared.prepared || prepared.characterSoid == 0 || prepared.acquiredInstanceSoid == 0
-        || prepared.accountSoid == 0
-        || prepared.acquiredDefinitionHash == authored_inventory::kNoDefinitionHash
-        || prepared.characterIndex >= kCharacterCapacity
-        || prepared.expectedInventoryCount >= authored_inventory::kCharacterItemCapacity
-        || prepared.inventoryIndex != prepared.expectedInventoryCount
-        || prepared.afterCharacter.inventory.count != prepared.expectedInventoryCount + 1U
-        || prepared.inventoryIndex >= prepared.afterCharacter.inventory.count
-        || prepared.afterCharacter.inventory.values[prepared.inventoryIndex].instanceSoid
-               != prepared.acquiredInstanceSoid
-        || prepared.afterCharacter.inventory.values[prepared.inventoryIndex].definitionHash
-               != prepared.acquiredDefinitionHash
-        || prepared.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
-        || prepared.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
-        return fail("mutation");
-    }
-
-    build_data::collectibles::Definition collectible{};
-    if (!build_data::find_collectible_definition(prepared.collectibleIndex, collectible)
-        || collectible.itemDefinitionIndex
-               == build_data::collectibles::kUnavailableItemDefinitionIndex
-        || collectible.materialRequirementSetHash != prepared.materialRequirementSetHash
-        || collectible.materialRequirementCount != prepared.materialRequirementCount) {
-        return fail("collectible");
-    }
-
-    report_acquisition("commit_begin",
-                       "ok",
-                       "ready",
-                       prepared.acquiredDefinitionHash,
-                       prepared.characterSoid,
-                       prepared.acquiredInstanceSoid,
-                       prepared.inventoryIndex,
-                       prepared.inventoryRow,
-                       prepared.equipmentSlot,
-                       prepared.afterCharacter.nextInventorySerial);
-
-    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
-    AccountState candidate = runtime::storage::g_state.account;
-    if (prepared.characterIndex >= candidate.characterCount
-        || candidate.primarySoid != prepared.accountSoid
-        || !same_profile_inventory(
-            candidate, prepared.beforeProfileItems, prepared.expectedProfileItemCount)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("account");
-    }
-    CharacterState& character = candidate.characters[prepared.characterIndex];
-    if (!character.selected || character.soid != prepared.characterSoid
-        || !same_character(character, prepared.beforeCharacter)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("stale");
-    }
-
-    candidate.profileItems = prepared.afterProfileItems;
-    candidate.profileItemCount = prepared.afterProfileItemCount;
-    character = prepared.afterCharacter;
-    family4_loadout::ResolvedLoadout resolved{};
-    std::uint16_t checkedRow = 0;
-    std::uint8_t checkedSlot = 0;
-    if (!account::valid(candidate) || !valid_profile_inventory(candidate)
-        || identity_uses_soid(candidate, prepared.acquiredInstanceSoid)
-        || !family4_loadout::resolve(candidate, prepared.characterIndex, resolved)
-        || !find_acquired_row(resolved, prepared.acquiredInstanceSoid, checkedRow, checkedSlot)
-        || checkedRow != prepared.inventoryRow || checkedSlot != prepared.equipmentSlot) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("resolve");
-    }
-    runtime::storage::g_state.account = candidate;
-    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-    report_acquisition("commit_end",
-                       "ok",
-                       "published",
-                       prepared.acquiredDefinitionHash,
-                       prepared.characterSoid,
-                       prepared.acquiredInstanceSoid,
-                       prepared.inventoryIndex,
-                       prepared.inventoryRow,
-                       prepared.equipmentSlot,
-                       prepared.afterCharacter.nextInventorySerial);
-    return true;
-}
-
-/** Prepares one checked profile-stack increment or append for a Collections pull. */
-bool prepare_profile_item_acquisition(std::uint16_t collectibleIndex,
-                                      std::uint32_t definitionHash,
-                                      PendingProfileItemAcquisition& mutation) noexcept {
-    mutation = {};
-    const AccountState account = account_snapshot();
-    build_data::collectibles::Definition collectible{};
-    build_data::items::Definition item{};
-    item_details::Definition detail{};
-    inventory_buckets::Descriptor bucket{};
-    if (definitionHash == authored_inventory::kNoDefinitionHash || !account::valid(account)
-        || !valid_profile_inventory(account)
-        || !build_data::find_collectible_definition(collectibleIndex, collectible)
-        || collectible.itemDefinitionIndex
-               == build_data::collectibles::kUnavailableItemDefinitionIndex
-        || !build_data::find_item_definition_hash(definitionHash, item)
-        || item.definitionHash != definitionHash
-        || item.definitionIndex != collectible.itemDefinitionIndex
-        || !build_data::find_configured_item_detail(item.definitionIndex, detail)
-        || detail.definitionIndex != item.definitionIndex || detail.definitionHash != definitionHash
-        || detail.bucketId != item.bucketId
-        || detail.instancedDefinitionState != item_details::InstancedDefinitionState::stackable
-        || detail.maxStackSize <= 0
-        || !build_data::find_inventory_bucket_descriptor(detail.bucketId, bucket)
-        || bucket.arraySelector != inventory_buckets::ArraySelector::profile) {
-        report_profile_acquisition("prepare",
-                                   "fail",
-                                   "definition_or_profile",
-                                   definitionHash,
-                                   account.primarySoid,
-                                   0,
-                                   0,
-                                   0,
-                                   account.profileItemCount,
-                                   0,
-                                   0,
-                                   false);
-        return false;
-    }
-    AccountState chargedAccount{};
-    bool materialsChanged = false;
-    if (!apply_collection_materials(account, collectible, chargedAccount, materialsChanged)) {
-        report_profile_acquisition("prepare",
-                                   "fail",
-                                   "materials",
-                                   definitionHash,
-                                   account.primarySoid,
-                                   0,
-                                   detail.bucketId,
-                                   0,
-                                   account.profileItemCount,
-                                   0,
-                                   0,
-                                   false);
-        return false;
-    }
-    (void)materialsChanged;
-    const bool actionSource =
-        build_data::is_profile_action_source(item.definitionIndex, item.bucketId);
-
-    std::size_t profileIndex = chargedAccount.profileItemCount;
-    std::int32_t previousQuantity = 0;
-    std::int32_t previousMutationSerial = 0;
-    std::int32_t greatestMutationSerial = 0;
-    bool appended = true;
-    for (std::size_t index = 0; index < chargedAccount.profileItemCount; ++index) {
-        greatestMutationSerial =
-            (std::max)(greatestMutationSerial, chargedAccount.profileItems[index].mutationSerial);
-    }
-    for (std::size_t index = 0; index < chargedAccount.profileItemCount; ++index) {
-        const authored_inventory::ProfileItem& existing = chargedAccount.profileItems[index];
-        if (existing.definitionHash != definitionHash) {
-            continue;
-        }
-        if (existing.quantity > detail.maxStackSize) {
-            report_profile_acquisition("prepare",
-                                       "fail",
-                                       "existing_quantity",
-                                       definitionHash,
-                                       account.primarySoid,
-                                       existing.instanceSoid,
-                                       detail.bucketId,
-                                       index,
-                                       chargedAccount.profileItemCount,
-                                       existing.quantity,
-                                       existing.quantity,
-                                       false);
-            return false;
-        }
-        if (appended && existing.quantity < detail.maxStackSize) {
-            profileIndex = index;
-            previousQuantity = existing.quantity;
-            previousMutationSerial = existing.mutationSerial;
-            appended = false;
-        }
-    }
-    if (greatestMutationSerial == (std::numeric_limits<std::int32_t>::max)()
-        || (appended && chargedAccount.profileItemCount >= chargedAccount.profileItems.size())) {
-        report_profile_acquisition(
-            "prepare",
-            "fail",
-            "state_capacity",
-            definitionHash,
-            account.primarySoid,
-            appended ? 0 : chargedAccount.profileItems[profileIndex].instanceSoid,
-            detail.bucketId,
-            profileIndex,
-            chargedAccount.profileItemCount,
-            0,
-            0,
-            true);
-        return false;
-    }
-
-    std::uint64_t acquiredInstanceSoid =
-        appended ? 0 : chargedAccount.profileItems[profileIndex].instanceSoid;
-    if (actionSource != (acquiredInstanceSoid != 0) && !appended) {
-        report_profile_acquisition("prepare",
-                                   "fail",
-                                   "identity_policy",
-                                   definitionHash,
-                                   account.primarySoid,
-                                   acquiredInstanceSoid,
-                                   detail.bucketId,
-                                   profileIndex,
-                                   chargedAccount.profileItemCount,
-                                   previousQuantity,
-                                   previousQuantity,
-                                   false);
-        return false;
-    }
-    if (appended && actionSource
-        && !next_profile_item_instance_soid(chargedAccount, acquiredInstanceSoid)) {
-        report_profile_acquisition("prepare",
-                                   "fail",
-                                   "identity_capacity",
-                                   definitionHash,
-                                   account.primarySoid,
-                                   acquiredInstanceSoid,
-                                   detail.bucketId,
-                                   profileIndex,
-                                   chargedAccount.profileItemCount,
-                                   0,
-                                   0,
-                                   true);
-        return false;
-    }
-
-    AccountState after = chargedAccount;
-    const std::int32_t acquiredMutationSerial = greatestMutationSerial + 1;
-    if (appended) {
-        after.profileItems[profileIndex] = {
-            acquiredInstanceSoid, definitionHash, 1, acquiredMutationSerial};
-        ++after.profileItemCount;
-    } else {
-        ++after.profileItems[profileIndex].quantity;
-        after.profileItems[profileIndex].mutationSerial = acquiredMutationSerial;
-    }
-    const std::int32_t acquiredQuantity = after.profileItems[profileIndex].quantity;
-    if (after.profileItems[profileIndex].instanceSoid != acquiredInstanceSoid
-        || acquiredQuantity <= previousQuantity || acquiredQuantity > detail.maxStackSize
-        || !account::valid(after) || !valid_profile_inventory(after)) {
-        report_profile_acquisition("prepare",
-                                   "fail",
-                                   "bucket_full_or_quantity",
-                                   definitionHash,
-                                   account.primarySoid,
-                                   acquiredInstanceSoid,
-                                   detail.bucketId,
-                                   profileIndex,
-                                   after.profileItemCount,
-                                   previousQuantity,
-                                   acquiredQuantity,
-                                   appended);
-        return false;
-    }
-
-    mutation.beforeItems = account.profileItems;
-    mutation.afterItems = after.profileItems;
-    mutation.accountSoid = account.primarySoid;
-    mutation.acquiredInstanceSoid = acquiredInstanceSoid;
-    mutation.acquiredDefinitionHash = definitionHash;
-    mutation.materialRequirementSetHash = collectible.materialRequirementSetHash;
-    mutation.expectedItemCount = account.profileItemCount;
-    mutation.afterItemCount = after.profileItemCount;
-    mutation.profileIndex = profileIndex;
-    mutation.previousQuantity = previousQuantity;
-    mutation.acquiredQuantity = acquiredQuantity;
-    mutation.previousMutationSerial = previousMutationSerial;
-    mutation.acquiredMutationSerial = acquiredMutationSerial;
-    mutation.collectibleIndex = collectibleIndex;
-    mutation.bucketId = detail.bucketId;
-    mutation.materialRequirementCount = collectible.materialRequirementCount;
-    mutation.actionSource = actionSource;
-    mutation.appended = appended;
-    mutation.prepared = true;
-    if (!valid_profile_mutation_shape(mutation)) {
-        mutation = {};
-        report_profile_acquisition("prepare",
-                                   "fail",
-                                   "mutation",
-                                   definitionHash,
-                                   account.primarySoid,
-                                   acquiredInstanceSoid,
-                                   detail.bucketId,
-                                   profileIndex,
-                                   after.profileItemCount,
-                                   previousQuantity,
-                                   acquiredQuantity,
-                                   appended);
-        return false;
-    }
-    report_profile_acquisition("prepare",
-                               "ok",
-                               "ready",
-                               definitionHash,
-                               account.primarySoid,
-                               acquiredInstanceSoid,
-                               detail.bucketId,
-                               profileIndex,
-                               after.profileItemCount,
-                               previousQuantity,
-                               acquiredQuantity,
-                               appended);
-    return true;
-}
-
-/** Produces the exact account after-image while the captured profile view is still current. */
-bool preview_profile_item_acquisition(const PendingProfileItemAcquisition& mutation,
-                                      AccountState& after) noexcept {
-    after = {};
-    const AccountState current = account_snapshot();
-    const bool ready = materialize_profile_acquisition(current, mutation, after);
-    report_profile_acquisition("preview",
-                               ready ? "ok" : "fail",
-                               ready ? "ready" : "stale_or_invalid",
-                               mutation.acquiredDefinitionHash,
-                               mutation.accountSoid,
-                               mutation.acquiredInstanceSoid,
-                               mutation.bucketId,
-                               mutation.profileIndex,
-                               mutation.afterItemCount,
-                               mutation.previousQuantity,
-                               mutation.acquiredQuantity,
-                               mutation.appended);
-    return ready;
-}
-
-/** Commits one profile-stack after-image only while its exact prepare-time view remains current. */
-bool commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcept {
-    const PendingProfileItemAcquisition prepared = mutation;
-    mutation = {};
-    if (!valid_profile_mutation_shape(prepared)) {
-        report_profile_acquisition("commit",
-                                   "fail",
-                                   "mutation",
-                                   prepared.acquiredDefinitionHash,
-                                   prepared.accountSoid,
-                                   prepared.acquiredInstanceSoid,
-                                   prepared.bucketId,
-                                   prepared.profileIndex,
-                                   prepared.afterItemCount,
-                                   prepared.previousQuantity,
-                                   prepared.acquiredQuantity,
-                                   prepared.appended);
-        return false;
-    }
-
-    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
-    AccountState candidate{};
-    const bool ready =
-        materialize_profile_acquisition(runtime::storage::g_state.account, prepared, candidate);
-    if (ready) {
-        runtime::storage::g_state.account = candidate;
-    }
-    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-    report_profile_acquisition("commit",
-                               ready ? "ok" : "fail",
-                               ready ? "published" : "stale_or_invalid",
-                               prepared.acquiredDefinitionHash,
-                               prepared.accountSoid,
-                               prepared.acquiredInstanceSoid,
-                               prepared.bucketId,
-                               prepared.profileIndex,
-                               prepared.afterItemCount,
-                               prepared.previousQuantity,
-                               prepared.acquiredQuantity,
-                               prepared.appended);
-    return ready;
-}
-
-/** Prepares one selected-character unequipped item removal without changing account State. */
-bool prepare_item_dismantle(std::uint64_t instanceSoid, PendingItemDismantle& mutation) noexcept {
-    mutation = {};
-    const AccountState account = account_snapshot();
-    if (instanceSoid == 0 || !account::valid(account)) {
-        report_dismantle("prepare", "fail", "input", 0, 0, instanceSoid, 0, 0, 0, 0, 0);
-        return false;
-    }
-
-    std::size_t characterIndex = account.characterCount;
-    for (std::size_t index = 0; index < account.characterCount; ++index) {
-        if (account.characters[index].selected) {
-            characterIndex = index;
-            break;
-        }
-    }
-    if (characterIndex >= account.characterCount
-        || !stage_item_dismantle(account, characterIndex, instanceSoid, mutation)) {
-        report_dismantle(
-            "prepare", "fail", "ownership_or_resolve", 0, 0, instanceSoid, 0, 0, 0, 0, 0);
-        return false;
-    }
-
-    report_dismantle("prepare",
-                     "ok",
-                     "ready",
-                     mutation.dismantledItem.definitionHash,
-                     mutation.characterSoid,
-                     mutation.dismantledInstanceSoid,
-                     mutation.inventoryIndex,
-                     mutation.inventoryRow,
-                     mutation.equipmentSlot,
-                     mutation.movedInventoryItemCount,
-                     mutation.afterCharacter.nextInventorySerial);
-    return true;
-}
-
-/** Produces the exact character-and-profile after-image while the captured views remain current. */
-bool preview_item_dismantle(const PendingItemDismantle& mutation,
-                            AccountState& after) noexcept {
-    const AccountState current = account_snapshot();
-    const bool ready = materialize_item_dismantle(current, mutation, after);
-    report_dismantle("preview",
-                     ready ? "ok" : "fail",
-                     ready ? "ready" : "stale_or_invalid",
-                     mutation.dismantledItem.definitionHash,
-                     mutation.characterSoid,
-                     mutation.dismantledInstanceSoid,
-                     mutation.inventoryIndex,
-                     mutation.inventoryRow,
-                     mutation.equipmentSlot,
-                     mutation.movedInventoryItemCount,
-                     mutation.afterCharacter.nextInventorySerial);
-    return ready;
-}
-
-/** Commits one prepared dismantle only while its complete account views are unchanged. */
-bool commit_item_dismantle(PendingItemDismantle& mutation) noexcept {
-    const PendingItemDismantle prepared = mutation;
-    mutation = {};
-    const auto fail = [&prepared](std::string_view reason) noexcept {
-        report_dismantle("commit",
-                         "fail",
-                         reason,
-                         prepared.dismantledItem.definitionHash,
-                         prepared.characterSoid,
-                         prepared.dismantledInstanceSoid,
-                         prepared.inventoryIndex,
-                         prepared.inventoryRow,
-                         prepared.equipmentSlot,
-                         prepared.movedInventoryItemCount,
-                         prepared.afterCharacter.nextInventorySerial);
-        return false;
-    };
-
-    report_dismantle("commit_begin",
-                     "ok",
-                     "ready",
-                     prepared.dismantledItem.definitionHash,
-                     prepared.characterSoid,
-                     prepared.dismantledInstanceSoid,
-                     prepared.inventoryIndex,
-                     prepared.inventoryRow,
-                     prepared.equipmentSlot,
-                     prepared.movedInventoryItemCount,
-                     prepared.afterCharacter.nextInventorySerial);
-
-    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
-    AccountState candidate{};
-    const bool ready =
-        materialize_item_dismantle(runtime::storage::g_state.account, prepared, candidate);
-    if (ready) {
-        runtime::storage::g_state.account = candidate;
-    }
-    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-
-    if (!ready) {
-        return fail("stale_or_invalid");
-    }
-
-    report_dismantle("commit_end",
-                     "ok",
-                     "published",
-                     prepared.dismantledItem.definitionHash,
-                     prepared.characterSoid,
-                     prepared.dismantledInstanceSoid,
-                     prepared.inventoryIndex,
-                     prepared.inventoryRow,
-                     prepared.equipmentSlot,
-                     prepared.movedInventoryItemCount,
-                     prepared.afterCharacter.nextInventorySerial);
-    return true;
-}
-
-/** Prepares one checked ordinary-socket selection without publishing account State. */
-bool prepare_socket_plug(std::uint64_t targetInstanceSoid,
-                         std::uint8_t socketLane,
-                         std::uint16_t plugDefinitionIndex,
-                         PendingSocketPlug& mutation) noexcept {
-    mutation = {};
-    const AccountState snapshot = account_snapshot();
-    std::size_t characterIndex = snapshot.characterCount;
-    if (targetInstanceSoid != 0 && account::valid(snapshot)) {
-        for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
-            if (snapshot.characters[index].selected) {
-                characterIndex = index;
-                break;
-            }
-        }
-    }
-    if (characterIndex >= snapshot.characterCount
-        || !stage_socket_plug(snapshot,
-                              characterIndex,
-                              targetInstanceSoid,
-                              socketLane,
-                              plugDefinitionIndex,
-                              mutation)) {
-        report_socket_plug("prepare",
-                           "fail",
-                           "ownership_definition_or_compatibility",
-                           0,
-                           targetInstanceSoid,
-                           0,
-                           socketLane,
-                           plugDefinitionIndex,
-                           0,
-                           0,
-                           false,
-                           0);
-        return false;
-    }
-    report_socket_plug("prepare",
-                       "ok",
-                       "ready",
-                       mutation.characterSoid,
-                       mutation.targetInstanceSoid,
-                       mutation.targetDefinitionIndex,
-                       mutation.socketLane,
-                       mutation.plugDefinitionIndex,
-                       mutation.targetBucketId,
-                       mutation.plugBucketId,
-                       mutation.targetEquipped,
-                       mutation.itemIndex);
-    return true;
-}
-
-/** Prepares a socket transition for the exact instance identity carried by opcode 1901. */
-bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
-                                            std::uint8_t requestedSocketLane,
-                                            std::uint16_t plugDefinitionIndex,
-                                            PendingSocketPlug& mutation) noexcept {
-    mutation = {};
-    const AccountState snapshot = account_snapshot();
-    std::size_t characterIndex = snapshot.characterCount;
-    if (account::valid(snapshot)) {
-        for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
-            if (snapshot.characters[index].selected) {
-                characterIndex = index;
-                break;
-            }
-        }
-    }
-
-    constexpr std::uint64_t kSelectorStride = 4;
-    constexpr std::uint64_t kInstanceIdentityMask = 0x3FFFFFFFFFFFFFFFULL;
-    if (characterIndex >= snapshot.characterCount || itemSelector == 0
-        || itemSelector % kSelectorStride != 0
-        || itemSelector / kSelectorStride > kInstanceIdentityMask
-        || requestedSocketLane >= authored_inventory::kPlugCapacity) {
-        report_socket_plug("prepare_location",
-                           "fail",
-                           "selection_or_slot",
-                           0,
-                           0,
-                           0,
-                           requestedSocketLane,
-                           plugDefinitionIndex,
-                           0,
-                           0,
-                           false,
-                           static_cast<std::size_t>(itemSelector / kSelectorStride));
-        return false;
-    }
-
-    family4_loadout::ResolvedLoadout resolvedLoadout{};
-    if (!family4_loadout::resolve(snapshot, characterIndex, resolvedLoadout)) {
-        return false;
-    }
-    const std::uint64_t identityToken = itemSelector / kSelectorStride;
-    const family4_loadout::ResolvedItem* resolvedTarget = nullptr;
-    for (std::size_t index = 0; index < resolvedLoadout.itemCount; ++index) {
-        const family4_loadout::ResolvedItem& item = resolvedLoadout.items[index];
-        if ((item.instance.instanceSoid & kInstanceIdentityMask) != identityToken) {
-            continue;
-        }
-        if (resolvedTarget != nullptr) {
-            return false;
-        }
-        resolvedTarget = &item;
-    }
-    const bool expectedEquipped = resolvedTarget != nullptr && resolvedTarget->equipped;
-
-    std::uint8_t resolvedSocketLane = static_cast<std::uint8_t>(authored_inventory::kPlugCapacity);
-    build_data::items::Definition targetDefinition{};
-    item_details::Definition targetDetail{};
-    bool ambiguousSocketLane = false;
-    if (resolvedTarget != nullptr
-        && build_data::find_item_definition_index(resolvedTarget->instance.baseDefinitionIndex,
-                                                  targetDefinition)
-        && targetDefinition.definitionIndex == resolvedTarget->instance.baseDefinitionIndex
-        && build_data::find_configured_item_detail(targetDefinition.definitionIndex, targetDetail)
-        && targetDetail.definitionIndex == targetDefinition.definitionIndex
-        && targetDetail.definitionHash == targetDefinition.definitionHash
-        && targetDetail.bucketId == targetDefinition.bucketId
-        && 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.
-        if (requestedSocketLane < targetDetail.ordinarySocketCount
-            && build_data::is_socket_plug_allowed(
-                targetDefinition.definitionIndex, requestedSocketLane, plugDefinitionIndex)) {
-            resolvedSocketLane = requestedSocketLane;
-        } else {
-            for (std::size_t lane = 0; lane < targetDetail.ordinarySocketCount; ++lane) {
-                if (!build_data::is_socket_plug_allowed(targetDefinition.definitionIndex,
-                                                        static_cast<std::uint8_t>(lane),
-                                                        plugDefinitionIndex)) {
-                    continue;
-                }
-                if (resolvedSocketLane < authored_inventory::kPlugCapacity) {
-                    ambiguousSocketLane = true;
-                    break;
-                }
-                resolvedSocketLane = static_cast<std::uint8_t>(lane);
-            }
-        }
-    }
-    if (resolvedTarget == nullptr || resolvedTarget->instance.instanceSoid == 0
-        || ambiguousSocketLane || resolvedSocketLane >= authored_inventory::kPlugCapacity
-        || !stage_socket_plug(snapshot,
-                              characterIndex,
-                              resolvedTarget->instance.instanceSoid,
-                              resolvedSocketLane,
-                              plugDefinitionIndex,
-                              mutation)
-        || mutation.targetEquipped != expectedEquipped) {
-        report_socket_plug("prepare_location",
-                           "fail",
-                           "equipment_or_compatibility",
-                           snapshot.characters[characterIndex].soid,
-                           resolvedTarget != nullptr ? resolvedTarget->instance.instanceSoid : 0,
-                           0,
-                           resolvedSocketLane,
-                           plugDefinitionIndex,
-                           0,
-                           0,
-                           expectedEquipped,
-                           static_cast<std::size_t>(identityToken));
-        mutation = {};
-        return false;
-    }
-
-    report_socket_plug("prepare_location",
-                       "ok",
-                       "ready",
-                       mutation.characterSoid,
-                       mutation.targetInstanceSoid,
-                       mutation.targetDefinitionIndex,
-                       mutation.socketLane,
-                       mutation.plugDefinitionIndex,
-                       mutation.targetBucketId,
-                       mutation.plugBucketId,
-                       mutation.targetEquipped,
-                       mutation.itemIndex);
-    return true;
-}
-
-/** Produces the complete account after-image while the prepared socket action remains current. */
-bool preview_socket_plug(const PendingSocketPlug& mutation, AccountState& after) noexcept {
-    after = {};
-    if (!mutation.prepared || mutation.accountSoid == 0 || mutation.characterSoid == 0
-        || mutation.targetInstanceSoid == 0 || mutation.characterIndex >= kCharacterCapacity
-        || mutation.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
-        || mutation.afterProfileItemCount > authored_inventory::kProfileItemCapacity) {
-        return false;
-    }
-    const AccountState current = account_snapshot();
-    if (mutation.characterIndex >= current.characterCount
-        || current.primarySoid != mutation.accountSoid
-        || !same_character(current.characters[mutation.characterIndex], mutation.beforeCharacter)
-        || !same_profile_inventory(
-            current, mutation.beforeProfileItems, mutation.expectedProfileItemCount)) {
-        return false;
-    }
-
-    PendingSocketPlug canonical{};
-    if (!stage_socket_plug(current,
-                           mutation.characterIndex,
-                           mutation.targetInstanceSoid,
-                           mutation.socketLane,
-                           mutation.plugDefinitionIndex,
-                           canonical)
-        || canonical.accountSoid != mutation.accountSoid
-        || canonical.characterSoid != mutation.characterSoid
-        || canonical.targetDefinitionHash != mutation.targetDefinitionHash
-        || canonical.plugDefinitionHash != mutation.plugDefinitionHash
-        || canonical.materialRequirementSetHash != mutation.materialRequirementSetHash
-        || canonical.characterIndex != mutation.characterIndex
-        || canonical.expectedProfileItemCount != mutation.expectedProfileItemCount
-        || canonical.afterProfileItemCount != mutation.afterProfileItemCount
-        || canonical.itemIndex != mutation.itemIndex
-        || canonical.targetDefinitionIndex != mutation.targetDefinitionIndex
-        || canonical.plugDefinitionIndex != mutation.plugDefinitionIndex
-        || canonical.materialRequirementSetIndex != mutation.materialRequirementSetIndex
-        || canonical.socketLane != mutation.socketLane
-        || canonical.targetBucketId != mutation.targetBucketId
-        || canonical.plugBucketId != mutation.plugBucketId
-        || canonical.materialRequirementCount != mutation.materialRequirementCount
-        || canonical.profileChanged != mutation.profileChanged
-        || canonical.targetEquipped != mutation.targetEquipped
-        || !same_character(canonical.beforeCharacter, mutation.beforeCharacter)
-        || !same_character(canonical.afterCharacter, mutation.afterCharacter)) {
-        return false;
-    }
-
-    after = current;
-    after.profileItems = canonical.afterProfileItems;
-    after.profileItemCount = canonical.afterProfileItemCount;
-    after.characters[mutation.characterIndex] = canonical.afterCharacter;
-    const bool balancesChanged = !same_profile_inventory(
-        after, mutation.beforeProfileItems, mutation.expectedProfileItemCount);
-    family4_loadout::ResolvedLoadout resolved{};
-    return balancesChanged == mutation.profileChanged
-           && same_profile_inventory(
-               after, mutation.afterProfileItems, mutation.afterProfileItemCount)
-           && account::valid(after) && valid_profile_inventory(after)
-           && family4_loadout::resolve(after, mutation.characterIndex, resolved);
-}
-
-/** Commits one prepared socket-and-material after-image behind exact staleness guards. */
-bool commit_socket_plug(PendingSocketPlug& mutation) noexcept {
-    const PendingSocketPlug prepared = mutation;
-    mutation = {};
-    const auto fail = [&prepared](std::string_view reason) noexcept {
-        report_socket_plug("commit",
-                           "fail",
-                           reason,
-                           prepared.characterSoid,
-                           prepared.targetInstanceSoid,
-                           prepared.targetDefinitionIndex,
-                           prepared.socketLane,
-                           prepared.plugDefinitionIndex,
-                           prepared.targetBucketId,
-                           prepared.plugBucketId,
-                           prepared.targetEquipped,
-                           prepared.itemIndex);
-        return false;
-    };
-    const std::size_t itemCapacity = prepared.targetEquipped
-                                         ? authored_inventory::kEquipmentSlotCount
-                                         : authored_inventory::kCharacterItemCapacity;
-    if (!prepared.prepared || prepared.accountSoid == 0 || prepared.characterSoid == 0
-        || prepared.targetInstanceSoid == 0
-        || prepared.targetDefinitionHash == authored_inventory::kNoDefinitionHash
-        || prepared.plugDefinitionHash == authored_inventory::kNoDefinitionHash
-        || prepared.characterIndex >= kCharacterCapacity || prepared.itemIndex >= itemCapacity
-        || prepared.expectedProfileItemCount > authored_inventory::kProfileItemCapacity
-        || prepared.afterProfileItemCount > authored_inventory::kProfileItemCapacity
-        || prepared.socketLane >= authored_inventory::kPlugCapacity
-        || prepared.beforeCharacter.soid != prepared.characterSoid
-        || prepared.afterCharacter.soid != prepared.characterSoid) {
-        return fail("mutation");
-    }
-
-    report_socket_plug("commit_begin",
-                       "ok",
-                       "ready",
-                       prepared.characterSoid,
-                       prepared.targetInstanceSoid,
-                       prepared.targetDefinitionIndex,
-                       prepared.socketLane,
-                       prepared.plugDefinitionIndex,
-                       prepared.targetBucketId,
-                       prepared.plugBucketId,
-                       prepared.targetEquipped,
-                       prepared.itemIndex);
-
-    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
-    AccountState candidate = runtime::storage::g_state.account;
-    if (prepared.characterIndex >= candidate.characterCount
-        || candidate.primarySoid != prepared.accountSoid
-        || !same_profile_inventory(
-            candidate, prepared.beforeProfileItems, prepared.expectedProfileItemCount)
-        || !same_character(candidate.characters[prepared.characterIndex],
-                           prepared.beforeCharacter)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("stale");
-    }
-
-    PendingSocketPlug canonical{};
-    if (!stage_socket_plug(candidate,
-                           prepared.characterIndex,
-                           prepared.targetInstanceSoid,
-                           prepared.socketLane,
-                           prepared.plugDefinitionIndex,
-                           canonical)
-        || canonical.characterSoid != prepared.characterSoid
-        || canonical.accountSoid != prepared.accountSoid
-        || canonical.targetDefinitionHash != prepared.targetDefinitionHash
-        || canonical.plugDefinitionHash != prepared.plugDefinitionHash
-        || canonical.materialRequirementSetHash != prepared.materialRequirementSetHash
-        || canonical.characterIndex != prepared.characterIndex
-        || canonical.expectedProfileItemCount != prepared.expectedProfileItemCount
-        || canonical.afterProfileItemCount != prepared.afterProfileItemCount
-        || canonical.itemIndex != prepared.itemIndex
-        || canonical.targetDefinitionIndex != prepared.targetDefinitionIndex
-        || canonical.plugDefinitionIndex != prepared.plugDefinitionIndex
-        || canonical.materialRequirementSetIndex != prepared.materialRequirementSetIndex
-        || canonical.socketLane != prepared.socketLane
-        || canonical.targetBucketId != prepared.targetBucketId
-        || canonical.plugBucketId != prepared.plugBucketId
-        || canonical.materialRequirementCount != prepared.materialRequirementCount
-        || canonical.profileChanged != prepared.profileChanged
-        || canonical.targetEquipped != prepared.targetEquipped
-        || !same_character(canonical.beforeCharacter, prepared.beforeCharacter)
-        || !same_character(canonical.afterCharacter, prepared.afterCharacter)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("transition");
-    }
-
-    candidate.profileItems = canonical.afterProfileItems;
-    candidate.profileItemCount = canonical.afterProfileItemCount;
-    if (!same_profile_inventory(
-            candidate, prepared.afterProfileItems, prepared.afterProfileItemCount)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("materials");
-    }
-    candidate.characters[prepared.characterIndex] = canonical.afterCharacter;
-    family4_loadout::ResolvedLoadout checked{};
-    if (!account::valid(candidate) || !valid_profile_inventory(candidate)
-        || !family4_loadout::resolve(candidate, prepared.characterIndex, checked)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("account_or_resolve");
-    }
-    runtime::storage::g_state.account = candidate;
-    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-
-    report_socket_plug("commit_end",
-                       "ok",
-                       "published",
-                       prepared.characterSoid,
-                       prepared.targetInstanceSoid,
-                       prepared.targetDefinitionIndex,
-                       prepared.socketLane,
-                       prepared.plugDefinitionIndex,
-                       prepared.targetBucketId,
-                       prepared.plugBucketId,
-                       prepared.targetEquipped,
-                       prepared.itemIndex);
-    return true;
-}
-
-/** Prepares one complete native item-state value for an owned selected-character instance. */
-bool prepare_item_state(std::uint64_t targetInstanceSoid,
-                        std::uint16_t targetDefinitionIndex,
-                        std::uint32_t flags,
-                        PendingItemState& mutation) noexcept {
-    mutation = {};
-    const AccountState snapshot = account_snapshot();
-    std::size_t characterIndex = snapshot.characterCount;
-    if (account::valid(snapshot)) {
-        for (std::size_t index = 0; index < snapshot.characterCount; ++index) {
-            if (snapshot.characters[index].selected) {
-                characterIndex = index;
-                break;
-            }
-        }
-    }
-    if (characterIndex >= snapshot.characterCount
-        || !stage_item_state(
-            snapshot, characterIndex, targetInstanceSoid, targetDefinitionIndex, flags, mutation)) {
-        report_item_state(
-            "prepare",
-            "fail",
-            "selection_or_item",
-            characterIndex < snapshot.characterCount ? snapshot.characters[characterIndex].soid : 0,
-            targetInstanceSoid,
-            targetDefinitionIndex,
-            0,
-            flags,
-            false,
-            0);
-        mutation = {};
-        return false;
-    }
-    report_item_state("prepare",
-                      "ok",
-                      "ready",
-                      mutation.characterSoid,
-                      mutation.targetInstanceSoid,
-                      mutation.targetDefinitionIndex,
-                      mutation.beforeFlags,
-                      mutation.afterFlags,
-                      mutation.targetEquipped,
-                      mutation.itemIndex);
-    return true;
-}
-
-/** Commits one prepared item-state after-image behind an exact character staleness guard. */
-bool commit_item_state(PendingItemState& mutation) noexcept {
-    const PendingItemState prepared = mutation;
-    mutation = {};
-    const auto fail = [&prepared](std::string_view reason) noexcept {
-        report_item_state("commit",
-                          "fail",
-                          reason,
-                          prepared.characterSoid,
-                          prepared.targetInstanceSoid,
-                          prepared.targetDefinitionIndex,
-                          prepared.beforeFlags,
-                          prepared.afterFlags,
-                          prepared.targetEquipped,
-                          prepared.itemIndex);
-        return false;
-    };
-    const std::size_t capacity = prepared.targetEquipped
-                                     ? authored_inventory::kEquipmentSlotCount
-                                     : authored_inventory::kCharacterItemCapacity;
-    if (!prepared.prepared || prepared.characterSoid == 0 || prepared.targetInstanceSoid == 0
-        || prepared.characterIndex >= kCharacterCapacity || prepared.itemIndex >= capacity
-        || prepared.beforeCharacter.soid != prepared.characterSoid
-        || prepared.afterCharacter.soid != prepared.characterSoid
-        || prepared.beforeFlags == prepared.afterFlags) {
-        return fail("mutation");
-    }
-
-    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
-    AccountState candidate = runtime::storage::g_state.account;
-    if (prepared.characterIndex >= candidate.characterCount
-        || !same_character(candidate.characters[prepared.characterIndex],
-                           prepared.beforeCharacter)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("stale");
-    }
-
-    PendingItemState canonical{};
-    if (!stage_item_state(candidate,
-                          prepared.characterIndex,
-                          prepared.targetInstanceSoid,
-                          prepared.targetDefinitionIndex,
-                          prepared.afterFlags,
-                          canonical)
-        || canonical.characterSoid != prepared.characterSoid
-        || canonical.characterIndex != prepared.characterIndex
-        || canonical.itemIndex != prepared.itemIndex
-        || canonical.targetDefinitionIndex != prepared.targetDefinitionIndex
-        || canonical.beforeFlags != prepared.beforeFlags
-        || canonical.afterFlags != prepared.afterFlags
-        || canonical.targetEquipped != prepared.targetEquipped
-        || !same_character(canonical.beforeCharacter, prepared.beforeCharacter)
-        || !same_character(canonical.afterCharacter, prepared.afterCharacter)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("transition");
-    }
-
-    candidate.characters[prepared.characterIndex] = prepared.afterCharacter;
-    family4_loadout::ResolvedLoadout checked{};
-    if (!account::valid(candidate)
-        || !family4_loadout::resolve(candidate, prepared.characterIndex, checked)) {
-        ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-        return fail("account_or_resolve");
-    }
-    runtime::storage::g_state.account = candidate;
-    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
-
-    report_item_state("commit",
-                      "ok",
-                      "published",
-                      prepared.characterSoid,
-                      prepared.targetInstanceSoid,
-                      prepared.targetDefinitionIndex,
-                      prepared.beforeFlags,
-                      prepared.afterFlags,
-                      prepared.targetEquipped,
-                      prepared.itemIndex);
-    return true;
-}
-
 /** @return A copy of the active account state, read under the lock. */
 AccountState account_snapshot() noexcept {
     AcquireSRWLockShared(&runtime::storage::g_stateLock);

+ 163 - 0
Sunrise/src/state/runtime/state_account_transaction_helpers.h

@@ -0,0 +1,163 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+
+#include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
+#include "../build_data/runtime.h"
+#include "runtime.h"
+
+namespace sunrise::state::runtime::detail {
+
+struct ResolvedPosition {
+    std::uint16_t inventoryRow{};
+    std::uint8_t equipmentSlot{};
+    bool equipped{};
+    std::int32_t mutationSerial{};
+};
+
+void report_equipment(std::string_view stage,
+                      std::string_view result,
+                      EquipmentMutationKind kind,
+                      std::uint64_t characterSoid,
+                      std::uint64_t previousSoid,
+                      std::uint64_t requestedSoid,
+                      std::size_t equipmentIndex,
+                      std::size_t inventoryIndex,
+                      std::uint8_t nativeSlot,
+                      std::size_t movedItemCount,
+                      std::uint32_t previousHash,
+                      std::uint32_t requestedHash) noexcept;
+void report_acquisition(std::string_view stage,
+                        std::string_view result,
+                        std::string_view reason,
+                        std::uint32_t definitionHash,
+                        std::uint64_t characterSoid,
+                        std::uint64_t instanceSoid,
+                        std::size_t inventoryIndex,
+                        std::uint16_t inventoryRow,
+                        std::uint8_t equipmentSlot,
+                        std::uint32_t nextInventorySerial) noexcept;
+void report_profile_acquisition(std::string_view stage,
+                                std::string_view result,
+                                std::string_view reason,
+                                std::uint32_t definitionHash,
+                                std::uint64_t accountSoid,
+                                std::uint64_t instanceSoid,
+                                std::uint8_t bucketId,
+                                std::size_t profileIndex,
+                                std::size_t itemCount,
+                                std::int32_t previousQuantity,
+                                std::int32_t acquiredQuantity,
+                                bool appended) noexcept;
+void report_dismantle(std::string_view stage,
+                      std::string_view result,
+                      std::string_view reason,
+                      std::uint32_t definitionHash,
+                      std::uint64_t characterSoid,
+                      std::uint64_t instanceSoid,
+                      std::size_t inventoryIndex,
+                      std::uint16_t inventoryRow,
+                      std::uint8_t equipmentSlot,
+                      std::size_t movedItemCount,
+                      std::uint32_t nextInventorySerial) noexcept;
+void report_socket_plug(std::string_view stage,
+                        std::string_view result,
+                        std::string_view reason,
+                        std::uint64_t characterSoid,
+                        std::uint64_t targetInstanceSoid,
+                        std::uint16_t targetDefinitionIndex,
+                        std::uint8_t socketLane,
+                        std::uint16_t plugDefinitionIndex,
+                        std::uint8_t targetBucketId,
+                        std::uint8_t plugBucketId,
+                        bool targetEquipped,
+                        std::size_t itemIndex) noexcept;
+void report_item_state(std::string_view stage,
+                       std::string_view result,
+                       std::string_view reason,
+                       std::uint64_t characterSoid,
+                       std::uint64_t instanceSoid,
+                       std::uint16_t definitionIndex,
+                       std::uint32_t beforeFlags,
+                       std::uint32_t afterFlags,
+                       bool equipped,
+                       std::size_t itemIndex) noexcept;
+
+[[nodiscard]] bool
+same_profile_inventory(const AccountState& account,
+                       const std::array<account::inventory::ProfileItem,
+                                        account::inventory::kProfileItemCapacity>& expected,
+                       std::size_t expectedCount) noexcept;
+[[nodiscard]] bool same_profile_views(
+    const std::array<account::inventory::ProfileItem, account::inventory::kProfileItemCapacity>&
+        left,
+    std::size_t leftCount,
+    const std::array<account::inventory::ProfileItem, account::inventory::kProfileItemCapacity>&
+        right,
+    std::size_t rightCount) noexcept;
+[[nodiscard]] bool valid_profile_inventory(const AccountState& account) noexcept;
+[[nodiscard]] bool
+apply_collection_materials(const AccountState& before,
+                           const build_data::collectibles::Definition& collectible,
+                           AccountState& after,
+                           bool& changed) noexcept;
+[[nodiscard]] bool
+valid_profile_mutation_shape(const PendingProfileItemAcquisition& mutation) noexcept;
+[[nodiscard]] bool materialize_profile_acquisition(const AccountState& current,
+                                                   const PendingProfileItemAcquisition& mutation,
+                                                   AccountState& after) noexcept;
+[[nodiscard]] bool native_equipment_slot(const account::inventory::Item& item,
+                                         std::uint8_t& slot) noexcept;
+[[nodiscard]] bool semantic_equipment_slot(std::uint8_t nativeSlot,
+                                           std::size_t& semanticIndex) noexcept;
+[[nodiscard]] bool
+find_resolved_position(const middleware::datagen::family4::loadout::ResolvedLoadout& loadout,
+                       std::uint64_t instanceSoid,
+                       ResolvedPosition& position) noexcept;
+[[nodiscard]] bool finalize_equipment_transition(
+    const AccountState& account,
+    std::size_t characterIndex,
+    std::uint64_t requestedInstanceSoid,
+    EquipmentMutationKind kind,
+    std::uint8_t expectedNativeSlot,
+    const middleware::datagen::family4::loadout::ResolvedLoadout& beforeLoadout,
+    CharacterState& after,
+    std::size_t& movedItemCount) noexcept;
+[[nodiscard]] bool same_character(const CharacterState& left, const CharacterState& right) noexcept;
+[[nodiscard]] bool stage_socket_plug(const AccountState& snapshot,
+                                     std::size_t characterIndex,
+                                     std::uint64_t targetInstanceSoid,
+                                     std::uint8_t socketLane,
+                                     std::uint16_t plugDefinitionIndex,
+                                     PendingSocketPlug& mutation) noexcept;
+[[nodiscard]] bool stage_item_state(const AccountState& snapshot,
+                                    std::size_t characterIndex,
+                                    std::uint64_t targetInstanceSoid,
+                                    std::uint16_t targetDefinitionIndex,
+                                    std::uint32_t flags,
+                                    PendingItemState& mutation) noexcept;
+[[nodiscard]] bool next_item_instance_soid(const AccountState& account,
+                                           std::uint64_t& output) noexcept;
+[[nodiscard]] bool next_profile_item_instance_soid(const AccountState& account,
+                                                   std::uint64_t& output) noexcept;
+[[nodiscard]] std::int32_t acquisition_level(const CharacterState& character) noexcept;
+[[nodiscard]] bool
+find_acquired_row(const middleware::datagen::family4::loadout::ResolvedLoadout& loadout,
+                  std::uint64_t instanceSoid,
+                  std::uint16_t& inventoryRow,
+                  std::uint8_t& equipmentSlot) noexcept;
+[[nodiscard]] bool stage_item_dismantle(const AccountState& account,
+                                        std::size_t characterIndex,
+                                        std::uint64_t instanceSoid,
+                                        PendingItemDismantle& mutation) noexcept;
+[[nodiscard]] bool same_dismantle_transition(const PendingItemDismantle& left,
+                                             const PendingItemDismantle& right) noexcept;
+[[nodiscard]] bool materialize_item_dismantle(const AccountState& current,
+                                              const PendingItemDismantle& mutation,
+                                              AccountState& after) noexcept;
+[[nodiscard]] bool identity_uses_soid(const AccountState& account, std::uint64_t soid) noexcept;
+
+} // namespace sunrise::state::runtime::detail