Files
ReSkate/Extension/Profile/local_profile.cpp
T
AyeZeeBB b763631b77 Trim per-frame multiplayer client work; size mesh pool to the GPU
Mesh streaming pool (fixes loading crashes on older GPUs)
- 1.0.0 forced MeshStreaming.PoolSize to 3.5 GiB on every card. An RX 580
  crashed while loading with DXGI_ERROR_DEVICE_RESET, and the oversized pool
  is the likely cause. The pool is now a quarter of the largest hardware
  adapter's dedicated video memory, capped at 3.5 GiB; cards of 6 GB or less
  keep the game's own 520 MB. The card, its memory and the chosen size are
  logged once at startup.

Remote players (busy servers)
- Transform interpolate is inline plain arithmetic: 22 -> 5.8 ns per joint.
  It was 4% of a busy server's client frame, since every player within
  200 m has the whole skeleton interpolated every frame.
- Players out of the camera's view and more than 8 m away are sampled at
  20 Hz, like far players. Their position still updates every frame for
  collision, sound and nametags, and a player coming into view is sampled
  that frame.

Expression hooks (run for every expression the game executes)
- No profiler zones in the expression hooks: taking a sample switched them
  on, inflating the very samples that measured the hooks.
- The throwdown lab pump does one sorted lookup of the running graph and
  skips everything else for graphs it doesn't watch (graph_throttle gets
  throttled_graph for this).

Settings and memory reads
- Script gameplay-setting and native-setting gets are served from small
  per-thread caches, keyed on the asset/key address, its first 8 name
  bytes and a new profile::Store::changes() counter, instead of a store
  lookup each call.
- Per-frame reads in the client tick, native settings and population
  controls use memory::peek instead of ReadProcessMemory.
- Script error logging probes each argument pointer once.
2026-10-02 21:07:43 -05:00

465 lines
22 KiB
C++

#include "embedded_profile_defaults.h"
#include "profile_internal.h"
#include "database_codec.h"
#include "local_profile.h"
#include "profile_update.h"
#include "Engine/Game/World/location_travel.h"
#include "Engine/Vfs/content_catalogs.h"
#include <algorithm>
#include <Windows.h>
#include <array>
#include <atomic>
#include <cmath>
#include <fstream>
#include <limits>
#include <stdexcept>
namespace dingosdk::profile {
using namespace detail;
namespace detail {
using embedded::embedded_profile_defaults;
void require(bool ok, const char* error) { if (!ok) throw std::runtime_error(error); }
bool valid_text(std::string_view s, bool empty) {
if ((!empty && s.empty()) || s.size() > 255) return false;
for (const unsigned char c : s) if (c < 32 || c == 127) return false;
return true;
}
std::uint32_t checksum(std::string_view s) {
std::uint32_t crc = 0xffffffff;
for (const unsigned char c : s) {
crc ^= c;
for (int i = 0; i < 8; ++i) crc = (crc >> 1) ^ (0xedb88320 & (0U - (crc & 1)));
}
return ~crc;
}
std::string read_file(const std::filesystem::path& path) {
const auto size = std::filesystem::file_size(path);
require(size <= max_bytes, "Local profile exceeds size limit");
std::ifstream in(path, std::ios::binary);
std::string text(static_cast<std::size_t>(size), '\0');
in.read(text.data(), static_cast<std::streamsize>(size));
require(in.good() && in.peek() == std::char_traits<char>::eof(), "Cannot read local profile");
return text;
}
}
namespace detail {
using Json = dingosdk::Json;
Json parse_json(std::string_view text) {
return Json::parse(text, {max_bytes, 32, max_records * 16});
}
std::uint64_t unsigned_value(const Json& value, std::uint64_t maximum) {
require(value.is_number_unsigned(), "Profile value must be a nonnegative integer");
const auto result = value.get<std::uint64_t>();
require(result <= maximum, "Profile integer is outside its supported range");
return result;
}
const Json& object_field(const Json& object, const char* name) {
const auto& value = object.at(name);
require(value.is_object(), "Profile section must be a JSON object");
return value;
}
void validate_object_dropper(const Snapshot& s) {
if (!s.extensions.contains("object_dropper")) return;
const auto& section = object_field(s.extensions, "object_dropper");
if (section.contains("limits")) {
const auto& limits = object_field(section, "limits");
if (limits.contains("enabled")) require(limits.at("enabled").is_boolean(), "Invalid building limits enabled flag");
if (limits.contains("max_objects")) {
const auto& count = limits.at("max_objects");
require(count.is_number_integer() && count >= 1 && count <= 1024,
"Building object limit must be an integer in 1..1024");
}
if (limits.contains("radius_metres")) {
const auto& radius = limits.at("radius_metres");
require(radius.is_number(), "Building radius must be a number");
const auto value = radius.get<double>();
require(std::isfinite(value) && value >= 1 && value <= 5000, "Building radius must be 1..5000 metres");
}
}
if (section.contains("inventory")) {
const auto& inventory = object_field(section, "inventory");
require(inventory.size() <= 8192, "Object inventory exceeds limit");
for (const auto& [key, owned] : inventory.items())
require(valid_text(key) && owned.is_boolean(), "Invalid object ownership record");
}
if (section.contains("catalog")) {
const auto& catalog = object_field(section, "catalog");
require(catalog.size() <= 8192, "Object metadata catalog exceeds limit");
for (const auto& [key, metadata] : catalog.items()) {
require(valid_text(key) && metadata.is_object(), "Invalid object metadata record");
for (const auto* field : {"title", "description", "rarity_id"})
if (metadata.contains(field))
require(metadata.at(field).is_string() && valid_text(metadata.at(field).get<std::string>(), true),
"Object metadata must be text of at most 255 bytes without control characters");
}
}
}
}
void detail::validate_settings(const Snapshot& s) {
if (s.settings.contains("gameplay")) {
const auto& values = s.settings.at("gameplay");
require(values.is_object() && values.size() <= 2048, "Invalid gameplay settings");
for (const auto& [key, value] : values.items()) {
require(valid_text(key), "Invalid gameplay setting key");
require((value.is_number_integer() && (!value.is_number_unsigned() ||
value.get<std::uint64_t>() <= INT64_MAX)) ||
(value.is_number_float() && std::isfinite(value.get<double>())) ||
(value.is_string() && value.string().size() <= 4096 &&
value.string().find('\0') == std::string::npos), "Invalid gameplay setting value");
}
}
if (s.settings.contains("native_profile_options")) {
const auto& groups = s.settings.at("native_profile_options");
require(groups.is_object(), "Invalid native profile option groups");
for (const auto& [location, values] : groups.items()) {
require((location == "1" || location == "2") && values.is_object() && values.size() <= 2048,
"Invalid native profile option location");
for (const auto& [key, value] : values.items()) {
require(valid_text(key), "Invalid native profile option key");
require(value.is_boolean() || (value.is_number_integer() && (!value.is_number_unsigned() ||
value.get<std::uint64_t>() <= INT64_MAX)) ||
(value.is_number_float() && std::isfinite(value.get<double>())) ||
(value.is_string() && value.string().size() <= 4096 &&
value.string().find('\0') == std::string::npos),
"Invalid native profile option value");
}
}
}
}
static Json profile_json(const Snapshot& s) {
require(s.offline_rank_cap > 0 && s.offline_rank_cap <= 10000, "Invalid offline neighborhood rank cap");
require(s.bool_options.size() + s.play_events.size() + s.quests.size() + s.entitlements.size() +
s.neighborhood_ranks.size() <= max_records, "Too many local profile records");
require(s.customization.is_object() && s.settings.is_object() && s.extensions.is_object(),
"Customization, settings and extensions must be objects");
detail::validate_settings(s);
validate_customization(s.customization);
validate_object_dropper(s);
validate_challenges(s);
(void)park_choices(s);
(void)world_layer_choices(s);
(void)world_controls(s);
(void)graphics_controls(s);
(void)noclip_binding(s);
(void)forward_velocity_binding(s);
(void)up_velocity_binding(s);
(void)news_feed(s);
(void)rip_score(s);
(void)location_travel_enabled(s.extensions);
Json root = s.extensions;
root["schema_version"] = 1;
root["revision"] = s.revision;
root["defaults_version"] = s.onboarding_seed;
root["neighborhood_rank_cap"] = s.offline_rank_cap;
root["customization"] = s.customization;
root["settings"] = s.settings;
root["options"] = Json::object();
root["entitlements"] = Json::object();
auto& progress = root["progress"];
require(progress.is_null() || progress.is_object(), "Progress must be an object");
progress["quests"] = Json::object();
progress["play_events"] = Json::object();
progress["neighborhood_ranks"] = Json::object();
for (const auto& [key, value] : s.bool_options) {
require(valid_text(key), "Invalid option key"); root["options"][key] = value;
}
for (const auto& [key, value] : s.entitlements) {
require(valid_text(key), "Invalid entitlement key"); root["entitlements"][key] = value;
}
for (const auto& [key, value] : s.quests) {
require(valid_text(key) && value >= 0 && value <= 6, "Invalid quest state");
progress["quests"][key] = value;
}
for (const auto& [key, value] : s.neighborhood_ranks) {
require(std::find(neighborhood_ids.begin(), neighborhood_ids.end(), key) != neighborhood_ids.end() &&
value <= 10000, "Invalid neighborhood rank");
progress["neighborhood_ranks"][key] = value;
}
for (const auto& [key, event] : s.play_events) {
require(key == event.id && valid_text(key) && valid_text(event.context, true) && event.count >= 0,
"Invalid play event");
progress["play_events"][key] = {{"context", event.context}, {"timestamp", event.timestamp}, {"count", event.count}};
}
return root;
}
std::string encode(const Snapshot& s) {
auto result = profile_json(s).dump(2) + '\n';
require(result.size() <= max_bytes, "Local profile exceeds size limit");
return result;
}
static Snapshot decode_document(Json root) {
require(root.is_object() && unsigned_value(root.at("schema_version"), 1) == 1,
"Unsupported local profile JSON schema");
Snapshot s;
s.revision = unsigned_value(root.at("revision"), (std::numeric_limits<std::uint64_t>::max)());
s.onboarding_seed = static_cast<std::uint32_t>(unsigned_value(root.at("defaults_version"), UINT32_MAX));
s.offline_rank_cap = static_cast<std::uint32_t>(unsigned_value(root.at("neighborhood_rank_cap"), 10000));
const auto parse_bools = [&](const char* name, auto& values) {
if (!root.contains(name)) return; // an absent section is empty
for (const auto& [key, value] : object_field(root, name).items()) {
require(value.is_boolean(), "Profile option/entitlement must be boolean");
values.emplace(key, value.get<bool>());
}
};
parse_bools("options", s.bool_options); parse_bools("entitlements", s.entitlements);
const auto& progress = object_field(root, "progress");
for (const auto& [key, value] : object_field(progress, "quests").items())
s.quests.emplace(key, static_cast<std::int32_t>(unsigned_value(value, 6)));
for (const auto& [key, value] : object_field(progress, "neighborhood_ranks").items())
s.neighborhood_ranks.emplace(key, static_cast<std::uint32_t>(unsigned_value(value, 10000)));
for (const auto& [key, value] : object_field(progress, "play_events").items()) {
require(value.is_object() && value.size() == 3 && value.at("context").is_string(), "Invalid play event fields");
s.play_events.emplace(key, PlayEvent{key, value.at("context").get<std::string>(),
unsigned_value(value.at("timestamp"), UINT64_MAX),
static_cast<std::int32_t>(unsigned_value(value.at("count"), INT32_MAX))});
}
s.customization = root.contains("customization") ? object_field(root, "customization") : Json::object();
s.settings = object_field(root, "settings");
for (const auto* key : {"schema_version", "revision", "defaults_version", "neighborhood_rank_cap",
"customization", "settings", "options", "entitlements"}) root.erase(key);
for (const auto* key : {"quests", "neighborhood_ranks", "play_events"}) root["progress"].erase(key);
if (root["progress"].empty()) root.erase("progress");
s.extensions = std::move(root);
(void)profile_json(s);
return s;
}
Snapshot decode(std::string_view text) { return decode_document(parse_json(text)); }
std::filesystem::path default_path() {
std::array<wchar_t, 32768> buffer{};
const auto n = GetEnvironmentVariableW(L"LOCALAPPDATA", buffer.data(), static_cast<DWORD>(buffer.size()));
require(n && n < buffer.size(), "LOCALAPPDATA is unavailable");
return std::filesystem::path(buffer.data()) / L"ReSkate" / L"profiles" / L"offline" / L"reskate.sqlite3";
}
std::filesystem::path defaults_path() {
std::array<wchar_t, 32768> buffer{};
const auto count = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
require(count && count < buffer.size(), "Cannot resolve ReSkate defaults path");
return std::filesystem::path(buffer.data()).parent_path() / L"reskate.defaults.json";
}
// Entitlement ids are game data: every one in the installed content cache is
// owned by default, alongside anything the repository defaults add.
std::string with_cached_entitlements(std::string text) {
const auto& content = content_cache::catalogs();
if (!content.available) return text;
auto document = Json::parse(text);
auto& entitlements = document["entitlements"];
for (const auto& id : content.entitlements)
if (!entitlements.contains(id)) entitlements[id] = true;
return document.dump();
}
std::string_view embedded_defaults() noexcept {
return {reinterpret_cast<const char*>(embedded_profile_defaults), sizeof(embedded_profile_defaults)};
}
namespace {
std::atomic<std::uint64_t> store_changes{1};
}
Store::Store(std::filesystem::path path, const std::filesystem::path& defaults) : path_(storage::database_path(std::move(path))),
database_(std::make_unique<storage::SaveDatabase>(path_, database::profile_schema())) {
const auto seed = decode(with_cached_entitlements(defaults.empty() || !std::filesystem::exists(defaults) ?
std::string(embedded_defaults()) : read_file(defaults)));
if (database_->exists()) {
value_ = decode_document(database::profile_document(database_->document()));
database_->backup();
} else {
const auto json = storage::json_source_path(path_);
const auto legacy = path_.parent_path() / L"profile.rsp";
if (std::filesystem::exists(json)) value_ = decode(read_file(json));
else {
require(!std::filesystem::exists(json.wstring() + L".bak"), "JSON profile missing while backup exists; restore before migrating");
if (std::filesystem::exists(legacy)) value_ = decode_legacy(read_file(legacy));
else require(!std::filesystem::exists(legacy.wstring() + L".bak"), "Legacy profile backup requires recovery");
}
}
auto next = value_;
seed_completed_onboarding(next, seed);
trim_challenge_changes(next);
// Object Browser categories and travel destinations come from the content
// cache (object_categories.h, location_travel.h); saves keep only settings.
if (next.extensions.contains("object_dropper") && next.extensions.at("object_dropper").is_object())
next.extensions.at("object_dropper").erase("categories");
if (next.extensions.contains("location_travel") && next.extensions.at("location_travel").is_object())
for (const auto* key : {"destinations", "access_points"}) next.extensions.at("location_travel").erase(key);
if (!database_->exists() || next != value_) commit(std::move(next));
store_changes.fetch_add(1, std::memory_order_release);
}
Store::~Store() = default;
std::uint64_t Store::changes() noexcept { return store_changes.load(std::memory_order_acquire); }
Snapshot Store::snapshot() const { std::lock_guard lock(mutex_); return value_; }
std::shared_ptr<const Snapshot> Store::shared_snapshot() const {
std::lock_guard lock(mutex_);
if (!snapshot_cache_ || snapshot_cache_->revision != value_.revision) snapshot_cache_ = std::make_shared<const Snapshot>(value_);
return snapshot_cache_;
}
std::string Store::export_json() const { std::lock_guard lock(mutex_); return encode(value_); }
void Store::import_json(std::string_view text) {
auto next = decode(text);
std::lock_guard lock(mutex_);
next.revision = value_.revision;
if (next != value_) commit(std::move(next));
}
std::uint64_t Store::revision() const { std::lock_guard lock(mutex_); return value_.revision; }
std::shared_ptr<const MissionState> Store::mission_state() const {
std::lock_guard lock(mutex_);
if (!mission_cache_ || mission_cache_->revision != value_.revision) {
auto result = std::make_shared<MissionState>();
result->revision = value_.revision;
result->play_events = value_.play_events;
result->quests = value_.quests;
mission_cache_ = std::move(result);
}
return mission_cache_;
}
NeighborhoodState Store::neighborhood_state(std::string_view id) const {
std::lock_guard lock(mutex_);
const auto enabled = [&](std::string_view key) {
const auto it = value_.bool_options.find(key);
return it != value_.bool_options.end() && it->second;
};
NeighborhoodState result{enabled(unlock_neighborhoods_option), enabled(max_neighborhood_ranks_option), {}};
const auto rank = value_.neighborhood_ranks.find(id);
if (rank != value_.neighborhood_ranks.end()) result.rank = rank->second;
return result;
}
std::uint32_t Store::offline_rank_cap() const {
std::lock_guard lock(mutex_); return value_.offline_rank_cap;
}
std::optional<RipScore> Store::saved_rip_score() const {
std::lock_guard lock(mutex_); return profile::rip_score(value_);
}
std::vector<std::string> Store::completed_challenge_criteria(std::string_view id) const {
std::lock_guard lock(mutex_); return profile::challenge_completed_criteria(value_, id);
}
std::optional<bool> Store::bool_option(std::string_view key) const {
std::lock_guard lock(mutex_);
const auto it = value_.bool_options.find(key);
return it == value_.bool_options.end() ? std::nullopt : std::optional<bool>(it->second);
}
std::optional<Json> Store::user_value(std::string_view key) const {
std::lock_guard lock(mutex_);
// Share Boolean storage with the existing profile-option expression hooks.
if (const auto it = value_.bool_options.find(key); it != value_.bool_options.end()) return Json(it->second);
if (!value_.settings.contains("gameplay")) return {};
const auto& values = value_.settings.at("gameplay");
const auto it = values.find(std::string(key));
return it == values.end() ? std::nullopt : std::optional<Json>(*it);
}
void Store::set_user_value(std::string_view key, const Json& value) {
if (value.is_boolean()) { set_bool_option(key, value.get<bool>()); return; }
std::lock_guard lock(mutex_);
require(valid_text(key) && !value_.bool_options.contains(key), "Invalid gameplay setting key or type");
Update update(*this);
update.json(value_.settings, {"gameplay", key}) = value;
update.commit();
}
void Store::set_user_values(const std::vector<std::pair<std::string, Json>>& values) {
if (values.empty()) return;
std::lock_guard lock(mutex_);
Update update(*this);
for (const auto& [key, value] : values) {
require(valid_text(key), "Invalid gameplay setting key");
if (value.is_boolean()) {
update.record(value_.bool_options, key, "player_settings") = value.get<bool>();
continue;
}
require(!value_.bool_options.contains(key), "Invalid gameplay setting key or type");
update.json(value_.settings, {"gameplay", key}) = value;
}
update.commit();
}
std::optional<std::int32_t> Store::quest_state(std::string_view key) const {
std::lock_guard lock(mutex_);
const auto it = value_.quests.find(key);
return it == value_.quests.end() ? std::nullopt : std::optional<std::int32_t>(it->second);
}
std::optional<Json> Store::native_profile_option(unsigned location, std::string_view key) const {
std::lock_guard lock(mutex_);
const auto section = value_.settings.find("native_profile_options");
if (section == value_.settings.end()) return {};
const auto group = section->find(std::to_string(location));
if (group == section->end()) return {};
const auto option = group->find(std::string(key));
return option == group->end() ? std::nullopt : std::optional<Json>(*option);
}
void Store::set_native_profile_option(unsigned location, std::string_view key, const Json& value) {
std::lock_guard lock(mutex_);
require((location == 1 || location == 2) && valid_text(key), "Invalid native profile option location or key");
Update update(*this);
update.json(value_.settings, {"native_profile_options", std::to_string(location), key}) = value;
update.commit();
}
PlayEvent Store::record_play_event(std::string_view id) {
std::lock_guard lock(mutex_);
require(valid_text(id), "Invalid event identifier");
Update update(*this);
auto& event = update.record(value_.play_events, id, "play_events");
if (event.id.empty()) event.id = id;
require(event.count < INT32_MAX, "Play-event count overflow");
++event.count;
const auto result = event;
update.commit();
return result;
}
std::optional<bool> Store::entitlement(std::string_view key) const {
std::lock_guard lock(mutex_);
const auto it = value_.entitlements.find(key);
return it == value_.entitlements.end() ? std::nullopt : std::optional<bool>(it->second);
}
void Store::set_neighborhood_rank(std::string_view key, std::uint32_t value) {
std::lock_guard lock(mutex_);
require(std::find(neighborhood_ids.begin(), neighborhood_ids.end(), key) != neighborhood_ids.end() && value <= 10000, "Invalid neighborhood rank");
Update update(*this);
update.record(value_.neighborhood_ranks, key, "neighborhood_ranks") = value;
update.commit();
}
void Store::set_bool_option(std::string_view key, bool value) {
std::lock_guard lock(mutex_);
require(valid_text(key), "Invalid option identifier");
Update update(*this);
update.record(value_.bool_options, key, "player_settings") = value;
update.commit();
}
void Store::set_quest_state(std::string_view id, std::int32_t value) {
std::lock_guard lock(mutex_);
require(valid_text(id) && value >= 0 && value <= 6, "Invalid quest state");
Update update(*this);
update.record(value_.quests, id, "quests") = value;
update.commit();
}
void Store::set_onboarding_completed(std::string_view id, bool completed) {
std::lock_guard lock(mutex_);
require(std::find(onboarding_ids.begin(), onboarding_ids.end(), id) != onboarding_ids.end(), "Unknown onboarding mission");
Update update(*this);
for (const auto* suffix : {":start", ":complete"}) {
const auto key = std::string(id) + suffix;
auto& event = update.record(value_.play_events, key, "play_events");
if (event.id.empty()) event.id = key;
event.count = completed ? (std::max)(1, event.count) : 0;
}
update.commit();
}
void Store::commit(Snapshot next) {
require(value_.revision != (std::numeric_limits<std::uint64_t>::max)(), "Profile revision overflow");
next.revision = value_.revision + 1;
auto tables = database::profile_tables(profile_json(next), database_->exists() ? &database_->document() : nullptr, &value_);
if (database_->exists()) database_->commit(std::move(tables));
else database_->initialize(std::move(tables));
value_ = std::move(next);
store_changes.fetch_add(1, std::memory_order_release);
}
}