Просмотр исходного кода

Total Triumph Score from claimed records, and correct what the completion flag means

Claiming a Triumph left the score alone, because score is a plain replicated value the client never
derives. Nothing was totalling it.

Total it. Each record's points sit at row +92, the records domain now carries that value, the claim
store keeps a running sum, and the account encoder adds it to account value row 2115. A repeated
claim of the same record scores once.

Correct the note on record +100. It said the flag marked a record complete rather than claimed;
authoring it alone marks a record Acquired, measured on Cache PAVONIS against an untouched Cache
ANSERIS. The earlier reading came from bulk-authoring those flags and seeing no score move, which
looked like the flag doing nothing when the flag was working and the score was simply separate.

Also record why some records never send a claim at all. ANSERIS is claimable while its flag is
unset, so the flag is not what makes a record claimable: a record the client does not consider
complete never sends opcode 1801. The two caches are identical in every measurable respect, so that
difference is account progress rather than anything a server can address.

Known gap: the score covers claims made through opcode 1801 only, so a record marked claimed by
authoring its flag reads Acquired and scores nothing.
Millie 3 недель назад
Родитель
Сommit
a7d88fafe3

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

@@ -1,103 +0,0 @@
-# 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.

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

@@ -94,6 +94,8 @@ inline constexpr std::size_t kRecordTableSlot = 72;
 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;
+/** Points the record is worth. Zero for lore and for the interval records that score per step. */
+inline constexpr std::size_t kRecordScoreOffset = 92;
 
 /** Investment root slot of the five unlock flag mapping tables. */
 inline constexpr std::size_t kUnlockFlagMapTableSlot = 111;

+ 7 - 0
Sunrise/src/middleware/datagen/family4/account/account_encoder.cpp

@@ -5,6 +5,7 @@
 #include <limits>
 
 #include "../../../../state/build_data/runtime.h"
+#include "../../../../state/build_data/records/definition.h"
 #include "../../../../state/record_claims/record_claims.h"
 #include "../../../../state/unlocks/unlocks_runtime.h"
 #include "../progression/progression_bank_keys.h"
@@ -99,6 +100,12 @@ bool encode(const state::AccountState& state, std::span<std::byte> output) noexc
     (void)state::record_claims::apply(object.acquiredFlags);
     object.profileUnlockFlags = unlocks.profileFlags;
     object.objectiveValues = unlocks.objectiveValues;
+    // Triumph Score is a plain replicated value the client never derives, so total the claims made
+    // this session over whatever the policy authored and publish the sum.
+    if (state::build_data::records::kTriumphScoreValueIndex < object.objectiveValues.size()) {
+        auto& score = object.objectiveValues[state::build_data::records::kTriumphScoreValueIndex];
+        score += static_cast<std::int32_t>(state::record_claims::total_score());
+    }
     for (layout::CharacterUnlockBlock& block : object.characterUnlocks) {
         block.flags = unlocks.characterFlags;
     }

+ 1 - 1
Sunrise/src/server/web_service/web_service_actions.cpp

@@ -869,7 +869,7 @@ void claim_record(const middleware::web_service::Message& message, Outcome& outc
             records::kUnavailableFlagIndex);
         return;
     }
-    if (!state::record_claims::claim(definition.completionFlagIndex)) {
+    if (!state::record_claims::claim(definition.completionFlagIndex, definition.scoreValue)) {
         report_record_claim(message,
                             "fail",
                             "flag_index_range",

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

@@ -8,6 +8,15 @@ 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;
 
+/**
+ * Account value bank row that holds Triumph Score.
+ *
+ * Found by authoring every account value slot to `100000 + its own row` and reading the score back
+ * as 102115. It is a plain replicated value, not a progression and not derived by the client, so a
+ * server that wants a score has to total one itself.
+ */
+inline constexpr std::uint16_t kTriumphScoreValueIndex = 2115U;
+
 /** A record whose completion flag no mapping table addresses carries this instead of an index. */
 inline constexpr std::uint16_t kUnavailableFlagIndex = 0xFFFFU;
 
@@ -23,6 +32,8 @@ struct Definition {
     std::uint16_t definitionIndex{};
     /** Account flag bank mapping row, or kUnavailableFlagIndex when the slot is unaddressable. */
     std::uint16_t completionFlagIndex{kUnavailableFlagIndex};
+    /** Points this record is worth, which the shipped table keeps at 500 or below. */
+    std::uint16_t scoreValue{};
 };
 
 } // namespace sunrise::state::build_data::records

+ 11 - 1
Sunrise/src/state/record_claims/record_claims.cpp

@@ -17,6 +17,7 @@ constexpr std::size_t kWordCount = (kIndexCapacity + kWordBits - 1) / kWordBits;
 std::mutex g_lock;
 std::array<std::uint64_t, kWordCount> g_claimed{};
 std::size_t g_count{};
+std::uint32_t g_score{};
 
 } // namespace
 
@@ -25,10 +26,11 @@ void clear() noexcept {
     const std::lock_guard<std::mutex> guard(g_lock);
     g_claimed.fill(0);
     g_count = 0;
+    g_score = 0;
 }
 
 /** Marks one account flag bank index claimed. */
-bool claim(std::uint16_t flagIndex) noexcept {
+bool claim(std::uint16_t flagIndex, std::uint16_t scoreValue) noexcept {
     if (static_cast<std::size_t>(flagIndex) >= kIndexCapacity) {
         return false;
     }
@@ -38,6 +40,8 @@ bool claim(std::uint16_t flagIndex) noexcept {
     if ((g_claimed[word] & bit) == 0) {
         g_claimed[word] |= bit;
         ++g_count;
+        // Only a first claim scores, so a repeated click cannot inflate the total.
+        g_score += scoreValue;
     }
     return true;
 }
@@ -65,6 +69,12 @@ std::size_t apply(std::span<std::uint8_t> accountFlags) noexcept {
     return changed;
 }
 
+/** @return Total score of every record claimed since boot. */
+std::uint32_t total_score() noexcept {
+    const std::lock_guard<std::mutex> guard(g_lock);
+    return g_score;
+}
+
 /** @return Number of distinct indices claimed since boot. */
 std::size_t count() noexcept {
     const std::lock_guard<std::mutex> guard(g_lock);

+ 7 - 2
Sunrise/src/state/record_claims/record_claims.h

@@ -19,11 +19,16 @@ namespace sunrise::state::record_claims {
 void clear() noexcept;
 
 /**
- * Marks one account flag bank index claimed.
+ * Marks one account flag bank index claimed and adds its record's score to the total.
+ * A repeated claim of the same index is held once and scores once.
  * @param flagIndex Mapping-table row whose object byte feeds the record's completion flag.
+ * @param scoreValue Points the record is worth, counted only on the first claim.
  * @return True when the index is in range and the claim is now held.
  */
-[[nodiscard]] bool claim(std::uint16_t flagIndex) noexcept;
+[[nodiscard]] bool claim(std::uint16_t flagIndex, std::uint16_t scoreValue) noexcept;
+
+/** @return Total score of every record claimed since boot. */
+[[nodiscard]] std::uint32_t total_score() noexcept;
 
 /**
  * Lays every held claim over one account flag bank.