|
@@ -1,5 +1,9 @@
|
|
|
#include "opcode501_request_codec.h"
|
|
#include "opcode501_request_codec.h"
|
|
|
|
|
|
|
|
|
|
+#include <array>
|
|
|
|
|
+#include <cstring>
|
|
|
|
|
+#include <type_traits>
|
|
|
|
|
+
|
|
|
#include "../../encoding/bit_reader.h"
|
|
#include "../../encoding/bit_reader.h"
|
|
|
|
|
|
|
|
namespace sunrise::middleware::web_service::messages::opcode501 {
|
|
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. */
|
|
/** The identity triple and appearance header end here; the 64-byte name follows. */
|
|
|
constexpr std::size_t kIdentityAndHeaderBits = 261;
|
|
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 kByteBias = 0x80U;
|
|
|
constexpr std::uint64_t kWordBias = 0x8000U;
|
|
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;
|
|
std::uint64_t present = 0;
|
|
|
- if (!reader.read(1, present) || present == 0) {
|
|
|
|
|
- return false;
|
|
|
|
|
- }
|
|
|
|
|
std::uint64_t raw = 0;
|
|
std::uint64_t raw = 0;
|
|
|
- if (!reader.read(8, raw)) {
|
|
|
|
|
|
|
+ if (!reader.read(kPresenceBits, present) || present == 0 || !reader.read(kByteBits, raw)) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
value = static_cast<std::uint8_t>(raw - kByteBias);
|
|
value = static_cast<std::uint8_t>(raw - kByteBias);
|
|
|
return true;
|
|
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;
|
|
std::uint64_t raw = 0;
|
|
|
- if (!reader.read(8, raw)) {
|
|
|
|
|
|
|
+ if (!reader.read(kByteBits, raw)) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
- put8(header, offset, raw - kByteBias);
|
|
|
|
|
|
|
+ value = static_cast<std::uint8_t>(raw - kByteBias);
|
|
|
return true;
|
|
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;
|
|
std::uint64_t raw = 0;
|
|
|
- if (!reader.read(16, raw)) {
|
|
|
|
|
|
|
+ if (!reader.read(kWordBits, raw)) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
- put16(header, offset, raw - kWordBias);
|
|
|
|
|
|
|
+ value = static_cast<std::uint16_t>(raw - kWordBias);
|
|
|
return true;
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
- put8(header, kOffsetF32, kRecordTrailingField);
|
|
|
|
|
|
|
+ std::memcpy(request.appearanceHeader.data(), &header, sizeof header);
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
|
|
|