فهرست منبع

Read the create-character header into a layout struct

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
stan 4 روز پیش
والد
کامیت
910243eacc

+ 3 - 6
Sunrise/src/middleware/web_service/messages/opcode501_codec.h

@@ -13,13 +13,10 @@ inline constexpr std::uint16_t kOpcode = 501;
 
 /**
  * Encodes the create-character response: the status pair then the character object id.
- * The status is the caller's, because this opcode has no single right answer. A refusal has to
- * say so in the code -- the SOID field alone cannot, since zero is what a refusal would send and
- * the Client reads the code, not the id -- and the value feeds the Family-4 version wait, which
- * needs the revision a staged publication will carry, or kNoFamily4Publication when none will.
+ * The client reads a refusal from the status code, not from the id.
  * @param message Parsed request whose envelope fields are echoed.
- * @param status Logical status pair: refusal code, and the revision the wait should expect.
- * @param characterSoid Character object id; must be published in family 3. Zero on a refusal.
+ * @param status Refusal code, and the Family-4 revision that carries the new character.
+ * @param characterSoid The new character's id; zero on a refusal.
  * @param output Caller-owned svc-11 response-body storage.
  * @param written Receives encoded response-body bytes.
  * @return True when the fixed response fits the output buffer.

+ 101 - 96
Sunrise/src/middleware/web_service/messages/opcode501_request_codec.cpp

@@ -1,5 +1,9 @@
 #include "opcode501_request_codec.h"
 
+#include <array>
+#include <cstring>
+#include <type_traits>
+
 #include "../../encoding/bit_reader.h"
 
 namespace sunrise::middleware::web_service::messages::opcode501 {
@@ -7,144 +11,145 @@ namespace {
 
 /** The identity triple and appearance header end here; the 64-byte name follows. */
 constexpr std::size_t kIdentityAndHeaderBits = 261;
-
+/** Each identity byte is preceded by a 1-bit presence flag. */
+constexpr std::uint8_t kPresenceBits = 1;
+/** Header fields travel as 8, 16 or 32 bits. */
+constexpr std::uint8_t kByteBits = 8;
+constexpr std::uint8_t kWordBits = 16;
+constexpr std::uint8_t kHashBits = 32;
+/** The request's last header field is 2 bits wide. */
+constexpr std::uint8_t kTrailingBits = 2;
+/** 8-bit fields travel biased by +0x80, 16-bit fields by +0x8000; 32-bit fields unbiased. */
 constexpr std::uint64_t kByteBias = 0x80U;
 constexpr std::uint64_t kWordBias = 0x8000U;
+/** Each of the header's two index runs holds 3 entries. */
+constexpr std::size_t kIndexRunCount = 3;
+/** The record requires 1 in the last field; the request always carries 0 there. */
+constexpr std::uint8_t kRecordTrailingValue = 1;
+/** Padding after the last field fills the record to its 4-byte alignment. */
+constexpr std::size_t kTailPadding = 3;
+
+/** The appearance header in record byte order. Field meanings are not decoded. */
+struct HeaderLayout {
+    std::uint16_t word00;
+    std::uint8_t byte02;
+    std::uint8_t byte03;
+    std::uint8_t byte04;
+    std::uint8_t padding05;
+    std::uint16_t word06;
+    std::uint16_t word08;
+    std::uint16_t word10;
+    std::array<std::uint16_t, kIndexRunCount> indices12;
+    std::array<std::uint16_t, kIndexRunCount> indices18;
+    std::uint16_t word24;
+    std::uint16_t padding26;
+    std::uint32_t hash28;
+    std::uint8_t trailing32;
+    std::array<std::uint8_t, kTailPadding> padding33;
+};
+static_assert(sizeof(HeaderLayout) == state::kAppearanceHeaderSize);
+static_assert(std::is_trivially_copyable_v<HeaderLayout>);
 
-/** Writes the record's little-endian byte order, which is not the wire's bit order. */
-void put8(state::AppearanceHeader& header, std::size_t offset, std::uint64_t value) noexcept {
-    header[offset] = static_cast<std::uint8_t>(value & 0xFFU);
-}
-
-void put16(state::AppearanceHeader& header, std::size_t offset, std::uint64_t value) noexcept {
-    header[offset] = static_cast<std::uint8_t>(value & 0xFFU);
-    header[offset + 1U] = static_cast<std::uint8_t>((value >> 8U) & 0xFFU);
-}
-
-void put32(state::AppearanceHeader& header, std::size_t offset, std::uint64_t value) noexcept {
-    put16(header, offset, value & 0xFFFFU);
-    put16(header, offset + 2U, (value >> 16U) & 0xFFFFU);
-}
+/** Highest valid race, gender and class wire values, matching the state enums. */
+constexpr std::uint8_t kMaxRace = 2;
+constexpr std::uint8_t kMaxGender = 1;
+constexpr std::uint8_t kMaxClass = 2;
 
 /**
- * Reads one 1-bit presence flag followed by an 8-bit biased value.
- * @return True when the field is present and readable. An absent field fails closed: nothing on
- *         the wire carries the value that was skipped, so there is nothing to fall back to.
+ * Reads one presence flag and the 8-bit biased value behind it.
+ * @param reader Request bit reader.
+ * @param value Receives the unbiased value.
+ * @return False when the field is absent or short. Nothing else carries an absent value.
  */
-[[nodiscard]] bool read_optional_byte(encoding::bits::Reader& reader, std::uint8_t& value) noexcept {
+[[nodiscard]] bool read_present_byte(encoding::bits::Reader& reader, std::uint8_t& value) noexcept {
     std::uint64_t present = 0;
-    if (!reader.read(1, present) || present == 0) {
-        return false;
-    }
     std::uint64_t raw = 0;
-    if (!reader.read(8, raw)) {
+    if (!reader.read(kPresenceBits, present) || present == 0 || !reader.read(kByteBits, raw)) {
         return false;
     }
     value = static_cast<std::uint8_t>(raw - kByteBias);
     return true;
 }
 
-/** Reads an 8-bit +0x80-biased header field straight into the record block. */
-[[nodiscard]] bool
-copy_byte(encoding::bits::Reader& reader, state::AppearanceHeader& header, std::size_t offset) noexcept {
+/** Reads one 8-bit +0x80-biased header field. @return False when the payload is short. */
+[[nodiscard]] bool read_byte(encoding::bits::Reader& reader, std::uint8_t& value) noexcept {
     std::uint64_t raw = 0;
-    if (!reader.read(8, raw)) {
+    if (!reader.read(kByteBits, raw)) {
         return false;
     }
-    put8(header, offset, raw - kByteBias);
+    value = static_cast<std::uint8_t>(raw - kByteBias);
     return true;
 }
 
-/** Reads a 16-bit +0x8000-biased header field straight into the record block. */
-[[nodiscard]] bool
-copy_word(encoding::bits::Reader& reader, state::AppearanceHeader& header, std::size_t offset) noexcept {
+/** Reads one 16-bit +0x8000-biased header field. @return False when the payload is short. */
+[[nodiscard]] bool read_word(encoding::bits::Reader& reader, std::uint16_t& value) noexcept {
     std::uint64_t raw = 0;
-    if (!reader.read(16, raw)) {
+    if (!reader.read(kWordBits, raw)) {
         return false;
     }
-    put16(header, offset, raw - kWordBias);
+    value = static_cast<std::uint16_t>(raw - kWordBias);
     return true;
 }
 
 /**
- * Byte offsets of every appearance-header field inside the 36-byte record block. The gaps at 5,
- * 26 and 33 are the struct's own alignment padding and stay zero.
+ * Reads the appearance header fields in wire order.
+ * @param reader Request bit reader, just past the identity triple.
+ * @param header Receives the fields; padding stays zero.
+ * @return False when the payload is short.
  */
-constexpr std::size_t kOffsetF0 = 0;
-constexpr std::size_t kOffsetF2 = 2;
-constexpr std::size_t kOffsetF3 = 3;
-constexpr std::size_t kOffsetF4 = 4;
-constexpr std::size_t kOffsetF6 = 6;
-constexpr std::size_t kOffsetF8 = 8;
-constexpr std::size_t kOffsetF10 = 10;
-constexpr std::size_t kOffsetF12 = 12;
-constexpr std::size_t kOffsetF18 = 18;
-constexpr std::size_t kOffsetF24 = 24;
-constexpr std::size_t kOffsetF28 = 28;
-constexpr std::size_t kOffsetF32 = 32;
-
-/** Both triples of 16-bit customization indices hold 3 entries. */
-constexpr std::size_t kIndexTripleCount = 3;
-/** Each entry in those triples is 2 bytes wide in the record block. */
-constexpr std::size_t kIndexTripleStride = 2;
-
-/** The record requires 1 in the trailing field; the request always carries 0 there. */
-constexpr std::uint64_t kRecordTrailingField = 1;
-
-/** Highest valid race, gender and class wire values, matching the state enums. */
-constexpr std::uint8_t kMaxRace = 2;
-constexpr std::uint8_t kMaxGender = 1;
-constexpr std::uint8_t kMaxClass = 2;
-
-} // namespace
-
-bool parse_request(const Message& message, Request& request) noexcept {
-    request = {};
-    if (message.opcode != kOpcode || message.payload.size() * 8U < kIdentityAndHeaderBits) {
-        return false;
-    }
-    encoding::bits::Reader reader(message.payload);
-    if (!read_optional_byte(reader, request.race) || !read_optional_byte(reader, request.gender)
-        || !read_optional_byte(reader, request.characterClass)) {
-        return false;
-    }
-    if (request.race > kMaxRace || request.gender > kMaxGender
-        || request.characterClass > kMaxClass) {
-        return false;
-    }
-
-    auto& header = request.appearanceHeader;
-    if (!copy_word(reader, header, kOffsetF0) || !copy_byte(reader, header, kOffsetF2)
-        || !copy_byte(reader, header, kOffsetF3) || !copy_byte(reader, header, kOffsetF4)
-        || !copy_word(reader, header, kOffsetF6) || !copy_word(reader, header, kOffsetF8)
-        || !copy_word(reader, header, kOffsetF10)) {
+[[nodiscard]] bool read_header(encoding::bits::Reader& reader, HeaderLayout& header) noexcept {
+    if (!read_word(reader, header.word00) || !read_byte(reader, header.byte02)
+        || !read_byte(reader, header.byte03) || !read_byte(reader, header.byte04)
+        || !read_word(reader, header.word06) || !read_word(reader, header.word08)
+        || !read_word(reader, header.word10)) {
         return false;
     }
-    for (std::size_t index = 0; index < kIndexTripleCount; ++index) {
-        if (!copy_word(reader, header, kOffsetF12 + index * kIndexTripleStride)) {
+    for (std::uint16_t& index : header.indices12) {
+        if (!read_word(reader, index)) {
             return false;
         }
     }
-    for (std::size_t index = 0; index < kIndexTripleCount; ++index) {
-        if (!copy_word(reader, header, kOffsetF18 + index * kIndexTripleStride)) {
+    for (std::uint16_t& index : header.indices18) {
+        if (!read_word(reader, index)) {
             return false;
         }
     }
-    if (!copy_word(reader, header, kOffsetF24)) {
+    std::uint64_t hash = 0;
+    std::uint64_t trailing = 0;
+    if (!read_word(reader, header.word24) || !reader.read(kHashBits, hash)
+        || !reader.read(kTrailingBits, trailing)) {
         return false;
     }
-    std::uint64_t emptyHashSentinel = 0;
-    if (!reader.read(32, emptyHashSentinel)) {
+    header.hash28 = static_cast<std::uint32_t>(hash);
+    // The request's own trailing value is read past but not kept.
+    header.trailing32 = kRecordTrailingValue;
+    return true;
+}
+
+} // namespace
+
+/**
+ * Parses the create-character request body: the identity triple, then the appearance header.
+ * The trailing 64-byte name is not read.
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the creation fields, the header in record byte order.
+ * @return False when the payload is short, a presence flag is clear, or a value is out of range.
+ */
+bool parse_request(const Message& message, Request& request) noexcept {
+    request = {};
+    if (message.opcode != kOpcode || message.payload.size() * kByteBits < kIdentityAndHeaderBits) {
         return false;
     }
-    put32(header, kOffsetF28, emptyHashSentinel);
-
-    // Read but not copied: the request's own value is 0 and the record rejects it.
-    std::uint64_t requestTrailingField = 0;
-    if (!reader.read(2, requestTrailingField)) {
+    encoding::bits::Reader reader(message.payload);
+    HeaderLayout header{};
+    if (!read_present_byte(reader, request.race) || !read_present_byte(reader, request.gender)
+        || !read_present_byte(reader, request.characterClass) || request.race > kMaxRace
+        || request.gender > kMaxGender || request.characterClass > kMaxClass
+        || !read_header(reader, header)) {
+        request = {};
         return false;
     }
-    put8(header, kOffsetF32, kRecordTrailingField);
+    std::memcpy(request.appearanceHeader.data(), &header, sizeof header);
     return true;
 }
 

+ 5 - 22
Sunrise/src/middleware/web_service/messages/opcode501_request_codec.h

@@ -13,33 +13,16 @@ struct Request {
     std::uint8_t characterClass{};
     std::uint8_t gender{};
     std::uint8_t race{};
-    /**
-     * The appearance header in record byte order, ready to memcpy into either character
-     * encoder's block. Its values index per-race customization tables, so it is only
-     * meaningful beside the `race` it arrived with.
-     */
+    /** Record byte order. Its values index per-race tables, so it only fits its own `race`. */
     state::AppearanceHeader appearanceHeader{};
 };
 
 /**
- * Parses the create-character request body.
- *
- * The payload is a big-endian bitstream, not a byte-aligned struct: every field is written at
- * whatever bit offset the previous one ended on. Wire order is depth-first over
- * `{ int8 triple[3]; CharHeader header; char name[64]; }`, where `triple` is race, gender and
- * class in that order. Each `triple` entry is a 1-bit presence flag followed by 8 value bits
- * biased by +0x80; the header's fields are 8, 16 or 32 bits biased by +0x80, +0x8000 or not at
- * all; and the 64-byte name begins at bit 261. This parser consumes the first 261 bits and
- * ignores the name, which every observed capture leaves as the placeholder "Player".
- *
- * The header is emitted in the record's own little-endian struct layout rather than as parsed
- * fields, since both character encoders want it as an opaque block. Its last field is the one
- * value that is not copied through: the request carries 0 where the record requires 1.
- *
+ * Parses the create-character request body, a big-endian bitstream with no byte alignment.
+ * Race, gender and class each carry a presence bit and a +0x80 bias; the header follows.
  * @param message Parsed Web Service envelope.
- * @param request Receives the parsed creation fields.
- * @return True when the payload holds all 261 bits, every presence flag is set, and race,
- *         gender and class are all in range.
+ * @param request Receives the creation fields, the header in record byte order.
+ * @return False when the payload is short, a presence flag is clear, or a value is out of range.
  */
 [[nodiscard]] bool parse_request(const Message& message, Request& request) noexcept;
 

+ 8 - 0
Sunrise/src/middleware/web_service/messages/opcode502.cpp

@@ -5,11 +5,19 @@
 namespace sunrise::middleware::web_service::messages::opcode502 {
 namespace {
 
+/** The request is one unbiased 64-bit character id with no presence bit. */
 constexpr std::uint8_t kCharacterSoidWidth = 64;
+/** Those 64 bits need 8 payload bytes. */
 constexpr std::size_t kMinimumPayloadSize = 8;
 
 } // namespace
 
+/**
+ * Parses the delete-character request: one 64-bit character id.
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the targeted character id.
+ * @return False when the opcode differs or the id is short.
+ */
 bool parse_request(const Message& message, Request& request) noexcept {
     request = {};
     if (message.opcode != kOpcode || message.payload.size() < kMinimumPayloadSize) {