Explorar el Código

Hold a claimed record and lay it over the account flag bank

Claiming a Triumph resolved its completion flag and then dropped it, so the record stayed
claimable however many times it was clicked.

Hold the claim instead. The authored unlock policy is immutable for the life of the process, so a
claim cannot write to it; keep claims made since boot in their own store and lay them over the
account flag bank inside the encoder, which is the one place every Family-4 account image passes
through. A claim is therefore carried by the next image the client receives.

Verified in play: claiming a Triumph and then dismantling an item, which stages a Family-4 refresh
for its own reasons, flips that Triumph to Acquired. That also settles what the completion flag at
record +100 means, which authoring alone could never show -- those flags can all be set while the
client still offers the claim.

A claim does not yet stage its own push, so it lands on the next image rather than immediately.
Millie hace 2 semanas
padre
commit
ef0ae19938

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

@@ -5,6 +5,7 @@
 #include <limits>
 
 #include "../../../../state/build_data/runtime.h"
+#include "../../../../state/record_claims/record_claims.h"
 #include "../../../../state/unlocks/unlocks_runtime.h"
 #include "../progression/progression_bank_keys.h"
 #include "layout.h"
@@ -93,6 +94,9 @@ bool encode(const state::AccountState& state, std::span<std::byte> output) noexc
     // Acquired flags and objective progress are authored policy, published once per process.
     const state::unlocks::Table& unlocks = state::unlocks::get();
     object.acquiredFlags = unlocks.accountFlags;
+    // Claims arrive after boot, so they cannot be in the authored policy. Lay them over the
+    // bank here, which is the one place every Family-4 account image passes through.
+    (void)state::record_claims::apply(object.acquiredFlags);
     object.profileUnlockFlags = unlocks.profileFlags;
     object.objectiveValues = unlocks.objectiveValues;
     for (layout::CharacterUnlockBlock& block : object.characterUnlocks) {

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

@@ -7,6 +7,7 @@
 
 #include "../../core/logging/log.h"
 #include "../../middleware/web_service/messages/opcode1801.h"
+#include "../../state/record_claims/record_claims.h"
 #include "../../middleware/web_service/messages/opcode1820.h"
 #include "../../middleware/web_service/messages/opcode1901.h"
 #include "../../middleware/web_service/messages/opcode402.h"
@@ -869,8 +870,16 @@ void claim_record(const middleware::web_service::Message& message, Outcome& outc
             records::kUnavailableFlagIndex);
         return;
     }
+    if (!state::record_claims::claim(definition.completionFlagIndex)) {
+        report_record_claim(message,
+                            "fail",
+                            "flag_index_range",
+                            request.recordIndex,
+                            definition.completionFlagIndex);
+        return;
+    }
     report_record_claim(
-        message, "ok", "resolved", request.recordIndex, definition.completionFlagIndex);
+        message, "ok", "claimed", request.recordIndex, definition.completionFlagIndex);
 }
 
 } // namespace sunrise::server::web_service

+ 74 - 0
Sunrise/src/state/record_claims/record_claims.cpp

@@ -0,0 +1,74 @@
+#include "record_claims.h"
+
+#include <array>
+#include <bit>
+#include <mutex>
+
+#include "../unlocks/definition.h"
+
+namespace sunrise::state::record_claims {
+namespace {
+
+/** One bit per addressable account flag index, which is cheaper than a set and never allocates. */
+constexpr std::size_t kIndexCapacity = unlocks::kAccountFlagCapacity;
+constexpr std::size_t kWordBits = 64;
+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{};
+
+} // namespace
+
+/** Forgets every claim made since boot. */
+void clear() noexcept {
+    const std::lock_guard<std::mutex> guard(g_lock);
+    g_claimed.fill(0);
+    g_count = 0;
+}
+
+/** Marks one account flag bank index claimed. */
+bool claim(std::uint16_t flagIndex) noexcept {
+    if (static_cast<std::size_t>(flagIndex) >= kIndexCapacity) {
+        return false;
+    }
+    const std::size_t word = static_cast<std::size_t>(flagIndex) / kWordBits;
+    const std::uint64_t bit = std::uint64_t{1} << (static_cast<std::size_t>(flagIndex) % kWordBits);
+    const std::lock_guard<std::mutex> guard(g_lock);
+    if ((g_claimed[word] & bit) == 0) {
+        g_claimed[word] |= bit;
+        ++g_count;
+    }
+    return true;
+}
+
+/** Lays every held claim over one account flag bank. */
+std::size_t apply(std::span<std::uint8_t> accountFlags) noexcept {
+    std::size_t changed = 0;
+    const std::lock_guard<std::mutex> guard(g_lock);
+    for (std::size_t word = 0; word < g_claimed.size(); ++word) {
+        std::uint64_t bits = g_claimed[word];
+        while (bits != 0) {
+            const auto offset = static_cast<std::size_t>(std::countr_zero(bits));
+            bits &= bits - 1;
+            const std::size_t index = word * kWordBits + offset;
+            // A bank shorter than the index space is not an error: the tail simply is not sent.
+            if (index >= accountFlags.size()) {
+                continue;
+            }
+            if (accountFlags[index] != unlocks::kFlagSet) {
+                accountFlags[index] = unlocks::kFlagSet;
+                ++changed;
+            }
+        }
+    }
+    return changed;
+}
+
+/** @return Number of distinct indices claimed since boot. */
+std::size_t count() noexcept {
+    const std::lock_guard<std::mutex> guard(g_lock);
+    return g_count;
+}
+
+} // namespace sunrise::state::record_claims

+ 38 - 0
Sunrise/src/state/record_claims/record_claims.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+namespace sunrise::state::record_claims {
+
+/**
+ * Records claimed through Web Service opcode 1801, as account flag bank indices.
+ *
+ * The authored unlock policy is immutable for the life of the process, so a claim cannot write to
+ * it. This holds the claims made since boot instead, and the account encoder lays them over the
+ * authored bank on its way out. A claim is therefore visible to the client on the next Family-4
+ * image, and is lost on restart unless the same flag is authored.
+ */
+
+/** Forgets every claim made since boot. */
+void clear() noexcept;
+
+/**
+ * Marks one account flag bank index claimed.
+ * @param flagIndex Mapping-table row whose object byte feeds the record's completion flag.
+ * @return True when the index is in range and the claim is now held.
+ */
+[[nodiscard]] bool claim(std::uint16_t flagIndex) noexcept;
+
+/**
+ * Lays every held claim over one account flag bank.
+ * @param accountFlags Bank already filled from the authored policy.
+ * @return Number of bytes this changed, so a caller can tell a no-op from real work.
+ */
+std::size_t apply(std::span<std::uint8_t> accountFlags) noexcept;
+
+/** @return Number of distinct indices claimed since boot. */
+[[nodiscard]] std::size_t count() noexcept;
+
+} // namespace sunrise::state::record_claims