Переглянути джерело

Implement WS-701 settings state updates

Gage Fulwood 2 тижнів тому
батько
коміт
e69bc1421b
22 змінених файлів з 1541 додано та 163 видалено
  1. 6 1
      Sunrise/Sunrise.vcxproj
  2. 25 23
      Sunrise/src/core/settings/state/key_bindings_parser.cpp
  3. 0 96
      Sunrise/src/middleware/datagen/family4/account/preferences/native_key_binding_map.h
  4. 6 3
      Sunrise/src/middleware/datagen/family4/account/preferences/preferences_encoder.cpp
  5. 875 0
      Sunrise/src/middleware/web_service/messages/opcode701/opcode701_codec.cpp
  6. 31 0
      Sunrise/src/middleware/web_service/messages/opcode701/opcode701_codec.h
  7. 6 0
      Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp
  8. 2 1
      Sunrise/src/server/bap/encrypted/encrypted_runtime.cpp
  9. 2 1
      Sunrise/src/server/bap/encrypted/internal.h
  10. 15 9
      Sunrise/src/server/bap/encrypted/transactions/service_outcome_commit.cpp
  11. 37 0
      Sunrise/src/server/web_service/web_service_actions.cpp
  12. 3 0
      Sunrise/src/server/web_service/web_service_actions.h
  13. 8 3
      Sunrise/src/server/web_service/web_service_runtime.cpp
  14. 6 2
      Sunrise/src/server/web_service/web_service_runtime.h
  15. 16 0
      Sunrise/src/state/account/settings/key_bindings.h
  16. 89 0
      Sunrise/src/state/account/settings/native_key_binding_map.h
  17. 121 0
      Sunrise/src/state/account/settings/settings_delta.cpp
  18. 120 0
      Sunrise/src/state/account/settings/settings_delta.h
  19. 52 23
      Sunrise/src/state/account/settings/settings_state.cpp
  20. 17 1
      Sunrise/src/state/account/settings/settings_state.h
  21. 38 0
      Sunrise/src/state/runtime/runtime.h
  22. 66 0
      Sunrise/src/state/runtime/state_account_settings_runtime.cpp

+ 6 - 1
Sunrise/Sunrise.vcxproj

@@ -136,6 +136,7 @@
     <ClCompile Include="src\client\hooks\cursor\cursor_hook_lifecycle.cpp" />
     <ClCompile Include="src\client\hooks\graphics\renderer\graphics_renderer_device.cpp" />
     <ClCompile Include="src\middleware\web_service\messages\opcode504_codec.cpp" />
+    <ClCompile Include="src\middleware\web_service\messages\opcode701\opcode701_codec.cpp" />
     <ClCompile Include="src\middleware\web_service\messages\opcode903_codec.cpp" />
     <ClCompile Include="src\middleware\web_service\messages\opcode1901_codec.cpp" />
     <ClCompile Include="src\middleware\web_service\messages\opcode402_codec.cpp" />
@@ -151,6 +152,7 @@
     <ClCompile Include="src\state\runtime\state_account_identity_runtime.cpp" />
     <ClCompile Include="src\state\runtime\state_account_profile_runtime.cpp" />
     <ClCompile Include="src\state\runtime\state_account_socket_runtime.cpp" />
+    <ClCompile Include="src\state\runtime\state_account_settings_runtime.cpp" />
     <ClCompile Include="src\state\runtime\state_rolled_socket_plugs.cpp" />
     <ClCompile Include="src\state\runtime\state_account_item_action_runtime.cpp" />
     <ClCompile Include="src\core\ui\busy\ui_busy_overlay.cpp" />
@@ -611,6 +613,7 @@
     <ClCompile Include="src\state\account\account_state.cpp" />
     <ClCompile Include="src\state\account\inventory\inventory_state.cpp" />
     <ClCompile Include="src\state\account\settings\settings_state.cpp" />
+    <ClCompile Include="src\state\account\settings\settings_delta.cpp" />
     <ClCompile Include="src\state\equipment\light\calculation\equipment_light_calculation.cpp" />
     <ClCompile Include="src\state\equipment\light\resolution\configured_equipment_light_resolver.cpp" />
     <ClCompile Include="src\state\content\content_catalog.cpp" />
@@ -1201,6 +1204,7 @@
     <ClInclude Include="src\client\hooks\banner\banner_bind.h" />
     <ClInclude Include="src\client\hooks\banner\banner_hook_lifecycle.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode504.h" />
+    <ClInclude Include="src\middleware\web_service\messages\opcode701\opcode701_codec.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode903.h" />
     <ClInclude Include="src\middleware\web_service\messages\opcode1901.h" />
     <ClInclude Include="src\core\ui\busy\busy.h" />
@@ -1387,7 +1391,9 @@
     <ClInclude Include="src\state\account\account_state.h" />
     <ClInclude Include="src\state\account\inventory\inventory_state.h" />
     <ClInclude Include="src\state\account\settings\settings_state.h" />
+    <ClInclude Include="src\state\account\settings\settings_delta.h" />
     <ClInclude Include="src\state\account\settings\key_bindings.h" />
+    <ClInclude Include="src\state\account\settings\native_key_binding_map.h" />
     <ClInclude Include="src\state\equipment\light\definition.h" />
     <ClInclude Include="src\state\equipment\light\calculation\equipment_light_calculation.h" />
     <ClInclude Include="src\state\equipment\light\resolution\configured_equipment_light_resolver.h" />
@@ -1640,7 +1646,6 @@
     <ClInclude Include="src\middleware\datagen\family4\account\account_encoder.h" />
     <ClInclude Include="src\middleware\datagen\family4\account\layout.h" />
     <ClInclude Include="src\middleware\datagen\family4\account\preferences\layout.h" />
-    <ClInclude Include="src\middleware\datagen\family4\account\preferences\native_key_binding_map.h" />
     <ClInclude Include="src\middleware\datagen\family4\account\preferences\preferences_encoder.h" />
     <ClInclude Include="src\middleware\datagen\family4\character\abi.h" />
     <ClInclude Include="src\middleware\datagen\family4\loadout\definition.h" />

+ 25 - 23
Sunrise/src/core/settings/state/key_bindings_parser.cpp

@@ -4,7 +4,6 @@
 #include "../parser.h"
 
 namespace sunrise::core::settings::parser {
-namespace {
 
 namespace bindings = state::account::settings::bindings;
 
@@ -77,7 +76,7 @@ constexpr std::array<std::string_view, bindings::kActionCount> kActionNames{
  * @param name Borrowed JSON property name.
  * @return Fixed State index, or the action count when the name is unknown.
  */
-[[nodiscard]] std::size_t action_index(std::string_view name) noexcept {
+[[nodiscard]] static std::size_t action_index(std::string_view name) noexcept {
     const auto found = std::find(kActionNames.begin(), kActionNames.end(), name);
     return static_cast<std::size_t>(found - kActionNames.begin());
 }
@@ -209,18 +208,13 @@ constexpr std::array<InputName, 121> kInputNames{{
     {"extra mouse button 2", 113},
     {"mouse wheel up", 114},
     {"mouse wheel down", 115},
-    {"unused", 116},
+    {"unused", bindings::kUnboundInputCode},
     {"ctrl", 106},
     {"left ctrl", 69},
     {"right ctrl", 76},
     {"\\\\", 43},
 }};
 
-/** A binding half carries its key code in the low byte and one modifier above it. */
-constexpr std::uint16_t kAltFlag = 0x0100;
-constexpr std::uint16_t kControlFlag = 0x0200;
-constexpr std::uint16_t kShiftFlag = 0x0400;
-
 /** One code that may prefix another key, and the flag it sets there. */
 struct ModifierName {
     std::uint16_t code;
@@ -229,19 +223,19 @@ struct ModifierName {
 
 /** Both sides of a modifier fold onto the same flag, as they do in the Client. */
 constexpr std::array<ModifierName, 9> kModifiers{{
-    {57, kShiftFlag},
-    {68, kShiftFlag},
-    {105, kShiftFlag},
-    {69, kControlFlag},
-    {76, kControlFlag},
-    {106, kControlFlag},
-    {71, kAltFlag},
-    {73, kAltFlag},
-    {108, kAltFlag},
+    {57, bindings::kShiftModifierFlag},
+    {68, bindings::kShiftModifierFlag},
+    {105, bindings::kShiftModifierFlag},
+    {69, bindings::kControlModifierFlag},
+    {76, bindings::kControlModifierFlag},
+    {106, bindings::kControlModifierFlag},
+    {71, bindings::kAltModifierFlag},
+    {73, bindings::kAltModifierFlag},
+    {108, bindings::kAltModifierFlag},
 }};
 
 /** @return The name without leading and trailing ASCII blanks. */
-[[nodiscard]] constexpr std::string_view trim(std::string_view text) noexcept {
+[[nodiscard]] static constexpr std::string_view trim(std::string_view text) noexcept {
     while (!text.empty() && (text.front() == ' ' || text.front() == '\t')) {
         text.remove_prefix(1);
     }
@@ -252,7 +246,8 @@ constexpr std::array<ModifierName, 9> kModifiers{{
 }
 
 /** @return True when the two names match with ASCII case folded, as the Client compares them. */
-[[nodiscard]] constexpr bool same_name(std::string_view left, std::string_view right) noexcept {
+[[nodiscard]] static constexpr bool same_name(std::string_view left,
+                                              std::string_view right) noexcept {
     if (left.size() != right.size()) {
         return false;
     }
@@ -274,7 +269,7 @@ constexpr std::array<ModifierName, 9> kModifiers{{
  * @param output Receives the code the table gives that name.
  * @return True when the name is in the table.
  */
-[[nodiscard]] bool named_code(std::string_view name, std::uint16_t& output) noexcept {
+[[nodiscard]] static bool named_code(std::string_view name, std::uint16_t& output) noexcept {
     for (const InputName& entry : kInputNames) {
         if (same_name(entry.name, name)) {
             output = entry.code;
@@ -289,7 +284,7 @@ constexpr std::array<ModifierName, 9> kModifiers{{
  * @param output Receives the flag that code sets on the key it prefixes.
  * @return True when the code is a modifier.
  */
-[[nodiscard]] bool modifier_flag(std::uint16_t code, std::uint16_t& output) noexcept {
+[[nodiscard]] static bool modifier_flag(std::uint16_t code, std::uint16_t& output) noexcept {
     for (const ModifierName& entry : kModifiers) {
         if (entry.code == code) {
             output = entry.flag;
@@ -299,8 +294,6 @@ constexpr std::array<ModifierName, 9> kModifiers{{
     return false;
 }
 
-} // namespace
-
 /** Parses the whole fixed action table under named JSON properties. */
 bool Parser::key_bindings(bindings::KeyBindings& output) noexcept {
     output = {};
@@ -388,6 +381,15 @@ bool Parser::optional_input_code(std::optional<std::uint16_t>& output) noexcept
     if (!string(name) || !input_code_value(name, code)) {
         return false;
     }
+    if (code == bindings::kUnboundInputCode) {
+        // The Client's "unused" table row is the wire sentinel, not a bindable input.
+        output.reset();
+        return true;
+    }
+    if ((code & bindings::kInputCodeMask) == bindings::kUnboundInputCode) {
+        // A modifier cannot turn the unbound sentinel into a real key.
+        return false;
+    }
     output = code;
     return true;
 }

+ 0 - 96
Sunrise/src/middleware/datagen/family4/account/preferences/native_key_binding_map.h

@@ -1,96 +0,0 @@
-#pragma once
-
-#include <array>
-#include <cstddef>
-
-#include "../../../../../state/account/settings/key_bindings.h"
-
-namespace sunrise::middleware::datagen::family4::account::preferences {
-
-using state::account::settings::bindings::Action;
-
-/** Array position is the fixed native account slot for the named semantic State action. */
-inline constexpr std::array<Action, state::account::settings::bindings::kActionCount>
-    kActionsBySlot{
-        Action::fire,
-        Action::toggleZoom,
-        Action::holdZoom,
-        Action::melee,
-        Action::grenade,
-        Action::super,
-        Action::reload,
-        Action::lightAttack,
-        Action::heavyAttack,
-        Action::block,
-        Action::switchWeapons,
-        Action::nextWeapon,
-        Action::previousWeapon,
-        Action::primaryWeapon,
-        Action::specialWeapon,
-        Action::heavyWeapon,
-        Action::moveForward,
-        Action::moveBackward,
-        Action::moveLeft,
-        Action::moveRight,
-        Action::jump,
-        Action::toggleCrouch,
-        Action::holdCrouch,
-        Action::toggleSprint,
-        Action::holdSprint,
-        Action::vehicleBoost,
-        Action::vehicleBrake,
-        Action::vehicleZoom,
-        Action::vehicleFirePrimary,
-        Action::vehicleFireSecondary,
-        Action::vehicleExit,
-        Action::interact,
-        Action::highlightPlayer,
-        Action::emoteOne,
-        Action::emoteTwo,
-        Action::emoteThree,
-        Action::emoteFour,
-        Action::airMove,
-        Action::classAbility,
-        Action::deathCameraZoomIn,
-        Action::deathCameraZoomOut,
-        Action::pushToTalk,
-        Action::uiGamepadButtonBack,
-        Action::uiOpenDirector,
-        Action::uiOpenDirectorStoreTab,
-        Action::uiOpenDirectorPursuitsTab,
-        Action::uiOpenDirectorMapTab,
-        Action::uiOpenDirectorDestinationsTab,
-        Action::uiOpenDirectorRosterTab,
-        Action::uiOpenDirectorSeasonsTab,
-        Action::uiOpenStartMenuAlternative,
-        Action::uiOpenStartMenuRecordsTab,
-        Action::uiOpenStartMenuCollectionsTab,
-        Action::uiOpenStartMenuClanTab,
-        Action::uiOpenStartMenuInventoryTab,
-        Action::uiOpenStartMenuSettingsTab,
-        Action::uiOpenExitDialogConfirm,
-        Action::uiAbortActivity,
-        Action::uiTextChatToggleState,
-        Action::screenshot,
-    };
-
-/**
- * Verifies that the native slot table contains every semantic action once.
- * @return True when the table is a complete one-to-one mapping.
- */
-[[nodiscard]] consteval bool complete() noexcept {
-    std::array<bool, state::account::settings::bindings::kActionCount> seen{};
-    for (const Action action : kActionsBySlot) {
-        const std::size_t stateIndex = static_cast<std::size_t>(action);
-        // Every State action must own exactly one native account slot.
-        if (stateIndex >= seen.size() || seen[stateIndex]) {
-            return false;
-        }
-        seen[stateIndex] = true;
-    }
-    return true;
-}
-
-static_assert(complete());
-
-} // namespace sunrise::middleware::datagen::family4::account::preferences

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

@@ -1,10 +1,12 @@
 #include "preferences_encoder.h"
 
-#include "native_key_binding_map.h"
+#include "../../../../../state/account/settings/native_key_binding_map.h"
 
 namespace sunrise::middleware::datagen::family4::account::preferences {
 namespace {
 
+namespace bindings = state::account::settings::bindings;
+
 /** Native keybinding halves use input code 0x74 as the unbound sentinel. */
 constexpr std::uint16_t kUnboundInputCode = 0x0074;
 /** Seed version 0 lets the client set up local mirrors once after sign-in. */
@@ -128,8 +130,9 @@ bool encode(const state::account::settings::AccountSettings& settings,
     record.chatAutoHideMode = social.chatAutoHideMode;
     bindingsRecord.voiceChatMirror = native_boolean(social.voiceChatEnabled);
 
-    for (std::size_t nativeSlot = 0; nativeSlot < kActionsBySlot.size(); ++nativeSlot) {
-        const auto action = kActionsBySlot[nativeSlot];
+    for (std::size_t nativeSlot = 0; nativeSlot < bindings::kActionsByNativeSlot.size();
+         ++nativeSlot) {
+        const auto action = bindings::kActionsByNativeSlot[nativeSlot];
         const std::size_t stateIndex = static_cast<std::size_t>(action);
         // Native ABI order stays independent from the semantic State enum order.
         bindingsRecord.keyBindings[nativeSlot] =

+ 875 - 0
Sunrise/src/middleware/web_service/messages/opcode701/opcode701_codec.cpp

@@ -0,0 +1,875 @@
+#include "opcode701_codec.h"
+
+#include <array>
+#include <bit>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+
+#include "../../../../state/account/settings/native_key_binding_map.h"
+#include "../../../../state/account/settings/settings_state.h"
+#include "../../../encoding/bit_reader.h"
+
+namespace sunrise::middleware::web_service::messages::opcode701 {
+namespace {
+
+using encoding::bits::Reader;
+namespace settings = state::account::settings;
+
+/**
+ * Schema 0x80807603 is a presence-driven reflected object. Every `optional` node starts with one
+ * presence bit; an absent node consumes no body bits. No field is byte-aligned.
+ *
+ * Implicit root (there is no root presence bit)
+ * |-- 0.0? client metadata
+ * |   |-- 0.0.0? [128] optional 64-bit publicity expiries
+ * |   `-- 0.0.1? [13] required 32-bit seen-message values
+ * `-- 0.1? account data
+ *     |-- 0.1.0? [2] optional vectors, each with two required real32 values
+ *     |-- 0.1.1? preference record
+ *     |   |-- 0.1.1.0-.61: 62 optional scalar preferences
+ *     |   `-- 0.1.1.62? [3][50]: 150 cells, each with its own presence bit and int32
+ *     |-- 0.1.2? seed, three local mirrors, source, and optional 60-word binding table
+ *     |-- 0.1.3? four required 16-bit values
+ *     |-- 0.1.4? mixed known-width record, semantic meaning unknown
+ *     |-- 0.1.5? 22 required 32-bit values
+ *     |-- 0.1.6? optional-region record
+ *     |   |-- 0.1.6.0? [100] optional int16 values, then two required int32 words
+ *     |   `-- 0.1.6.1? one int16 value
+ *     |-- 0.1.7? bool
+ *     |-- 0.1.8? bool
+ *     |-- 0.1.9? bool
+ *     |-- 0.1.10? 8-bit scalar
+ *     |-- 0.1.11? 32-bit scalar
+ *     |-- 0.1.12? 30 required int16 values, then two required int32 words
+ *     `-- 0.1.13? one 32-bit value
+ *
+ * Two optional length-prefixed blobs follow the reflected object. The final partial byte, if any,
+ * is zero padding. Traversal must therefore follow every presence flag even for unsupported data;
+ * a fixed wire offset would become invalid as soon as any earlier optional node is absent.
+ */
+
+/** Wire primitive widths used by schema 0x80807603. */
+constexpr std::uint8_t kPresenceWidthBits = 1;
+constexpr std::uint8_t kBooleanWidthBits = 1;
+constexpr std::uint8_t kTwoWidthBits = 2;
+constexpr std::uint8_t kThreeWidthBits = 3;
+constexpr std::uint8_t kFourWidthBits = 4;
+constexpr std::uint8_t kFiveWidthBits = 5;
+constexpr std::uint8_t kSixWidthBits = 6;
+constexpr std::uint8_t kByteWidthBits = 8;
+constexpr std::uint8_t kScalar16WidthBits = 16;
+constexpr std::uint8_t kScalar32WidthBits = 32;
+constexpr std::uint8_t kScalar64WidthBits = 64;
+
+/** Schema array dimensions, kept separate from scalar widths. */
+constexpr std::size_t kPublicityExpiryCount = 128;
+constexpr std::size_t kSeenMessageCount = 13;
+constexpr std::size_t kCalibrationVectorCount = 2;
+constexpr std::size_t kCalibrationValuesPerVector = 2;
+constexpr std::size_t kPreferenceMatrixRowCount = 3;
+constexpr std::size_t kPreferenceMatrixColumnCount = 50;
+constexpr std::size_t kGroup_0_1_4OptionalFieldCount = 8;
+constexpr std::size_t kGroup_0_1_3ValueCount = 4;
+constexpr std::size_t kGroup_0_1_5ValueCount = 22;
+constexpr std::size_t kGroup_0_1_6EntryCount = 100;
+constexpr std::size_t kGroup_0_1_6FixedWordCount = 2;
+constexpr std::size_t kGroup_0_1_12ValueCount = 30;
+constexpr std::size_t kGroup_0_1_12FixedWordCount = 2;
+constexpr std::size_t kOuterBlobCount = 2;
+
+/** Catalog invariants used to detect accidental table/schema drift at compile time. */
+constexpr std::size_t kCatalogPreferenceFieldCount = 62;
+constexpr std::size_t kCatalogKeyBindingCount = 60;
+constexpr std::size_t kCatalogRootMetadataMaximumBits = 8'739;
+constexpr std::size_t kCatalogPreferencesMaximumBits = 5'300;
+constexpr std::size_t kCatalogBindingsMaximumBits = 1'996;
+constexpr std::size_t kCatalogAccountBranchMaximumBits = 10'956;
+constexpr std::size_t kCatalogMaximumBits = 19'695;
+
+/** A packed binding half with this value represents no assigned input. */
+constexpr std::uint16_t kUnboundInputCode = settings::bindings::kUnboundInputCode;
+/** Each packed binding word stores one primary half followed by one secondary half. */
+constexpr unsigned kBindingHalfWidthBits = kScalar16WidthBits;
+
+/** Outer blobs encode their byte length in one unsigned 16-bit field. */
+constexpr std::uint8_t kOuterBlobLengthWidthBits = kScalar16WidthBits;
+/** A whole unread byte is data, while fewer than eight final bits may be terminal padding. */
+constexpr std::size_t kTerminalPaddingLimitBits = kByteWidthBits;
+
+/** Returns the bit count of a required fixed-width array. */
+[[nodiscard]] constexpr std::size_t fixed_array_width_bits(std::size_t count,
+                                                           std::size_t widthBits) noexcept {
+    return count * widthBits;
+}
+
+/** One scalar descriptor's stored width, destination width, and modular wire bias. */
+struct ScalarEncoding {
+    std::uint8_t wireWidthBits;
+    std::uint8_t nativeWidthBits;
+    std::uint64_t bias;
+};
+
+/** Compact signed selectors store the destination value plus one. */
+constexpr std::uint64_t kCompactIntegerBias = 1;
+/** Reflected signed 32-bit values store the destination bit pattern plus INT32_MIN. */
+constexpr std::uint64_t kSigned32Bias = 0x80000000ULL;
+
+constexpr ScalarEncoding kBoolEncoding{kBooleanWidthBits, kBooleanWidthBits, 0};
+constexpr ScalarEncoding kInt8TwoBitEncoding{kTwoWidthBits, kByteWidthBits, kCompactIntegerBias};
+constexpr ScalarEncoding kInt8ThreeBitEncoding{
+    kThreeWidthBits, kByteWidthBits, kCompactIntegerBias};
+constexpr ScalarEncoding kInt8FourBitEncoding{kFourWidthBits, kByteWidthBits, kCompactIntegerBias};
+constexpr ScalarEncoding kInt32Encoding{kScalar32WidthBits, kScalar32WidthBits, kSigned32Bias};
+constexpr ScalarEncoding kReal32Encoding{kScalar32WidthBits, kScalar32WidthBits, 0};
+
+constexpr std::size_t kPreferenceFieldCount = kCatalogPreferenceFieldCount;
+constexpr std::size_t kKeyBindingCount = settings::bindings::kActionCount;
+
+static_assert(kKeyBindingCount == kCatalogKeyBindingCount);
+
+/** Reinterprets an already-unbiased byte pattern as its signed destination value. */
+[[nodiscard]] constexpr std::int8_t as_int8(std::uint64_t value) noexcept {
+    return std::bit_cast<std::int8_t>(static_cast<std::uint8_t>(value));
+}
+
+/** Reinterprets an already-unbiased 32-bit pattern as its signed destination value. */
+[[nodiscard]] constexpr std::int32_t as_int32(std::uint64_t value) noexcept {
+    return std::bit_cast<std::int32_t>(static_cast<std::uint32_t>(value));
+}
+
+/** Reinterprets a raw IEEE-754 32-bit pattern without applying an integer conversion. */
+[[nodiscard]] constexpr float as_real32(std::uint64_t value) noexcept {
+    return std::bit_cast<float>(static_cast<std::uint32_t>(value));
+}
+
+/** Typed assignment adapters let each schema descriptor name its exact nested delta member. */
+template <auto GroupMember, auto FieldMember>
+void assign_bool(std::uint64_t value, settings::SettingsDelta& delta) noexcept {
+    (delta.*GroupMember).*FieldMember = value != 0;
+}
+
+template <auto GroupMember, auto FieldMember>
+void assign_int8(std::uint64_t value, settings::SettingsDelta& delta) noexcept {
+    (delta.*GroupMember).*FieldMember = as_int8(value);
+}
+
+template <auto GroupMember, auto FieldMember>
+void assign_int32(std::uint64_t value, settings::SettingsDelta& delta) noexcept {
+    (delta.*GroupMember).*FieldMember = as_int32(value);
+}
+
+template <auto GroupMember, auto FieldMember>
+void assign_real32(std::uint64_t value, settings::SettingsDelta& delta) noexcept {
+    (delta.*GroupMember).*FieldMember = as_real32(value);
+}
+
+/** A plain function pointer keeps the descriptor table constexpr and allocation-free. */
+using PreferenceAssignment = void (*)(std::uint64_t, settings::SettingsDelta&) noexcept;
+
+/** One schema path's index, wire decoding rule, and optional semantic State destination. */
+struct PreferenceDescriptor {
+    std::size_t schemaIndex;
+    ScalarEncoding encoding;
+    PreferenceAssignment assign;
+};
+
+/**
+ * Single source of truth for preference paths 0.1.1.0 through 0.1.1.61.
+ * A null assignment marks a structurally known field that is intentionally traversal-only.
+ */
+constexpr std::array<PreferenceDescriptor, kPreferenceFieldCount> kPreferenceDescriptors{
+    PreferenceDescriptor{0, kBoolEncoding, nullptr},  // profile setup marker
+    PreferenceDescriptor{1, kInt32Encoding, nullptr}, // post-processing seed version
+    PreferenceDescriptor{
+        2,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::controls, &settings::ControlsDelta::buttonLayout>},
+    PreferenceDescriptor{
+        3,
+        kInt8ThreeBitEncoding,
+        assign_int8<&settings::SettingsDelta::controls, &settings::ControlsDelta::movementMode>},
+    PreferenceDescriptor{4,
+                         kInt8FourBitEncoding,
+                         assign_int8<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::controllerLookSensitivity>},
+    PreferenceDescriptor{5,
+                         kInt8ThreeBitEncoding,
+                         assign_int8<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::doublePressDelay>},
+    PreferenceDescriptor{6,
+                         kInt32Encoding,
+                         assign_int32<&settings::SettingsDelta::controls,
+                                      &settings::ControlsDelta::mouseLookSensitivity>},
+    PreferenceDescriptor{7,
+                         kReal32Encoding,
+                         assign_real32<&settings::SettingsDelta::controls,
+                                       &settings::ControlsDelta::adsSensitivityModifier>},
+    PreferenceDescriptor{
+        8,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::interface, &settings::InterfaceDelta::subtitlesMode>},
+    PreferenceDescriptor{
+        9,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::interface, &settings::InterfaceDelta::textSize>},
+    PreferenceDescriptor{
+        10,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::interface, &settings::InterfaceDelta::textColor>},
+    PreferenceDescriptor{11,
+                         kInt8FourBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::textBackgroundStyle>},
+    PreferenceDescriptor{12,
+                         kInt8FourBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::textBackgroundOpacity>},
+    PreferenceDescriptor{13,
+                         kInt8FourBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::reservedTextMode>},
+    PreferenceDescriptor{14,
+                         kInt8FourBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::subtitleOptionsEntry>},
+    PreferenceDescriptor{
+        15,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::voiceOutputMode>},
+    PreferenceDescriptor{
+        16,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::teamVoiceChannel>},
+    PreferenceDescriptor{
+        17,
+        kInt8ThreeBitEncoding,
+        assign_int8<&settings::SettingsDelta::display, &settings::DisplayDelta::brightness>},
+    PreferenceDescriptor{
+        18,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::interface, &settings::InterfaceDelta::helmetMode>},
+    PreferenceDescriptor{19,
+                         kInt8ThreeBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::colorblindMode>},
+    PreferenceDescriptor{
+        20,
+        kInt8ThreeBitEncoding,
+        assign_int8<&settings::SettingsDelta::interface, &settings::InterfaceDelta::reticleColor>},
+    PreferenceDescriptor{
+        21,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::reservedMode>},
+    PreferenceDescriptor{22, kInt8ThreeBitEncoding, nullptr}, // unmapped audio-padding field
+    PreferenceDescriptor{
+        23,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::migrationVersion>},
+    PreferenceDescriptor{
+        24,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::soundEffectsVolume>},
+    PreferenceDescriptor{
+        25,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::dialogueVolume>},
+    PreferenceDescriptor{
+        26,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::musicVolume>},
+    PreferenceDescriptor{
+        27,
+        kInt8FourBitEncoding,
+        assign_int8<&settings::SettingsDelta::audio, &settings::AudioDelta::chatVolume>},
+    PreferenceDescriptor{
+        28,
+        kBoolEncoding,
+        assign_bool<&settings::SettingsDelta::audio, &settings::AudioDelta::muteWhenUnfocused>},
+    PreferenceDescriptor{29,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::controllerInvertVertical>},
+    PreferenceDescriptor{30,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::controllerInvertHorizontal>},
+    PreferenceDescriptor{31,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::mouseInvertVertical>},
+    PreferenceDescriptor{32,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::mouseInvertHorizontal>},
+    PreferenceDescriptor{33,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::controllerAutoLookCentering>},
+    PreferenceDescriptor{34,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::social,
+                                     &settings::SocialDelta::preferGoodConnection>},
+    PreferenceDescriptor{35,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::controllerVibration>},
+    PreferenceDescriptor{36,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::unidentifiedToggle>},
+    PreferenceDescriptor{37,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::mouseAimSmoothing>},
+    PreferenceDescriptor{38,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::controls,
+                                     &settings::ControlsDelta::controllerSwapShoulders>},
+    PreferenceDescriptor{39, kBoolEncoding, nullptr}, // first unmapped identity-padding field
+    PreferenceDescriptor{40, kBoolEncoding, nullptr}, // second unmapped identity-padding field
+    PreferenceDescriptor{
+        41,
+        kBoolEncoding,
+        assign_bool<&settings::SettingsDelta::social, &settings::SocialDelta::showRealNames>},
+    PreferenceDescriptor{
+        42,
+        kBoolEncoding,
+        assign_bool<&settings::SettingsDelta::interface, &settings::InterfaceDelta::displayHints>},
+    PreferenceDescriptor{
+        43,
+        kBoolEncoding,
+        assign_bool<&settings::SettingsDelta::display, &settings::DisplayDelta::showFps>},
+    PreferenceDescriptor{44,
+                         kInt8TwoBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::reticleLocation>},
+    PreferenceDescriptor{45,
+                         kBoolEncoding,
+                         assign_bool<&settings::SettingsDelta::social,
+                                     &settings::SocialDelta::clanInviteNotifications>},
+    PreferenceDescriptor{
+        46,
+        kBoolEncoding,
+        assign_bool<&settings::SettingsDelta::social, &settings::SocialDelta::profanityFilter>},
+    PreferenceDescriptor{47,
+                         kInt8ThreeBitEncoding,
+                         assign_int8<&settings::SettingsDelta::interface,
+                                     &settings::InterfaceDelta::backgroundOpacity>},
+    PreferenceDescriptor{
+        48,
+        kInt8ThreeBitEncoding,
+        assign_int8<&settings::SettingsDelta::interface, &settings::InterfaceDelta::hudOpacity>},
+    PreferenceDescriptor{
+        49,
+        kBoolEncoding,
+        assign_bool<&settings::SettingsDelta::social, &settings::SocialDelta::voiceChatEnabled>},
+    PreferenceDescriptor{
+        50,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::social, &settings::SocialDelta::whisperChatMode>},
+    PreferenceDescriptor{
+        51,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::social, &settings::SocialDelta::teamChatJoinMode>},
+    PreferenceDescriptor{
+        52,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::social, &settings::SocialDelta::localChatJoinMode>},
+    PreferenceDescriptor{
+        53,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::social, &settings::SocialDelta::clanChatJoinMode>},
+    PreferenceDescriptor{
+        54,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::display, &settings::DisplayDelta::hdrMode>},
+    PreferenceDescriptor{55,
+                         kReal32Encoding,
+                         assign_real32<&settings::SettingsDelta::display,
+                                       &settings::DisplayDelta::calibrationPrimary>},
+    PreferenceDescriptor{56,
+                         kReal32Encoding,
+                         assign_real32<&settings::SettingsDelta::display,
+                                       &settings::DisplayDelta::calibrationAlpha>},
+    PreferenceDescriptor{
+        57,
+        kInt8ThreeBitEncoding,
+        assign_int8<&settings::SettingsDelta::social, &settings::SocialDelta::textChatMode>},
+    PreferenceDescriptor{
+        58,
+        kInt8TwoBitEncoding,
+        assign_int8<&settings::SettingsDelta::social, &settings::SocialDelta::chatAutoHideMode>},
+    PreferenceDescriptor{59, kBoolEncoding, nullptr}, // motion-blur mirror
+    PreferenceDescriptor{60, kBoolEncoding, nullptr}, // film-grain mirror
+    PreferenceDescriptor{61, kBoolEncoding, nullptr}, // chromatic-aberration mirror
+};
+
+/** Ensures explicit schema indices stay aligned with descriptor array positions. */
+[[nodiscard]] consteval bool valid_preference_descriptors() noexcept {
+    for (std::size_t index = 0; index < kPreferenceDescriptors.size(); ++index) {
+        const PreferenceDescriptor& descriptor = kPreferenceDescriptors[index];
+        if (descriptor.schemaIndex != index || descriptor.encoding.wireWidthBits == 0
+            || descriptor.encoding.wireWidthBits > kScalar64WidthBits
+            || descriptor.encoding.nativeWidthBits == 0
+            || descriptor.encoding.nativeWidthBits > kScalar64WidthBits) {
+            return false;
+        }
+    }
+    return true;
+}
+
+static_assert(valid_preference_descriptors());
+
+/** Optional widths for unknown mixed record path 0.1.4 fields 0 through 7. */
+constexpr std::array<std::uint8_t, kGroup_0_1_4OptionalFieldCount> kGroup_0_1_4OptionalWidths{
+    kScalar64WidthBits,
+    kScalar64WidthBits,
+    kScalar64WidthBits,
+    kTwoWidthBits,
+    kScalar64WidthBits,
+    kTwoWidthBits,
+    kSixWidthBits,
+    kFiveWidthBits,
+};
+
+/** Builds a low-bit mask without evaluating the invalid expression `1 << 64`. */
+[[nodiscard]] constexpr std::uint64_t width_mask(std::uint8_t nativeWidthBits) noexcept {
+    return nativeWidthBits == kScalar64WidthBits ? (std::numeric_limits<std::uint64_t>::max)()
+                                                 : (std::uint64_t{1} << nativeWidthBits) - 1U;
+}
+
+/** Reads the one-bit flag that precedes every optional schema node. */
+[[nodiscard]] bool read_presence(Reader& reader, bool& present) noexcept {
+    std::uint64_t value = 0;
+    if (!reader.read(kPresenceWidthBits, value)) {
+        return false;
+    }
+    present = value != 0;
+    return true;
+}
+
+/** Reads one stored scalar and removes its bias modulo the destination type width. */
+[[nodiscard]] bool
+read_scalar(Reader& reader, const ScalarEncoding& encoding, std::uint64_t& value) noexcept {
+    std::uint64_t stored = 0;
+    if (!reader.read(encoding.wireWidthBits, stored)) {
+        return false;
+    }
+    const std::uint64_t mask = width_mask(encoding.nativeWidthBits);
+    value = (stored - (encoding.bias & mask)) & mask;
+    return true;
+}
+
+/** Reads an optional scalar while preserving absent versus present-zero semantics. */
+[[nodiscard]] bool read_optional_scalar(Reader& reader,
+                                        const ScalarEncoding& encoding,
+                                        bool& present,
+                                        std::uint64_t& value) noexcept {
+    present = false;
+    value = 0;
+    return read_presence(reader, present) && (!present || read_scalar(reader, encoding, value));
+}
+
+/** Consumes one optional field whose value is deliberately not retained. */
+[[nodiscard]] bool skip_optional_bits(Reader& reader, std::size_t wireWidthBits) noexcept {
+    bool present = false;
+    return read_presence(reader, present) && (!present || reader.skip(wireWidthBits));
+}
+
+/** Consumes one optional scalar by its declared encoding without retaining its value. */
+[[nodiscard]] bool skip_optional_scalar(Reader& reader, const ScalarEncoding& encoding) noexcept {
+    return skip_optional_bits(reader, encoding.wireWidthBits);
+}
+
+/** Reads one optional group and delegates its body only when the group is present. */
+template <typename ReadBody>
+[[nodiscard]] bool read_optional_group(Reader& reader, ReadBody readBody) noexcept {
+    bool present = false;
+    return read_presence(reader, present) && (!present || readBody(reader));
+}
+
+/** Consumes the body of optional publicity-expiry bank path 0.0.0. */
+[[nodiscard]] bool skip_publicity_expiry_bank(Reader& reader) noexcept {
+    for (std::size_t index = 0; index < kPublicityExpiryCount; ++index) {
+        if (!skip_optional_bits(reader, kScalar64WidthBits)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Consumes the fixed seen-message bank at path 0.0.1. */
+[[nodiscard]] bool skip_seen_message_bank(Reader& reader) noexcept {
+    return reader.skip(fixed_array_width_bits(kSeenMessageCount, kScalar32WidthBits));
+}
+
+/** Consumes optional root branch 0.0 in child descriptor order. */
+[[nodiscard]] bool skip_publicity_and_seen_messages(Reader& reader) noexcept {
+    return read_optional_group(reader, skip_publicity_expiry_bank)
+           && read_optional_group(reader, skip_seen_message_bank);
+}
+
+/** Consumes one present two-scalar element under calibration path 0.1.0. */
+[[nodiscard]] bool skip_calibration_vector(Reader& reader) noexcept {
+    return reader.skip(fixed_array_width_bits(kCalibrationValuesPerVector, kScalar32WidthBits));
+}
+
+/** Consumes present group 0.1.0, including each element's own presence bit. */
+[[nodiscard]] bool skip_group_0_1_0(Reader& reader) noexcept {
+    for (std::size_t index = 0; index < kCalibrationVectorCount; ++index) {
+        if (!read_optional_group(reader, skip_calibration_vector)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Consumes present preference matrix path 0.1.1.62 in row-major descriptor order. */
+[[nodiscard]] bool skip_preference_matrix(Reader& reader) noexcept {
+    for (std::size_t row = 0; row < kPreferenceMatrixRowCount; ++row) {
+        for (std::size_t column = 0; column < kPreferenceMatrixColumnCount; ++column) {
+            if (!skip_optional_bits(reader, kScalar32WidthBits)) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+/** Decodes present preference group 0.1.1 and consumes its optional opaque matrix. */
+[[nodiscard]] bool read_preference_record(Reader& reader, settings::SettingsDelta& delta) noexcept {
+    for (const PreferenceDescriptor& descriptor : kPreferenceDescriptors) {
+        bool present = false;
+        std::uint64_t value = 0;
+        if (!read_optional_scalar(reader, descriptor.encoding, present, value)) {
+            return false;
+        }
+        if (present && descriptor.assign != nullptr) {
+            descriptor.assign(value, delta);
+        }
+    }
+    return read_optional_group(reader, skip_preference_matrix);
+}
+
+/** Decodes the body of present fixed keybinding table path 0.1.2.5 atomically. */
+[[nodiscard]] bool read_key_binding_table(Reader& reader, settings::SettingsDelta& delta) noexcept {
+    settings::bindings::KeyBindings staged{};
+    for (std::size_t nativeSlot = 0; nativeSlot < settings::bindings::kActionsByNativeSlot.size();
+         ++nativeSlot) {
+        std::uint64_t value = 0;
+        if (!read_scalar(reader, kInt32Encoding, value)) {
+            return false;
+        }
+
+        // After bias removal, bits 0-15 are primary and bits 16-31 are secondary. The value
+        // 0x0074 in either half is the protocol's unbound sentinel, not a bindable input.
+        const std::uint32_t packed = static_cast<std::uint32_t>(value);
+        const std::uint16_t primary = static_cast<std::uint16_t>(packed);
+        const std::uint16_t secondary = static_cast<std::uint16_t>(packed >> kBindingHalfWidthBits);
+        const auto action = settings::bindings::kActionsByNativeSlot[nativeSlot];
+        auto& binding = staged.values[static_cast<std::size_t>(action)];
+        if (primary != kUnboundInputCode) {
+            binding.primary = primary;
+        }
+        if (secondary != kUnboundInputCode) {
+            binding.secondary = secondary;
+        }
+    }
+    staged.configured = true;
+    delta.keyBindings = staged;
+    return true;
+}
+
+/** Decodes present binding record 0.1.2 in exact child descriptor order. */
+[[nodiscard]] bool read_binding_record(Reader& reader, settings::SettingsDelta& delta) noexcept {
+    // 0.1.2.0: seed/version marker; structurally consumed but not authoritative.
+    if (!skip_optional_scalar(reader, kInt32Encoding)) {
+        return false;
+    }
+    // 0.1.2.1: client-local one-bit mirror; traversal-only.
+    if (!skip_optional_scalar(reader, kBoolEncoding)) {
+        return false;
+    }
+    // 0.1.2.2: client-local VSync mirror.
+    if (!skip_optional_scalar(reader, kInt8ThreeBitEncoding)) {
+        return false;
+    }
+    // 0.1.2.3: client-local FOV mirror.
+    if (!skip_optional_scalar(reader, kInt32Encoding)) {
+        return false;
+    }
+
+    // 0.1.2.4: authored keybinding source and routing input for the optional table.
+    bool sourceSelectorPresent = false;
+    std::uint64_t sourceSelector = 0;
+    if (!read_optional_scalar(reader, kBoolEncoding, sourceSelectorPresent, sourceSelector)) {
+        return false;
+    }
+    if (sourceSelectorPresent) {
+        delta.keyBindingSource = sourceSelector != 0 ? settings::KeyBindingSource::computer
+                                                     : settings::KeyBindingSource::account;
+    }
+
+    // 0.1.2.5: one presence bit covers the complete 60-entry table.
+    return read_optional_group(reader, [&delta](Reader& tableReader) noexcept {
+        return read_key_binding_table(tableReader, delta);
+    });
+}
+
+/** Consumes present fixed-width group 0.1.3. */
+[[nodiscard]] bool skip_group_0_1_3(Reader& reader) noexcept {
+    return reader.skip(fixed_array_width_bits(kGroup_0_1_3ValueCount, kScalar16WidthBits));
+}
+
+/** Consumes present mixed-width group 0.1.4 without assigning unknown semantics. */
+[[nodiscard]] bool skip_group_0_1_4(Reader& reader) noexcept {
+    for (const std::uint8_t width : kGroup_0_1_4OptionalWidths) {
+        if (!skip_optional_bits(reader, width)) {
+            return false;
+        }
+    }
+    // Field 8 is required u64, field 9 is optional 3-bit, and field 10 is required bool.
+    return reader.skip(kScalar64WidthBits) && skip_optional_bits(reader, kThreeWidthBits)
+           && reader.skip(kBooleanWidthBits);
+}
+
+/** Consumes present fixed-width group 0.1.5. */
+[[nodiscard]] bool skip_group_0_1_5(Reader& reader) noexcept {
+    return reader.skip(fixed_array_width_bits(kGroup_0_1_5ValueCount, kScalar32WidthBits));
+}
+
+/** Consumes present nested entry array 0.1.6.0. */
+[[nodiscard]] bool skip_group_0_1_6_0(Reader& reader) noexcept {
+    for (std::size_t index = 0; index < kGroup_0_1_6EntryCount; ++index) {
+        if (!skip_optional_bits(reader, kScalar16WidthBits)) {
+            return false;
+        }
+    }
+    return reader.skip(fixed_array_width_bits(kGroup_0_1_6FixedWordCount, kScalar32WidthBits));
+}
+
+/** Consumes present group 0.1.6, including optional children 0 and 1. */
+[[nodiscard]] bool skip_group_0_1_6(Reader& reader) noexcept {
+    return read_optional_group(reader, skip_group_0_1_6_0)
+           && skip_optional_bits(reader, kScalar16WidthBits);
+}
+
+/** Consumes present fixed-tail group 0.1.12. */
+[[nodiscard]] bool skip_group_0_1_12(Reader& reader) noexcept {
+    return reader.skip(fixed_array_width_bits(kGroup_0_1_12ValueCount, kScalar16WidthBits))
+           && reader.skip(fixed_array_width_bits(kGroup_0_1_12FixedWordCount, kScalar32WidthBits));
+}
+
+/** Traverses every child of present account branch 0.1 in descriptor order. */
+[[nodiscard]] bool read_account_branch(Reader& reader, settings::SettingsDelta& delta) noexcept {
+    // 0.1.0: calibration vectors.
+    if (!read_optional_group(reader, skip_group_0_1_0)) {
+        return false;
+    }
+
+    // 0.1.1: preference scalars and the optional 3-by-50 matrix.
+    if (!read_optional_group(reader, [&delta](Reader& groupReader) noexcept {
+            return read_preference_record(groupReader, delta);
+        })) {
+        return false;
+    }
+
+    // 0.1.2: local mirrors, keybinding source, and packed binding table.
+    if (!read_optional_group(reader, [&delta](Reader& groupReader) noexcept {
+            return read_binding_record(groupReader, delta);
+        })) {
+        return false;
+    }
+
+    // 0.1.3: four required 16-bit values.
+    if (!read_optional_group(reader, skip_group_0_1_3)) {
+        return false;
+    }
+
+    // 0.1.4: mixed-width record with unknown semantics.
+    if (!read_optional_group(reader, skip_group_0_1_4)) {
+        return false;
+    }
+
+    // 0.1.5: 22 required 32-bit values.
+    if (!read_optional_group(reader, skip_group_0_1_5)) {
+        return false;
+    }
+
+    // 0.1.6: optional 100-entry region, two required words, and optional 16-bit tail.
+    if (!read_optional_group(reader, skip_group_0_1_6)) {
+        return false;
+    }
+
+    // 0.1.7: optional Boolean.
+    if (!skip_optional_bits(reader, kBooleanWidthBits)) {
+        return false;
+    }
+
+    // 0.1.8: optional Boolean.
+    if (!skip_optional_bits(reader, kBooleanWidthBits)) {
+        return false;
+    }
+
+    // 0.1.9: optional Boolean.
+    if (!skip_optional_bits(reader, kBooleanWidthBits)) {
+        return false;
+    }
+
+    // 0.1.10: optional 8-bit scalar.
+    if (!skip_optional_bits(reader, kByteWidthBits)) {
+        return false;
+    }
+
+    // 0.1.11: optional 32-bit scalar.
+    if (!skip_optional_bits(reader, kScalar32WidthBits)) {
+        return false;
+    }
+
+    // 0.1.12: 30 required 16-bit values followed by two required 32-bit words.
+    if (!read_optional_group(reader, skip_group_0_1_12)) {
+        return false;
+    }
+
+    // 0.1.13: optional 32-bit scalar.
+    return skip_optional_bits(reader, kScalar32WidthBits);
+}
+
+/** Consumes both optional length-prefixed blobs following the reflected object. */
+[[nodiscard]] bool skip_outer_blobs(Reader& reader) noexcept {
+    for (std::size_t index = 0; index < kOuterBlobCount; ++index) {
+        bool present = false;
+        std::uint64_t byteCount = 0;
+        if (!read_presence(reader, present)) {
+            return false;
+        }
+        if (present
+            && (!reader.read(kOuterBlobLengthWidthBits, byteCount)
+                || !reader.skip(static_cast<std::size_t>(byteCount) * kByteWidthBits))) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Requires any final partial byte to contain only zero padding. */
+[[nodiscard]] bool finish_padding(Reader& reader) noexcept {
+    const std::size_t remaining = reader.remaining_bits();
+    if (remaining >= kTerminalPaddingLimitBits) {
+        return false;
+    }
+    std::uint64_t padding = 0;
+    return reader.read(static_cast<std::uint8_t>(remaining), padding) && padding == 0
+           && reader.remaining_bits() == 0;
+}
+
+/** Compile-time proof that the declared traversal still matches the catalog's maximum form. */
+namespace schema_size_proof {
+
+[[nodiscard]] consteval std::size_t optional_scalar(std::size_t widthBits) noexcept {
+    return kPresenceWidthBits + widthBits;
+}
+
+[[nodiscard]] consteval std::size_t optional_group(std::size_t bodyBits) noexcept {
+    return kPresenceWidthBits + bodyBits;
+}
+
+[[nodiscard]] consteval std::size_t optional_scalar_array(std::size_t count,
+                                                          std::size_t widthBits) noexcept {
+    return count * optional_scalar(widthBits);
+}
+
+[[nodiscard]] consteval std::size_t preference_fields() noexcept {
+    std::size_t total = 0;
+    for (const PreferenceDescriptor& descriptor : kPreferenceDescriptors) {
+        total += optional_scalar(descriptor.encoding.wireWidthBits);
+    }
+    return total;
+}
+
+[[nodiscard]] consteval std::size_t group_0_1_4_body() noexcept {
+    std::size_t total = 0;
+    for (const std::uint8_t widthBits : kGroup_0_1_4OptionalWidths) {
+        total += optional_scalar(widthBits);
+    }
+    // Field 8 is required u64, field 9 is optional 3-bit, and field 10 is required bool.
+    return total + kScalar64WidthBits + optional_scalar(kThreeWidthBits) + kBooleanWidthBits;
+}
+
+constexpr std::size_t kPublicityExpiryBankBits =
+    optional_group(optional_scalar_array(kPublicityExpiryCount, kScalar64WidthBits));
+constexpr std::size_t kSeenMessageBankBits =
+    optional_group(fixed_array_width_bits(kSeenMessageCount, kScalar32WidthBits));
+constexpr std::size_t kRootMetadataBits =
+    optional_group(kPublicityExpiryBankBits + kSeenMessageBankBits);
+
+constexpr std::size_t kCalibrationGroupBits = optional_group(
+    kCalibrationVectorCount
+    * optional_group(fixed_array_width_bits(kCalibrationValuesPerVector, kScalar32WidthBits)));
+constexpr std::size_t kPreferenceMatrixBits = optional_group(optional_scalar_array(
+    kPreferenceMatrixRowCount * kPreferenceMatrixColumnCount, kScalar32WidthBits));
+constexpr std::size_t kPreferencesBits =
+    optional_group(preference_fields() + kPreferenceMatrixBits);
+constexpr std::size_t kBindingsBits =
+    optional_group(optional_scalar(kScalar32WidthBits) + optional_scalar(kBooleanWidthBits)
+                   + optional_scalar(kThreeWidthBits) + optional_scalar(kScalar32WidthBits)
+                   + optional_scalar(kBooleanWidthBits)
+                   + optional_group(fixed_array_width_bits(kKeyBindingCount, kScalar32WidthBits)));
+constexpr std::size_t kGroup_0_1_3Bits =
+    optional_group(fixed_array_width_bits(kGroup_0_1_3ValueCount, kScalar16WidthBits));
+constexpr std::size_t kGroup_0_1_4Bits = optional_group(group_0_1_4_body());
+constexpr std::size_t kGroup_0_1_5Bits =
+    optional_group(fixed_array_width_bits(kGroup_0_1_5ValueCount, kScalar32WidthBits));
+constexpr std::size_t kGroup_0_1_6Bits = optional_group(
+    optional_group(optional_scalar_array(kGroup_0_1_6EntryCount, kScalar16WidthBits)
+                   + fixed_array_width_bits(kGroup_0_1_6FixedWordCount, kScalar32WidthBits))
+    + optional_scalar(kScalar16WidthBits));
+constexpr std::size_t kAccountTailBits =
+    optional_scalar(kBooleanWidthBits) + optional_scalar(kBooleanWidthBits)
+    + optional_scalar(kBooleanWidthBits) + optional_scalar(kByteWidthBits)
+    + optional_scalar(kScalar32WidthBits);
+constexpr std::size_t kGroup_0_1_12Bits =
+    optional_group(fixed_array_width_bits(kGroup_0_1_12ValueCount, kScalar16WidthBits)
+                   + fixed_array_width_bits(kGroup_0_1_12FixedWordCount, kScalar32WidthBits));
+constexpr std::size_t kGroup_0_1_13Bits = optional_scalar(kScalar32WidthBits);
+
+constexpr std::size_t kAccountBranchBits =
+    optional_group(kCalibrationGroupBits + kPreferencesBits + kBindingsBits + kGroup_0_1_3Bits
+                   + kGroup_0_1_4Bits + kGroup_0_1_5Bits + kGroup_0_1_6Bits + kAccountTailBits
+                   + kGroup_0_1_12Bits + kGroup_0_1_13Bits);
+constexpr std::size_t kSchemaBits = kRootMetadataBits + kAccountBranchBits;
+
+static_assert(kRootMetadataBits == kCatalogRootMetadataMaximumBits);
+static_assert(kPreferencesBits == kCatalogPreferencesMaximumBits);
+static_assert(kBindingsBits == kCatalogBindingsMaximumBits);
+static_assert(kAccountBranchBits == kCatalogAccountBranchMaximumBits);
+static_assert(kSchemaBits == kCatalogMaximumBits);
+
+} // namespace schema_size_proof
+
+} // namespace
+
+/** Decodes the complete schema-0x80807603 request without touching authoritative State. */
+bool parse_request(const Message& message, Request& output) noexcept {
+    output = {};
+    if (message.opcode != kOpcode) {
+        return false;
+    }
+
+    Reader reader(message.payload);
+    Request candidate{};
+    if (!read_optional_group(reader, skip_publicity_and_seen_messages)
+        || !read_optional_group(reader,
+                                [&candidate](Reader& groupReader) noexcept {
+                                    return read_account_branch(groupReader, candidate.settings);
+                                })
+        || !skip_outer_blobs(reader) || !finish_padding(reader)) {
+        return false;
+    }
+
+    output = candidate;
+    return true;
+}
+
+} // namespace sunrise::middleware::web_service::messages::opcode701

+ 31 - 0
Sunrise/src/middleware/web_service/messages/opcode701/opcode701_codec.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../../../../state/account/settings/settings_delta.h"
+#include "../../web_service_envelope.h"
+
+namespace sunrise::middleware::web_service::messages::opcode701 {
+
+/** Web Service opcode used by the Client's account-settings writeback. */
+inline constexpr std::uint16_t kOpcode = 701;
+
+/** Semantic result decoded from one schema-0x80807603 request. */
+struct Request {
+    state::account::settings::SettingsDelta settings;
+};
+
+/**
+ * Decodes the complete presence-driven opcode-701 request body.
+ *
+ * Unsupported schema branches are still traversed so every later field is read at its actual
+ * wire position. Output is cleared on entry; decoded values replace it only after the entire
+ * schema, optional outer blobs, and zero terminal padding validate.
+ *
+ * @param message Parsed Web Service envelope whose payload begins at schema bit zero.
+ * @param output Receives supported fields, the authored binding source, and the atomic table.
+ * @return True only when opcode and complete request encoding are valid.
+ */
+[[nodiscard]] bool parse_request(const Message& message, Request& output) noexcept;
+
+} // namespace sunrise::middleware::web_service::messages::opcode701

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

@@ -254,6 +254,12 @@ bool process(const ServiceRoute& route,
             web_service::mutation_if<state::PendingProfileItemAcquisition>(webOutcome);
         const auto* itemDismantle =
             web_service::mutation_if<state::PendingItemDismantle>(webOutcome);
+        const auto* settingsUpdate =
+            web_service::mutation_if<state::PendingSettingsUpdate>(webOutcome);
+        if (settingsUpdate != nullptr) {
+            // WS-701 promises no immediate QueueZ after-image, so State alone is delayed.
+            outcome.transaction.emplace<state::PendingSettingsUpdate>(*settingsUpdate);
+        }
         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 it authoritative. Stage that

+ 2 - 1
Sunrise/src/server/bap/encrypted/encrypted_runtime.cpp

@@ -293,7 +293,8 @@ bool consume(Session& session,
         || transaction_if<CurrentActivityTransaction>(outcome) != nullptr
         || transaction_if<ItemAcquisitionTransaction>(outcome) != nullptr
         || transaction_if<ProfileItemAcquisitionTransaction>(outcome) != nullptr
-        || transaction_if<ItemDismantleTransaction>(outcome) != nullptr;
+        || transaction_if<ItemDismantleTransaction>(outcome) != nullptr
+        || transaction_if<state::PendingSettingsUpdate>(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);

+ 2 - 1
Sunrise/src/server/bap/encrypted/internal.h

@@ -104,7 +104,8 @@ struct ServiceOutcome {
                                      CurrentActivityTransaction,
                                      ItemAcquisitionTransaction,
                                      ProfileItemAcquisitionTransaction,
-                                     ItemDismantleTransaction>;
+                                     ItemDismantleTransaction,
+                                     state::PendingSettingsUpdate>;
     Transaction transaction{};
 };
 

+ 15 - 9
Sunrise/src/server/bap/encrypted/transactions/service_outcome_commit.cpp

@@ -13,15 +13,15 @@
 #include "../internal.h"
 
 namespace sunrise::server::bap::encrypted::transactions {
-namespace {
 
 namespace slots = state::activity::entity_slots;
 
 /** Log names for each lease operation, in the enum's own order. */
-constexpr std::array<const char*, 4> kLeaseKinds = {"none", "join", "grant", "release"};
+static constexpr std::array<const char*, 4> kLeaseKinds = {"none", "join", "grant", "release"};
 
 /** Retains one newly committed private ActivityClient generation for its BAP link. */
-[[nodiscard]] bool retain_private(std::uint64_t sessionId, Publication& publication) noexcept {
+[[nodiscard]] static bool retain_private(std::uint64_t sessionId,
+                                         Publication& publication) noexcept {
     state::activity::SessionBinding binding{};
     if (!state::activity::snapshot_binding(sessionId, binding)
         || !state::activity::retain_binding(binding)) {
@@ -37,8 +37,8 @@ constexpr std::array<const char*, 4> kLeaseKinds = {"none", "join", "grant", "re
 }
 
 /** Retains one exact advertised public target before its join mutation commits. */
-[[nodiscard]] bool retain_public(const activity_message::ActivityPlan& plan,
-                                 Publication& publication) noexcept {
+[[nodiscard]] static bool retain_public(const activity_message::ActivityPlan& plan,
+                                        Publication& publication) noexcept {
     server::gameplay::group::HostSessionBinding current{};
     if (!server::gameplay::group::host_session_for_activity(plan.sessionId, current)
         || current.generation != plan.publicHost.generation
@@ -70,7 +70,7 @@ constexpr std::array<const char*, 4> kLeaseKinds = {"none", "join", "grant", "re
 }
 
 /** Releases provisional activity owners when the following State commit fails. */
-void discard_activity_publication(Publication& publication) noexcept {
+static void discard_activity_publication(Publication& publication) noexcept {
     if (publication.activity.hostGeneration != 0) {
         server::gameplay::group::release_host_session(publication.activity.hostGeneration);
     }
@@ -87,7 +87,7 @@ void discard_activity_publication(Publication& publication) noexcept {
  * @param mutation Plan as it was before the commit consumed it.
  * @param committed Whether the commit succeeded.
  */
-void report_lease(const slots::PendingMutation& mutation, bool committed) noexcept {
+static void report_lease(const slots::PendingMutation& mutation, bool committed) noexcept {
     std::size_t held = 0;
     std::size_t reserved = 0;
     const bool known = slots::lease_counts(mutation.sessionId, held, reserved);
@@ -113,8 +113,6 @@ void report_lease(const slots::PendingMutation& mutation, bool committed) noexce
     }
 }
 
-} // namespace
-
 /**
  * Commits at most one delayed State transaction.
  * @param outcome Checked service result whose pending transaction is used up.
@@ -206,6 +204,14 @@ bool commit(ServiceOutcome& outcome, Publication& publication, const char*& reas
         reason = "matchmaking";
         return state::matchmaking::commit(*mutation);
     }
+    if (auto* mutation = transaction_if<state::PendingSettingsUpdate>(outcome)) {
+        const bool committed = state::commit_settings_update(*mutation);
+        core::log::write(core::log::Channel::server,
+                         committed ? core::log::Level::debug : core::log::Level::warn,
+                         committed ? "ev=ws701 stage=transaction_commit result=ok"
+                                   : "ev=ws701 stage=transaction_commit result=fail");
+        return committed;
+    }
     if (auto* transaction = transaction_if<EquipmentSwapTransaction>(outcome)) {
         const bool isSubclassSlot =
             transaction->pending.equipmentSlotIndex

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

@@ -12,6 +12,7 @@
 #include "../../middleware/web_service/messages/opcode403.h"
 #include "../../middleware/web_service/messages/opcode406.h"
 #include "../../middleware/web_service/messages/opcode504.h"
+#include "../../middleware/web_service/messages/opcode701/opcode701_codec.h"
 #include "../../middleware/web_service/messages/opcode801.h"
 #include "../../middleware/web_service/messages/opcode903.h"
 #include "../../state/account/account_state.h"
@@ -96,6 +97,42 @@ void report_acquisition_preparation(const middleware::web_service::Message& mess
 
 } // namespace
 
+/** Decodes and prepares one sparse account-settings writeback without publishing State. */
+state::SettingsUpdateDisposition mutate_settings(const middleware::web_service::Message& message,
+                                                 Outcome& outcome) noexcept {
+    namespace opcode701 = middleware::web_service::messages::opcode701;
+
+    opcode701::Request request{};
+    if (!opcode701::parse_request(message, request)) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::warn,
+                         "ev=ws701 stage=prepare result=rejected reason=parse");
+        return state::SettingsUpdateDisposition::rejected;
+    }
+
+    state::PendingSettingsUpdate mutation{};
+    const state::SettingsUpdateDisposition disposition =
+        state::prepare_settings_update(request.settings, mutation);
+    if (disposition == state::SettingsUpdateDisposition::preparedMutation) {
+        outcome.mutation = mutation;
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::debug,
+                         "ev=ws701 stage=prepare result=ready");
+        return disposition;
+    }
+    if (disposition == state::SettingsUpdateDisposition::acceptedNoChange) {
+        core::log::write(core::log::Channel::server,
+                         core::log::Level::debug,
+                         "ev=ws701 stage=prepare result=no_change");
+        return disposition;
+    }
+
+    core::log::write(core::log::Channel::server,
+                     core::log::Level::warn,
+                     "ev=ws701 stage=prepare result=rejected reason=validation");
+    return state::SettingsUpdateDisposition::rejected;
+}
+
 /** 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,

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

@@ -17,6 +17,9 @@ void mutate_subclass_selection(const middleware::web_service::Message& message,
 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;
+/** Decodes and prepares one WS-701 settings update without publishing State. */
+[[nodiscard]] state::SettingsUpdateDisposition
+mutate_settings(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;
 

+ 8 - 3
Sunrise/src/server/web_service/web_service_runtime.cpp

@@ -22,6 +22,7 @@
 #include "../../middleware/web_service/messages/opcode503.h"
 #include "../../middleware/web_service/messages/opcode504.h"
 #include "../../middleware/web_service/messages/opcode601/opcode601_codec.h"
+#include "../../middleware/web_service/messages/opcode701/opcode701_codec.h"
 #include "../../middleware/web_service/messages/opcode702.h"
 #include "../../middleware/web_service/messages/opcode801.h"
 #include "../../middleware/web_service/messages/opcode901/opcode901_codec.h"
@@ -290,9 +291,10 @@ bool consume(std::span<const std::byte> request,
                             && messages::opcode206::parse_request(message, subscription);
 
     // The action runs before its reply is encoded, because the reply reports whether it worked.
-    // An action fills the outcome only once it has prepared its whole transition, so an outcome
-    // still empty afterwards is that action refusing the request. Nothing is published here.
+    // Most actions fill the outcome only after preparing a whole transition. WS-701 also accepts
+    // a valid no-op heartbeat, so that one success is tracked separately from mutation presence.
     bool dispatched = true;
+    bool acceptedWithoutMutation = false;
     if (message.opcode == messages::opcode504::kOpcode) {
         select_character(message, outcome);
     } else if (message.opcode == messages::opcode402::kOpcode) {
@@ -309,6 +311,9 @@ bool consume(std::span<const std::byte> request,
         mutate_equipped_socket_plug(message, outcome);
     } else if (message.opcode == messages::opcode406::kOpcode) {
         mutate_item_state(message, outcome);
+    } else if (message.opcode == messages::opcode701::kOpcode) {
+        const state::SettingsUpdateDisposition disposition = mutate_settings(message, outcome);
+        acceptedWithoutMutation = disposition == state::SettingsUpdateDisposition::acceptedNoChange;
     } else if (message.opcode == messages::opcode1820::kOpcode) {
         acquire_item(message, outcome);
     } else {
@@ -323,7 +328,7 @@ bool consume(std::span<const std::byte> request,
         // Nothing is published from here. A staged mutation re-encodes this with its own revision.
         status.value = middleware::web_service::kNoFamily4Publication;
     }
-    if (dispatched && !prepared) {
+    if (dispatched && !prepared && !acceptedWithoutMutation) {
         status.code = middleware::web_service::kRefusedStatusCode;
     }
     if (!middleware::web_service::encode_response(message, shape, status, response, written)) {

+ 6 - 2
Sunrise/src/server/web_service/web_service_runtime.h

@@ -17,7 +17,10 @@ struct Outcome {
     /** An opcode-504 pick moved the selection and its Family-4 object still has to follow. */
     bool hasSelectedCharacter{};
     std::uint64_t selectedCharacterSoid{};
-    /** A request prepares at most one State mutation; the alternative owns only that payload. */
+    /**
+     * A request prepares at most one State mutation; the alternative owns only that payload.
+     * Rejected actions and accepted no-op writebacks both leave this empty.
+     */
     using Mutation = std::variant<std::monostate,
                                   state::PendingEquipmentSwap,
                                   state::PendingSubclassSelection,
@@ -25,7 +28,8 @@ struct Outcome {
                                   state::PendingProfileItemAcquisition,
                                   state::PendingItemDismantle,
                                   state::PendingSocketPlug,
-                                  state::PendingItemState>;
+                                  state::PendingItemState,
+                                  state::PendingSettingsUpdate>;
     Mutation mutation{};
 };
 

+ 16 - 0
Sunrise/src/state/account/settings/key_bindings.h

@@ -7,6 +7,18 @@
 
 namespace sunrise::state::account::settings::bindings {
 
+/** Low byte of a native input code. The high byte carries at most one modifier flag. */
+inline constexpr std::uint16_t kInputCodeMask = 0x00FF;
+inline constexpr std::uint16_t kModifierMask = 0xFF00;
+/** Native table value used on the wire to mean that a binding half is empty. */
+inline constexpr std::uint16_t kUnboundInputCode = 0x0074;
+/** Highest real input index. The following value is the unbound sentinel, not a key. */
+inline constexpr std::uint16_t kMaximumBindableInputCode = kUnboundInputCode - 1;
+/** Native modifier flags occupy one mutually exclusive bit above the low-byte input index. */
+inline constexpr std::uint16_t kAltModifierFlag = 0x0100;
+inline constexpr std::uint16_t kControlModifierFlag = 0x0200;
+inline constexpr std::uint16_t kShiftModifierFlag = 0x0400;
+
 /** Authored actions whose primary and secondary inputs are replicated with account settings. */
 enum class Action : std::uint8_t {
     fire,
@@ -79,6 +91,8 @@ inline constexpr std::size_t kActionCount = static_cast<std::size_t>(Action::cou
 struct Binding {
     std::optional<std::uint16_t> primary;
     std::optional<std::uint16_t> secondary;
+
+    bool operator==(const Binding&) const = default;
 };
 
 /** Fixed authored input table independent of the native packed account representation. */
@@ -86,6 +100,8 @@ struct KeyBindings {
     std::array<Binding, kActionCount> values;
     /** True only when configuration supplied every supported action. */
     bool configured{};
+
+    bool operator==(const KeyBindings&) const = default;
 };
 
 } // namespace sunrise::state::account::settings::bindings

+ 89 - 0
Sunrise/src/state/account/settings/native_key_binding_map.h

@@ -0,0 +1,89 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+#include "key_bindings.h"
+
+namespace sunrise::state::account::settings::bindings {
+
+/** Array position is the fixed native account slot for the named semantic State action. */
+inline constexpr std::array<Action, kActionCount> kActionsByNativeSlot{
+    Action::fire,
+    Action::toggleZoom,
+    Action::holdZoom,
+    Action::melee,
+    Action::grenade,
+    Action::super,
+    Action::reload,
+    Action::lightAttack,
+    Action::heavyAttack,
+    Action::block,
+    Action::switchWeapons,
+    Action::nextWeapon,
+    Action::previousWeapon,
+    Action::primaryWeapon,
+    Action::specialWeapon,
+    Action::heavyWeapon,
+    Action::moveForward,
+    Action::moveBackward,
+    Action::moveLeft,
+    Action::moveRight,
+    Action::jump,
+    Action::toggleCrouch,
+    Action::holdCrouch,
+    Action::toggleSprint,
+    Action::holdSprint,
+    Action::vehicleBoost,
+    Action::vehicleBrake,
+    Action::vehicleZoom,
+    Action::vehicleFirePrimary,
+    Action::vehicleFireSecondary,
+    Action::vehicleExit,
+    Action::interact,
+    Action::highlightPlayer,
+    Action::emoteOne,
+    Action::emoteTwo,
+    Action::emoteThree,
+    Action::emoteFour,
+    Action::airMove,
+    Action::classAbility,
+    Action::deathCameraZoomIn,
+    Action::deathCameraZoomOut,
+    Action::pushToTalk,
+    Action::uiGamepadButtonBack,
+    Action::uiOpenDirector,
+    Action::uiOpenDirectorStoreTab,
+    Action::uiOpenDirectorPursuitsTab,
+    Action::uiOpenDirectorMapTab,
+    Action::uiOpenDirectorDestinationsTab,
+    Action::uiOpenDirectorRosterTab,
+    Action::uiOpenDirectorSeasonsTab,
+    Action::uiOpenStartMenuAlternative,
+    Action::uiOpenStartMenuRecordsTab,
+    Action::uiOpenStartMenuCollectionsTab,
+    Action::uiOpenStartMenuClanTab,
+    Action::uiOpenStartMenuInventoryTab,
+    Action::uiOpenStartMenuSettingsTab,
+    Action::uiOpenExitDialogConfirm,
+    Action::uiAbortActivity,
+    Action::uiTextChatToggleState,
+    Action::screenshot,
+};
+
+/** Verifies that every semantic action owns exactly one native account slot. */
+[[nodiscard]] consteval bool complete_native_slot_map() noexcept {
+    std::array<bool, kActionCount> seen{};
+    for (const Action action : kActionsByNativeSlot) {
+        const std::size_t stateIndex = static_cast<std::size_t>(action);
+        if (stateIndex >= seen.size() || seen[stateIndex]) {
+            return false;
+        }
+        seen[stateIndex] = true;
+    }
+    return true;
+}
+
+static_assert(complete_native_slot_map());
+
+} // namespace sunrise::state::account::settings::bindings

+ 121 - 0
Sunrise/src/state/account/settings/settings_delta.cpp

@@ -0,0 +1,121 @@
+#include "settings_delta.h"
+
+#include <optional>
+
+#include "settings_state.h"
+
+namespace sunrise::state::account::settings {
+
+/** Assigns a sparse value without treating present zero or false as absence. */
+template <typename Value>
+static void apply_if_present(const std::optional<Value>& update, Value& target) noexcept {
+    if (update.has_value()) {
+        target = *update;
+    }
+}
+
+/** Applies the supported controller and mouse fields. */
+static void apply_controls(const ControlsDelta& update, Controls& target) noexcept {
+    apply_if_present(update.buttonLayout, target.buttonLayout);
+    apply_if_present(update.movementMode, target.movementMode);
+    apply_if_present(update.controllerLookSensitivity, target.controllerLookSensitivity);
+    apply_if_present(update.controllerInvertVertical, target.controllerInvertVertical);
+    apply_if_present(update.controllerAutoLookCentering, target.controllerAutoLookCentering);
+    apply_if_present(update.controllerVibration, target.controllerVibration);
+    apply_if_present(update.controllerSwapShoulders, target.controllerSwapShoulders);
+    apply_if_present(update.controllerInvertHorizontal, target.controllerInvertHorizontal);
+    apply_if_present(update.mouseLookSensitivity, target.mouseLookSensitivity);
+    apply_if_present(update.mouseInvertVertical, target.mouseInvertVertical);
+    apply_if_present(update.mouseInvertHorizontal, target.mouseInvertHorizontal);
+    apply_if_present(update.unidentifiedToggle, target.unidentifiedToggle);
+    apply_if_present(update.mouseAimSmoothing, target.mouseAimSmoothing);
+    apply_if_present(update.adsSensitivityModifier, target.adsSensitivityModifier);
+    apply_if_present(update.doublePressDelay, target.doublePressDelay);
+}
+
+/** Applies the supported voice and volume fields. */
+static void apply_audio(const AudioDelta& update, Audio& target) noexcept {
+    apply_if_present(update.voiceOutputMode, target.voiceOutputMode);
+    apply_if_present(update.teamVoiceChannel, target.teamVoiceChannel);
+    apply_if_present(update.reservedMode, target.reservedMode);
+    apply_if_present(update.migrationVersion, target.migrationVersion);
+    apply_if_present(update.chatVolume, target.chatVolume);
+    apply_if_present(update.muteWhenUnfocused, target.muteWhenUnfocused);
+    apply_if_present(update.soundEffectsVolume, target.soundEffectsVolume);
+    apply_if_present(update.dialogueVolume, target.dialogueVolume);
+    apply_if_present(update.musicVolume, target.musicVolume);
+}
+
+/** Applies the account-backed display fields present in the preference record. */
+static void apply_display(const DisplayDelta& update, Display& target) noexcept {
+    apply_if_present(update.brightness, target.brightness);
+    apply_if_present(update.showFps, target.showFps);
+    apply_if_present(update.hdrMode, target.hdrMode);
+    apply_if_present(update.calibrationPrimary, target.calibrationPrimary);
+    apply_if_present(update.calibrationAlpha, target.calibrationAlpha);
+}
+
+/** Applies the supported HUD, subtitle, reticle, and text fields. */
+static void apply_interface(const InterfaceDelta& update, Interface& target) noexcept {
+    apply_if_present(update.subtitlesMode, target.subtitlesMode);
+    apply_if_present(update.colorblindMode, target.colorblindMode);
+    apply_if_present(update.helmetMode, target.helmetMode);
+    apply_if_present(update.hudOpacity, target.hudOpacity);
+    apply_if_present(update.displayHints, target.displayHints);
+    apply_if_present(update.backgroundOpacity, target.backgroundOpacity);
+    apply_if_present(update.reticleLocation, target.reticleLocation);
+    apply_if_present(update.reticleColor, target.reticleColor);
+    apply_if_present(update.textSize, target.textSize);
+    apply_if_present(update.textColor, target.textColor);
+    apply_if_present(update.textBackgroundStyle, target.textBackgroundStyle);
+    apply_if_present(update.textBackgroundOpacity, target.textBackgroundOpacity);
+    apply_if_present(update.reservedTextMode, target.reservedTextMode);
+    apply_if_present(update.subtitleOptionsEntry, target.subtitleOptionsEntry);
+}
+
+/** Applies the supported matchmaking, identity, voice, and chat fields. */
+static void apply_social(const SocialDelta& update, Social& target) noexcept {
+    apply_if_present(update.preferGoodConnection, target.preferGoodConnection);
+    apply_if_present(update.textChatMode, target.textChatMode);
+    apply_if_present(update.showRealNames, target.showRealNames);
+    apply_if_present(update.clanInviteNotifications, target.clanInviteNotifications);
+    apply_if_present(update.profanityFilter, target.profanityFilter);
+    apply_if_present(update.voiceChatEnabled, target.voiceChatEnabled);
+    apply_if_present(update.whisperChatMode, target.whisperChatMode);
+    apply_if_present(update.teamChatJoinMode, target.teamChatJoinMode);
+    apply_if_present(update.localChatJoinMode, target.localChatJoinMode);
+    apply_if_present(update.clanChatJoinMode, target.clanChatJoinMode);
+    apply_if_present(update.chatAutoHideMode, target.chatAutoHideMode);
+}
+
+/** Applies a sparse update to a local candidate without exposing partial or invalid output. */
+bool apply_delta(const SettingsDelta& delta,
+                 const AccountSettings& before,
+                 AccountSettings& after,
+                 bool& changed) noexcept {
+    after = {};
+    changed = false;
+    if (!valid(before)) {
+        return false;
+    }
+
+    AccountSettings candidate = before;
+    apply_controls(delta.controls, candidate.controls);
+    apply_audio(delta.audio, candidate.audio);
+    apply_display(delta.display, candidate.display);
+    apply_interface(delta.interface, candidate.interface);
+    apply_social(delta.social, candidate.social);
+    apply_if_present(delta.keyBindingSource, candidate.keyBindingSource);
+    if (candidate.keyBindingSource == KeyBindingSource::account) {
+        apply_if_present(delta.keyBindings, candidate.keyBindings);
+    }
+    if (!valid(candidate)) {
+        return false;
+    }
+
+    changed = candidate != before;
+    after = candidate;
+    return true;
+}
+
+} // namespace sunrise::state::account::settings

+ 120 - 0
Sunrise/src/state/account/settings/settings_delta.h

@@ -0,0 +1,120 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+
+#include "key_bindings.h"
+
+namespace sunrise::state::account::settings {
+
+enum class KeyBindingSource : std::uint8_t;
+struct AccountSettings;
+
+/** Sparse controller and mouse settings supplied by one client writeback. */
+struct ControlsDelta {
+    std::optional<std::int8_t> buttonLayout;
+    std::optional<std::int8_t> movementMode;
+    std::optional<std::int8_t> controllerLookSensitivity;
+    std::optional<bool> controllerInvertVertical;
+    std::optional<bool> controllerAutoLookCentering;
+    std::optional<bool> controllerVibration;
+    std::optional<bool> controllerSwapShoulders;
+    std::optional<bool> controllerInvertHorizontal;
+    std::optional<std::int32_t> mouseLookSensitivity;
+    std::optional<bool> mouseInvertVertical;
+    std::optional<bool> mouseInvertHorizontal;
+    std::optional<bool> unidentifiedToggle;
+    std::optional<bool> mouseAimSmoothing;
+    std::optional<float> adsSensitivityModifier;
+    std::optional<std::int8_t> doublePressDelay;
+};
+
+/** Sparse audio settings supplied by one client writeback. */
+struct AudioDelta {
+    std::optional<std::int8_t> voiceOutputMode;
+    std::optional<std::int8_t> teamVoiceChannel;
+    std::optional<std::int8_t> reservedMode;
+    std::optional<std::int8_t> migrationVersion;
+    std::optional<std::int8_t> chatVolume;
+    std::optional<bool> muteWhenUnfocused;
+    std::optional<std::int8_t> soundEffectsVolume;
+    std::optional<std::int8_t> dialogueVolume;
+    std::optional<std::int8_t> musicVolume;
+};
+
+/** Sparse display settings supplied by one client writeback. */
+struct DisplayDelta {
+    std::optional<std::int8_t> brightness;
+    std::optional<bool> showFps;
+    std::optional<std::int8_t> hdrMode;
+    std::optional<float> calibrationPrimary;
+    std::optional<float> calibrationAlpha;
+};
+
+/** Sparse interface settings supplied by one client writeback. */
+struct InterfaceDelta {
+    std::optional<std::int8_t> subtitlesMode;
+    std::optional<std::int8_t> colorblindMode;
+    std::optional<std::int8_t> helmetMode;
+    std::optional<std::int8_t> hudOpacity;
+    std::optional<bool> displayHints;
+    std::optional<std::int8_t> backgroundOpacity;
+    std::optional<std::int8_t> reticleLocation;
+    std::optional<std::int8_t> reticleColor;
+    std::optional<std::int8_t> textSize;
+    std::optional<std::int8_t> textColor;
+    std::optional<std::int8_t> textBackgroundStyle;
+    std::optional<std::int8_t> textBackgroundOpacity;
+    std::optional<std::int8_t> reservedTextMode;
+    std::optional<std::int8_t> subtitleOptionsEntry;
+};
+
+/** Sparse social settings supplied by one client writeback. */
+struct SocialDelta {
+    std::optional<bool> preferGoodConnection;
+    std::optional<std::int8_t> textChatMode;
+    std::optional<bool> showRealNames;
+    std::optional<bool> clanInviteNotifications;
+    std::optional<bool> profanityFilter;
+    std::optional<bool> voiceChatEnabled;
+    std::optional<std::int8_t> whisperChatMode;
+    std::optional<std::int8_t> teamChatJoinMode;
+    std::optional<std::int8_t> localChatJoinMode;
+    std::optional<std::int8_t> clanChatJoinMode;
+    std::optional<std::int8_t> chatAutoHideMode;
+};
+
+/**
+ * Sparse account-settings writeback plus binding-table routing information.
+ *
+ * Every scalar uses optional presence so zero and false remain ordinary authored values. The
+ * keybinding source is both an authored setting and routing input for the binding-table merge.
+ * The keybinding table is one optional fixed-size object; individual slots cannot be partially
+ * published by the decoder or committed by State.
+ */
+struct SettingsDelta {
+    ControlsDelta controls;
+    AudioDelta audio;
+    DisplayDelta display;
+    InterfaceDelta interface;
+    SocialDelta social;
+    std::optional<KeyBindingSource> keyBindingSource;
+    std::optional<bindings::KeyBindings> keyBindings;
+};
+
+/**
+ * Applies only present fields to one complete settings object and validates the result.
+ * A binding table is applied only when the resulting binding source is account-backed. A table
+ * accompanying computer-local bindings is intentionally ignored because it may be stale.
+ * @param delta Sparse client-authored values.
+ * @param before Complete authoritative settings before the update.
+ * @param after Receives the complete validated candidate; cleared on failure.
+ * @param changed Receives whether any supported semantic value differs.
+ * @return True when both the input settings and merged candidate are valid.
+ */
+[[nodiscard]] bool apply_delta(const SettingsDelta& delta,
+                               const AccountSettings& before,
+                               AccountSettings& after,
+                               bool& changed) noexcept;
+
+} // namespace sunrise::state::account::settings

+ 52 - 23
Sunrise/src/state/account/settings/settings_state.cpp

@@ -2,11 +2,13 @@
 
 #include <array>
 #include <cstddef>
+#include <optional>
 
 namespace sunrise::state::account::settings {
 namespace {
 
-template <typename Value> struct Range {
+/** Inclusive domain for one authored setting value. */
+template <typename Value> struct SettingsRange {
     Value minimum;
     Value maximum;
 };
@@ -14,61 +16,62 @@ template <typename Value> struct Range {
 /** The controller layout menu publishes these 7 stored values. */
 constexpr std::array<std::int8_t, 7> kButtonLayouts{0, 1, 2, 3, 5, 6, 9};
 /** The movement-layout menu stores 4 choices, 0 to 3. */
-constexpr Range<std::int8_t> kMovementModes{0, 3};
+constexpr SettingsRange<std::int8_t> kMovementModes{0, 3};
 /** Controller sensitivity stores the 10 menu choices as 0 to 9. */
-constexpr Range<std::int8_t> kControllerSensitivity{0, 9};
+constexpr SettingsRange<std::int8_t> kControllerSensitivity{0, 9};
 /** Mouse sensitivity accepts the menu's 1 to 100 scale. */
-constexpr Range<std::int32_t> kMouseSensitivity{1, 100};
+constexpr SettingsRange<std::int32_t> kMouseSensitivity{1, 100};
 /** ADS sensitivity stores the menu's 0.5 through 1.5 multiplier. */
-constexpr Range<float> kAdsSensitivity{0.5F, 1.5F};
+constexpr SettingsRange<float> kAdsSensitivity{0.5F, 1.5F};
 /** Double-press delay stores the 5 menu choices, 0 to 4. */
-constexpr Range<std::int8_t> kDoublePressDelay{0, 4};
+constexpr SettingsRange<std::int8_t> kDoublePressDelay{0, 4};
 
 /** Two-choice selectors store only the menu values 0 and 1. */
-constexpr Range<std::int8_t> kTwoChoiceSelector{0, 1};
+constexpr SettingsRange<std::int8_t> kTwoChoiceSelector{0, 1};
 /** Voice output stores 3 menu choices, 0 to 2. */
-constexpr Range<std::int8_t> kVoiceOutputMode{0, 2};
+constexpr SettingsRange<std::int8_t> kVoiceOutputMode{0, 2};
 /** Chat volume exposes 9 menu levels, 0 to 8. */
-constexpr Range<std::int8_t> kChatVolume{0, 8};
+constexpr SettingsRange<std::int8_t> kChatVolume{0, 8};
 /** Sound, dialogue and music expose 11 levels, 0 to 10. */
-constexpr Range<std::int8_t> kAudioVolume{0, 10};
+constexpr SettingsRange<std::int8_t> kAudioVolume{0, 10};
 
 /** Brightness stores the 7 menu choices, 0 to 6. */
-constexpr Range<std::int8_t> kBrightness{0, 6};
+constexpr SettingsRange<std::int8_t> kBrightness{0, 6};
 /** VSync stores the DXGI presentation interval from 0 to 4. */
-constexpr Range<std::uint8_t> kVerticalSyncInterval{0, 4};
+constexpr SettingsRange<std::uint8_t> kVerticalSyncInterval{0, 4};
 /** Field of view accepts the game's full 55 through 155 degree range. */
-constexpr Range<std::int32_t> kFieldOfView{55, 155};
+constexpr SettingsRange<std::int32_t> kFieldOfView{55, 155};
 /** The first unidentified calibration field uses the working renderer fallback. */
 constexpr float kCalibrationPrimary = 10000.0F;
 /** The second unidentified calibration field uses the working renderer alpha. */
 constexpr float kCalibrationAlpha = 0.0F;
 
 /** Subtitle mode stores 3 menu choices, 0 to 2. */
-constexpr Range<std::int8_t> kSubtitlesMode{0, 2};
+constexpr SettingsRange<std::int8_t> kSubtitlesMode{0, 2};
 /** Colorblind mode stores 4 menu choices, 0 to 3. */
-constexpr Range<std::int8_t> kColorblindMode{0, 3};
+constexpr SettingsRange<std::int8_t> kColorblindMode{0, 3};
 /** HUD opacity stores 4 menu choices, 0 to 3. */
-constexpr Range<std::int8_t> kHudOpacity{0, 3};
+constexpr SettingsRange<std::int8_t> kHudOpacity{0, 3};
 /** Background opacity stores 5 menu choices, 0 to 4. */
-constexpr Range<std::int8_t> kBackgroundOpacity{0, 4};
+constexpr SettingsRange<std::int8_t> kBackgroundOpacity{0, 4};
 /** Reticle color stores 7 menu choices, 0 to 6. */
-constexpr Range<std::int8_t> kReticleColor{0, 6};
+constexpr SettingsRange<std::int8_t> kReticleColor{0, 6};
 /** Text size stores 5 menu choices, 0 to 4. */
-constexpr Range<std::int8_t> kTextSize{0, 4};
+constexpr SettingsRange<std::int8_t> kTextSize{0, 4};
 /** Text color and background style each store 4 choices, 0 to 3. */
-constexpr Range<std::int8_t> kTextPresentationMode{0, 3};
+constexpr SettingsRange<std::int8_t> kTextPresentationMode{0, 3};
 /** Unidentified text settings stay on their only known working value. */
 constexpr std::int8_t kUnidentifiedTextValue = 0;
 /** Text chat stores 4 menu choices, 0 to 3. */
-constexpr Range<std::int8_t> kTextChatMode{0, 3};
+constexpr SettingsRange<std::int8_t> kTextChatMode{0, 3};
 
 /**
  * Tests one scalar against an inclusive Sunrise settings policy range.
  * @param range Inclusive supported range.
  * @return True when the value is inside the range.
  */
-template <typename Value> [[nodiscard]] bool within(Value value, Range<Value> range) noexcept {
+template <typename Value>
+[[nodiscard]] bool within(Value value, SettingsRange<Value> range) noexcept {
     return value >= range.minimum && value <= range.maximum;
 }
 
@@ -157,13 +160,39 @@ template <typename Value, std::size_t Count>
            && within(value.chatAutoHideMode, kTwoChoiceSelector);
 }
 
+/** Checks one optional native input code without interpreting an absent binding half. */
+[[nodiscard]] bool valid_input_code(const std::optional<std::uint16_t>& value) noexcept {
+    if (!value.has_value()) {
+        return true;
+    }
+    const std::uint16_t input = *value & bindings::kInputCodeMask;
+    const std::uint16_t modifier = *value & bindings::kModifierMask;
+    return input <= bindings::kMaximumBindableInputCode
+           && (modifier == 0 || modifier == bindings::kAltModifierFlag
+               || modifier == bindings::kControlModifierFlag
+               || modifier == bindings::kShiftModifierFlag);
+}
+
+/** Checks every fixed binding row and rejects unsupported or combined modifier bits. */
+[[nodiscard]] bool valid_key_bindings(const bindings::KeyBindings& value) noexcept {
+    if (!value.configured) {
+        return false;
+    }
+    for (const bindings::Binding& binding : value.values) {
+        if (!valid_input_code(binding.primary) || !valid_input_code(binding.secondary)) {
+            return false;
+        }
+    }
+    return true;
+}
+
 } // namespace
 
 /** Checks a whole account-settings object against the supported menu domains. */
 bool valid(const AccountSettings& value) noexcept {
     const bool validBindingSource = value.keyBindingSource == KeyBindingSource::account
                                     || value.keyBindingSource == KeyBindingSource::computer;
-    return value.configured && value.keyBindings.configured && validBindingSource
+    return value.configured && valid_key_bindings(value.keyBindings) && validBindingSource
            && valid_controls(value.controls) && valid_audio(value.audio)
            && valid_display(value.display) && valid_interface(value.interface)
            && valid_social(value.social);

+ 17 - 1
Sunrise/src/state/account/settings/settings_state.h

@@ -28,11 +28,17 @@ struct Controls {
     std::int32_t mouseLookSensitivity{};
     bool mouseInvertVertical{};
     bool mouseInvertHorizontal{};
-    /** Kept toggle whose user-facing role the target build does not expose. */
+    /**
+     * WS-701 preference field 36: a replicated Boolean stored between controller vibration and
+     * mouse aim smoothing in the native preference record. Its exact user-facing purpose has not
+     * been identified, so Sunrise preserves it for lossless round trips without guessing a name.
+     */
     bool unidentifiedToggle{};
     bool mouseAimSmoothing{};
     float adsSensitivityModifier{};
     std::int8_t doublePressDelay{};
+
+    bool operator==(const Controls&) const = default;
 };
 
 /** Authored voice and volume preferences. */
@@ -47,6 +53,8 @@ struct Audio {
     std::int8_t soundEffectsVolume{};
     std::int8_t dialogueVolume{};
     std::int8_t musicVolume{};
+
+    bool operator==(const Audio&) const = default;
 };
 
 /** Authored screen and renderer preferences. */
@@ -62,6 +70,8 @@ struct Display {
     float calibrationPrimary{};
     /** Second unidentified renderer-calibration scalar. */
     float calibrationAlpha{};
+
+    bool operator==(const Display&) const = default;
 };
 
 /** Authored HUD, subtitle, reticle, and text presentation preferences. */
@@ -81,6 +91,8 @@ struct Interface {
     /** Kept text mode with no localized title in the target build. */
     std::int8_t reservedTextMode{};
     std::int8_t subtitleOptionsEntry{};
+
+    bool operator==(const Interface&) const = default;
 };
 
 /** Authored matchmaking, identity, voice, and chat preferences. */
@@ -96,6 +108,8 @@ struct Social {
     std::int8_t localChatJoinMode{};
     std::int8_t clanChatJoinMode{};
     std::int8_t chatAutoHideMode{};
+
+    bool operator==(const Social&) const = default;
 };
 
 /** Complete authored account-setting values, independent of their native record layout. */
@@ -109,6 +123,8 @@ struct AccountSettings {
     bindings::KeyBindings keyBindings;
     /** True only when a settings object was supplied by configuration. */
     bool configured{};
+
+    bool operator==(const AccountSettings&) const = default;
 };
 
 /**

+ 38 - 0
Sunrise/src/state/runtime/runtime.h

@@ -7,6 +7,12 @@
 
 #include "state.h"
 
+namespace sunrise::state::account::settings {
+
+struct SettingsDelta;
+
+} // namespace sunrise::state::account::settings
+
 namespace sunrise::state {
 
 /**
@@ -244,6 +250,21 @@ struct PendingCurrentActivity {
     bool prepared{};
 };
 
+/** Result of validating one sparse settings writeback against authoritative State. */
+enum class SettingsUpdateDisposition : std::uint8_t {
+    rejected,
+    acceptedNoChange,
+    preparedMutation,
+};
+
+/** Complete checked settings before/after images held until the BAP transaction commits. */
+struct PendingSettingsUpdate {
+    account::settings::AccountSettings beforeSettings{};
+    account::settings::AccountSettings afterSettings{};
+    std::uint64_t accountSoid{};
+    bool prepared{};
+};
+
 /**
  * Loads cached build data and generates secrets with Sunrise's authored activity defaults.
  * @param module Loaded Sunrise module, or null to disable disk persistence.
@@ -489,6 +510,23 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 /** Commits one prepared current-activity change behind an exact character staleness guard. */
 [[nodiscard]] bool commit_current_activity(PendingCurrentActivity& mutation) noexcept;
 
+/**
+ * Merges and validates a sparse WS-701 settings update without publishing it.
+ * @param delta Supported fields decoded from one reflected settings request.
+ * @param mutation Receives a complete before/after pair only when State would change.
+ * @return Rejection, an accepted no-op, or a prepared mutation.
+ */
+[[nodiscard]] SettingsUpdateDisposition
+prepare_settings_update(const account::settings::SettingsDelta& delta,
+                        PendingSettingsUpdate& mutation) noexcept;
+
+/**
+ * Publishes one prepared settings after-image behind account-key and settings staleness guards.
+ * @param mutation Prepared update, always cleared before this function returns.
+ * @return True when the after-image was already current or was committed successfully.
+ */
+[[nodiscard]] bool commit_settings_update(PendingSettingsUpdate& mutation) noexcept;
+
 /** @return A copy of the active account state, read under the lock. */
 [[nodiscard]] AccountState account_snapshot() noexcept;
 

+ 66 - 0
Sunrise/src/state/runtime/state_account_settings_runtime.cpp

@@ -0,0 +1,66 @@
+#include "../account/settings/settings_delta.h"
+#include "runtime.h"
+#include "storage/internal.h"
+
+namespace sunrise::state {
+
+/** Builds a complete checked settings candidate without changing authoritative State. */
+SettingsUpdateDisposition prepare_settings_update(const account::settings::SettingsDelta& delta,
+                                                  PendingSettingsUpdate& mutation) noexcept {
+    mutation = {};
+
+    const AccountState current = account_snapshot();
+    if (!account::valid(current)) {
+        return SettingsUpdateDisposition::rejected;
+    }
+
+    account::settings::AccountSettings after{};
+    bool changed = false;
+    if (!account::settings::apply_delta(delta, current.settings, after, changed)) {
+        return SettingsUpdateDisposition::rejected;
+    }
+    if (!changed) {
+        return SettingsUpdateDisposition::acceptedNoChange;
+    }
+
+    mutation.beforeSettings = current.settings;
+    mutation.afterSettings = after;
+    mutation.accountSoid = current.primarySoid;
+    mutation.prepared = true;
+    return SettingsUpdateDisposition::preparedMutation;
+}
+
+/** Commits a prepared settings image only while its account and settings view remain current. */
+bool commit_settings_update(PendingSettingsUpdate& mutation) noexcept {
+    const PendingSettingsUpdate prepared = mutation;
+    mutation = {};
+
+    if (!prepared.prepared || prepared.accountSoid == 0
+        || prepared.beforeSettings == prepared.afterSettings
+        || !account::settings::valid(prepared.beforeSettings)
+        || !account::settings::valid(prepared.afterSettings)) {
+        return false;
+    }
+
+    AcquireSRWLockExclusive(&runtime::storage::g_stateLock);
+    AccountState candidate = runtime::storage::g_state.account;
+    bool committed = false;
+
+    if (candidate.primarySoid == prepared.accountSoid) {
+        if (candidate.settings == prepared.afterSettings) {
+            // A second identical transaction may observe the first one's completed after-image.
+            committed = account::valid(candidate);
+        } else if (candidate.settings == prepared.beforeSettings) {
+            candidate.settings = prepared.afterSettings;
+            if (account::valid(candidate)) {
+                runtime::storage::g_state.account = candidate;
+                committed = true;
+            }
+        }
+    }
+
+    ReleaseSRWLockExclusive(&runtime::storage::g_stateLock);
+    return committed;
+}
+
+} // namespace sunrise::state