Files
ReSkate/Extension/Profile/local_profile_preferences.cpp
T
andrewnakasandClaude Opus 5.5 0bcb0ba2ac Merge ReSkate v2.1.0 into the trainer, in its new menu layout
The trainer keeps upstream's page and naming (SANDBOX; noclip, No Bail and
the boosts on Skater > Cheats; the full screen controller menu) with its own
TUNE sections, presets card and search inside. The MOVEMENT tab is gone:
`trainer open movement` opens Skater > Cheats, where the Revert Boost rows
now sit. Upstream's Hardcore preset sits beside Realistic; saving
trainer.json and reading the map file stay off the client thread.

Board Break's wear and the trainer no longer undo each other: wear writes
four tuning values itself, and every trainer write used to put them back to
the trainer's copy (and a level loaded on a worn board took them as stock).
board_break::handling_value() tells the trainer what wear wants; values the
player has not changed are handed back to it, their stock is the game's own.

Tests: Hardcore's check follows the sections (topic, not the old lists);
the session test pokes a flip speed past the trainer's own limit (100).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 00:13:49 -06:00

360 lines
17 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "runtime_internal.h"
#include <condition_variable>
#include <deque>
#include <map>
#include <mutex>
#include <thread>
namespace dingosdk {
using namespace profile_runtime;
namespace profile_runtime { std::string binding_feedback; }
ControllerBindingsModel local_profile_controller_bindings() {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
ControllerBindingsModel result;
result.status = binding_feedback;
if (!s.active || !s.store) return result;
result.freecam_controller = s.store->freecam_controller();
result.freecam_controller_combo = s.store->freecam_controller_binding();
result.freecam_combo = s.store->freecam_binding();
result.tp_to_freecam_combo = s.store->tp_to_freecam_binding();
result.noclip_combo = s.store->noclip_binding();
result.forward_velocity_combo = s.store->forward_velocity_binding();
result.up_velocity_combo = s.store->up_velocity_binding();
result.offboard_up_velocity_combo = s.store->offboard_up_velocity_binding();
result.vote_yes_combo = s.store->vote_binding(true);
result.vote_no_combo = s.store->vote_binding(false);
for (std::size_t i = 0; i < action_binds.size(); ++i) result.action_combos[i] = s.store->action_binding(action_binds[i].key);
result.available = true;
return result;
}
bool set_local_noclip_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_noclip_binding(combo);
binding_feedback = combo ? "Noclip binding saved." : "Noclip binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","noclip"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool local_freecam_controller() {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
return s.active && s.store && s.store->freecam_controller();
}
bool set_local_freecam_controller(bool value) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store) return false;
try {
s.store->save_freecam_controller(value);
return true;
} catch (...) { return false; }
}
bool set_local_freecam_controller_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_freecam_controller_binding(combo);
binding_feedback = combo ? "Freecam Controller binding saved." : "Freecam Controller binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","freecam_controller"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_freecam_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_freecam_binding(combo);
binding_feedback = combo ? "Freecam binding saved." : "Freecam binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","freecam"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_action_binding(std::size_t index, std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || index >= action_binds.size() || !valid_action_binding(combo)) return false;
try {
const auto& slot = action_binds[index];
s.store->save_action_binding(slot.key, combo);
binding_feedback = std::string(slot.label) + (combo ? " binding saved." : " binding cleared.");
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action",std::string(slot.key)},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_vote_binding(bool yes, std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_vote_binding(yes, combo);
binding_feedback = std::string(yes ? "Vote yes" : "Vote no") + (combo ? " binding saved." : " binding cleared.");
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action",yes ? "vote_yes" : "vote_no"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_tp_to_freecam_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_tp_to_freecam_binding(combo);
binding_feedback = combo ? "TP to Freecam binding saved." : "TP to Freecam binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","tp_to_freecam"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_forward_velocity_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_forward_velocity_binding(combo);
binding_feedback = combo ? "Forward Boost binding saved." : "Forward Boost binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","forward_velocity"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_offboard_up_velocity_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_offboard_up_velocity_binding(combo);
binding_feedback = combo ? "Off-board Up Boost binding saved." : "Off-board Up Boost binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","offboard_up_velocity"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
bool set_local_up_velocity_binding(std::uint32_t combo) {
auto& s = local_runtime(); std::lock_guard lock(s.native_mutex);
if (!s.active || !s.store || !valid_action_binding(combo)) return false;
try {
s.store->save_up_velocity_binding(combo);
binding_feedback = combo ? "Up Boost binding saved." : "Up Boost binding cleared.";
dingosdk::logging::event(dingosdk::logging::Channel::profile, dingosdk::Json{{"event","controller_binding_saved"},{"action","up_velocity"},{"combo",combo}}.dump().c_str());
return true;
} catch (...) { binding_feedback = "Could not save the binding. See the console log."; return false; }
}
namespace profile_runtime {
bool local_profile_store_ready() noexcept {
try { return local_runtime().store != nullptr; } catch (...) { return false; }
}
namespace {
// ReSkate's own preferences and values are saved on a thread of their own, in the order they were
// set. They are set from the game's threads: a menu click on the render thread, a console command
// or a session's slider on the client thread, and a save (a database transaction) held that
// frame up. Until one is saved, it is what reads of it give.
struct PendingValues {
std::mutex mutex;
std::condition_variable wake;
std::deque<std::vector<std::pair<std::string, Json>>> queue;
std::map<std::string, Json, std::less<>> latest; // set and not saved yet, by full key
bool started{};
};
PendingValues& pending_values() {
static auto* pending = new PendingValues; // process lifetime, as its writer
return *pending;
}
void save_values(std::vector<std::pair<std::string, Json>> values) {
auto& s = local_runtime();
try {
s.store->set_user_values(values);
for (const auto& [key, value] : values) {
if (!value.is_boolean()) continue;
const auto saved = s.store->user_value(key);
if (!saved || !saved->is_boolean() || saved->get<bool>() != value.get<bool>())
dingosdk::logging::event(dingosdk::logging::Channel::profile,
dingosdk::Json{{"event", "local_preference_save_failed"}, {"key", key}, {"reason", "readback_mismatch"}}.dump().c_str());
else
dingosdk::logging::event(dingosdk::logging::Channel::profile,
dingosdk::Json{{"event", "local_preference_saved"}, {"key", key}, {"value", value.get<bool>()}}.dump().c_str());
}
} catch (...) {
dingosdk::logging::event(dingosdk::logging::Channel::profile, "{\"event\":\"local_preference_save_failed\",\"reason\":\"exception\"}");
}
}
void writer() {
auto& p = pending_values();
for (;;) {
std::vector<std::pair<std::string, Json>> values;
{
std::unique_lock lock(p.mutex);
p.wake.wait(lock, [&] { return !p.queue.empty(); });
values = std::move(p.queue.front());
p.queue.pop_front();
}
save_values(values);
std::lock_guard lock(p.mutex);
// (A value set again meanwhile is still waiting: it stays.)
for (const auto& [key, value] : values)
if (const auto found = p.latest.find(key); found != p.latest.end() && found->second == value) p.latest.erase(found);
}
}
// Full keys ("ReSkate." ...). Saved in place when no writer could be started.
void queue_values(std::vector<std::pair<std::string, Json>> values) {
auto& p = pending_values();
{
std::lock_guard lock(p.mutex);
if (!p.started) {
try {
std::thread(writer).detach();
p.started = true;
} catch (...) {}
}
if (p.started) {
for (const auto& [key, value] : values) p.latest[key] = value;
p.queue.push_back(std::move(values));
p.wake.notify_one();
return;
}
}
save_values(std::move(values));
}
std::optional<Json> pending_value(std::string_view full_key) {
auto& p = pending_values();
std::lock_guard lock(p.mutex);
if (const auto found = p.latest.find(full_key); found != p.latest.end()) return found->second;
return {};
}
} // namespace
std::optional<bool> local_preference(std::string_view key) noexcept {
try {
auto& s = local_runtime();
if (!s.store) return {};
const auto full_key = "ReSkate." + std::string(key);
if (const auto waiting = pending_value(full_key); waiting && waiting->is_boolean()) return waiting->get<bool>();
const auto value = s.store->user_value(full_key);
if (value && value->is_boolean()) return value->get<bool>();
} catch (...) { /* A preference is never worth failing a caller over. */ }
return {};
}
void set_local_preference(std::string_view key, bool value) noexcept {
try {
auto& s = local_runtime();
if (!s.store) {
dingosdk::logging::event(dingosdk::logging::Channel::profile,
"{\"event\":\"local_preference_save_failed\",\"reason\":\"profile_unavailable\"}");
return;
}
queue_values({{"ReSkate." + std::string(key), Json(value)}});
} catch (...) {
dingosdk::logging::event(dingosdk::logging::Channel::profile,
"{\"event\":\"local_preference_save_failed\",\"reason\":\"exception\"}");
}
}
std::optional<Json> local_value(std::string_view key) noexcept {
try {
auto& s = local_runtime();
if (!s.store) return {};
const auto full_key = "ReSkate." + std::string(key);
if (auto waiting = pending_value(full_key)) return waiting;
return s.store->user_value(full_key);
} catch (...) { /* An unreadable value just means the default. */ }
return {};
}
void set_local_values(const std::vector<std::pair<std::string, Json>>& values) noexcept {
try {
auto& s = local_runtime();
if (!s.store) return;
std::vector<std::pair<std::string, Json>> prefixed;
prefixed.reserve(values.size());
for (const auto& [key, value] : values) prefixed.emplace_back("ReSkate." + key, value);
queue_values(std::move(prefixed));
} catch (...) { /* Saving is best effort; the runtime values already applied. */ }
}
// One of the game's own gameplay options as the local profile holds it (local_profile_runtime.h).
std::optional<double> local_game_option(std::string_view key) noexcept {
try {
auto& s = local_runtime();
if (!s.store || !s.active.load(std::memory_order_acquire)) return {};
const auto saved = s.store->user_value(key);
if (!saved) return {};
if (saved->is_boolean()) return saved->get<bool>() ? 1.0 : 0.0;
if (saved->is_number()) return saved->get<double>();
} catch (...) {}
return {};
}
bool set_local_game_option(std::string_view key, double value, bool boolean) noexcept {
try {
auto& s = local_runtime();
if (!s.store || !s.active.load(std::memory_order_acquire)) return false;
// A value keeps the type the game saved it with (its getters answer only with a value of their own type).
const auto saved = s.store->user_value(key);
if (boolean || (saved && saved->is_boolean())) s.store->set_user_value(key, value != 0);
else if (saved && saved->is_number_float()) s.store->set_user_value(key, value);
else s.store->set_user_value(key, static_cast<std::int64_t>(value));
return true;
} catch (...) { return false; }
}
// Shared with the overlay through the model; clamped to a legible range. The window and the full
// screen menu each keep their own (ReSkate.MenuScale, ReSkate.MenuScaleFullscreen).
std::atomic<float>& menu_scale_cache(bool fullscreen) noexcept {
static std::atomic<float> window{0}, full{0};
return fullscreen ? full : window;
}
const char* menu_scale_key(bool fullscreen) noexcept { return fullscreen ? "ReSkate.MenuScaleFullscreen" : "ReSkate.MenuScale"; }
float local_menu_scale(bool fullscreen) noexcept {
auto& cache = menu_scale_cache(fullscreen);
if (const auto value = cache.load(std::memory_order_relaxed); value > 0) return value;
float value = default_menu_scale;
try {
auto& s = local_runtime();
if (s.store)
if (const auto saved = s.store->user_value(menu_scale_key(fullscreen)); saved && saved->is_number())
value = std::clamp(static_cast<float>(saved->get<double>()), min_menu_scale, max_menu_scale);
} catch (...) { /* An unreadable preference just means the default scale. */ }
cache.store(value, std::memory_order_relaxed);
return value;
}
std::atomic<int>& menu_fullscreen_cache() noexcept { static std::atomic<int> value{-1}; return value; }
bool local_menu_fullscreen() noexcept {
auto& cache = menu_fullscreen_cache();
if (const auto value = cache.load(std::memory_order_relaxed); value >= 0) return value != 0;
bool value = false;
try {
auto& s = local_runtime();
if (s.store)
if (const auto saved = s.store->user_value("ReSkate.MenuFullscreen"); saved && saved->is_boolean())
value = saved->get<bool>();
} catch (...) { /* An unreadable preference just means a window. */ }
cache.store(value ? 1 : 0, std::memory_order_relaxed);
return value;
}
void set_local_menu_fullscreen(bool fullscreen) noexcept {
menu_fullscreen_cache().store(fullscreen ? 1 : 0, std::memory_order_relaxed);
try {
auto& s = local_runtime();
if (s.store) s.store->set_user_value("ReSkate.MenuFullscreen", fullscreen);
} catch (...) { /* Saving is best effort; the runtime value already applied. */ }
}
bool set_local_menu_scale(float value, bool fullscreen) noexcept {
if (!(value >= min_menu_scale && value <= max_menu_scale)) return false;
menu_scale_cache(fullscreen).store(value, std::memory_order_relaxed);
try {
auto& s = local_runtime();
if (s.store) s.store->set_user_value(menu_scale_key(fullscreen), static_cast<double>(value));
} catch (...) { /* Saving is best effort; the runtime value already applied. */ }
return true;
}
} // namespace profile_runtime
}