|
|
@@ -2,8 +2,15 @@
|
|
|
|
|
|
#include <array>
|
|
|
#include <bit>
|
|
|
+#include <cstdio>
|
|
|
+#include <cstring>
|
|
|
#include <mutex>
|
|
|
+#include <vector>
|
|
|
|
|
|
+#include <windows.h>
|
|
|
+
|
|
|
+#include "../../core/filesystem/path.h"
|
|
|
+#include "../../core/logging/log.h"
|
|
|
#include "../unlocks/definition.h"
|
|
|
|
|
|
namespace sunrise::state::record_claims {
|
|
|
@@ -14,22 +21,175 @@ constexpr std::size_t kIndexCapacity = unlocks::kAccountFlagCapacity;
|
|
|
constexpr std::size_t kWordBits = 64;
|
|
|
constexpr std::size_t kWordCount = (kIndexCapacity + kWordBits - 1) / kWordBits;
|
|
|
|
|
|
+/** The claim file lives beside the build data cache, which already owns this directory. */
|
|
|
+constexpr std::wstring_view kClaimFileSuffix = L"\\cache\\record_claims.bin";
|
|
|
+/** Identifies the file on sight, so an unrelated file of the right length cannot be read as one. */
|
|
|
+constexpr std::array<char, 8> kMagic{'S', 'N', 'R', 'S', 'C', 'L', 'M', '1'};
|
|
|
+/** A claim is a flag bank row and the score its record carries. */
|
|
|
+constexpr std::size_t kEntrySize = 2 * sizeof(std::uint16_t);
|
|
|
+/** Far above the 2242 records the build ships, and small enough to read in one go. */
|
|
|
+constexpr std::uint32_t kMaximumEntries = 8192;
|
|
|
+
|
|
|
std::mutex g_lock;
|
|
|
std::array<std::uint64_t, kWordCount> g_claimed{};
|
|
|
+std::array<std::uint16_t, kIndexCapacity> g_scoreByIndex{};
|
|
|
std::size_t g_count{};
|
|
|
std::uint32_t g_score{};
|
|
|
+core::path::Buffer g_path{};
|
|
|
+bool g_pathReady{};
|
|
|
+
|
|
|
+void report(const char* stage, const char* result, std::size_t detail) noexcept {
|
|
|
+ std::array<char, 128> line{};
|
|
|
+ const int written = std::snprintf(
|
|
|
+ line.data(), line.size(), "ev=claims stage=%s result=%s entries=%zu", stage, result, detail);
|
|
|
+ if (written > 0) {
|
|
|
+ core::log::write(core::log::Channel::state,
|
|
|
+ std::string_view{result} == "ok" ? core::log::Level::info
|
|
|
+ : core::log::Level::warn,
|
|
|
+ {line.data(), static_cast<std::size_t>(written)});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** Writes every held claim. The caller holds the lock. */
|
|
|
+void store_locked() noexcept {
|
|
|
+ if (!g_pathReady) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ std::vector<char> document{};
|
|
|
+ document.insert(document.end(), kMagic.begin(), kMagic.end());
|
|
|
+ const auto entries = static_cast<std::uint32_t>(g_count);
|
|
|
+ const auto* entryBytes = reinterpret_cast<const char*>(&entries);
|
|
|
+ document.insert(document.end(), entryBytes, entryBytes + sizeof entries);
|
|
|
+ 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;
|
|
|
+ const auto packedIndex = static_cast<std::uint16_t>(index);
|
|
|
+ const std::uint16_t packedScore = g_scoreByIndex[index];
|
|
|
+ const auto* indexBytes = reinterpret_cast<const char*>(&packedIndex);
|
|
|
+ const auto* scoreBytes = reinterpret_cast<const char*>(&packedScore);
|
|
|
+ document.insert(document.end(), indexBytes, indexBytes + sizeof packedIndex);
|
|
|
+ document.insert(document.end(), scoreBytes, scoreBytes + sizeof packedScore);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const HANDLE file = CreateFileW(g_path.chars.data(),
|
|
|
+ GENERIC_WRITE,
|
|
|
+ 0,
|
|
|
+ nullptr,
|
|
|
+ CREATE_ALWAYS,
|
|
|
+ FILE_ATTRIBUTE_NORMAL,
|
|
|
+ nullptr);
|
|
|
+ if (file == INVALID_HANDLE_VALUE) {
|
|
|
+ report("store", "open_fail", g_count);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ DWORD written = 0;
|
|
|
+ const auto size = static_cast<DWORD>(document.size());
|
|
|
+ bool complete =
|
|
|
+ WriteFile(file, document.data(), size, &written, nullptr) != FALSE && written == size;
|
|
|
+ complete = CloseHandle(file) != FALSE && complete;
|
|
|
+ report("store", complete ? "ok" : "write_fail", g_count);
|
|
|
+}
|
|
|
+
|
|
|
+/** Reads every claim the file holds. The caller holds the lock. */
|
|
|
+void load_locked() noexcept {
|
|
|
+ const HANDLE file = CreateFileW(g_path.chars.data(),
|
|
|
+ GENERIC_READ,
|
|
|
+ FILE_SHARE_READ,
|
|
|
+ nullptr,
|
|
|
+ OPEN_EXISTING,
|
|
|
+ FILE_ATTRIBUTE_NORMAL,
|
|
|
+ nullptr);
|
|
|
+ if (file == INVALID_HANDLE_VALUE) {
|
|
|
+ // No file is an account that has claimed nothing, which is the first-run state.
|
|
|
+ report("load", "absent", 0);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ std::array<char, sizeof(kMagic) + sizeof(std::uint32_t)> header{};
|
|
|
+ DWORD read = 0;
|
|
|
+ if (ReadFile(file, header.data(), static_cast<DWORD>(header.size()), &read, nullptr) == FALSE
|
|
|
+ || read != header.size()
|
|
|
+ || std::memcmp(header.data(), kMagic.data(), kMagic.size()) != 0) {
|
|
|
+ (void)CloseHandle(file);
|
|
|
+ report("load", "header_fail", 0);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ std::uint32_t entries = 0;
|
|
|
+ std::memcpy(&entries, header.data() + kMagic.size(), sizeof entries);
|
|
|
+ if (entries > kMaximumEntries) {
|
|
|
+ (void)CloseHandle(file);
|
|
|
+ report("load", "count_fail", entries);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ std::vector<char> payload(static_cast<std::size_t>(entries) * kEntrySize);
|
|
|
+ read = 0;
|
|
|
+ const bool readOk =
|
|
|
+ payload.empty()
|
|
|
+ || (ReadFile(file, payload.data(), static_cast<DWORD>(payload.size()), &read, nullptr)
|
|
|
+ != FALSE
|
|
|
+ && read == payload.size());
|
|
|
+ (void)CloseHandle(file);
|
|
|
+ if (!readOk) {
|
|
|
+ report("load", "read_fail", entries);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::size_t restored = 0;
|
|
|
+ for (std::uint32_t entry = 0; entry < entries; ++entry) {
|
|
|
+ std::uint16_t index = 0;
|
|
|
+ std::uint16_t score = 0;
|
|
|
+ std::memcpy(&index, payload.data() + static_cast<std::size_t>(entry) * kEntrySize,
|
|
|
+ sizeof index);
|
|
|
+ std::memcpy(&score,
|
|
|
+ payload.data() + static_cast<std::size_t>(entry) * kEntrySize + sizeof index,
|
|
|
+ sizeof score);
|
|
|
+ if (static_cast<std::size_t>(index) >= kIndexCapacity) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const std::size_t word = static_cast<std::size_t>(index) / kWordBits;
|
|
|
+ const std::uint64_t bit = std::uint64_t{1}
|
|
|
+ << (static_cast<std::size_t>(index) % kWordBits);
|
|
|
+ if ((g_claimed[word] & bit) != 0) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ g_claimed[word] |= bit;
|
|
|
+ g_scoreByIndex[index] = score;
|
|
|
+ g_score += score;
|
|
|
+ ++g_count;
|
|
|
+ ++restored;
|
|
|
+ }
|
|
|
+ report("load", "ok", restored);
|
|
|
+}
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
-/** Forgets every claim made since boot. */
|
|
|
+/** Derives the claim file path and loads any claims already held. */
|
|
|
+bool initialize(void* module) noexcept {
|
|
|
+ const std::lock_guard<std::mutex> guard(g_lock);
|
|
|
+ g_pathReady = false;
|
|
|
+ if (!core::path::artifact_directory(module, g_path)
|
|
|
+ || !core::path::append(g_path, kClaimFileSuffix)) {
|
|
|
+ report("initialize", "path_fail", 0);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ g_pathReady = true;
|
|
|
+ load_locked();
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+/** Forgets every held claim, in memory only. */
|
|
|
void clear() noexcept {
|
|
|
const std::lock_guard<std::mutex> guard(g_lock);
|
|
|
g_claimed.fill(0);
|
|
|
+ g_scoreByIndex.fill(0);
|
|
|
g_count = 0;
|
|
|
g_score = 0;
|
|
|
}
|
|
|
|
|
|
-/** Marks one account flag bank index claimed. */
|
|
|
+/** Marks one account flag bank index claimed, adds its score, and writes the claim file. */
|
|
|
bool claim(std::uint16_t flagIndex, std::uint16_t scoreValue) noexcept {
|
|
|
if (static_cast<std::size_t>(flagIndex) >= kIndexCapacity) {
|
|
|
return false;
|
|
|
@@ -39,9 +199,13 @@ bool claim(std::uint16_t flagIndex, std::uint16_t scoreValue) noexcept {
|
|
|
const std::lock_guard<std::mutex> guard(g_lock);
|
|
|
if ((g_claimed[word] & bit) == 0) {
|
|
|
g_claimed[word] |= bit;
|
|
|
+ g_scoreByIndex[flagIndex] = scoreValue;
|
|
|
++g_count;
|
|
|
// Only a first claim scores, so a repeated click cannot inflate the total.
|
|
|
g_score += scoreValue;
|
|
|
+ // Written per claim rather than at shutdown: a crash must not lose what the client is
|
|
|
+ // already showing as Acquired.
|
|
|
+ store_locked();
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
@@ -69,13 +233,13 @@ std::size_t apply(std::span<std::uint8_t> accountFlags) noexcept {
|
|
|
return changed;
|
|
|
}
|
|
|
|
|
|
-/** @return Total score of every record claimed since boot. */
|
|
|
+/** @return Total score of every held claim. */
|
|
|
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. */
|
|
|
+/** @return Number of distinct indices held. */
|
|
|
std::size_t count() noexcept {
|
|
|
const std::lock_guard<std::mutex> guard(g_lock);
|
|
|
return g_count;
|