Explorar el Código

Decode the Triumphs claim request and resolve the record it names

A record whose requirements are met reads Ready to Claim, and clicking it sends Web Service
opcode 1801. The server answered that with a bare success and never looked at it, so the entry
stayed claimable no matter what else was authored.

Decode the request and resolve the record. The payload is three bytes in the same shape as the
opcode-1820 Collections pull: a presence bit then a fifteen-bit record row. The codec refuses an
absent record, non-zero padding, a wrong length and a wrong opcode.

Add a records domain so a claim can go from that row to the account bank index of the record's
completion flag. Record row +100 holds the flag's unlock slot, and a slot is not an array index:
the byte that sets slot s lives at the row of the mapping table whose destination is s. Resolve
that once at extraction rather than walking the mapping tables per request.

The dispatch hook deliberately runs before the dispatch chain. The shared reply path answers any
dispatched opcode that prepared no mutation with the refusal status, so wiring a claim into the
chain before it has a mutation would turn a silently accepted claim into an explicit refusal.
Move it into the chain in the same change that gives it one.

No state transition yet. Tracing three claims in play shows the client accepts the reply and
sends nothing further, so what is missing is a pushed state change rather than a richer response.
Sunrise/docs/record-claims.md records that trace, the wire format, and the three remaining pieces.
Millie hace 3 semanas
padre
commit
db83c04b43

+ 103 - 0
Sunrise/docs/record-claims.md

@@ -0,0 +1,103 @@
+# Claiming a Triumph: Web Service opcode 1801
+
+A record whose requirements are met reads **Ready to Claim** in the Triumphs screen. Clicking it
+sends opcode 1801. This build decodes that request and resolves the record it names; it does not yet
+change any state, so the entry stays claimable.
+
+## What the client does, measured
+
+Three claims were made in play and traced end to end at debug level:
+
+```
+ev=ws stage=request opcode=1801 transaction=0 payload_bytes=3 payload_hex=80DD00
+ev=ws1801 stage=claim result=ok reason=decoded record_index=221
+ev=transport stage=frame conn=1 type=1 bytes=37
+ev=bap svc=10 rsp=11 result=ok
+```
+
+The full opcode sequence around them:
+
+```
+39391 op104
+48358 op1801      claim
+52742 op1801      claim
+53212 op1801      claim
+67824 op701       14 s later, unrelated
+```
+
+**The client accepts the reply and asks for nothing else.** No retry, no state fetch, no follow-up
+opcode. A grep for push or queuez activity after the claims returns nothing, because the server
+sends nothing.
+
+That isolates the gap precisely: the reply shape is already correct, and the client is waiting on a
+**push** that never arrives. It is not rejecting the response, so a richer response payload is not
+what is missing.
+
+## The request
+
+Three bytes, the same shape as the opcode-1820 Collections pull: a presence bit then a fifteen-bit
+record row index.
+
+```
+payload_hex=80DD00  ->  0x80DD, presence 1, record row 221
+payload_hex=80E300  ->  0x80E3, presence 1, record row 227
+payload_hex=80E000  ->  0x80E0, presence 1, record row 224
+```
+
+The row indexes the records and lore table `0x81319339`. `opcode1801::parse_request` refuses an
+absent record, non-zero padding, a wrong length and a wrong opcode; 13 cases are covered by a
+standalone test run against payloads captured from real clicks.
+
+## Why the dispatch hook sits outside the chain
+
+`web_service_runtime.cpp` computes `prepared` from the outcome and answers any **dispatched** opcode
+that prepared no mutation with `kRefusedStatus`. A claim prepares nothing yet, so adding 1801 to the
+dispatch chain would convert today's silently accepted claim into an explicit refusal -- a
+regression wearing the shape of progress. The hook therefore runs before the chain and leaves the
+outcome untouched.
+
+Move it into the chain in the same commit that gives it a mutation, not before.
+
+## The records domain
+
+`state::build_data::records` exists so a claim can go from a record row to the bank index it has to
+set, without walking the mapping tables per request.
+
+```c
+struct Definition {
+    uint16_t definitionIndex;      // native record row, what the claim names
+    uint16_t completionFlagIndex;  // account flag bank row, or 0xFFFF when unaddressable
+};
+```
+
+`package_record_build.cpp` reads both tables at extraction time:
+
+- record row `+100` holds the unlock **slot** of the record's completion flag
+- the account flag mapping table (root slot 111, descriptor 8) maps a **destination slot** to the
+  **row number** whose object byte feeds it
+
+**A slot is not an array index.** The byte that sets slot `s` lives at the row of the mapping table
+whose destination is `s`, so the resolution is done once here rather than per claim.
+
+## What is still missing
+
+Only the state transition and its push. Three pieces:
+
+1. **A mutable claimed-record set.** `state::unlocks` is an immutable policy by contract --
+   `publish`, `get`, `clear` and nothing else -- so claimed records need their own store.
+2. **An encoder change.** The family-4 account encoder has to OR those flags into the account flag
+   bank when it builds the object.
+3. **A push.** Follow the existing `Pending*` mutation pattern: add a `PendingRecordClaim` to the
+   `Outcome::Mutation` variant, prepare it in `claim_record`, and let the established publication
+   path carry the new Family-4 version to the client.
+
+## One thing to confirm before building step 2
+
+**It is not established that the completion flag is what marks a record claimed.** Every record
+completion flag can be set while the client still offers the claim, which was observed directly over
+a long session. What the traced claims prove is the *delivery mechanism* -- a push rather than a
+response -- not the payload.
+
+Steps 1 and 3 are needed for any per-record state change and are safe to build. Only step 2 depends
+on `+100` being the right field, and if it turns out to be a different one that is a small change at
+the end rather than a redesign.

+ 21 - 0
Sunrise/src/client/content/items/packages/internal.h

@@ -85,6 +85,10 @@ struct Storage {
     std::array<state::build_data::progressions::Definition,
                state::build_data::progressions::kDefinitionCapacity>
         progressionRows{};
+    /** Record rows held until the completion flag mapping is resolved. */
+    std::array<state::build_data::records::Definition,
+               state::build_data::records::kDefinitionCapacity>
+        recordRows{};
     std::array<state::build_data::collectibles::Definition,
                state::build_data::collectibles::kDefinitionCapacity>
         collectibleRows{};
@@ -248,6 +252,23 @@ read_investment_constants(const reader::Source& source,
  * @param count Receives the number of rows read.
  * @return True when the table reads and every row fits.
  */
+/**
+ * Reads the records table and resolves each record's completion flag to a bank index.
+ * @param source Package source.
+ * @param scratch Reader scratch.
+ * @param root Investment root bytes.
+ * @param blob Scratch storage for the tables.
+ * @param output Row storage in native record order.
+ * @param count Receives the number of rows read.
+ * @return True when both tables read and every row fits.
+ */
+[[nodiscard]] bool build_records(const reader::Source& source,
+                                 reader::Scratch& scratch,
+                                 std::span<const std::byte> root,
+                                 std::vector<std::byte>& blob,
+                                 std::span<state::build_data::records::Definition> output,
+                                 std::size_t& count) noexcept;
+
 [[nodiscard]] bool build_progressions(const reader::Source& source,
                                       reader::Scratch& scratch,
                                       std::span<const std::byte> root,

+ 12 - 0
Sunrise/src/client/content/items/packages/package_item_build.cpp

@@ -155,6 +155,18 @@ bool build() noexcept {
                         std::span(storage.progressionRows).first(progressionCount));
                 }
             }
+            if (!state::build_data::record_definitions_ready()) {
+                std::size_t recordCount = 0;
+                if (build_records(source,
+                                  storage.scratch,
+                                  std::span<const std::byte>{storage.root},
+                                  storage.child,
+                                  storage.recordRows,
+                                  recordCount)) {
+                    (void)state::build_data::publish_record_definitions(
+                        std::span(storage.recordRows).first(recordCount));
+                }
+            }
             if (!state::build_data::investment_constants_ready()) {
                 state::build_data::constants::InvestmentConstants extracted{};
                 if (read_investment_constants(source,

+ 100 - 0
Sunrise/src/client/content/items/packages/package_record_build.cpp

@@ -0,0 +1,100 @@
+#include <cstring>
+
+#include "../../../../state/build_data/runtime.h"
+#include "internal.h"
+
+namespace sunrise::client::content::items::packages {
+namespace {
+
+/** A record with no completion flag carries a non-positive slot, which addresses nothing. */
+[[nodiscard]] constexpr bool addressable_slot(std::int16_t slot) noexcept {
+    return slot > 0;
+}
+
+} // namespace
+
+/**
+ * Reads the records table and resolves each record's completion flag to a bank index.
+ *
+ * A record row carries the unlock slot of its completion flag, and a slot is not an array index:
+ * the byte that feeds a slot sits at the row number of the mapping table whose destination is that
+ * slot. Both tables are walked here so a claim can go straight from a record row to the index it
+ * has to set.
+ */
+bool build_records(const reader::Source& source,
+                   reader::Scratch& scratch,
+                   std::span<const std::byte> root,
+                   std::vector<std::byte>& blob,
+                   std::span<state::build_data::records::Definition> output,
+                   std::size_t& count) noexcept {
+    namespace domain = state::build_data::records;
+    count = 0;
+
+    // The account flag mapping table, read first because the record rows are matched against it.
+    std::uint32_t mapTag = 0;
+    tables::Array mapRows{};
+    if (!tables::slot_tag(root, tables::kUnlockFlagMapTableSlot, mapTag) || mapTag == 0
+        || tables::package_of(mapTag) == tables::kAbsentPackageId
+        || !reader::read_tag(source, scratch, mapTag, blob)
+        || !tables::find_array_at(std::span<const std::byte>{blob},
+                                  tables::kAccountFlagMapDescriptor,
+                                  mapRows)
+        || mapRows.count == 0
+        || mapRows.dataOffset
+                   + static_cast<std::size_t>(mapRows.count) * tables::kUnlockMapRowStride
+               > blob.size()) {
+        return false;
+    }
+
+    // Destination slot to mapping row. The first row wins, matching how a bank is addressed.
+    constexpr std::size_t kSlotSpace = 32768;
+    static_assert(domain::kUnavailableFlagIndex == 0xFFFFU);
+    std::vector<std::uint16_t> indexBySlot{};
+    indexBySlot.assign(kSlotSpace, domain::kUnavailableFlagIndex);
+    for (std::uint64_t row = 0; row < mapRows.count; ++row) {
+        const std::size_t at =
+            mapRows.dataOffset + static_cast<std::size_t>(row) * tables::kUnlockMapRowStride;
+        std::int16_t slot = 0;
+        std::memcpy(&slot, blob.data() + at + tables::kUnlockMapDestinationSlotOffset, sizeof slot);
+        if (!addressable_slot(slot) || static_cast<std::size_t>(slot) >= kSlotSpace
+            || row > domain::kUnavailableFlagIndex) {
+            continue;
+        }
+        std::uint16_t& existing = indexBySlot[static_cast<std::size_t>(slot)];
+        if (existing == domain::kUnavailableFlagIndex) {
+            existing = static_cast<std::uint16_t>(row);
+        }
+    }
+
+    std::uint32_t tableTag = 0;
+    tables::Array rows{};
+    if (!tables::slot_tag(root, tables::kRecordTableSlot, tableTag) || tableTag == 0
+        || tables::package_of(tableTag) == tables::kAbsentPackageId
+        || !reader::read_tag(source, scratch, tableTag, blob)
+        || !tables::find_array_at(
+            std::span<const std::byte>{blob}, tables::kTableArrayDescriptor, rows)
+        || rows.count == 0 || rows.count > output.size()
+        || rows.dataOffset + static_cast<std::size_t>(rows.count) * tables::kRecordRowStride
+               > blob.size()) {
+        return false;
+    }
+
+    for (std::uint64_t row = 0; row < rows.count; ++row) {
+        const std::size_t at =
+            rows.dataOffset + static_cast<std::size_t>(row) * tables::kRecordRowStride;
+        std::int16_t slot = 0;
+        std::memcpy(&slot,
+                    blob.data() + at + tables::kRecordCompletionFlagOffset,
+                    sizeof slot);
+        domain::Definition& definition = output[static_cast<std::size_t>(row)];
+        definition = {};
+        definition.definitionIndex = static_cast<std::uint16_t>(row);
+        if (addressable_slot(slot) && static_cast<std::size_t>(slot) < kSlotSpace) {
+            definition.completionFlagIndex = indexBySlot[static_cast<std::size_t>(slot)];
+        }
+        ++count;
+    }
+    return count != 0;
+}
+
+} // namespace sunrise::client::content::items::packages

+ 16 - 0
Sunrise/src/middleware/content/packages/tables/definition_index_table.h

@@ -88,6 +88,22 @@ inline constexpr std::size_t kStatBlockOffset = 112;
 /** The investment root holds the constants blob at this slot. */
 inline constexpr std::size_t kInvestmentConstantsSlot = 11;
 /** The investment root holds the progression definition table at this slot. */
+/** Investment root slot of the records and lore table. */
+inline constexpr std::size_t kRecordTableSlot = 72;
+/** One record row, wider than any field this pass reads. */
+inline constexpr std::size_t kRecordRowStride = 216;
+/** Unlock slot of the record's completion flag, or a non-positive value when it has none. */
+inline constexpr std::size_t kRecordCompletionFlagOffset = 100;
+
+/** Investment root slot of the five unlock flag mapping tables. */
+inline constexpr std::size_t kUnlockFlagMapTableSlot = 111;
+/** Array descriptor of the account object's flag mapping table, the bank flag runs author. */
+inline constexpr std::size_t kAccountFlagMapDescriptor = 8;
+/** One mapping row: the unlock hash, then the destination slot its object byte feeds. */
+inline constexpr std::size_t kUnlockMapRowStride = 8;
+/** Destination slot offset inside one mapping row. */
+inline constexpr std::size_t kUnlockMapDestinationSlotOffset = 4;
+
 inline constexpr std::size_t kProgressionTableSlot = 68;
 /** Element class of the progression definition table. */
 inline constexpr std::uint32_t kProgressionTableClass = 0x80807CDDU;

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

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../web_service_envelope.h"
+
+namespace sunrise::middleware::web_service::messages::opcode1801 {
+
+/** Web Service opcode the Triumphs screen uses to claim one completed record. */
+inline constexpr std::uint16_t kOpcode = 1801;
+
+/** The one logical field carried by the native record claim descriptor. */
+struct Request {
+    std::uint16_t recordIndex{};
+};
+
+/**
+ * Parses the exact reflected opcode-1801 record claim descriptor.
+ *
+ * The record is an optional native field, so the descriptor carries a presence bit before its
+ * fifteen-bit row, exactly as the Collections pull does. A request naming no record is not a claim
+ * and is refused here. The row is not range-checked against the installed record table: that is the
+ * caller's decision, not the codec's.
+ *
+ * @param message Parsed Web Service envelope.
+ * @param request Receives the named record row.
+ * @return True only for the complete canonical three-byte request naming a record.
+ */
+[[nodiscard]] bool parse_request(const Message& message, Request& request) noexcept;
+
+} // namespace sunrise::middleware::web_service::messages::opcode1801

+ 39 - 0
Sunrise/src/middleware/web_service/messages/opcode1801_codec.cpp

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

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

@@ -6,6 +6,7 @@
 #include <string_view>
 
 #include "../../core/logging/log.h"
+#include "../../middleware/web_service/messages/opcode1801.h"
 #include "../../middleware/web_service/messages/opcode1820.h"
 #include "../../middleware/web_service/messages/opcode1901.h"
 #include "../../middleware/web_service/messages/opcode402.h"
@@ -666,6 +667,33 @@ void dismantle_item(const middleware::web_service::Message& message, Outcome& ou
                           kSingleQuantity);
 }
 
+/** Records opcode-1801 Triumphs claim requests and the record row each one names. */
+void report_record_claim(const middleware::web_service::Message& message,
+                         std::string_view result,
+                         std::string_view reason,
+                         std::uint32_t recordIndex,
+                         std::uint32_t completionFlagIndex) noexcept {
+    std::array<char, core::log::kLineCapacity> line{};
+    const int count = std::snprintf(
+        line.data(),
+        line.size(),
+        "ev=ws1801 stage=claim result=%.*s reason=%.*s transaction=%u payload_bytes=%zu "
+        "record_index=%u completion_flag_index=%u",
+        static_cast<int>(result.size()),
+        result.data(),
+        static_cast<int>(reason.size()),
+        reason.data(),
+        static_cast<unsigned>(message.transactionId),
+        message.payload.size(),
+        recordIndex,
+        completionFlagIndex);
+    if (count > 0) {
+        core::log::write(core::log::Channel::server,
+                         result == "ok" ? core::log::Level::debug : core::log::Level::warn,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
 /** Records strict opcode-1820 parsing, installed mapping, and State preparation outcomes. */
 void report_item_acquisition(const middleware::web_service::Message& message,
                              std::string_view result,
@@ -815,4 +843,34 @@ void acquire_item(const middleware::web_service::Message& message, Outcome& outc
                             mutation.acquiredInstanceSoid);
 }
 
+/** Decodes one opcode-1801 Triumphs claim and reports the record it names. */
+void claim_record(const middleware::web_service::Message& message, Outcome& outcome) noexcept {
+    // Deliberately no mutation. The shared reply path answers an action that prepares nothing with
+    // the refusal status, so preparing a placeholder here would turn a silently-accepted claim into
+    // an explicitly rejected one. Attach the transition here once claimed state is identified.
+    (void)outcome;
+    namespace records = state::build_data::records;
+    middleware::web_service::messages::opcode1801::Request request{};
+    if (!middleware::web_service::messages::opcode1801::parse_request(message, request)) {
+        report_record_claim(message, "fail", "payload_bits", 0, records::kUnavailableFlagIndex);
+        return;
+    }
+    records::Definition definition{};
+    if (!state::build_data::find_record_definition(request.recordIndex, definition)) {
+        report_record_claim(
+            message, "fail", "record_definition", request.recordIndex,
+            records::kUnavailableFlagIndex);
+        return;
+    }
+    if (definition.completionFlagIndex == records::kUnavailableFlagIndex) {
+        // The record carries no completion flag, or its slot has no row in the account bank.
+        report_record_claim(
+            message, "fail", "no_completion_flag", request.recordIndex,
+            records::kUnavailableFlagIndex);
+        return;
+    }
+    report_record_claim(
+        message, "ok", "resolved", request.recordIndex, definition.completionFlagIndex);
+}
+
 } // namespace sunrise::server::web_service

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

@@ -20,4 +20,17 @@ void mutate_item_state(const middleware::web_service::Message& message, Outcome&
 void dismantle_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
 void acquire_item(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
 
+/**
+ * Decodes one opcode-1801 Triumphs claim request and reports the record it names.
+ *
+ * The request carries a record row index and nothing else. No state transition is prepared yet:
+ * the state separating a claimed record from a merely completed one is not identified, since every
+ * record completion flag can be set while the client still offers the claim. This is the seam that
+ * transition attaches to once that state is known.
+ *
+ * @param message Parsed Web Service envelope.
+ * @param outcome Left untouched, so the shared reply path still answers the claim with success.
+ */
+void claim_record(const middleware::web_service::Message& message, Outcome& outcome) noexcept;
+
 } // namespace sunrise::server::web_service

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

@@ -13,6 +13,7 @@
 #include "../../middleware/encoding/byte_order.h"
 #include "../../middleware/web_service/messages/opcode1901.h"
 #include "../../middleware/web_service/messages/opcode205.h"
+#include "../../middleware/web_service/messages/opcode1801.h"
 #include "../../middleware/web_service/messages/opcode206.h"
 #include "../../middleware/web_service/messages/opcode501_codec.h"
 #include "../../middleware/web_service/messages/opcode503.h"
@@ -273,6 +274,13 @@ bool consume(std::span<const std::byte> request,
     // The action runs before its reply is encoded, because the reply reports whether it worked.
     // An action fills the outcome only once it has prepared its whole transition, so an outcome
     // still empty afterwards is that action refusing the request. Nothing is published here.
+    // Decoded and reported outside the dispatch chain on purpose: a claim prepares no mutation
+    // yet, and any opcode inside the chain that prepares nothing is answered with the refusal
+    // status. Reporting here keeps the claim's existing successful reply intact.
+    if (message.opcode == middleware::web_service::messages::opcode1801::kOpcode) {
+        claim_record(message, outcome);
+    }
+
     bool dispatched = true;
     if (message.opcode == middleware::web_service::messages::opcode504::kOpcode) {
         select_character(message, outcome);

+ 28 - 0
Sunrise/src/state/build_data/records/definition.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+namespace sunrise::state::build_data::records {
+
+/** The shipped build declares 2242 records and lore entries. The domain leaves room above that. */
+inline constexpr std::size_t kDefinitionCapacity = 4096;
+
+/** A record whose completion flag no mapping table addresses carries this instead of an index. */
+inline constexpr std::uint16_t kUnavailableFlagIndex = 0xFFFFU;
+
+/**
+ * One record reduced to what a claim needs.
+ *
+ * A record row carries the unlock slot of its completion flag. A slot is not an array index: the
+ * byte that feeds it lives at the row number of the mapping table whose destination is that slot.
+ * The index is resolved once here, at extraction, so a claim never has to walk the mapping tables.
+ */
+struct Definition {
+    /** Native record row, which is what an opcode-1801 claim names. */
+    std::uint16_t definitionIndex{};
+    /** Account flag bank mapping row, or kUnavailableFlagIndex when the slot is unaddressable. */
+    std::uint16_t completionFlagIndex{kUnavailableFlagIndex};
+};
+
+} // namespace sunrise::state::build_data::records

+ 25 - 0
Sunrise/src/state/build_data/records/record_build_data_runtime.cpp

@@ -0,0 +1,25 @@
+#include "../runtime.h"
+#include "../runtime/persistence/publication_transaction.h"
+#include "record_catalog.h"
+
+namespace sunrise::state::build_data {
+
+/** @return True when the whole native record table is published. */
+bool record_definitions_ready() noexcept {
+    return records::count() != 0;
+}
+
+/** Publishes one complete dense record-to-completion-flag table. */
+bool publish_record_definitions(std::span<const records::Definition> definitions) noexcept {
+    runtime::persistence::Transaction transaction;
+    return transaction.active() && records::valid(definitions)
+           && transaction.finish(records::replace(definitions), records::clear);
+}
+
+/** Resolves the native record row an opcode-1801 claim names. */
+bool find_record_definition(std::uint16_t definitionIndex,
+                            records::Definition& definition) noexcept {
+    return records::find(definitionIndex, definition);
+}
+
+} // namespace sunrise::state::build_data

+ 58 - 0
Sunrise/src/state/build_data/records/record_catalog.cpp

@@ -0,0 +1,58 @@
+#include "record_catalog.h"
+
+#include "../table.h"
+
+namespace sunrise::state::build_data::records {
+namespace {
+
+Lock g_lock;
+Table<Definition, kDefinitionCapacity> g_definitions;
+
+} // namespace
+
+/** Clears every generated record definition under the catalog lock. */
+void clear() noexcept {
+    const Lock::Exclusive guard(g_lock);
+    g_definitions.clear();
+}
+
+/** Checks that the definitions are dense and in native index order. */
+bool valid(std::span<const Definition> definitions) noexcept {
+    if (definitions.empty() || definitions.size() > kDefinitionCapacity) {
+        return false;
+    }
+    for (std::size_t row = 0; row < definitions.size(); ++row) {
+        if (definitions[row].definitionIndex != row) {
+            return false;
+        }
+    }
+    return true;
+}
+
+/** Replaces the generated record definitions in one step. */
+bool replace(std::span<const Definition> definitions) noexcept {
+    if (!valid(definitions)) {
+        return false;
+    }
+    const Lock::Exclusive guard(g_lock);
+    return g_definitions.replace(definitions);
+}
+
+/** Finds one record by the native row a claim names. */
+bool find(std::uint16_t definitionIndex, Definition& definition) noexcept {
+    const Lock::Shared guard(g_lock);
+    const std::span<const Definition> rows = g_definitions.rows();
+    if (definitionIndex >= rows.size()) {
+        return false;
+    }
+    definition = rows[definitionIndex];
+    return true;
+}
+
+/** @return Number of generated record definitions, read under the lock. */
+std::size_t count() noexcept {
+    const Lock::Shared guard(g_lock);
+    return g_definitions.count();
+}
+
+} // namespace sunrise::state::build_data::records

+ 38 - 0
Sunrise/src/state/build_data/records/record_catalog.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <cstddef>
+#include <span>
+
+#include "definition.h"
+
+namespace sunrise::state::build_data::records {
+
+/** Clears every generated record definition. */
+void clear() noexcept;
+
+/**
+ * Checks that the definitions are dense and in native index order.
+ * @param definitions Candidate rows.
+ * @return True when the rows fit storage and index n sits at position n.
+ */
+[[nodiscard]] bool valid(std::span<const Definition> definitions) noexcept;
+
+/**
+ * Replaces the generated record definitions in one step.
+ * @param definitions Complete dense rows in native index order.
+ * @return True when the rows pass the checks and fit fixed State storage.
+ */
+[[nodiscard]] bool replace(std::span<const Definition> definitions) noexcept;
+
+/**
+ * Finds one record by the native row a claim names.
+ * @param definitionIndex Native record row.
+ * @param definition Receives the row only on success.
+ * @return True when the domain is complete and the row exists.
+ */
+[[nodiscard]] bool find(std::uint16_t definitionIndex, Definition& definition) noexcept;
+
+/** @return Number of generated record definitions, read under the lock. */
+[[nodiscard]] std::size_t count() noexcept;
+
+} // namespace sunrise::state::build_data::records

+ 21 - 0
Sunrise/src/state/build_data/runtime.h

@@ -17,6 +17,7 @@
 #include "items/socket_plugs/definition.h"
 #include "material_requirements/material_requirement_catalog.h"
 #include "progressions/definition.h"
+#include "records/definition.h"
 #include "scenarios/definition.h"
 #include "socket_entry_buckets/definition.h"
 #include "socket_entry_lists/definition.h"
@@ -207,6 +208,26 @@ publish_socket_plug_rules(std::span<const items::socket_plugs::Rule> rules,
 [[nodiscard]] bool is_consumed_on_apply(std::uint16_t itemDefinitionIndex,
                                         std::uint8_t bucketId) noexcept;
 
+/** @return True when the whole record definition table is in State. */
+[[nodiscard]] bool record_definitions_ready() noexcept;
+
+/**
+ * Publishes the whole record definition table in one step.
+ * @param definitions Complete dense rows in native record order.
+ * @return True when the rows pass the domain checks and fit fixed State storage.
+ */
+[[nodiscard]] bool
+publish_record_definitions(std::span<const records::Definition> definitions) noexcept;
+
+/**
+ * Resolves the native record row an opcode-1801 claim names.
+ * @param definitionIndex Native record row carried by the claim.
+ * @param definition Receives the row, including its completion flag index, only on success.
+ * @return True when the table is complete and the row exists.
+ */
+[[nodiscard]] bool find_record_definition(std::uint16_t definitionIndex,
+                                          records::Definition& definition) noexcept;
+
 /** @return True when the whole progression definition table is in State. */
 [[nodiscard]] bool progression_definitions_ready() noexcept;