Prechádzať zdrojové kódy

Split the Sense body readers from the msg-6 packet framing

The per-schema body readers move to their own file, which keeps the decoder
under the source size limit. The engagement and attachment body widths are
named constants.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
stan 4 dní pred
rodič
commit
81264a2bc7

+ 2 - 0
Sunrise/Sunrise.vcxproj

@@ -1280,6 +1280,7 @@
     <ClCompile Include="src\middleware\bap\activity_message\activity_telemetry_parser.cpp" />
     <ClCompile Include="src\middleware\bap\activity_message\activity_start_activity_parser.cpp" />
     <ClCompile Include="src\middleware\bap\activity_message\activity_sense_update_decoder.cpp" />
+    <ClCompile Include="src\middleware\bap\activity_message\activity_sense_body_decoder.cpp" />
     <ClCompile Include="src\middleware\bap\activity_message\activity_sense_roster_delta.cpp" />
     <ClCompile Include="src\server\bap\encrypted\activity_message\receipts\activity_message_receipts.cpp" />
     <ClCompile Include="src\server\bap\encrypted\activity_message\receipts\activity_incident_grants.cpp" />
@@ -2195,6 +2196,7 @@
     <ClInclude Include="src\middleware\bap\activity_message\start_activity.h" />
     <ClInclude Include="src\middleware\bap\activity_message\sense_roster_delta.h" />
     <ClInclude Include="src\middleware\bap\activity_message\sense_update.h" />
+    <ClInclude Include="src\middleware\bap\activity_message\sense_update_body.h" />
     <ClInclude Include="src\server\bap\encrypted\activity_message\receipts\activity_message_receipts.h" />
     <ClInclude Include="src\server\bap\encrypted\activity_message\receipts\activity_incident_grants.h" />
     <ClInclude Include="src\state\activity\receipts\definition.h" />

+ 686 - 0
Sunrise/src/middleware/bap/activity_message/activity_sense_body_decoder.cpp

@@ -0,0 +1,686 @@
+#include <bit>
+#include <limits>
+
+#include "sense_update_body.h"
+
+// The per-schema msg-6 Sense body readers. Each reads one root body inside its object budget.
+
+namespace sunrise::middleware::bap::activity_message::sense_update::body {
+namespace {
+
+/** Schema tags the sense-update objects and their nested arrays declare. */
+constexpr std::uint32_t kDevice = 0x80804F47U, kScene = 0x8080626AU;
+constexpr std::uint32_t kSquad = 0x80807ECCU, kObjective = 0x80807F04U;
+constexpr std::uint32_t kOccupancy = 0x80809531U;
+constexpr std::uint32_t kAttachment = 0x8080954AU;
+constexpr std::uint32_t kGhostLink = 0x80804D3EU;
+constexpr std::uint32_t kObject = 0x8080992EU;
+constexpr std::uint32_t kObjectSpawnMask = 0x80809E1BU, kObjectReplies = 0x80809AEAU;
+constexpr std::uint32_t kSceneEvents = 0x808094DFU, kSceneList = 0x808094E1U;
+constexpr std::uint32_t kSquadCounts = 0x80809491U, kSquadList = 0x80807ECFU;
+constexpr std::uint32_t kSquadReals = 0x80807ECDU;
+constexpr std::uint32_t kObjectiveBlock = 0x80807F07U, kObjectiveTasks = 0x80807F08U;
+constexpr std::uint32_t kCombatant = 0x80807DA2U, kCombatantAtoms = 0x80807F6EU;
+constexpr std::uint32_t kCombatantKeyed = 0x80807DA3U, kCombatantLanes = 0x80807DA4U;
+constexpr std::uint32_t kEngagement = 0x808094F0U, kEngagementList = 0x808094F8U;
+/** The engagement list: a five-bit element count that the native list caps at 16. */
+constexpr std::uint8_t kEngagementCountWidth = 5;
+constexpr std::uint64_t kEngagementCapacity = 16;
+/** Engagement ordinal 1 is a 16-bit value biased by 32768. */
+constexpr std::uint16_t kEngagementValueOrdinal = 1;
+constexpr std::uint8_t kEngagementValueWidth = 16;
+constexpr std::int32_t kEngagementValueBias = 32'768;
+/** The type-26 attachment body: three required signed 32-bit counters, then one flag. */
+constexpr std::uint16_t kAttachmentCounterCount = 3;
+constexpr std::uint8_t kAttachmentCounterWidth = 32;
+constexpr std::uint16_t kAttachmentFlagOrdinal = kAttachmentCounterCount;
+/** Type-13 participation root and its nested blocks, in body order. */
+constexpr std::uint32_t kPlayer = 0x80804F2FU, kPlayerState = 0x808094E4U;
+constexpr std::uint32_t kPlayerScalars = 0x80804F39U, kPlayerTail = 0x80804F35U;
+constexpr std::uint32_t kPlayerIdentity = 0x808094DDU, kPlayerKeys = 0x808094DFU;
+constexpr std::uint32_t kPlayerPairs = 0x808094E3U, kPlayerFlags = 0x80804F31U;
+constexpr std::uint32_t kPlayerOptional = 0x80804F34U;
+/** Type-20 damage monitor root: health, shield, echoed revision. */
+constexpr std::uint32_t kDamage = 0x80809562U;
+/** Registered object replies: the interaction latch and the ownership key. */
+constexpr std::uint32_t kInteractionReply = 0x80804FB7U, kOwnershipReply = 0x80809ACCU;
+/** Counted lists inside the participation body. */
+constexpr std::uint64_t kPlayerScalarCountMaximum = 4, kPlayerKeyCountMaximum = 32;
+/** Quantization ceilings the combatant reals carry in their own descriptors. */
+constexpr std::uint32_t kSpatialMaximumBits = 0x45000000U, kUnitMaximumBits = 0x3F800000U;
+
+/** @param width Field width in bits. @return Low-bit mask of that width. */
+[[nodiscard]] constexpr std::uint64_t mask(std::uint8_t width) noexcept {
+    return width == 64 ? (std::numeric_limits<std::uint64_t>::max)()
+                       : (std::uint64_t{1} << width) - 1;
+}
+[[nodiscard]] std::int64_t
+signed_value(std::uint64_t raw, std::int32_t bias, std::uint8_t storageWidth) noexcept {
+    const std::uint64_t decoded =
+        (raw - static_cast<std::uint64_t>(static_cast<std::int64_t>(bias))) & mask(storageWidth);
+    if (storageWidth == 64) {
+        return static_cast<std::int64_t>(decoded);
+    }
+    const std::uint64_t sign = std::uint64_t{1} << (storageWidth - 1);
+    return static_cast<std::int64_t>((decoded ^ sign) - sign);
+}
+
+/** Required fields are present; optional fields consume one presence bit within the budget. */
+[[nodiscard]] bool present(Reader& reader, bool optional, bool& output) noexcept {
+    output = true;
+    if (!optional) {
+        return true;
+    }
+    std::uint64_t raw = 0;
+    if (!reader.read(1, raw)) {
+        return false;
+    }
+    output = raw != 0;
+    return true;
+}
+/** Records an unsigned value only when its presence bit and selected value fit. */
+[[nodiscard]] bool read_unsigned(Reader& reader,
+                                 Values& values,
+                                 std::uint32_t schema,
+                                 std::uint16_t ordinal,
+                                 std::uint8_t width,
+                                 bool optional,
+                                 std::uint32_t occurrence = 0) noexcept {
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    bool exists = true;
+    if (!present(reader, optional, exists)) {
+        return false;
+    }
+    std::uint64_t raw = 0;
+    if (exists && !reader.read(width, raw)) {
+        return false;
+    }
+    values.put(schema,
+               ordinal,
+               occurrence,
+               at,
+               exists ? width : 0,
+               ValueKind::unsignedInteger,
+               raw,
+               0,
+               0.0F,
+               exists);
+    return true;
+}
+/** Removes wire bias and sign-extends to the host width after reading a complete selected field. */
+[[nodiscard]] bool read_signed(Reader& reader,
+                               Values& values,
+                               std::uint32_t schema,
+                               std::uint16_t ordinal,
+                               std::uint8_t wireWidth,
+                               std::uint8_t storageWidth,
+                               std::int32_t bias,
+                               bool optional,
+                               std::uint32_t occurrence = 0) noexcept {
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    bool exists = true;
+    if (!present(reader, optional, exists)) {
+        return false;
+    }
+    std::uint64_t raw = 0;
+    if (exists && !reader.read(wireWidth, raw)) {
+        return false;
+    }
+    const std::int64_t decoded = exists ? signed_value(raw, bias, storageWidth) : 0;
+    values.put(schema,
+               ordinal,
+               occurrence,
+               at,
+               exists ? wireWidth : 0,
+               ValueKind::signedInteger,
+               static_cast<std::uint64_t>(decoded),
+               decoded,
+               0.0F,
+               exists);
+    return true;
+}
+/** Reads one boolean field into the value table. */
+[[nodiscard]] bool read_bool(Reader& reader,
+                             Values& values,
+                             std::uint32_t schema,
+                             std::uint16_t ordinal,
+                             bool optional,
+                             std::uint32_t occurrence = 0) noexcept {
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    bool exists = true;
+    if (!present(reader, optional, exists)) {
+        return false;
+    }
+    std::uint64_t raw = 0;
+    if (exists && !reader.read(1, raw)) {
+        return false;
+    }
+    values.put(
+        schema, ordinal, occurrence, at, exists ? 1 : 0, ValueKind::boolean, raw, 0, 0.0F, exists);
+    return true;
+}
+/** Quantized endpoints stay exact; width 32 carries raw float bits. */
+[[nodiscard]] float
+real_value(std::uint64_t raw, std::uint8_t width, std::uint32_t maximumBits) noexcept {
+    if (width == 32) {
+        return std::bit_cast<float>(static_cast<std::uint32_t>(raw));
+    }
+    const float maximum = std::bit_cast<float>(maximumBits);
+    const std::uint64_t levels = std::uint64_t{1} << width;
+    if (raw == 0) {
+        return 0.0F;
+    }
+    if (raw == levels - 1) {
+        return maximum;
+    }
+    const float step = maximum / static_cast<float>(levels - 2);
+    return static_cast<float>(raw - 1) * step + step * 0.5F;
+}
+/** Records a real only when its presence bit and selected value fit. */
+[[nodiscard]] bool read_real(Reader& reader,
+                             Values& values,
+                             std::uint32_t schema,
+                             std::uint16_t ordinal,
+                             std::uint8_t width,
+                             bool optional,
+                             std::uint32_t occurrence = 0,
+                             std::uint32_t maximumBits = 0) noexcept {
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    bool exists = true;
+    if (!present(reader, optional, exists)) {
+        return false;
+    }
+    std::uint64_t raw = 0;
+    if (exists && !reader.read(width, raw)) {
+        return false;
+    }
+    values.put(schema,
+               ordinal,
+               occurrence,
+               at,
+               exists ? width : 0,
+               ValueKind::real32,
+               raw,
+               0,
+               exists ? real_value(raw, width, maximumBits) : 0.0F,
+               exists);
+    return true;
+}
+
+/** Decodes three device channels within the object's bit budget. */
+[[nodiscard]] NativeStatus decode_device(Reader& reader, Values& values) noexcept {
+    for (std::uint16_t channel = 0; channel < 3; ++channel) {
+        if (!read_real(reader, values, kDevice, channel * 2, 32, true)
+            || !read_signed(reader,
+                            values,
+                            kDevice,
+                            channel * 2 + 1,
+                            32,
+                            32,
+                            (std::numeric_limits<std::int32_t>::min)(),
+                            true)) {
+            return NativeStatus::malformed;
+        }
+    }
+    return NativeStatus::complete;
+}
+/** Decodes the occupancy flags and signed values within the object's bit budget. */
+[[nodiscard]] NativeStatus decode_occupancy(Reader& reader, Values& values) noexcept {
+    return read_bool(reader, values, kOccupancy, 0, false)
+                   && read_bool(reader, values, kOccupancy, 1, false)
+                   && read_signed(reader,
+                                  values,
+                                  kOccupancy,
+                                  2,
+                                  32,
+                                  32,
+                                  (std::numeric_limits<std::int32_t>::min)(),
+                                  false)
+                   && read_signed(reader,
+                                  values,
+                                  kOccupancy,
+                                  3,
+                                  32,
+                                  32,
+                                  (std::numeric_limits<std::int32_t>::min)(),
+                                  false)
+               ? NativeStatus::complete
+               : NativeStatus::malformed;
+}
+/** Decodes a scene and refuses event counts above the native capacity. */
+[[nodiscard]] NativeStatus decode_scene(Reader& reader, Values& values) noexcept {
+    if (!read_signed(
+            reader, values, kScene, 0, 32, 32, (std::numeric_limits<std::int32_t>::min)(), false)
+        || !read_bool(reader, values, kScene, 1, false)
+        || !read_signed(reader, values, kScene, 2, 31, 32, 0, true)
+        || !read_signed(reader, values, kScene, 3, 2, 8, 1, false)) {
+        return NativeStatus::malformed;
+    }
+    std::uint64_t count = 0;
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    if (!reader.read(6, count)) {
+        return NativeStatus::malformed;
+    }
+    if (count > 32) {
+        return NativeStatus::unsafeCount;
+    }
+    values.put(kSceneList, 0, 0, at, 6, ValueKind::unsignedInteger, count, 0, 0.0F, true);
+    for (std::uint32_t index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kSceneEvents, 0, 32, false, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    return NativeStatus::complete;
+}
+/** Decodes squad fields and bounded member lists within the object's bit budget. */
+[[nodiscard]] NativeStatus decode_squad(Reader& reader, Values& values) noexcept {
+    if (!read_signed(reader, values, kSquad, 0, 31, 32, 0, true)
+        || !read_signed(reader, values, kSquad, 1, 31, 32, 0, true)
+        || !read_signed(reader, values, kSquad, 2, 31, 32, 0, true)
+        || !read_signed(reader, values, kSquad, 3, 6, 32, 0, true)
+        || !read_real(reader, values, kSquad, 4, 7, true, 0, 1157562368U)
+        || !read_signed(reader, values, kSquad, 5, 31, 32, 0, true)
+        || !read_signed(reader, values, kSquad, 6, 2, 8, 1, false)
+        || !read_signed(reader, values, kSquad, 7, 3, 8, 1, false)
+        || !read_bool(reader, values, kSquad, 8, false)
+        || !read_bool(reader, values, kSquad, 9, false)
+        || !read_bool(reader, values, kSquad, 10, false)) {
+        return NativeStatus::malformed;
+    }
+    bool exists = false;
+    if (!present(reader, true, exists)) {
+        return NativeStatus::malformed;
+    }
+    if (exists) {
+        std::uint64_t count = 0;
+        const auto at = static_cast<std::uint32_t>(reader.position());
+        if (!reader.read(4, count)) {
+            return NativeStatus::malformed;
+        }
+        if (count > 8) {
+            return NativeStatus::unsafeCount;
+        }
+        values.put(kSquadList, 0, 0, at, 4, ValueKind::unsignedInteger, count, 0, 0.0F, true);
+        for (std::uint32_t index = 0; index < count; ++index) {
+            if (!read_signed(reader,
+                             values,
+                             kSquadCounts,
+                             0,
+                             32,
+                             32,
+                             (std::numeric_limits<std::int32_t>::min)(),
+                             false,
+                             index)) {
+                return NativeStatus::malformed;
+            }
+        }
+    }
+    if (!present(reader, true, exists)) {
+        return NativeStatus::malformed;
+    }
+    if (exists) {
+        for (std::uint32_t index = 0; index < 24; ++index) {
+            if (!read_real(reader, values, kSquadReals, 0, 7, true, index, 1157562368U)) {
+                return NativeStatus::malformed;
+            }
+        }
+    }
+    return NativeStatus::complete;
+}
+/** Decodes the optional objective blocks and their fixed task arrays. */
+[[nodiscard]] NativeStatus decode_objective(Reader& reader, Values& values) noexcept {
+    bool exists = false;
+    if (!present(reader, true, exists)) {
+        return NativeStatus::malformed;
+    }
+    if (exists) {
+        for (std::uint32_t block = 0; block < 24; ++block) {
+            if (!read_signed(reader, values, kObjectiveBlock, 0, 7, 8, 0, true, block)
+                || !read_unsigned(reader, values, kObjectiveBlock, 1, 1, false, block)) {
+                return NativeStatus::malformed;
+            }
+            bool tasks = false;
+            if (!present(reader, true, tasks)) {
+                return NativeStatus::malformed;
+            }
+            if (!tasks) {
+                continue;
+            }
+            for (std::uint32_t task = 0; task < 24; ++task) {
+                if (!read_signed(
+                        reader, values, kObjectiveTasks, 0, 7, 8, 0, true, block * 24 + task)) {
+                    return NativeStatus::malformed;
+                }
+            }
+        }
+    }
+    return read_signed(reader, values, kObjective, 1, 31, 32, 0, true)
+                   && read_unsigned(reader, values, kObjective, 2, 32, true)
+               ? NativeStatus::complete
+               : NativeStatus::malformed;
+}
+/** Decodes an object and accepts only the registered interaction and ownership replies. */
+[[nodiscard]] NativeStatus decode_object(Reader& reader, Values& values) noexcept {
+    if (!read_signed(
+            reader, values, kObject, 0, 32, 32, (std::numeric_limits<std::int32_t>::min)(), false)
+        || !read_bool(reader, values, kObject, 1, false)
+        || !read_bool(reader, values, kObject, 2, false)
+        || !read_signed(reader,
+                        values,
+                        kObject,
+                        3,
+                        32,
+                        32,
+                        (std::numeric_limits<std::int32_t>::min)(),
+                        false)) {
+        return NativeStatus::malformed;
+    }
+    for (std::uint32_t index = 0; index < 2; ++index) {
+        if (!read_unsigned(reader, values, kObjectSpawnMask, 0, 32, false, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    std::uint64_t count = 0;
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    if (!reader.read(2, count)) {
+        return NativeStatus::malformed;
+    }
+    values.put(kObjectReplies, 0, 0, at, 2, ValueKind::unsignedInteger, count, 0, 0.0F, true);
+    for (std::uint32_t index = 0; index < count; ++index) {
+        std::uint64_t exists = 0, schema = 0;
+        if (!reader.read(1, exists)) {
+            return NativeStatus::malformed;
+        }
+        if (!exists) {
+            continue;
+        }
+        if (!reader.read(32, schema)) {
+            return NativeStatus::malformed;
+        }
+        if (schema == kInteractionReply) {
+            // The interaction latch and the revision that set it.
+            if (!read_bool(reader, values, kInteractionReply, 0, false, index)
+                || !read_signed(reader,
+                                values,
+                                kInteractionReply,
+                                1,
+                                32,
+                                32,
+                                (std::numeric_limits<std::int32_t>::min)(),
+                                false,
+                                index)) {
+                return NativeStatus::malformed;
+            }
+        } else if (schema == kOwnershipReply) {
+            // The owner key is always written, zero when nobody holds the object.
+            if (!read_bool(reader, values, kOwnershipReply, 0, false, index)
+                || !read_unsigned(reader, values, kOwnershipReply, 1, 64, false, index)) {
+                return NativeStatus::malformed;
+            }
+        } else {
+            return NativeStatus::unsupported;
+        }
+    }
+    return NativeStatus::complete;
+}
+/** Decodes combatant generations, program state, keyed lanes and root flags within the budget. */
+[[nodiscard]] NativeStatus decode_combatant(Reader& reader, Values& values) noexcept {
+    if (!read_signed(reader, values, kCombatant, 0, 31, 32, 0, true)
+        || !read_real(reader, values, kCombatant, 1, 9, true, 0, kSpatialMaximumBits)
+        || !read_signed(reader, values, kCombatant, 2, 31, 32, 0, true)) {
+        return NativeStatus::malformed;
+    }
+    bool exists = false;
+    if (!present(reader, true, exists)) {
+        return NativeStatus::malformed;
+    }
+    if (exists
+        && (!read_signed(reader, values, kCombatantAtoms, 0, 6, 32, 0, true)
+            || !read_signed(reader, values, kCombatantAtoms, 1, 31, 32, 0, true)
+            || !read_signed(reader, values, kCombatantAtoms, 2, 31, 32, 0, true)
+            || !read_bool(reader, values, kCombatantAtoms, 3, false))) {
+        return NativeStatus::malformed;
+    }
+    if (!present(reader, true, exists)) {
+        return NativeStatus::malformed;
+    }
+    if (exists) {
+        bool lanes = false;
+        if (!present(reader, true, lanes)) {
+            return NativeStatus::malformed;
+        }
+        for (std::uint32_t index = 0; lanes && index < 8; ++index) {
+            if (!read_signed(reader, values, kCombatantLanes, 0, 31, 32, 0, true, index)) {
+                return NativeStatus::malformed;
+            }
+        }
+        if (!read_unsigned(reader, values, kCombatantKeyed, 1, 32, true)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_signed(reader, values, kCombatant, 5, 31, 32, 0, true)
+        || !read_signed(reader, values, kCombatant, 6, 2, 8, 1, false)
+        || !read_signed(reader, values, kCombatant, 7, 31, 32, 0, true)
+        || !read_real(reader, values, kCombatant, 8, 7, true, 0, kUnitMaximumBits)
+        || !read_real(reader, values, kCombatant, 9, 7, true, 0, kUnitMaximumBits)
+        || !read_bool(reader, values, kCombatant, 10, false)
+        || !read_bool(reader, values, kCombatant, 11, false)) {
+        return NativeStatus::malformed;
+    }
+    return NativeStatus::complete;
+}
+/**
+ * Decodes the engagement body. Only an empty element list is supported: the element codec is
+ * unknown, and its storage size says nothing about its wire width.
+ */
+[[nodiscard]] NativeStatus decode_engagement(Reader& reader, Values& values) noexcept {
+    std::uint64_t count = 0;
+    const auto at = static_cast<std::uint32_t>(reader.position());
+    if (!reader.read(kEngagementCountWidth, count)) {
+        return NativeStatus::malformed;
+    }
+    if (count > kEngagementCapacity) {
+        return NativeStatus::unsafeCount;
+    }
+    if (count != 0) {
+        return NativeStatus::unsupported;
+    }
+    values.put(kEngagementList,
+               0,
+               0,
+               at,
+               kEngagementCountWidth,
+               ValueKind::unsignedInteger,
+               count,
+               0,
+               0.0F,
+               true);
+    return read_signed(reader,
+                       values,
+                       kEngagement,
+                       kEngagementValueOrdinal,
+                       kEngagementValueWidth,
+                       kEngagementValueWidth,
+                       kEngagementValueBias,
+                       false)
+               ? NativeStatus::complete
+               : NativeStatus::malformed;
+}
+
+/** Decodes the type-26 attachment body. */
+[[nodiscard]] NativeStatus decode_attachment(Reader& reader, Values& values) noexcept {
+    for (std::uint16_t ordinal = 0; ordinal < kAttachmentCounterCount; ++ordinal) {
+        if (!read_signed(reader,
+                         values,
+                         kAttachment,
+                         ordinal,
+                         kAttachmentCounterWidth,
+                         kAttachmentCounterWidth,
+                         (std::numeric_limits<std::int32_t>::min)(),
+                         false)) {
+            return NativeStatus::malformed;
+        }
+    }
+    return read_bool(reader, values, kAttachment, kAttachmentFlagOrdinal, false)
+               ? NativeStatus::complete
+               : NativeStatus::malformed;
+}
+
+/**
+ * Decodes the type-13 participation body. The whole body is read before its Ghost flag counts,
+ * because a player mid-load publishes a body with the flag but without a settled actor.
+ */
+[[nodiscard]] NativeStatus decode_player(Reader& reader, Values& values) noexcept {
+    // The participation field uses a signed 32-bit midpoint bias.
+    constexpr auto signedBias = (std::numeric_limits<std::int32_t>::min)();
+    if (!read_signed(reader, values, kPlayerState, 0, 32, 32, signedBias, true)
+        || !read_unsigned(reader, values, kPlayerState, 1, 32, true)) {
+        return NativeStatus::malformed;
+    }
+    for (std::uint16_t ordinal = 2; ordinal <= 5; ++ordinal) {
+        if (!read_bool(reader, values, kPlayerState, ordinal, false)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_signed(reader, values, kPlayerState, 6, 3, 8, 1, false)
+        || !read_signed(reader, values, kPlayerState, 7, 2, 8, 1, false)) {
+        return NativeStatus::malformed;
+    }
+    std::uint64_t count = 0;
+    if (!reader.read(3, count)) {
+        return NativeStatus::malformed;
+    }
+    if (count > kPlayerScalarCountMaximum) {
+        return NativeStatus::unsafeCount;
+    }
+    for (unsigned index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kPlayerScalars, 0, 32, false, index)
+            || !read_real(reader, values, kPlayerScalars, 1, 32, false, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_real(reader, values, kPlayerTail, 1, 32, false)
+        || !read_signed(reader, values, kPlayerTail, 2, 5, 32, 1, false)
+        || !read_signed(reader, values, kPlayerTail, 3, 32, 32, signedBias, true)) {
+        return NativeStatus::malformed;
+    }
+    for (std::uint16_t ordinal = 4; ordinal <= 6; ++ordinal) {
+        if (!read_bool(reader, values, kPlayerTail, ordinal, false)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_unsigned(reader, values, kPlayerIdentity, 0, 64, true)
+        || !read_unsigned(reader, values, kPlayerIdentity, 1, 5, false)
+        || !read_signed(reader, values, kPlayerIdentity, 2, 6, 8, 3, false)) {
+        return NativeStatus::malformed;
+    }
+    if (!reader.read(6, count)) {
+        return NativeStatus::malformed;
+    }
+    if (count > kPlayerKeyCountMaximum) {
+        return NativeStatus::unsafeCount;
+    }
+    for (unsigned index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kPlayerKeys, 0, 32, false, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!reader.read(6, count)) {
+        return NativeStatus::malformed;
+    }
+    if (count > kPlayerKeyCountMaximum) {
+        return NativeStatus::unsafeCount;
+    }
+    for (unsigned index = 0; index < count; ++index) {
+        if (!read_unsigned(reader, values, kPlayerPairs, 0, 32, true, index)
+            || !read_unsigned(reader, values, kPlayerPairs, 1, 64, true, index)) {
+            return NativeStatus::malformed;
+        }
+    }
+    if (!read_bool(reader, values, kPlayerFlags, 0, false)
+        || !read_unsigned(reader, values, kPlayerFlags, 1, 4, false)
+        || !read_unsigned(reader, values, kPlayerFlags, 2, 16, true)
+        || !read_unsigned(reader, values, kPlayerFlags, 3, 16, true)) {
+        return NativeStatus::malformed;
+    }
+    bool exists = false;
+    if (!present(reader, true, exists)) {
+        return NativeStatus::malformed;
+    }
+    if (exists && !read_unsigned(reader, values, kPlayerOptional, 0, 32, false)) {
+        return NativeStatus::malformed;
+    }
+    return read_bool(reader, values, kPlayerFlags, 5, false)
+                   && read_signed(reader, values, kPlayerFlags, 6, 8, 8, 128, false)
+                   && read_signed(reader, values, kPlayer, 4, 32, 32, signedBias, false)
+               ? NativeStatus::complete
+               : NativeStatus::malformed;
+}
+
+} // namespace
+
+/** Decodes one root body of the given schema; the status names how far it got. */
+[[nodiscard]] NativeStatus
+decode_body(std::uint32_t schema, Reader& reader, Values& values) noexcept {
+    std::uint64_t root = 0;
+    if (!reader.read(1, root)) {
+        return NativeStatus::malformed;
+    }
+    if (root == 0) {
+        return NativeStatus::complete;
+    }
+    switch (schema) {
+    case kPlayer:
+        return decode_player(reader, values);
+    case kDamage:
+        // Three mandatory fields: health, shield, then the echoed Auth revision.
+        return read_real(reader, values, schema, 0, 32, false)
+                       && read_real(reader, values, schema, 1, 32, false)
+                       && read_signed(reader,
+                                      values,
+                                      schema,
+                                      2,
+                                      32,
+                                      32,
+                                      (std::numeric_limits<std::int32_t>::min)(),
+                                      false)
+                   ? NativeStatus::complete
+                   : NativeStatus::malformed;
+    case kGhostLink:
+        // Three mandatory fields: active, elapsed over duration, accepted Auth generation.
+        return read_bool(reader, values, kGhostLink, 0, false)
+                       && read_real(reader, values, kGhostLink, 1, 32, false)
+                       && read_signed(reader,
+                                      values,
+                                      kGhostLink,
+                                      2,
+                                      32,
+                                      32,
+                                      (std::numeric_limits<std::int32_t>::min)(),
+                                      false)
+                   ? NativeStatus::complete
+                   : NativeStatus::malformed;
+    case kDevice:
+        return decode_device(reader, values);
+    case kScene:
+        return decode_scene(reader, values);
+    case kSquad:
+        return decode_squad(reader, values);
+    case kObjective:
+        return decode_objective(reader, values);
+    case kOccupancy:
+        return decode_occupancy(reader, values);
+    case kAttachment:
+        return decode_attachment(reader, values);
+    case kObject:
+        return decode_object(reader, values);
+    case kCombatant:
+        return decode_combatant(reader, values);
+    case kEngagement:
+        return decode_engagement(reader, values);
+    default:
+        return NativeStatus::unsupported;
+    }
+}
+
+} // namespace sunrise::middleware::bap::activity_message::sense_update::body

+ 5 - 722
Sunrise/src/middleware/bap/activity_message/activity_sense_update_decoder.cpp

@@ -1,10 +1,8 @@
-#include <bit>
-#include <limits>
-
 #include "../../encoding/bit_reader.h"
 #include "../../encoding/byte_order.h"
 #include "sense_roster_delta.h"
 #include "sense_update.h"
+#include "sense_update_body.h"
 
 namespace sunrise::middleware::bap::activity_message::sense_update {
 namespace {
@@ -14,727 +12,12 @@ namespace bits = middleware::encoding::bits;
 constexpr std::uint8_t kKeyWidth = 32, kTypeWidth = 7, kIndexWidth = 16;
 /** The slot type and index are sent unsigned, biased so their absent value is zero. */
 constexpr std::uint32_t kTypeBias = 1, kIndexBias = 32768;
-/** Schema tags the sense-update objects and their nested arrays declare. */
-constexpr std::uint32_t kDevice = 0x80804F47U, kScene = 0x8080626AU;
-constexpr std::uint32_t kSquad = 0x80807ECCU, kObjective = 0x80807F04U;
-constexpr std::uint32_t kOccupancy = 0x80809531U;
-constexpr std::uint32_t kAttachment = 0x8080954AU;
-constexpr std::uint32_t kGhostLink = 0x80804D3EU;
-constexpr std::uint32_t kObject = 0x8080992EU;
-constexpr std::uint32_t kObjectSpawnMask = 0x80809E1BU, kObjectReplies = 0x80809AEAU;
-constexpr std::uint32_t kSceneEvents = 0x808094DFU, kSceneList = 0x808094E1U;
-constexpr std::uint32_t kSquadCounts = 0x80809491U, kSquadList = 0x80807ECFU;
-constexpr std::uint32_t kSquadReals = 0x80807ECDU;
-constexpr std::uint32_t kObjectiveBlock = 0x80807F07U, kObjectiveTasks = 0x80807F08U;
-constexpr std::uint32_t kCombatant = 0x80807DA2U, kCombatantAtoms = 0x80807F6EU;
-constexpr std::uint32_t kCombatantKeyed = 0x80807DA3U, kCombatantLanes = 0x80807DA4U;
-constexpr std::uint32_t kEngagement = 0x808094F0U, kEngagementList = 0x808094F8U;
-/** Type-13 participation root and its nested blocks, in body order. */
-constexpr std::uint32_t kPlayer = 0x80804F2FU, kPlayerState = 0x808094E4U;
-constexpr std::uint32_t kPlayerScalars = 0x80804F39U, kPlayerTail = 0x80804F35U;
-constexpr std::uint32_t kPlayerIdentity = 0x808094DDU, kPlayerKeys = 0x808094DFU;
-constexpr std::uint32_t kPlayerPairs = 0x808094E3U, kPlayerFlags = 0x80804F31U;
-constexpr std::uint32_t kPlayerOptional = 0x80804F34U;
-/** Type-20 damage monitor root: health, shield, echoed revision. */
-constexpr std::uint32_t kDamage = 0x80809562U;
-/** Registered object replies: the interaction latch and the ownership key. */
-constexpr std::uint32_t kInteractionReply = 0x80804FB7U, kOwnershipReply = 0x80809ACCU;
-/** Counted lists inside the participation body. */
-constexpr std::uint64_t kPlayerScalarCountMaximum = 4, kPlayerKeyCountMaximum = 32;
-/** Quantization ceilings the combatant reals carry in their own descriptors. */
-constexpr std::uint32_t kSpatialMaximumBits = 0x45000000U, kUnitMaximumBits = 0x3F800000U;
-
-enum class NativeStatus : std::uint8_t { complete, unsupported, unsafeCount, malformed };
-
-/** Bit reader with its own budget, so one object cannot consume the whole packet. */
-class Reader final {
-public:
-    Reader(bits::Reader& source, std::size_t budget, std::size_t total) noexcept
-        : source_(source), left_(budget), total_(total) {}
-    [[nodiscard]] bool read(std::uint8_t width, std::uint64_t& value) noexcept {
-        if (width > left_ || !source_.read(width, value)) {
-            return false;
-        }
-        left_ -= width;
-        return true;
-    }
-    [[nodiscard]] bool skip(std::size_t width) noexcept {
-        if (width > left_ || !source_.skip(width)) {
-            return false;
-        }
-        left_ -= width;
-        return true;
-    }
-    [[nodiscard]] std::size_t left() const noexcept {
-        return left_;
-    }
-    [[nodiscard]] std::size_t position() const noexcept {
-        return total_ - source_.remaining_bits();
-    }
-
-private:
-    bits::Reader& source_;
-    std::size_t left_{};
-    std::size_t total_{};
-};
-
-/** @param width Field width in bits. @return Low-bit mask of that width. */
-[[nodiscard]] constexpr std::uint64_t mask(std::uint8_t width) noexcept {
-    return width == 64 ? (std::numeric_limits<std::uint64_t>::max)()
-                       : (std::uint64_t{1} << width) - 1;
-}
-[[nodiscard]] std::int64_t
-signed_value(std::uint64_t raw, std::int32_t bias, std::uint8_t storageWidth) noexcept {
-    const std::uint64_t decoded =
-        (raw - static_cast<std::uint64_t>(static_cast<std::int64_t>(bias))) & mask(storageWidth);
-    if (storageWidth == 64) {
-        return static_cast<std::int64_t>(decoded);
-    }
-    const std::uint64_t sign = std::uint64_t{1} << (storageWidth - 1);
-    return static_cast<std::int64_t>((decoded ^ sign) - sign);
-}
-
-/** Collects decoded values into the packet, dropping them once the object's room runs out. */
-class Values final {
-public:
-    Values(DecodedPacket& packet, DecodedObject* object) noexcept
-        : packet_(packet), object_(object) {}
-    /** Retains one field value, marking truncation when the packet's storage is full. */
-    void put(std::uint32_t schema,
-             std::uint16_t ordinal,
-             std::uint32_t occurrence,
-             std::uint32_t at,
-             std::uint8_t width,
-             ValueKind kind,
-             std::uint64_t raw,
-             std::int64_t signedRaw,
-             float real,
-             bool present) noexcept {
-        if (object_ == nullptr) {
-            return;
-        }
-        if (packet_.valueCount == packet_.values.size()) {
-            packet_.valuesTruncated = true;
-            return;
-        }
-        DecodedValue& value = packet_.values[packet_.valueCount++];
-        value.schemaRow = schema;
-        value.fieldRow = ordinal;
-        value.fieldOrdinal = ordinal;
-        value.occurrence = occurrence;
-        value.bitOffset = at;
-        value.width = width;
-        value.kind = kind;
-        value.unsignedValue = raw;
-        value.signedValue = signedRaw;
-        value.realValue = real;
-        value.present = present;
-    }
-
-private:
-    DecodedPacket& packet_;
-    DecodedObject* object_{};
-};
-
-/** Required fields are present; optional fields consume one presence bit within the budget. */
-[[nodiscard]] bool present(Reader& reader, bool optional, bool& output) noexcept {
-    output = true;
-    if (!optional) {
-        return true;
-    }
-    std::uint64_t raw = 0;
-    if (!reader.read(1, raw)) {
-        return false;
-    }
-    output = raw != 0;
-    return true;
-}
-/** Records an unsigned value only when its presence bit and selected value fit. */
-[[nodiscard]] bool read_unsigned(Reader& reader,
-                                 Values& values,
-                                 std::uint32_t schema,
-                                 std::uint16_t ordinal,
-                                 std::uint8_t width,
-                                 bool optional,
-                                 std::uint32_t occurrence = 0) noexcept {
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    bool exists = true;
-    if (!present(reader, optional, exists)) {
-        return false;
-    }
-    std::uint64_t raw = 0;
-    if (exists && !reader.read(width, raw)) {
-        return false;
-    }
-    values.put(schema,
-               ordinal,
-               occurrence,
-               at,
-               exists ? width : 0,
-               ValueKind::unsignedInteger,
-               raw,
-               0,
-               0.0F,
-               exists);
-    return true;
-}
-/** Removes wire bias and sign-extends to the host width after reading a complete selected field. */
-[[nodiscard]] bool read_signed(Reader& reader,
-                               Values& values,
-                               std::uint32_t schema,
-                               std::uint16_t ordinal,
-                               std::uint8_t wireWidth,
-                               std::uint8_t storageWidth,
-                               std::int32_t bias,
-                               bool optional,
-                               std::uint32_t occurrence = 0) noexcept {
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    bool exists = true;
-    if (!present(reader, optional, exists)) {
-        return false;
-    }
-    std::uint64_t raw = 0;
-    if (exists && !reader.read(wireWidth, raw)) {
-        return false;
-    }
-    const std::int64_t decoded = exists ? signed_value(raw, bias, storageWidth) : 0;
-    values.put(schema,
-               ordinal,
-               occurrence,
-               at,
-               exists ? wireWidth : 0,
-               ValueKind::signedInteger,
-               static_cast<std::uint64_t>(decoded),
-               decoded,
-               0.0F,
-               exists);
-    return true;
-}
-/** Reads one boolean field into the value table. */
-[[nodiscard]] bool read_bool(Reader& reader,
-                             Values& values,
-                             std::uint32_t schema,
-                             std::uint16_t ordinal,
-                             bool optional,
-                             std::uint32_t occurrence = 0) noexcept {
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    bool exists = true;
-    if (!present(reader, optional, exists)) {
-        return false;
-    }
-    std::uint64_t raw = 0;
-    if (exists && !reader.read(1, raw)) {
-        return false;
-    }
-    values.put(
-        schema, ordinal, occurrence, at, exists ? 1 : 0, ValueKind::boolean, raw, 0, 0.0F, exists);
-    return true;
-}
-/** Quantized endpoints stay exact; width 32 carries raw float bits. */
-[[nodiscard]] float
-real_value(std::uint64_t raw, std::uint8_t width, std::uint32_t maximumBits) noexcept {
-    if (width == 32) {
-        return std::bit_cast<float>(static_cast<std::uint32_t>(raw));
-    }
-    const float maximum = std::bit_cast<float>(maximumBits);
-    const std::uint64_t levels = std::uint64_t{1} << width;
-    if (raw == 0) {
-        return 0.0F;
-    }
-    if (raw == levels - 1) {
-        return maximum;
-    }
-    const float step = maximum / static_cast<float>(levels - 2);
-    return static_cast<float>(raw - 1) * step + step * 0.5F;
-}
-/** Records a real only when its presence bit and selected value fit. */
-[[nodiscard]] bool read_real(Reader& reader,
-                             Values& values,
-                             std::uint32_t schema,
-                             std::uint16_t ordinal,
-                             std::uint8_t width,
-                             bool optional,
-                             std::uint32_t occurrence = 0,
-                             std::uint32_t maximumBits = 0) noexcept {
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    bool exists = true;
-    if (!present(reader, optional, exists)) {
-        return false;
-    }
-    std::uint64_t raw = 0;
-    if (exists && !reader.read(width, raw)) {
-        return false;
-    }
-    values.put(schema,
-               ordinal,
-               occurrence,
-               at,
-               exists ? width : 0,
-               ValueKind::real32,
-               raw,
-               0,
-               exists ? real_value(raw, width, maximumBits) : 0.0F,
-               exists);
-    return true;
-}
-
-/** Decodes three device channels within the object's bit budget. */
-[[nodiscard]] NativeStatus decode_device(Reader& reader, Values& values) noexcept {
-    for (std::uint16_t channel = 0; channel < 3; ++channel) {
-        if (!read_real(reader, values, kDevice, channel * 2, 32, true)
-            || !read_signed(reader,
-                            values,
-                            kDevice,
-                            channel * 2 + 1,
-                            32,
-                            32,
-                            (std::numeric_limits<std::int32_t>::min)(),
-                            true)) {
-            return NativeStatus::malformed;
-        }
-    }
-    return NativeStatus::complete;
-}
-/** Decodes the occupancy flags and signed values within the object's bit budget. */
-[[nodiscard]] NativeStatus decode_occupancy(Reader& reader, Values& values) noexcept {
-    return read_bool(reader, values, kOccupancy, 0, false)
-                   && read_bool(reader, values, kOccupancy, 1, false)
-                   && read_signed(reader,
-                                  values,
-                                  kOccupancy,
-                                  2,
-                                  32,
-                                  32,
-                                  (std::numeric_limits<std::int32_t>::min)(),
-                                  false)
-                   && read_signed(reader,
-                                  values,
-                                  kOccupancy,
-                                  3,
-                                  32,
-                                  32,
-                                  (std::numeric_limits<std::int32_t>::min)(),
-                                  false)
-               ? NativeStatus::complete
-               : NativeStatus::malformed;
-}
-/** Decodes a scene and refuses event counts above the native capacity. */
-[[nodiscard]] NativeStatus decode_scene(Reader& reader, Values& values) noexcept {
-    if (!read_signed(
-            reader, values, kScene, 0, 32, 32, (std::numeric_limits<std::int32_t>::min)(), false)
-        || !read_bool(reader, values, kScene, 1, false)
-        || !read_signed(reader, values, kScene, 2, 31, 32, 0, true)
-        || !read_signed(reader, values, kScene, 3, 2, 8, 1, false)) {
-        return NativeStatus::malformed;
-    }
-    std::uint64_t count = 0;
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    if (!reader.read(6, count)) {
-        return NativeStatus::malformed;
-    }
-    if (count > 32) {
-        return NativeStatus::unsafeCount;
-    }
-    values.put(kSceneList, 0, 0, at, 6, ValueKind::unsignedInteger, count, 0, 0.0F, true);
-    for (std::uint32_t index = 0; index < count; ++index) {
-        if (!read_unsigned(reader, values, kSceneEvents, 0, 32, false, index)) {
-            return NativeStatus::malformed;
-        }
-    }
-    return NativeStatus::complete;
-}
-/** Decodes squad fields and bounded member lists within the object's bit budget. */
-[[nodiscard]] NativeStatus decode_squad(Reader& reader, Values& values) noexcept {
-    if (!read_signed(reader, values, kSquad, 0, 31, 32, 0, true)
-        || !read_signed(reader, values, kSquad, 1, 31, 32, 0, true)
-        || !read_signed(reader, values, kSquad, 2, 31, 32, 0, true)
-        || !read_signed(reader, values, kSquad, 3, 6, 32, 0, true)
-        || !read_real(reader, values, kSquad, 4, 7, true, 0, 1157562368U)
-        || !read_signed(reader, values, kSquad, 5, 31, 32, 0, true)
-        || !read_signed(reader, values, kSquad, 6, 2, 8, 1, false)
-        || !read_signed(reader, values, kSquad, 7, 3, 8, 1, false)
-        || !read_bool(reader, values, kSquad, 8, false)
-        || !read_bool(reader, values, kSquad, 9, false)
-        || !read_bool(reader, values, kSquad, 10, false)) {
-        return NativeStatus::malformed;
-    }
-    bool exists = false;
-    if (!present(reader, true, exists)) {
-        return NativeStatus::malformed;
-    }
-    if (exists) {
-        std::uint64_t count = 0;
-        const auto at = static_cast<std::uint32_t>(reader.position());
-        if (!reader.read(4, count)) {
-            return NativeStatus::malformed;
-        }
-        if (count > 8) {
-            return NativeStatus::unsafeCount;
-        }
-        values.put(kSquadList, 0, 0, at, 4, ValueKind::unsignedInteger, count, 0, 0.0F, true);
-        for (std::uint32_t index = 0; index < count; ++index) {
-            if (!read_signed(reader,
-                             values,
-                             kSquadCounts,
-                             0,
-                             32,
-                             32,
-                             (std::numeric_limits<std::int32_t>::min)(),
-                             false,
-                             index)) {
-                return NativeStatus::malformed;
-            }
-        }
-    }
-    if (!present(reader, true, exists)) {
-        return NativeStatus::malformed;
-    }
-    if (exists) {
-        for (std::uint32_t index = 0; index < 24; ++index) {
-            if (!read_real(reader, values, kSquadReals, 0, 7, true, index, 1157562368U)) {
-                return NativeStatus::malformed;
-            }
-        }
-    }
-    return NativeStatus::complete;
-}
-/** Decodes the optional objective blocks and their fixed task arrays. */
-[[nodiscard]] NativeStatus decode_objective(Reader& reader, Values& values) noexcept {
-    bool exists = false;
-    if (!present(reader, true, exists)) {
-        return NativeStatus::malformed;
-    }
-    if (exists) {
-        for (std::uint32_t block = 0; block < 24; ++block) {
-            if (!read_signed(reader, values, kObjectiveBlock, 0, 7, 8, 0, true, block)
-                || !read_unsigned(reader, values, kObjectiveBlock, 1, 1, false, block)) {
-                return NativeStatus::malformed;
-            }
-            bool tasks = false;
-            if (!present(reader, true, tasks)) {
-                return NativeStatus::malformed;
-            }
-            if (!tasks) {
-                continue;
-            }
-            for (std::uint32_t task = 0; task < 24; ++task) {
-                if (!read_signed(
-                        reader, values, kObjectiveTasks, 0, 7, 8, 0, true, block * 24 + task)) {
-                    return NativeStatus::malformed;
-                }
-            }
-        }
-    }
-    return read_signed(reader, values, kObjective, 1, 31, 32, 0, true)
-                   && read_unsigned(reader, values, kObjective, 2, 32, true)
-               ? NativeStatus::complete
-               : NativeStatus::malformed;
-}
-/** Decodes an object and accepts only the registered interaction and ownership replies. */
-[[nodiscard]] NativeStatus decode_object(Reader& reader, Values& values) noexcept {
-    if (!read_signed(
-            reader, values, kObject, 0, 32, 32, (std::numeric_limits<std::int32_t>::min)(), false)
-        || !read_bool(reader, values, kObject, 1, false)
-        || !read_bool(reader, values, kObject, 2, false)
-        || !read_signed(reader,
-                        values,
-                        kObject,
-                        3,
-                        32,
-                        32,
-                        (std::numeric_limits<std::int32_t>::min)(),
-                        false)) {
-        return NativeStatus::malformed;
-    }
-    for (std::uint32_t index = 0; index < 2; ++index) {
-        if (!read_unsigned(reader, values, kObjectSpawnMask, 0, 32, false, index)) {
-            return NativeStatus::malformed;
-        }
-    }
-    std::uint64_t count = 0;
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    if (!reader.read(2, count)) {
-        return NativeStatus::malformed;
-    }
-    values.put(kObjectReplies, 0, 0, at, 2, ValueKind::unsignedInteger, count, 0, 0.0F, true);
-    for (std::uint32_t index = 0; index < count; ++index) {
-        std::uint64_t exists = 0, schema = 0;
-        if (!reader.read(1, exists)) {
-            return NativeStatus::malformed;
-        }
-        if (!exists) {
-            continue;
-        }
-        if (!reader.read(32, schema)) {
-            return NativeStatus::malformed;
-        }
-        if (schema == kInteractionReply) {
-            // The interaction latch and the revision that set it.
-            if (!read_bool(reader, values, kInteractionReply, 0, false, index)
-                || !read_signed(reader,
-                                values,
-                                kInteractionReply,
-                                1,
-                                32,
-                                32,
-                                (std::numeric_limits<std::int32_t>::min)(),
-                                false,
-                                index)) {
-                return NativeStatus::malformed;
-            }
-        } else if (schema == kOwnershipReply) {
-            // The owner key is always written, zero when nobody holds the object.
-            if (!read_bool(reader, values, kOwnershipReply, 0, false, index)
-                || !read_unsigned(reader, values, kOwnershipReply, 1, 64, false, index)) {
-                return NativeStatus::malformed;
-            }
-        } else {
-            return NativeStatus::unsupported;
-        }
-    }
-    return NativeStatus::complete;
-}
-/** Decodes combatant generations, program state, keyed lanes and root flags within the budget. */
-[[nodiscard]] NativeStatus decode_combatant(Reader& reader, Values& values) noexcept {
-    if (!read_signed(reader, values, kCombatant, 0, 31, 32, 0, true)
-        || !read_real(reader, values, kCombatant, 1, 9, true, 0, kSpatialMaximumBits)
-        || !read_signed(reader, values, kCombatant, 2, 31, 32, 0, true)) {
-        return NativeStatus::malformed;
-    }
-    bool exists = false;
-    if (!present(reader, true, exists)) {
-        return NativeStatus::malformed;
-    }
-    if (exists
-        && (!read_signed(reader, values, kCombatantAtoms, 0, 6, 32, 0, true)
-            || !read_signed(reader, values, kCombatantAtoms, 1, 31, 32, 0, true)
-            || !read_signed(reader, values, kCombatantAtoms, 2, 31, 32, 0, true)
-            || !read_bool(reader, values, kCombatantAtoms, 3, false))) {
-        return NativeStatus::malformed;
-    }
-    if (!present(reader, true, exists)) {
-        return NativeStatus::malformed;
-    }
-    if (exists) {
-        bool lanes = false;
-        if (!present(reader, true, lanes)) {
-            return NativeStatus::malformed;
-        }
-        for (std::uint32_t index = 0; lanes && index < 8; ++index) {
-            if (!read_signed(reader, values, kCombatantLanes, 0, 31, 32, 0, true, index)) {
-                return NativeStatus::malformed;
-            }
-        }
-        if (!read_unsigned(reader, values, kCombatantKeyed, 1, 32, true)) {
-            return NativeStatus::malformed;
-        }
-    }
-    if (!read_signed(reader, values, kCombatant, 5, 31, 32, 0, true)
-        || !read_signed(reader, values, kCombatant, 6, 2, 8, 1, false)
-        || !read_signed(reader, values, kCombatant, 7, 31, 32, 0, true)
-        || !read_real(reader, values, kCombatant, 8, 7, true, 0, kUnitMaximumBits)
-        || !read_real(reader, values, kCombatant, 9, 7, true, 0, kUnitMaximumBits)
-        || !read_bool(reader, values, kCombatant, 10, false)
-        || !read_bool(reader, values, kCombatant, 11, false)) {
-        return NativeStatus::malformed;
-    }
-    return NativeStatus::complete;
-}
-/**
- * Decodes the engagement body. Only an empty element list is supported: the element codec is
- * unknown, and its storage size says nothing about its wire width.
- */
-[[nodiscard]] NativeStatus decode_engagement(Reader& reader, Values& values) noexcept {
-    constexpr std::uint64_t capacity = 16;
-    std::uint64_t count = 0;
-    const auto at = static_cast<std::uint32_t>(reader.position());
-    if (!reader.read(5, count)) {
-        return NativeStatus::malformed;
-    }
-    if (count > capacity) {
-        return NativeStatus::unsafeCount;
-    }
-    if (count != 0) {
-        return NativeStatus::unsupported;
-    }
-    values.put(kEngagementList, 0, 0, at, 5, ValueKind::unsignedInteger, count, 0, 0.0F, true);
-    return read_signed(reader, values, kEngagement, 1, 16, 16, 32768, false)
-               ? NativeStatus::complete
-               : NativeStatus::malformed;
-}
 
-/** Decodes the type-26 attachment body: three required signed 32-bit counters, then one flag. */
-[[nodiscard]] NativeStatus decode_attachment(Reader& reader, Values& values) noexcept {
-    for (std::uint16_t ordinal = 0; ordinal < 3; ++ordinal) {
-        if (!read_signed(reader,
-                         values,
-                         kAttachment,
-                         ordinal,
-                         32,
-                         32,
-                         (std::numeric_limits<std::int32_t>::min)(),
-                         false)) {
-            return NativeStatus::malformed;
-        }
-    }
-    return read_bool(reader, values, kAttachment, 3, false) ? NativeStatus::complete
-                                                            : NativeStatus::malformed;
-}
+using body::decode_body;
+using body::NativeStatus;
+using body::Reader;
+using body::Values;
 
-/**
- * Decodes the type-13 participation body. The whole body is read before its Ghost flag counts,
- * because a player mid-load publishes a body with the flag but without a settled actor.
- */
-[[nodiscard]] NativeStatus decode_player(Reader& reader, Values& values) noexcept {
-    // The participation field uses a signed 32-bit midpoint bias.
-    constexpr auto signedBias = (std::numeric_limits<std::int32_t>::min)();
-    if (!read_signed(reader, values, kPlayerState, 0, 32, 32, signedBias, true)
-        || !read_unsigned(reader, values, kPlayerState, 1, 32, true)) {
-        return NativeStatus::malformed;
-    }
-    for (std::uint16_t ordinal = 2; ordinal <= 5; ++ordinal) {
-        if (!read_bool(reader, values, kPlayerState, ordinal, false)) {
-            return NativeStatus::malformed;
-        }
-    }
-    if (!read_signed(reader, values, kPlayerState, 6, 3, 8, 1, false)
-        || !read_signed(reader, values, kPlayerState, 7, 2, 8, 1, false)) {
-        return NativeStatus::malformed;
-    }
-    std::uint64_t count = 0;
-    if (!reader.read(3, count)) {
-        return NativeStatus::malformed;
-    }
-    if (count > kPlayerScalarCountMaximum) {
-        return NativeStatus::unsafeCount;
-    }
-    for (unsigned index = 0; index < count; ++index) {
-        if (!read_unsigned(reader, values, kPlayerScalars, 0, 32, false, index)
-            || !read_real(reader, values, kPlayerScalars, 1, 32, false, index)) {
-            return NativeStatus::malformed;
-        }
-    }
-    if (!read_real(reader, values, kPlayerTail, 1, 32, false)
-        || !read_signed(reader, values, kPlayerTail, 2, 5, 32, 1, false)
-        || !read_signed(reader, values, kPlayerTail, 3, 32, 32, signedBias, true)) {
-        return NativeStatus::malformed;
-    }
-    for (std::uint16_t ordinal = 4; ordinal <= 6; ++ordinal) {
-        if (!read_bool(reader, values, kPlayerTail, ordinal, false)) {
-            return NativeStatus::malformed;
-        }
-    }
-    if (!read_unsigned(reader, values, kPlayerIdentity, 0, 64, true)
-        || !read_unsigned(reader, values, kPlayerIdentity, 1, 5, false)
-        || !read_signed(reader, values, kPlayerIdentity, 2, 6, 8, 3, false)) {
-        return NativeStatus::malformed;
-    }
-    if (!reader.read(6, count)) {
-        return NativeStatus::malformed;
-    }
-    if (count > kPlayerKeyCountMaximum) {
-        return NativeStatus::unsafeCount;
-    }
-    for (unsigned index = 0; index < count; ++index) {
-        if (!read_unsigned(reader, values, kPlayerKeys, 0, 32, false, index)) {
-            return NativeStatus::malformed;
-        }
-    }
-    if (!reader.read(6, count)) {
-        return NativeStatus::malformed;
-    }
-    if (count > kPlayerKeyCountMaximum) {
-        return NativeStatus::unsafeCount;
-    }
-    for (unsigned index = 0; index < count; ++index) {
-        if (!read_unsigned(reader, values, kPlayerPairs, 0, 32, true, index)
-            || !read_unsigned(reader, values, kPlayerPairs, 1, 64, true, index)) {
-            return NativeStatus::malformed;
-        }
-    }
-    if (!read_bool(reader, values, kPlayerFlags, 0, false)
-        || !read_unsigned(reader, values, kPlayerFlags, 1, 4, false)
-        || !read_unsigned(reader, values, kPlayerFlags, 2, 16, true)
-        || !read_unsigned(reader, values, kPlayerFlags, 3, 16, true)) {
-        return NativeStatus::malformed;
-    }
-    bool exists = false;
-    if (!present(reader, true, exists)) {
-        return NativeStatus::malformed;
-    }
-    if (exists && !read_unsigned(reader, values, kPlayerOptional, 0, 32, false)) {
-        return NativeStatus::malformed;
-    }
-    return read_bool(reader, values, kPlayerFlags, 5, false)
-                   && read_signed(reader, values, kPlayerFlags, 6, 8, 8, 128, false)
-                   && read_signed(reader, values, kPlayer, 4, 32, 32, signedBias, false)
-               ? NativeStatus::complete
-               : NativeStatus::malformed;
-}
-
-/** Decodes one root body of the given schema; the status names how far it got. */
-[[nodiscard]] NativeStatus
-decode_body(std::uint32_t schema, Reader& reader, Values& values) noexcept {
-    std::uint64_t root = 0;
-    if (!reader.read(1, root)) {
-        return NativeStatus::malformed;
-    }
-    if (root == 0) {
-        return NativeStatus::complete;
-    }
-    switch (schema) {
-    case kPlayer:
-        return decode_player(reader, values);
-    case kDamage:
-        // Three mandatory fields: health, shield, then the echoed Auth revision.
-        return read_real(reader, values, schema, 0, 32, false)
-                       && read_real(reader, values, schema, 1, 32, false)
-                       && read_signed(reader,
-                                      values,
-                                      schema,
-                                      2,
-                                      32,
-                                      32,
-                                      (std::numeric_limits<std::int32_t>::min)(),
-                                      false)
-                   ? NativeStatus::complete
-                   : NativeStatus::malformed;
-    case kGhostLink:
-        // Three mandatory fields: active, elapsed over duration, accepted Auth generation.
-        return read_bool(reader, values, kGhostLink, 0, false)
-                       && read_real(reader, values, kGhostLink, 1, 32, false)
-                       && read_signed(reader,
-                                      values,
-                                      kGhostLink,
-                                      2,
-                                      32,
-                                      32,
-                                      (std::numeric_limits<std::int32_t>::min)(),
-                                      false)
-                   ? NativeStatus::complete
-                   : NativeStatus::malformed;
-    case kDevice:
-        return decode_device(reader, values);
-    case kScene:
-        return decode_scene(reader, values);
-    case kSquad:
-        return decode_squad(reader, values);
-    case kObjective:
-        return decode_objective(reader, values);
-    case kOccupancy:
-        return decode_occupancy(reader, values);
-    case kAttachment:
-        return decode_attachment(reader, values);
-    case kObject:
-        return decode_object(reader, values);
-    case kCombatant:
-        return decode_combatant(reader, values);
-    case kEngagement:
-        return decode_engagement(reader, values);
-    default:
-        return NativeStatus::unsupported;
-    }
-}
 [[nodiscard]] bool skip_group(Reader& reader) noexcept {
     if (reader.left() == 0 || !reader.skip(reader.left() - 1)) {
         return false;

+ 97 - 0
Sunrise/src/middleware/bap/activity_message/sense_update_body.h

@@ -0,0 +1,97 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "../../encoding/bit_reader.h"
+#include "sense_update.h"
+
+// Shared by the msg-6 packet framing and the per-schema Sense body readers.
+
+namespace sunrise::middleware::bap::activity_message::sense_update::body {
+
+/** How far one body decode got. */
+enum class NativeStatus : std::uint8_t { complete, unsupported, unsafeCount, malformed };
+
+/** Bit reader with its own budget, so one object cannot consume the whole packet. */
+class Reader final {
+public:
+    Reader(encoding::bits::Reader& source, std::size_t budget, std::size_t total) noexcept
+        : source_(source), left_(budget), total_(total) {}
+    [[nodiscard]] bool read(std::uint8_t width, std::uint64_t& value) noexcept {
+        if (width > left_ || !source_.read(width, value)) {
+            return false;
+        }
+        left_ -= width;
+        return true;
+    }
+    [[nodiscard]] bool skip(std::size_t width) noexcept {
+        if (width > left_ || !source_.skip(width)) {
+            return false;
+        }
+        left_ -= width;
+        return true;
+    }
+    [[nodiscard]] std::size_t left() const noexcept {
+        return left_;
+    }
+    [[nodiscard]] std::size_t position() const noexcept {
+        return total_ - source_.remaining_bits();
+    }
+
+private:
+    encoding::bits::Reader& source_;
+    std::size_t left_{};
+    std::size_t total_{};
+};
+
+/** Collects decoded values into the packet, dropping them once the object's room runs out. */
+class Values final {
+public:
+    Values(DecodedPacket& packet, DecodedObject* object) noexcept
+        : packet_(packet), object_(object) {}
+    /** Retains one field value, marking truncation when the packet's storage is full. */
+    void put(std::uint32_t schema,
+             std::uint16_t ordinal,
+             std::uint32_t occurrence,
+             std::uint32_t at,
+             std::uint8_t width,
+             ValueKind kind,
+             std::uint64_t raw,
+             std::int64_t signedRaw,
+             float real,
+             bool present) noexcept {
+        if (object_ == nullptr) {
+            return;
+        }
+        if (packet_.valueCount == packet_.values.size()) {
+            packet_.valuesTruncated = true;
+            return;
+        }
+        DecodedValue& value = packet_.values[packet_.valueCount++];
+        value.schemaRow = schema;
+        value.fieldRow = ordinal;
+        value.fieldOrdinal = ordinal;
+        value.occurrence = occurrence;
+        value.bitOffset = at;
+        value.width = width;
+        value.kind = kind;
+        value.unsignedValue = raw;
+        value.signedValue = signedRaw;
+        value.realValue = real;
+        value.present = present;
+    }
+
+private:
+    DecodedPacket& packet_;
+    DecodedObject* object_{};
+};
+
+/**
+ * Decodes one root body of the given schema within the reader's budget.
+ * @return How far it got; any status but malformed leaves the group skippable.
+ */
+[[nodiscard]] NativeStatus
+decode_body(std::uint32_t schema, Reader& reader, Values& values) noexcept;
+
+} // namespace sunrise::middleware::bap::activity_message::sense_update::body