Sfoglia il codice sorgente

[1] refactor(protocol): decode every request in middleware

Review: request encoding / decoding should be in middleware not state
(opcode 1901 in this case).
Resolution: the five descriptors still decoded outside middleware now
have codecs beside the ones that already did.

Opcode 1901 was the named example, but four more descriptors were being
decoded where they were used rather than in middleware. The Server read
the dismantle, equip, item-state and Collections descriptors inline from
bit and byte readers, holding nineteen wire widths, biases and pad runs
of its own. State went further and did arithmetic on the 1901 selector,
dividing it by its stride and masking an identity out of it, so a layer
that should only know about items knew how one reaches the wire.

Adds opcode402, opcode403, opcode406 and opcode1820 codecs beside the
existing ones, and moves the 1901 selector decode into its codec, which
now yields the item-instance identity the selector encodes. Comparing
that identity against a whole instance key is identifies_instance, so
State never masks anything. Nothing outside middleware constructs a bit
reader any more.

Validation is carried across unchanged, including the acquisition pad
check that the Server made after its decode rather than during it.

One diagnostic does change. A refused decode reported which stage
refused it, and now reports one parse failure carrying whatever the
codec reached. That matches how the already-codec'd opcodes report, so
the behaviour is consistent rather than newly quiet, but it is less
detail than the inline decoders gave.

Thomas Shields 3 settimane fa
parent
commit
b1c366d6f6

+ 4 - 0
Sunrise/Sunrise.vcxproj

@@ -127,6 +127,10 @@
     <ClCompile Include="src\middleware\web_service\messages\opcode504_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" />
+    <ClCompile Include="src\middleware\web_service\messages\opcode403_codec.cpp" />
+    <ClCompile Include="src\middleware\web_service\messages\opcode406_codec.cpp" />
+    <ClCompile Include="src\middleware\web_service\messages\opcode1820_codec.cpp" />
     <ClCompile Include="src\state\runtime\state_account_runtime.cpp" />
     <ClCompile Include="src\state\runtime\state_account_acquisition_runtime.cpp" />
     <ClCompile Include="src\state\runtime\state_account_dismantle_runtime.cpp" />

+ 29 - 0
Sunrise/src/middleware/web_service/messages/opcode1820.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../web_service_envelope.h"
+
+namespace sunrise::middleware::web_service::messages::opcode1820 {
+
+/** Web Service opcode used by Collections to create one item instance. */
+inline constexpr std::uint16_t kOpcode = 1820;
+
+/** The one logical field carried by the native Collections pull descriptor. */
+struct Request {
+    std::uint16_t collectibleIndex{};
+};
+
+/**
+ * Parses the exact reflected opcode-1820 Collections descriptor.
+ *
+ * The collectible is an optional native field, so the descriptor carries a presence bit before its
+ * fifteen-bit row. A request naming no collectible is not a pull and is refused here.
+ *
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the named collectible row.
+ * @return True only for the complete canonical three-byte request naming a collectible.
+ */
+[[nodiscard]] bool parse_request(const Message& message, Request& request) noexcept;
+
+} // namespace sunrise::middleware::web_service::messages::opcode1820

+ 40 - 0
Sunrise/src/middleware/web_service/messages/opcode1820_codec.cpp

@@ -0,0 +1,40 @@
+#include <cstddef>
+
+#include "../../encoding/bit_reader.h"
+#include "opcode1820.h"
+
+namespace sunrise::middleware::web_service::messages::opcode1820 {
+namespace {
+
+/** The reflected Collections pull request occupies exactly 24 bits. */
+constexpr std::size_t kPayloadSize = 3;
+/** The optional collectible carries one presence bit before its row. */
+constexpr std::uint8_t kPresenceWidth = 1;
+/** Native collectible rows are addressed by a fifteen-bit index. */
+constexpr std::uint8_t kCollectibleIndexWidth = 15;
+/** The descriptor pads its two payload bytes out to three. */
+constexpr std::uint8_t kPaddingWidth = 8;
+
+} // namespace
+
+/** Parses the exact native Collections pull descriptor. */
+bool parse_request(const Message& message, Request& request) noexcept {
+    request = {};
+    if (message.opcode != kOpcode || message.payload.size() != kPayloadSize) {
+        return false;
+    }
+    encoding::bits::Reader reader(message.payload);
+    std::uint64_t present = 0;
+    std::uint64_t encodedCollectibleIndex = 0;
+    std::uint64_t padding = 0;
+    if (!reader.read(kPresenceWidth, present)
+        || !reader.read(kCollectibleIndexWidth, encodedCollectibleIndex)
+        || !reader.read(kPaddingWidth, padding) || reader.remaining_bits() != 0 || present == 0
+        || padding != 0) {
+        return false;
+    }
+    request.collectibleIndex = static_cast<std::uint16_t>(encodedCollectibleIndex);
+    return true;
+}
+
+} // namespace sunrise::middleware::web_service::messages::opcode1820

+ 20 - 0
Sunrise/src/middleware/web_service/messages/opcode1901.h

@@ -9,6 +9,9 @@ namespace sunrise::middleware::web_service::messages::opcode1901 {
 /** Web Service opcode used by the equipped-item shader application action. */
 inline constexpr std::uint16_t kOpcode = 1901;
 
+/** The equipment selector carries only the low 62 bits of the item instance's identity. */
+inline constexpr std::uint64_t kInstanceIdentityMask = 0x3FFFFFFFFFFFFFFFULL;
+
 /** Exact logical fields carried by the native 192-bit equipped socket-action descriptor. */
 struct Request {
     std::uint16_t plugDefinitionIndex{};
@@ -16,9 +19,26 @@ struct Request {
     std::uint8_t modelSocketKind{};
     std::uint32_t socketIndex{};
     std::uint64_t auxiliary{};
+    /** Selector exactly as the descriptor carries it, kept for the request trace. */
     std::uint64_t equipmentSelector{};
+    /** The item-instance identity that selector encodes, already decoded. */
+    std::uint64_t instanceIdentityToken{};
 };
 
+/**
+ * Answers whether one decoded selector names a given item instance.
+ *
+ * The selector carries only part of the instance identity, so naming an instance is a comparison
+ * against that part rather than an equality. Callers hold whole identities and must not have to
+ * know which part of one reaches the wire.
+ *
+ * @param instanceIdentityToken Decoded selector from a parsed request.
+ * @param instanceSoid Whole item-instance identity held by State.
+ * @return True when the selector names that instance.
+ */
+[[nodiscard]] bool identifies_instance(std::uint64_t instanceIdentityToken,
+                                       std::uint64_t instanceSoid) noexcept;
+
 /**
  * Parses the exact reflected opcode-1901 descriptor.
  *

+ 20 - 0
Sunrise/src/middleware/web_service/messages/opcode1901_codec.cpp

@@ -33,9 +33,18 @@ constexpr std::uint64_t kModelSocketKindBias = 1;
 constexpr std::uint64_t kShaderModelSocketKind = 0;
 /** Collection-backed shader action sources carry no auxiliary instance identity. */
 constexpr std::uint64_t kShaderAuxiliary = 0;
+/** The character screen's selector is four times the instance identity it names. */
+constexpr std::uint64_t kSelectorStride = 4;
 
 } // namespace
 
+/** Compares a decoded selector against the part of an instance identity the wire carries. */
+bool identifies_instance(std::uint64_t instanceIdentityToken,
+                         std::uint64_t instanceSoid) noexcept {
+    return instanceIdentityToken != 0
+           && (instanceSoid & kInstanceIdentityMask) == instanceIdentityToken;
+}
+
 /** Parses the complete native equipped shader socket-action descriptor. */
 bool parse_request(const Message& message, Request& request) noexcept {
     request = {};
@@ -71,6 +80,17 @@ bool parse_request(const Message& message, Request& request) noexcept {
         return false;
     }
 
+    // The selector is a wire encoding of an instance identity, so it is decoded here rather than
+    // handed on as a raw number for a later layer to divide.
+    if (request.equipmentSelector == 0 || request.equipmentSelector % kSelectorStride != 0
+        || request.equipmentSelector / kSelectorStride > kInstanceIdentityMask) {
+        const std::uint64_t selector = request.equipmentSelector;
+        request = {};
+        request.equipmentSelector = selector;
+        return false;
+    }
+    request.instanceIdentityToken = request.equipmentSelector / kSelectorStride;
+
     request.plugDefinitionIndex = static_cast<std::uint16_t>(encodedPlugDefinition);
     request.canonicalSocketKind =
         static_cast<std::uint8_t>(encodedCanonicalSocketKind - kCanonicalSocketKindBias);

+ 36 - 0
Sunrise/src/middleware/web_service/messages/opcode402.h

@@ -0,0 +1,36 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../web_service_envelope.h"
+
+namespace sunrise::middleware::web_service::messages::opcode402 {
+
+/** Web Service opcode used by the Character screen's Dismantle action. */
+inline constexpr std::uint16_t kOpcode = 402;
+/** The descriptor only ever names a single unit, whatever the stack holds. */
+inline constexpr std::uint32_t kSingleQuantity = 1;
+
+/** Exact logical fields carried by the native 128-bit dismantle descriptor. */
+struct Request {
+    std::uint64_t instanceSoid{};
+    std::uint16_t definitionIndex{};
+};
+
+/**
+ * Parses the exact reflected opcode-402 dismantle descriptor.
+ *
+ * Both identities are optional native fields carrying a presence bit. The quantity, the required
+ * flag, and all three pad runs are fixed for this action, so a descriptor that differs in any of
+ * them is not the action this codec supports.
+ *
+ * Fields are filled as far as the parse reaches, so a refused request still carries what it
+ * decoded for the caller's trace.
+ *
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the named instance and its definition row.
+ * @return True only for the complete canonical 16-byte single-unit request.
+ */
+[[nodiscard]] bool parse_request(const Message& message, Request& request) noexcept;
+
+} // namespace sunrise::middleware::web_service::messages::opcode402

+ 67 - 0
Sunrise/src/middleware/web_service/messages/opcode402_codec.cpp

@@ -0,0 +1,67 @@
+#include <cstddef>
+#include <limits>
+
+#include "../../encoding/bit_reader.h"
+#include "opcode402.h"
+
+namespace sunrise::middleware::web_service::messages::opcode402 {
+namespace {
+
+/** The reflected dismantle request occupies exactly 128 bits. */
+constexpr std::size_t kPayloadSize = 16;
+/** The optional instance identity fills one whole 64-bit field. */
+constexpr std::uint8_t kInstanceWidth = 64;
+/** Signed native definition indices use one presence bit followed by fifteen value bits. */
+constexpr std::uint8_t kDefinitionIndexWidth = 15;
+/** The quantity fills one signed 32-bit field. */
+constexpr std::uint8_t kQuantityWidth = 32;
+/** A single unit after the descriptor's INT32_MIN bias. */
+constexpr std::uint32_t kSingleQuantityWire = 0x80000001U;
+/** The action's own required flag, which is always set. */
+constexpr std::uint8_t kRequiredFlagWidth = 1;
+/** Pad runs closing the nested descriptor, its outer trailers, and the whole payload. */
+constexpr std::uint8_t kNestedPaddingWidth = 6;
+constexpr std::uint8_t kOuterTrailerWidth = 2;
+constexpr std::uint8_t kFinalPaddingWidth = 6;
+
+} // namespace
+
+/** Parses the exact native single-unit dismantle descriptor. */
+bool parse_request(const Message& message, Request& request) noexcept {
+    request = {};
+    if (message.opcode != kOpcode) {
+        return false;
+    }
+    encoding::bits::Reader reader(message.payload);
+    std::uint64_t instancePresent = 0;
+    std::uint64_t instanceSoid = 0;
+    std::uint64_t definitionPresent = 0;
+    std::uint64_t encodedDefinitionIndex = 0;
+    std::uint64_t encodedQuantity = 0;
+    std::uint64_t requiredFlag = 0;
+    std::uint64_t nestedPadding = 0;
+    std::uint64_t outerTrailers = 0;
+    std::uint64_t finalPadding = 0;
+    const bool read =
+        message.payload.size() == kPayloadSize && reader.read(1, instancePresent)
+        && reader.read(kInstanceWidth, instanceSoid) && reader.read(1, definitionPresent)
+        && reader.read(kDefinitionIndexWidth, encodedDefinitionIndex)
+        && reader.read(kQuantityWidth, encodedQuantity)
+        && reader.read(kRequiredFlagWidth, requiredFlag)
+        && reader.read(kNestedPaddingWidth, nestedPadding)
+        && reader.read(kOuterTrailerWidth, outerTrailers)
+        && reader.read(kFinalPaddingWidth, finalPadding) && reader.remaining_bits() == 0;
+
+    // Whatever the read reached is kept, so a refused request still describes itself.
+    request.instanceSoid = instanceSoid;
+    if (encodedDefinitionIndex <= (std::numeric_limits<std::uint16_t>::max)()) {
+        request.definitionIndex = static_cast<std::uint16_t>(encodedDefinitionIndex);
+    }
+
+    return read && instancePresent != 0 && definitionPresent != 0 && instanceSoid != 0
+           && nestedPadding == 0 && outerTrailers == 0 && finalPadding == 0
+           && static_cast<std::uint32_t>(encodedQuantity) == kSingleQuantityWire
+           && requiredFlag == 1;
+}
+
+} // namespace sunrise::middleware::web_service::messages::opcode402

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

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../web_service_envelope.h"
+
+namespace sunrise::middleware::web_service::messages::opcode403 {
+
+/** Web Service opcode used by the Character screen's Equip action. */
+inline constexpr std::uint16_t kOpcode = 403;
+/** Web Service opcode used by the Character screen's Unequip action. */
+inline constexpr std::uint16_t kUnequipOpcode = 404;
+
+/** The one logical field carried by the shared equip and unequip descriptor. */
+struct Request {
+    std::uint64_t instanceSoid{};
+};
+
+/**
+ * Parses the exact shared opcode-403 and opcode-404 item-instance descriptor.
+ *
+ * Both actions name one item instance and differ only in direction, so they share this
+ * descriptor. The trailing byte is a required alignment pad and has to be clear.
+ *
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the named item-instance identity.
+ * @return True only for the complete canonical nine-byte request naming a nonzero instance.
+ */
+[[nodiscard]] bool parse_request(const Message& message, Request& request) noexcept;
+
+} // namespace sunrise::middleware::web_service::messages::opcode403

+ 32 - 0
Sunrise/src/middleware/web_service/messages/opcode403_codec.cpp

@@ -0,0 +1,32 @@
+#include <cstddef>
+#include <span>
+
+#include "../../encoding/byte_order.h"
+#include "opcode403.h"
+
+namespace sunrise::middleware::web_service::messages::opcode403 {
+namespace {
+
+/** The descriptor is one big-endian identity followed by a single alignment byte. */
+constexpr std::size_t kPayloadSize = encoding::kU64Size + 1U;
+
+} // namespace
+
+/** Parses the shared equip and unequip item-instance descriptor. */
+bool parse_request(const Message& message, Request& request) noexcept {
+    request = {};
+    if ((message.opcode != kOpcode && message.opcode != kUnequipOpcode)
+        || message.payload.size() != kPayloadSize
+        || message.payload[encoding::kU64Size] != std::byte{}) {
+        return false;
+    }
+    request.instanceSoid = encoding::read_u64_be(
+        std::span<const std::byte, encoding::kU64Size>{message.payload.data(), encoding::kU64Size});
+    if (request.instanceSoid == 0) {
+        request = {};
+        return false;
+    }
+    return true;
+}
+
+} // namespace sunrise::middleware::web_service::messages::opcode403

+ 35 - 0
Sunrise/src/middleware/web_service/messages/opcode406.h

@@ -0,0 +1,35 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../web_service_envelope.h"
+
+namespace sunrise::middleware::web_service::messages::opcode406 {
+
+/** Web Service opcode used by item-state actions such as lock and finisher Favorite. */
+inline constexpr std::uint16_t kOpcode = 406;
+
+/** Exact logical fields carried by the native 120-bit item-state descriptor. */
+struct Request {
+    std::uint64_t instanceSoid{};
+    std::uint16_t definitionIndex{};
+    /** Accumulated item-state bits, already lifted off the descriptor's bias. */
+    std::uint32_t flags{};
+};
+
+/**
+ * Parses the exact reflected opcode-406 item-state descriptor.
+ *
+ * Both identities are optional native fields carrying a presence bit, and the state value is
+ * biased from INT32_MIN. Only the two supported state bits are accepted.
+ *
+ * Fields are filled as far as the parse reaches, so a refused request still carries what it
+ * decoded for the caller's trace.
+ *
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the instance, definition row, and unbiased state bits.
+ * @return True only for the complete canonical 15-byte request.
+ */
+[[nodiscard]] bool parse_request(const Message& message, Request& request) noexcept;
+
+} // namespace sunrise::middleware::web_service::messages::opcode406

+ 62 - 0
Sunrise/src/middleware/web_service/messages/opcode406_codec.cpp

@@ -0,0 +1,62 @@
+#include <cstddef>
+#include <limits>
+
+#include "../../encoding/bit_reader.h"
+#include "opcode406.h"
+
+namespace sunrise::middleware::web_service::messages::opcode406 {
+namespace {
+
+/** The reflected item-state request occupies exactly 120 bits. */
+constexpr std::size_t kPayloadSize = 15;
+/** Signed native definition indices use one presence bit followed by fifteen value bits. */
+constexpr std::uint8_t kDefinitionIndexWidth = 15;
+/** The accumulated state value fills one signed 32-bit field. */
+constexpr std::uint8_t kValueWidth = 32;
+/** The descriptor pads its fields out to whole bytes. */
+constexpr std::uint8_t kPaddingWidth = 7;
+/** Nonnegative signed 32-bit values have this bit set after native descriptor biasing. */
+constexpr std::uint64_t kValueBias = 0x80000000ULL;
+/** Only the two lowest state bits are supported by this build. */
+constexpr std::uint64_t kSupportedStateBits = 0x3U;
+
+} // namespace
+
+/** Parses the exact native item-state descriptor. */
+bool parse_request(const Message& message, Request& request) noexcept {
+    request = {};
+    if (message.opcode != kOpcode) {
+        return false;
+    }
+    encoding::bits::Reader reader(message.payload);
+    std::uint64_t instancePresent = 0;
+    std::uint64_t instanceSoid = 0;
+    std::uint64_t definitionPresent = 0;
+    std::uint64_t definitionIndex = 0;
+    std::uint64_t encodedFlags = 0;
+    std::uint64_t padding = 0;
+    const bool read = message.payload.size() == kPayloadSize && reader.read(1, instancePresent)
+                      && reader.read(64, instanceSoid) && reader.read(1, definitionPresent)
+                      && reader.read(kDefinitionIndexWidth, definitionIndex)
+                      && reader.read(kValueWidth, encodedFlags)
+                      && reader.read(kPaddingWidth, padding) && reader.remaining_bits() == 0;
+
+    // Whatever the read reached is kept, so a refused request still describes itself.
+    request.instanceSoid = instanceSoid;
+    if (definitionIndex <= (std::numeric_limits<std::uint16_t>::max)()) {
+        request.definitionIndex = static_cast<std::uint16_t>(definitionIndex);
+    }
+    if (encodedFlags >= kValueBias) {
+        request.flags = static_cast<std::uint32_t>(encodedFlags - kValueBias);
+    }
+
+    if (!read || instancePresent == 0 || instanceSoid == 0 || definitionPresent == 0
+        || definitionIndex > (std::numeric_limits<std::uint16_t>::max)()
+        || encodedFlags < kValueBias || padding != 0
+        || encodedFlags - kValueBias > kSupportedStateBits) {
+        return false;
+    }
+    return true;
+}
+
+} // namespace sunrise::middleware::web_service::messages::opcode406

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

@@ -6,8 +6,10 @@
 #include <string_view>
 
 #include "../../core/logging/log.h"
-#include "../../middleware/encoding/bit_reader.h"
-#include "../../middleware/encoding/byte_order.h"
+#include "../../middleware/web_service/messages/opcode402.h"
+#include "../../middleware/web_service/messages/opcode403.h"
+#include "../../middleware/web_service/messages/opcode406.h"
+#include "../../middleware/web_service/messages/opcode1820.h"
 #include "../../middleware/web_service/messages/opcode1901.h"
 #include "../../middleware/web_service/messages/opcode504.h"
 #include "../../middleware/web_service/messages/opcode903.h"
@@ -19,27 +21,7 @@ namespace sunrise::server::web_service {
 
 namespace {
 
-constexpr std::size_t kEquipmentActionPayloadSize = middleware::encoding::kU64Size + 1U;
-constexpr std::size_t kItemStatePayloadSize = 15;
-constexpr std::uint8_t kItemStateDefinitionIndexWidth = 15;
-constexpr std::uint8_t kItemStateValueWidth = 32;
-constexpr std::uint8_t kItemStatePaddingWidth = 7;
-constexpr std::uint64_t kItemStateValueBias = 0x80000000ULL;
-constexpr std::size_t kItemDismantlePayloadSize = 16;
-constexpr std::uint8_t kItemDismantleInstanceWidth = 64;
-constexpr std::uint8_t kItemDismantleDefinitionIndexWidth = 15;
-constexpr std::uint8_t kItemDismantleQuantityWidth = 32;
-constexpr std::uint32_t kItemDismantleSingleQuantityWire = 0x80000001U;
-constexpr std::uint8_t kItemDismantleRequiredFlagWidth = 1;
-constexpr std::uint8_t kItemDismantleNestedPaddingWidth = 6;
-constexpr std::uint8_t kItemDismantleOuterTrailerWidth = 2;
-constexpr std::uint8_t kItemDismantleFinalPaddingWidth = 6;
-constexpr std::uint64_t kEquipmentSelectorStride = 4;
 constexpr std::uint8_t kEquippedShaderModelSocketKind = 0;
-constexpr std::size_t kItemAcquisitionPayloadSize = 3;
-constexpr std::uint8_t kItemAcquisitionPresenceWidth = 1;
-constexpr std::uint8_t kItemAcquisitionCollectibleIndexWidth = 15;
-constexpr std::uint8_t kItemAcquisitionPaddingWidth = 8;
 constexpr std::uint32_t kUnavailableDefinitionIndex = (std::numeric_limits<std::uint16_t>::max)();
 } // namespace
 
@@ -246,18 +228,13 @@ void select_character(const middleware::web_service::Message& message, Outcome&
     }
 }
 
-/** Parses the exact shared opcode-403/404 SOID descriptor. */
+/** Reads the shared opcode-403/404 SOID descriptor through its codec. */
 [[nodiscard]] bool parse_equipment_instance(const middleware::web_service::Message& message,
                                             std::uint64_t& instanceSoid) noexcept {
-    instanceSoid = 0;
-    if (message.payload.size() != kEquipmentActionPayloadSize
-        || message.payload[middleware::encoding::kU64Size] != std::byte{}) {
-        return false;
-    }
-    instanceSoid = middleware::encoding::read_u64_be(
-        std::span<const std::byte, middleware::encoding::kU64Size>{message.payload.data(),
-                                                                   middleware::encoding::kU64Size});
-    return instanceSoid != 0;
+    middleware::web_service::messages::opcode403::Request request{};
+    const bool parsed = middleware::web_service::messages::opcode403::parse_request(message, request);
+    instanceSoid = request.instanceSoid;
+    return parsed;
 }
 
 /** Prepares one opcode-403/404 equipment mutation without publishing State early. */
@@ -408,8 +385,7 @@ void mutate_equipped_socket_plug(const middleware::web_service::Message& message
         || request.canonicalSocketKind != request.socketIndex
         || request.modelSocketKind != kEquippedShaderModelSocketKind || request.auxiliary != 0
         || request.socketIndex >= state::account::inventory::kPlugCapacity
-        || request.equipmentSelector == 0
-        || request.equipmentSelector % kEquipmentSelectorStride != 0) {
+        || request.instanceIdentityToken == 0) {
         std::array<char, 256> line{};
         const int count = std::snprintf(
             line.data(),
@@ -433,10 +409,10 @@ void mutate_equipped_socket_plug(const middleware::web_service::Message& message
         return;
     }
 
-    const std::uint64_t identityToken = request.equipmentSelector / kEquipmentSelectorStride;
+    const std::uint64_t identityToken = request.instanceIdentityToken;
     state::PendingSocketPlug mutation{};
     if (!state::prepare_character_selector_socket_plug(
-            request.equipmentSelector,
+            request.instanceIdentityToken,
             static_cast<std::uint8_t>(request.socketIndex),
             request.plugDefinitionIndex,
             mutation)) {
@@ -494,34 +470,19 @@ void mutate_equipped_socket_plug(const middleware::web_service::Message& message
 
 /** Parses and prepares one complete accumulated item-state value from opcode 406. */
 void mutate_item_state(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    middleware::encoding::bits::Reader reader(message.payload);
-    std::uint64_t instancePresent = 0;
-    std::uint64_t instanceSoid = 0;
-    std::uint64_t definitionPresent = 0;
-    std::uint64_t definitionIndex = 0;
-    std::uint64_t encodedFlags = 0;
-    std::uint64_t padding = 0;
-    if (message.payload.size() != kItemStatePayloadSize || !reader.read(1, instancePresent)
-        || !reader.read(64, instanceSoid) || !reader.read(1, definitionPresent)
-        || !reader.read(kItemStateDefinitionIndexWidth, definitionIndex)
-        || !reader.read(kItemStateValueWidth, encodedFlags)
-        || !reader.read(kItemStatePaddingWidth, padding) || reader.remaining_bits() != 0
-        || instancePresent == 0 || instanceSoid == 0 || definitionPresent == 0
-        || definitionIndex > (std::numeric_limits<std::uint16_t>::max)()
-        || encodedFlags < kItemStateValueBias || padding != 0
-        || encodedFlags - kItemStateValueBias > 0x3U) {
+    middleware::web_service::messages::opcode406::Request request{};
+    if (!middleware::web_service::messages::opcode406::parse_request(message, request)) {
         std::array<char, 224> line{};
         const int count = std::snprintf(
             line.data(),
             line.size(),
             "ev=ws406 stage=parse result=fail transaction=%u payload_bytes=%zu instance=0x%llX "
-            "definition=%llu flags_wire=0x%llX padding=0x%llX",
+            "definition=%u flags=0x%X",
             static_cast<unsigned>(message.transactionId),
             message.payload.size(),
-            static_cast<unsigned long long>(instanceSoid),
-            static_cast<unsigned long long>(definitionIndex),
-            static_cast<unsigned long long>(encodedFlags),
-            static_cast<unsigned long long>(padding));
+            static_cast<unsigned long long>(request.instanceSoid),
+            static_cast<unsigned>(request.definitionIndex),
+            request.flags);
         if (count > 0) {
             core::log::write(core::log::Channel::server,
                              core::log::Level::warn,
@@ -530,10 +491,10 @@ void mutate_item_state(const middleware::web_service::Message& message, Outcome&
         return;
     }
 
-    const std::uint32_t flags = static_cast<std::uint32_t>(encodedFlags - kItemStateValueBias);
+    const std::uint64_t instanceSoid = request.instanceSoid;
+    const std::uint32_t flags = request.flags;
     state::PendingItemState mutation{};
-    if (!state::prepare_item_state(
-            instanceSoid, static_cast<std::uint16_t>(definitionIndex), flags, mutation)) {
+    if (!state::prepare_item_state(instanceSoid, request.definitionIndex, flags, mutation)) {
         return;
     }
     outcome.mutation = mutation;
@@ -591,53 +552,21 @@ void report_item_dismantle(const middleware::web_service::Message& message,
 
 /** Prepares the exact fixed-width opcode-402 Character-inventory removal request. */
 void dismantle_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    if (message.payload.size() != kItemDismantlePayloadSize) {
-        report_item_dismantle(
-            message, "fail", "payload_size", 0, kUnavailableDefinitionIndex, 0, 0);
-        return;
-    }
-
-    middleware::encoding::bits::Reader reader(message.payload);
-    std::uint64_t instancePresent = 0;
-    std::uint64_t instanceSoid = 0;
-    std::uint64_t definitionPresent = 0;
-    std::uint64_t encodedDefinitionIndex = 0;
-    std::uint64_t encodedQuantity = 0;
-    std::uint64_t requiredFlag = 0;
-    std::uint64_t nestedPadding = 0;
-    std::uint64_t outerTrailers = 0;
-    std::uint64_t finalPadding = 0;
-    if (!reader.read(1, instancePresent) || !reader.read(kItemDismantleInstanceWidth, instanceSoid)
-        || !reader.read(1, definitionPresent)
-        || !reader.read(kItemDismantleDefinitionIndexWidth, encodedDefinitionIndex)
-        || !reader.read(kItemDismantleQuantityWidth, encodedQuantity)
-        || !reader.read(kItemDismantleRequiredFlagWidth, requiredFlag)
-        || !reader.read(kItemDismantleNestedPaddingWidth, nestedPadding)
-        || !reader.read(kItemDismantleOuterTrailerWidth, outerTrailers)
-        || !reader.read(kItemDismantleFinalPaddingWidth, finalPadding)
-        || reader.remaining_bits() != 0) {
-        report_item_dismantle(
-            message, "fail", "payload_bits", 0, kUnavailableDefinitionIndex, 0, 0);
-        return;
-    }
-    const auto definitionIndex = static_cast<std::uint16_t>(encodedDefinitionIndex);
-    const auto quantityWire = static_cast<std::uint32_t>(encodedQuantity);
-    constexpr std::uint32_t kSingleQuantity = 1;
-    if (instancePresent == 0 || definitionPresent == 0 || instanceSoid == 0) {
-        report_item_dismantle(
-            message, "fail", "required_field", instanceSoid, definitionIndex, 0, 0);
-        return;
-    }
-    if (nestedPadding != 0 || outerTrailers != 0 || finalPadding != 0) {
-        report_item_dismantle(
-            message, "fail", "padding_or_trailer", instanceSoid, definitionIndex, 0, 0);
-        return;
-    }
-    if (quantityWire != kItemDismantleSingleQuantityWire || requiredFlag != 1) {
-        report_item_dismantle(
-            message, "fail", "quantity_or_flag", instanceSoid, definitionIndex, 0, 0);
+    middleware::web_service::messages::opcode402::Request request{};
+    if (!middleware::web_service::messages::opcode402::parse_request(message, request)) {
+        report_item_dismantle(message,
+                              "fail",
+                              "payload_bits",
+                              request.instanceSoid,
+                              request.definitionIndex,
+                              0,
+                              0);
         return;
     }
+    const std::uint64_t instanceSoid = request.instanceSoid;
+    const std::uint16_t definitionIndex = request.definitionIndex;
+    constexpr std::uint32_t kSingleQuantity =
+        middleware::web_service::messages::opcode402::kSingleQuantity;
 
     state::build_data::items::Definition definition{};
     if (!state::build_data::find_item_definition_index(definitionIndex, definition)) {
@@ -710,24 +639,8 @@ void report_item_acquisition(const middleware::web_service::Message& message,
 
 /** Prepares the exact three-byte opcode-1820 Collections item request. */
 void acquire_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
-    if (message.payload.size() != kItemAcquisitionPayloadSize) {
-        report_item_acquisition(message,
-                                "fail",
-                                "payload_size",
-                                kUnavailableDefinitionIndex,
-                                kUnavailableDefinitionIndex,
-                                0,
-                                0);
-        return;
-    }
-
-    middleware::encoding::bits::Reader reader(message.payload);
-    std::uint64_t present = 0;
-    std::uint64_t encodedCollectibleIndex = 0;
-    std::uint64_t padding = 0;
-    if (!reader.read(kItemAcquisitionPresenceWidth, present)
-        || !reader.read(kItemAcquisitionCollectibleIndexWidth, encodedCollectibleIndex)
-        || !reader.read(kItemAcquisitionPaddingWidth, padding) || reader.remaining_bits() != 0) {
+    middleware::web_service::messages::opcode1820::Request request{};
+    if (!middleware::web_service::messages::opcode1820::parse_request(message, request)) {
         report_item_acquisition(message,
                                 "fail",
                                 "payload_bits",
@@ -737,23 +650,7 @@ void acquire_item(const middleware::web_service::Message& message, Outcome& outc
                                 0);
         return;
     }
-    const auto collectibleIndex = static_cast<std::uint16_t>(encodedCollectibleIndex);
-    if (present == 0) {
-        report_item_acquisition(message,
-                                "fail",
-                                "collectible_absent",
-                                collectibleIndex,
-                                kUnavailableDefinitionIndex,
-                                0,
-                                0);
-        return;
-    }
-    if (padding != 0) {
-        report_item_acquisition(
-            message, "fail", "padding", collectibleIndex, kUnavailableDefinitionIndex, 0, 0);
-        return;
-    }
-
+    const std::uint16_t collectibleIndex = request.collectibleIndex;
     std::uint16_t itemDefinitionIndex = 0;
     if (!state::build_data::find_collectible_item_definition_index(collectibleIndex,
                                                                    itemDefinitionIndex)) {

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

@@ -385,11 +385,11 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
 /**
  * Prepares one ordinary-socket selection for an exact character-screen item selector.
  *
- * Opcode 1901 carries four times the item instance's low 62-bit identity. The resolved selected-
- * character instance is passed through the same checked transition as an instance-addressed
- * action, so acquired and unequipped items do not depend on a coincidental menu-row ordinal.
+ * The resolved selected-character instance is passed through the same checked transition as an
+ * instance-addressed action, so acquired and unequipped items do not depend on a coincidental
+ * menu-row ordinal.
  *
- * @param itemSelector Encoded character-item identity selector carried by opcode 1901.
+ * @param instanceIdentityToken Item-instance identity decoded from the opcode-1901 selector.
  * @param requestedSocketLane Native socket action lane; the installed compatibility relation
  * resolves the target's exact physical lane.
  * @param plugDefinitionIndex Installed plug-definition row selected by the Client.
@@ -397,7 +397,7 @@ commit_profile_item_acquisition(PendingProfileItemAcquisition& mutation) noexcep
  * @return True when the location has one matching item, the plug resolves to exactly the
  * requested compatible ordinary socket lane, and the socket transition is valid.
  */
-[[nodiscard]] bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
+[[nodiscard]] bool prepare_character_selector_socket_plug(std::uint64_t instanceIdentityToken,
                                                           std::uint8_t requestedSocketLane,
                                                           std::uint16_t plugDefinitionIndex,
                                                           PendingSocketPlug& mutation) noexcept;

+ 7 - 10
Sunrise/src/state/runtime/state_account_item_action_runtime.cpp

@@ -6,6 +6,7 @@
 #include <limits>
 
 #include "../../middleware/datagen/family4/loadout/loadout_resolver.h"
+#include "../../middleware/web_service/messages/opcode1901.h"
 #include "../build_data/runtime.h"
 #include "runtime.h"
 #include "state_account_transaction_helpers.h"
@@ -17,6 +18,7 @@ using namespace runtime::detail;
 namespace authored_inventory = account::inventory;
 namespace item_details = build_data::items::details;
 namespace family4_loadout = middleware::datagen::family4::loadout;
+namespace opcode1901 = middleware::web_service::messages::opcode1901;
 
 /** Prepares one checked ordinary-socket selection without publishing account State. */
 bool prepare_socket_plug(std::uint64_t targetInstanceSoid,
@@ -71,7 +73,7 @@ bool prepare_socket_plug(std::uint64_t targetInstanceSoid,
 }
 
 /** Prepares a socket transition for the exact instance identity carried by opcode 1901. */
-bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
+bool prepare_character_selector_socket_plug(std::uint64_t instanceIdentityToken,
                                             std::uint8_t requestedSocketLane,
                                             std::uint16_t plugDefinitionIndex,
                                             PendingSocketPlug& mutation) noexcept {
@@ -87,11 +89,7 @@ bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
         }
     }
 
-    constexpr std::uint64_t kSelectorStride = 4;
-    constexpr std::uint64_t kInstanceIdentityMask = 0x3FFFFFFFFFFFFFFFULL;
-    if (characterIndex >= snapshot.characterCount || itemSelector == 0
-        || itemSelector % kSelectorStride != 0
-        || itemSelector / kSelectorStride > kInstanceIdentityMask
+    if (characterIndex >= snapshot.characterCount || instanceIdentityToken == 0
         || requestedSocketLane >= authored_inventory::kPlugCapacity) {
         report_socket_plug("prepare_location",
                            "fail",
@@ -104,7 +102,7 @@ bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
                            0,
                            0,
                            false,
-                           static_cast<std::size_t>(itemSelector / kSelectorStride));
+                           static_cast<std::size_t>(instanceIdentityToken));
         return false;
     }
 
@@ -112,11 +110,10 @@ bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
     if (!family4_loadout::resolve(snapshot, characterIndex, resolvedLoadout)) {
         return false;
     }
-    const std::uint64_t identityToken = itemSelector / kSelectorStride;
     const family4_loadout::ResolvedItem* resolvedTarget = nullptr;
     for (std::size_t index = 0; index < resolvedLoadout.itemCount; ++index) {
         const family4_loadout::ResolvedItem& item = resolvedLoadout.items[index];
-        if ((item.instance.instanceSoid & kInstanceIdentityMask) != identityToken) {
+        if (!opcode1901::identifies_instance(instanceIdentityToken, item.instance.instanceSoid)) {
             continue;
         }
         if (resolvedTarget != nullptr) {
@@ -183,7 +180,7 @@ bool prepare_character_selector_socket_plug(std::uint64_t itemSelector,
                            0,
                            0,
                            expectedEquipped,
-                           static_cast<std::size_t>(identityToken));
+                           static_cast<std::size_t>(instanceIdentityToken));
         mutation = {};
         return false;
     }