Trainer 0.4.0 (in progress): camera tab, Skate 3 presets, grind pop dial, search, controller menu, preset sharing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
andrewnakasandClaude Opus 5.5 committed 2026-10-06 02:34:22 -06:00
1 parent 2cdc5ee8df
commit eeca84efea
21 files changed
+1267 -42

No files matched your search

@@ -34,6 +34,12 @@ struct State {
Clock::time_point at;
float hx{}, hz{1}; // direction of travel, eased
std::array<float, 3> last{};
// The player's own camera.
CustomCamera custom;
Clock::time_point custom_at;
float custom_hx{}, custom_hz{1};
std::array<float, 3> custom_last{}, custom_eye{};
bool custom_running{};
};
State &state() {
static auto *value = new State;
@@ -126,4 +132,67 @@ std::array<float, 16> follow_camera(std::uint64_t id, const Transform &root, flo
fov = p.fov;
return {right[0], right[1], right[2], 0, up[0], up[1], up[2], 0, back[0], back[1], back[2], 0, eye[0], eye[1], eye[2], 1};
}
void set_custom_camera(const CustomCamera &camera) noexcept {
auto &s = state();
std::lock_guard lock(s.mutex);
s.custom = camera;
if (!camera.on) s.custom_running = false;
}
CameraProfile gameplay_camera_profile() noexcept {
auto &s = state();
std::lock_guard lock(s.mutex);
return {s.profile.distance, s.profile.height, s.profile.pitch * 57.2958f, s.profile.fov, s.samples};
}
std::optional<std::array<float, 16>> custom_camera(float &fov) noexcept {
auto &s = state();
std::lock_guard lock(s.mutex);
const auto &c = s.custom;
if (!c.on) return std::nullopt;
const auto now = Clock::now();
const auto &pos = c.position;
if (!std::isfinite(pos[0]) || !std::isfinite(pos[1]) || !std::isfinite(pos[2])) return std::nullopt;
const bool fresh = !s.custom_running || now - s.custom_at > std::chrono::milliseconds(500);
if (fresh) {
// Start behind the way the skater faces; their travel takes over once they move.
s.custom_hx = std::sin(c.heading / 57.2958f);
s.custom_hz = std::cos(c.heading / 57.2958f);
s.custom_last = pos;
s.custom_at = now;
}
const float dt = std::clamp(seconds(now - s.custom_at), 0.0005f, 0.1f);
const float vx = (pos[0] - s.custom_last[0]) / dt, vz = (pos[2] - s.custom_last[2]) / dt;
const float speed = std::sqrt(vx * vx + vz * vz);
if (speed > 1.0f && speed < 120.0f) {
const float turn = 1.0f - std::exp(-dt / 0.25f);
float hx = s.custom_hx + (vx / speed - s.custom_hx) * turn, hz = s.custom_hz + (vz / speed - s.custom_hz) * turn;
const float length = std::sqrt(hx * hx + hz * hz);
if (length > .01f) { s.custom_hx = hx / length; s.custom_hz = hz / length; }
}
s.custom_last = pos;
s.custom_at = now;
const float hx = s.custom_hx, hz = s.custom_hz;
const float distance = std::clamp(c.distance, 0.2f, 30.0f), height = std::clamp(c.height, -2.0f, 20.0f);
const float side = std::clamp(c.side, -5.0f, 5.0f), pitch = std::clamp(c.pitch, -85.0f, 85.0f) / 57.2958f;
// Right of the direction of travel, on the ground plane.
const std::array<float, 3> target{pos[0] - hx * distance - hz * side, pos[1] + height, pos[2] - hz * distance + hx * side};
// The camera trails its place by `lag`; a teleport (or the first frame) snaps.
const float gap = std::abs(target[0] - s.custom_eye[0]) + std::abs(target[1] - s.custom_eye[1]) + std::abs(target[2] - s.custom_eye[2]);
if (fresh || c.lag < 0.005f || gap > 25.0f) s.custom_eye = target;
else {
const float k = 1.0f - std::exp(-dt / std::min(c.lag, 2.0f));
for (std::size_t i = 0; i < 3; ++i) s.custom_eye[i] += (target[i] - s.custom_eye[i]) * k;
}
s.custom_running = true;
const auto &eye = s.custom_eye;
const float cp = std::cos(pitch), sp = std::sin(pitch);
const std::array<float, 3> back{-hx * cp, -sp, -hz * cp};
std::array<float, 3> right{back[2], 0, -back[0]};
const float flat = std::sqrt(right[0] * right[0] + right[2] * right[2]);
if (flat > .001f) { right[0] /= flat; right[2] /= flat; } else right = {1, 0, 0};
const std::array<float, 3> up{back[1] * right[2] - back[2] * right[1], back[2] * right[0] - back[0] * right[2],
back[0] * right[1] - back[1] * right[0]};
fov = c.fov >= 20.0f && c.fov <= 150.0f ? c.fov : s.profile.fov;
return std::array<float, 16>{right[0], right[1], right[2], 0, up[0], up[1], up[2], 0, back[0], back[1], back[2], 0, eye[0], eye[1], eye[2], 1};
}
} // namespace dingosdk::multiplayer
+27
View File
@@ -3,6 +3,7 @@
#include "Extension/Multiplayer/Net/protocol.h"
#include <array>
#include <cstdint>
#include <optional>
// The spectate camera (party Spectate and throwdown turns). It frames another skater the way
// the game's own camera frames the local one: while the player skates, the game camera's
@@ -18,4 +19,30 @@ void observe_gameplay_camera(std::uintptr_t base, const GameView &view) noexcept
// The camera for spectating `id` standing at `root` this frame (world matrix rows: right, up,
// back, position), and the FOV to give it.
std::array<float, 16> follow_camera(std::uint64_t id, const Transform &root, float &fov) noexcept;
// The player's own camera (the trainer's Camera tab): a follow camera behind the local skater's
// direction of travel at a distance, height and tilt of the player's choosing, in place of the
// game's. It does not avoid walls.
struct CustomCamera {
bool on{};
float distance{3.4f}; // metres behind the skater
float height{1.5f}; // metres above the skater's root
float pitch{-9.0f}; // degrees; negative looks down
float side{}; // metres to the right: over the shoulder
float fov{}; // degrees; 0: the game's own
float lag{0.08f}; // seconds the camera trails the skater by
// Where the local skater is this tick, from whoever switches the camera on.
std::array<float, 3> position{};
float heading{}; // degrees, 0 = +Z, clockwise seen from above: used until the skater moves
};
// Any thread.
void set_custom_camera(const CustomCamera &camera) noexcept;
// Client thread, every tick: the camera to hold, or nothing while it is off or there is no skater.
std::optional<std::array<float, 16>> custom_camera(float &fov) noexcept;
// How the game's own camera has been framing the local skater (see observe_gameplay_camera).
struct CameraProfile {
float distance{}, height{}, pitch{}, fov{}; // pitch in degrees
unsigned samples{};
};
CameraProfile gameplay_camera_profile() noexcept;
} // namespace dingosdk::multiplayer
+2
View File
@@ -34,6 +34,8 @@ void update_party_position(const Pose *pose) noexcept;
// Points the party Spectate camera at a session player's skater from code (the
// throwdown turn camera). 0 stops a spectate this function started. Game thread.
void spectate_party_member(std::uint64_t id) noexcept;
// Whether the Spectate camera is in use (it and the trainer's camera share one camera lease).
bool spectating_party_member() noexcept;
// Returns only a record owned by this adapter, validated against this manager.
std::uint64_t native_party_player_info(std::uintptr_t manager, std::size_t slot) noexcept;
std::string native_party_status();
@@ -809,6 +809,10 @@ void spectate_party_member(std::uint64_t id) noexcept {
}
} catch (...) {}
}
bool spectating_party_member() noexcept {
auto &s = state();
return s.spectating.load(std::memory_order_acquire) != 0 || s.camera_owned;
}
void tick_native_party_actions(std::uintptr_t base, std::uintptr_t client, bool ready, bool camera_phase) noexcept {
auto &s = state();
if (!s.installed.load(std::memory_order_acquire)) return;
+27
View File
@@ -1,5 +1,32 @@
# Changelog
## Next (0.4.0, in progress)
From what players asked for in the ReSkate Discord. Everything marked "untested" is in so that it
can be tried: tell us what it does.
- **Camera tab.** Move the game's own cameras: distance, height and side for the low camera, the
high camera and the on-foot camera, with a "SunJay's Low Cam" button (the numbers of SunJay's
mod, which showed where the game keeps them). The cameras keep their smoothing and stay out of
walls. Also an experimental camera of your own that replaces the game's.
- **Skate 3 presets.** Skate 3, Skate 3 Hardcore and Skate 3 Easy set every value this game still
shares with Skate 3 by name (77 of them differ) to Skate 3's number: pop, grind pops, pushing,
pumping, steering, manuals, body spins and flips, bails. Not yet measured against Skate 3.
- **Pop out of grinds, finished.** All six values are named (grinds, board slides, nose and tail
slides; the highest pop and the quick-pop minimum) and one dial moves them together.
- **More dials:** on-board gravity, get-on-board speed (untested), body flip air time (untested),
landing speed you can take, landing compression (untested), revert friction, pumping strength.
Manual pop, the stick boost when popping, and the bad-landing strictness are named values.
- **Search finds everything:** dials, switches, trick sliders and every value, on all three lists.
- **The menu works from the controller.** LB + RB + click the right stick opens and closes it;
D-pad or left stick moves, A presses, B goes back. It can be switched off on the Practice tab.
- **Share presets.** "Share" copies a preset as one line of text; "Import from clipboard" adds
one someone sent you. Your own presets now carry the trick sliders too.
- **You can see when you are stock:** a line beside Reset everything says STOCK or MODIFIED.
- **Plainer words:** named values explain themselves, with units, when you point at them.
- No comply and boneless heights now also apply to weak upward launches (riding downhill).
- The menu tab is called PHYSICS.
## In ReSkate
Changes made when the trainer moved into ReSkate itself.
+7 -5
View File
@@ -8,18 +8,20 @@ telemetry HUD. It ships no game data: the list of values is built at run time fr
| Tab | What you get |
|---|---|
| **TUNE** | Three lists: REALISTIC, FUN and EVERYTHING. Each opens on the dials: every built-in preset as one slider (Ollie height, Push speed, Body flip and spin speed, Bail resistance, Grind lock-on and friction, on-foot jump and sprint, glide, torpedo), where 1 is the game's own and the preset's button jumps to the preset; then the switches (Auto Push, No Speed Wobble, Smooth Surfaces, Long Wheelbase, Never bail), the trick sliders (flip trick speed, no comply, boneless, hippy and off-board jump height) and the values themselves: a short plainly named list, or under EVERYTHING the whole table (the physics tuning's values, one multiplier per curve and graph, and the 314 values of the game's other tuning classes) with search, groups, "only what I changed", locks, your own saved presets and a preset a map applies every time it loads. Changes apply while you skate. Values the game was never found or seen reading are hidden unless you ask for them. **Reset everything**, above the tabs, puts the game back as it shipped. |
| **TUNE** | Three lists: REALISTIC, FUN and EVERYTHING. Each opens on the dials: every built-in preset as one slider (Ollie height, pop out of grinds, on-board gravity, landings, revert friction, pumping, Push speed, Body flip and spin speed, Bail resistance, Grind lock-on and friction, on-foot jump and sprint, glide, torpedo), where 1 is the game's own and the preset's button jumps to the preset; then the switches (Auto Push, No Speed Wobble, Smooth Surfaces, Long Wheelbase, Never bail), the trick sliders (flip trick speed, no comply, boneless, hippy and off-board jump height) and the values themselves: a short plainly named list, or under EVERYTHING the whole table (the physics tuning's values, one multiplier per curve and graph, and the 314 values of the game's other tuning classes) with groups, "only what I changed", locks, your own saved presets and a preset a map applies every time it loads. One search box finds dials, trick sliders and values on every list; pointing at a named value says what it does. The built-in presets include Skate 3 (and its Hardcore and Easy): Skate 3's numbers for every value this game still shares with it. "Share" copies one of your presets as a line of text and "Import from clipboard" adds one you were sent. Changes apply while you skate. Values the game was never found or seen reading are hidden unless you ask for them. **Reset everything**, above the tabs, puts the game back as it shipped. |
| **PRACTICE** | Game speed and pause, five marker slots per map (save / go / clear), return to the marker after a bail, teleport to coordinates, copy your position (game or Blender axes). |
| **CAMERA** | Move the game's own cameras: distance, height, camera-only height and side for the low camera, the high camera and the on-foot camera, with buttons for SunJay's Low Cam numbers and a low camera that shows more of the skater. The cameras keep their smoothing and stay out of walls. Below it, an experimental camera of your own that replaces the game's (it does not avoid walls). |
| **MAP & HUD** | Speed and air-time HUD, a read-out after every jump (takeoff speed and angle, height, distance, drop, landing speed, spin and flip), telemetry recording to CSV, and whatever the map's author ships for the trainer. |
The HUD, the jump read-out and the controller shortcuts are off until you switch them on (MAP & HUD,
PRACTICE): a player who never opens the trainer sees and feels nothing of it.
Controller, once switched on: hold **LB + RB**, then D-pad **up** saves the marker, **down** goes to it,
**left / right** pick the slot.
Controller: **LB + RB + click the right stick** opens and closes the menu (D-pad or left stick moves, A presses, B goes back);
it can be switched off on PRACTICE. With the marker shortcuts switched on: hold **LB + RB**, then D-pad **up** saves the marker,
**down** goes to it, **left / right** pick the slot.
Everything is also a console command (`~`): `trainer open [tune|practice|map|realistic|fun|everything]`, `trainer status`, `trainer set <id> <value>`,
`trainer find <words>`, `trainer preset apply|remove <name>`, `trainer dial <multiplier> <preset name>`,
Everything is also a console command (`~`): `trainer open [tune|practice|camera|map|realistic|fun|everything]`, `trainer status`, `trainer set <id> <value>`,
`trainer find <words>`, `trainer preset apply|remove|export <name>`, `trainer preset import`, `trainer camera rig low|high|foot distance|height|raise|side <number>`, `trainer dial <multiplier> <preset name>`,
`trainer reset <id>|all|tricks|presets|everything`, `trainer marker save|go|clear [slot]`,
`trainer tp <x> <y> <z>`, `trainer where`, `trainer jumps`, `trainer dump`, `trainer selftest`.
+392 -11
View File
@@ -3,6 +3,7 @@
#include "trainer_jump.h"
#include "trainer_presets.h"
#include "trainer_session.h"
#include "Extension/Multiplayer/Hud/follow_camera.h"
#include "Engine/Core/Json/json.h"
#include "Engine/Core/Log/logging.h"
#include "Engine/Game/Build/addresses.h"
@@ -29,6 +30,7 @@
#include <numbers>
#include <optional>
// Game thread only. See trainer.h for how the menu reaches this.
namespace dingosdk::trainer {
namespace {
@@ -113,6 +115,10 @@ struct State {
MapFile map_file;
int slot{};
bool auto_return{}, pad_shortcuts{}, hud{}, hud_jump{}, logging{};
bool pad_menu{true}; // LB + RB + R3 opens and closes the menu
std::array<RigSetting, camera_rigs> rigs{};
bool camera_on{};
CameraSet camera_board, camera_foot{1.9f, 1.6f, -8.0f, 0.4f, 0.0f, 0.06f};
// The hippy jump's height is set by the game's trick scripts, not by tuning: the trainer
// scales the upward velocity when it sees one start.
float hippy_height{1};
@@ -141,6 +147,8 @@ struct State {
SessionExtras host;
std::array<float, class_field_count> host_classes{};
Boosts boosts; // what is in force this tick
std::string share_text; // the last exported preset, for the menu to put on the clipboard
std::uint64_t share_serial{};
bool extras_due{true}; // the player's own extras are to be published for a session again
bool wipeouts_known{}; // the wipeout count the session started with was read
int open_tab{};
@@ -223,6 +231,50 @@ std::optional<Vec3> read_position(const Json &value) {
}
return result;
}
// ---- the player's own camera -------------------------------------------------------------------
struct CameraPreset {
std::string_view name, note;
bool foot; // it is a framing for walking, not skating
CameraSet set;
};
constexpr CameraPreset camera_presets[]{
{"Low", "Close to the ground behind the board, looking slightly up: the Skate 1 feel.", false, {2.6f, 0.45f, 4.0f, 0.0f, 78.0f, 0.06f}},
{"Low plus", "The low camera raised a little, so more of the skater shows.", false, {2.8f, 0.8f, 0.0f, 0.0f, 72.0f, 0.06f}},
{"Skate 3", "Close chase camera with a wide lens.", false, {2.3f, 0.9f, -2.0f, 0.0f, 80.0f, 0.1f}},
{"High", "Further back and above, for lines and big gaps.", false, {4.8f, 2.6f, -18.0f, 0.0f, 62.0f, 0.1f}},
{"Shoulder", "On foot: over the right shoulder.", true, {1.7f, 1.55f, -6.0f, 0.45f, 62.0f, 0.05f}},
{"Shoulder left", "On foot: over the left shoulder.", true, {1.7f, 1.55f, -6.0f, -0.45f, 62.0f, 0.05f}},
};
float *camera_field(CameraSet &set, std::string_view name) {
return name == "distance" ? &set.distance : name == "height" ? &set.height : name == "pitch" ? &set.pitch
: name == "side" ? &set.side : name == "fov" ? &set.fov : name == "lag" ? &set.lag : nullptr;
}
CameraSet sane_camera(CameraSet set) {
const auto finite = [](float value, float low, float high, float otherwise) { return std::isfinite(value) ? std::clamp(value, low, high) : otherwise; };
set.distance = finite(set.distance, 0.2f, 30.0f, 3.4f);
set.height = finite(set.height, -2.0f, 20.0f, 1.5f);
set.pitch = finite(set.pitch, -85.0f, 85.0f, -9.0f);
set.side = finite(set.side, -5.0f, 5.0f, 0.0f);
set.fov = set.fov < 20.0f ? 0.0f : finite(set.fov, 20.0f, 150.0f, 0.0f);
set.lag = finite(set.lag, 0.0f, 2.0f, 0.08f);
return set;
}
Json write_camera(const CameraSet &set) {
Json row = Json::object();
row["distance"] = set.distance;
row["height"] = set.height;
row["pitch"] = set.pitch;
row["side"] = set.side;
row["fov"] = set.fov;
row["lag"] = set.lag;
return row;
}
CameraSet read_camera(const Json &row, CameraSet set) {
if (!row.is_object()) return set;
for (const auto name : {"distance", "height", "pitch", "side", "fov", "lag"})
if (row.contains(name) && row.at(name).is_number()) *camera_field(set, name) = row.at(name).get<float>();
return sane_camera(set);
}
Json write_position(const Vec3 &p) { return Json::array({Json(p[0]), Json(p[1]), Json(p[2])}); }
void load_store() {
@@ -235,6 +287,7 @@ void load_store() {
const auto &o = json->at("options");
s.auto_return = o.value("auto_return", false);
s.pad_shortcuts = o.value("pad_shortcuts", false);
s.pad_menu = o.value("pad_menu", true);
s.hud = o.value("hud", false);
s.hud_jump = o.value("hud_jump", false);
s.return_delay = std::clamp(o.value("return_delay", 1.5f), 0.0f, 10.0f);
@@ -259,6 +312,25 @@ void load_store() {
if (json->contains("active") && json->at("active").is_array())
for (const auto &name : json->at("active"))
if (name.is_string()) s.active.push_back(name.string());
if (json->contains("camera") && json->at("camera").is_object()) {
const auto &camera = json->at("camera");
s.camera_on = camera.value("on", false);
if (camera.contains("board")) s.camera_board = read_camera(camera.at("board"), s.camera_board);
if (camera.contains("foot")) s.camera_foot = read_camera(camera.at("foot"), s.camera_foot);
if (camera.contains("rigs") && camera.at("rigs").is_object())
for (std::size_t i = 0; i < camera_rigs; ++i) {
const auto *name = i == 0 ? "low" : i == 1 ? "high" : "foot";
if (!camera.at("rigs").contains(name) || !camera.at("rigs").at(name).is_object()) continue;
const auto &row = camera.at("rigs").at(name);
RigSetting rig;
rig.distance = row.value("distance", 1.0f);
rig.height = row.value("height", 0.0f);
rig.raise = row.value("raise", 0.0f);
rig.side = row.value("side", 1.0f);
const auto finite = [](float value, float low, float high, float otherwise) { return std::isfinite(value) ? std::clamp(value, low, high) : otherwise; };
s.rigs[i] = {finite(rig.distance, 0.1f, 10.0f, 1.0f), finite(rig.height, -3.0f, 10.0f, 0.0f), finite(rig.raise, -3.0f, 10.0f, 0.0f), finite(rig.side, -5.0f, 5.0f, 1.0f)};
}
}
if (json->contains("maps") && json->at("maps").is_object())
for (const auto &[level, row] : json->at("maps").items()) {
if (!row.is_object()) continue;
@@ -281,6 +353,7 @@ void save_store() {
Json options = Json::object();
options["auto_return"] = s.auto_return;
options["pad_shortcuts"] = s.pad_shortcuts;
options["pad_menu"] = s.pad_menu;
options["hud"] = s.hud;
options["hud_jump"] = s.hud_jump;
options["return_delay"] = s.return_delay;
@@ -321,6 +394,21 @@ void save_store() {
maps[level] = std::move(row);
}
json["maps"] = std::move(maps);
Json camera = Json::object();
camera["on"] = s.camera_on;
camera["board"] = write_camera(s.camera_board);
camera["foot"] = write_camera(s.camera_foot);
Json rigs = Json::object();
for (std::size_t i = 0; i < camera_rigs; ++i) {
Json row = Json::object();
row["distance"] = s.rigs[i].distance;
row["height"] = s.rigs[i].height;
row["raise"] = s.rigs[i].raise;
row["side"] = s.rigs[i].side;
rigs[i == 0 ? "low" : i == 1 ? "high" : "foot"] = std::move(row);
}
camera["rigs"] = std::move(rigs);
json["camera"] = std::move(camera);
const auto directory = data_directory();
if (directory.empty()) return;
std::error_code error;
@@ -592,6 +680,7 @@ void adopt(const tuning::Values &live) {
changed(false);
}
std::string apply_preset(std::string_view name);
void drive_camera(bool playing);
bool editable(std::string *why = nullptr) {
// A guest's physics are the host's while the session enforces them: the tuning
// (physics_tuning::enforce), and what the host shares beyond it (sync_session). Writing
@@ -726,6 +815,11 @@ bool matches(const std::string &key, std::string_view pattern) {
}
// What each built-in preset's rules reach in the running game: (entry, rule) pairs, worked out
// once per table of entries.
// Whether a preset rule is about this value (`scale`: it is a curve or graph multiplier).
bool rule_matches(const Entry &e, bool scale, const PresetRule &rule) {
if (rule.exact) return !scale && e.key == rule.pattern;
return !e.detail && scale == rule.curves && matches(e.key, rule.pattern);
}
const std::vector<std::vector<std::pair<std::size_t, std::size_t>>> &preset_targets() {
static std::vector<std::vector<std::pair<std::size_t, std::size_t>>> cache;
static const Entry *cached_for{};
@@ -742,7 +836,7 @@ const std::vector<std::vector<std::pair<std::size_t, std::size_t>>> &preset_targ
for (std::size_t i = 0; i < s.entries.size(); ++i) {
const auto &e = s.entries[i];
const bool scale = e.kind == Kind::curve || e.kind == Kind::graph;
if (e.detail || scale != rule.curves || !matches(e.key, rule.pattern)) continue;
if (!rule_matches(e, scale, rule)) continue;
if (e.kind == Kind::flag && rule.multiply) continue;
cache[p].emplace_back(i, r);
}
@@ -780,6 +874,22 @@ std::string apply_dial(std::string_view name, double factor) {
changed();
return std::format("{} x{:.2f}: {} values set{}.", presets[p].name, factor, count, locked ? std::format("; {} locked values left alone", locked) : "");
}
// A preset of the player's own also carries the trick sliders that are off 1, under these keys.
constexpr std::string_view trick_prefix = "trick.";
constexpr std::string_view trick_names[]{"flip_speed", "hippy_height", "nocomply_height", "boneless_height", "offboard_height"};
float *trick_option(std::string_view name) {
auto &s = state();
return name == "flip_speed" ? &s.flip_speed : name == "hippy_height" ? &s.hippy_height : name == "nocomply_height" ? &s.nocomply_height
: name == "boneless_height" ? &s.boneless_height : name == "offboard_height" ? &s.offboard_height : nullptr;
}
// Sets a trick slider named by a preset key; false when the key is not one.
bool set_trick_key(std::string_view key, double value) {
if (!key.starts_with(trick_prefix)) return false;
const auto name = key.substr(trick_prefix.size());
if (auto *option = trick_option(name))
*option = name == "flip_speed" ? std::clamp(static_cast<float>(value), flip_low, flip_high) : std::clamp(static_cast<float>(value), height_low, height_high);
return true;
}
std::string remove_preset(std::string_view name) {
auto &s = state();
const auto wanted = lower(name);
@@ -801,8 +911,10 @@ std::string remove_preset(std::string_view name) {
};
for (const auto &[user_name, values] : s.user) {
if (lower(user_name) != wanted) continue;
for (const auto &saved : values)
if (auto *e = find_entry(saved.first)) put_back(*e);
for (const auto &saved : values) {
if (set_trick_key(saved.first, 1.0)) ++count;
else if (auto *e = find_entry(saved.first)) put_back(*e);
}
removed = user_name;
break;
}
@@ -812,7 +924,7 @@ std::string remove_preset(std::string_view name) {
for (const auto &rule : preset.rules)
for (auto &e : s.entries) {
const bool scale = e.kind == Kind::curve || e.kind == Kind::graph;
if (e.detail || scale != rule.curves || !matches(e.key, rule.pattern)) continue;
if (!rule_matches(e, scale, rule)) continue;
put_back(e);
}
removed = preset.name;
@@ -856,8 +968,10 @@ std::string apply_preset(std::string_view name) {
if (applied.empty())
for (const auto &[user_name, values] : s.user) {
if (lower(user_name) != wanted) continue;
for (const auto &[key, value] : values)
if (auto *e = find_entry(key); e && !is_locked(*e)) { set_entry(*e, value); ++count; }
for (const auto &[key, value] : values) {
if (set_trick_key(key, value)) ++count;
else if (auto *e = find_entry(key); e && !is_locked(*e)) { set_entry(*e, value); ++count; }
}
applied = user_name;
break;
}
@@ -867,7 +981,7 @@ std::string apply_preset(std::string_view name) {
for (const auto &rule : preset.rules)
for (auto &e : s.entries) {
const bool scale = e.kind == Kind::curve || e.kind == Kind::graph;
if (e.detail || scale != rule.curves || !matches(e.key, rule.pattern)) continue;
if (!rule_matches(e, scale, rule)) continue;
if (e.kind == Kind::flag && rule.multiply) continue;
if (is_locked(e)) {
++locked;
@@ -924,6 +1038,7 @@ MapFile read_map_file(const std::string &level) {
}
void enter_map(const std::string &level) {
auto &s = state();
forget_camera_rigs();
s.map = level;
s.map_file = level.empty() ? MapFile{} : read_map_file(level);
s.motion = {};
@@ -1313,6 +1428,7 @@ void build_view() {
const auto friendly = essential_name(e.key, &rank, &modes);
next->rows.push_back({e.id, e.label, e.group, e.kind, e.value, e.stock, e.touched, e.frozen, e.detail, std::string(friendly), rank, modes,
e.used});
next->rows.back().help = essential_help(e.key);
if (e.touched) ++next->touched;
if (std::ranges::find(next->groups, e.group) == next->groups.end()) next->groups.push_back(e.group);
}
@@ -1359,6 +1475,7 @@ void build_view() {
next->boneless_height = s.boneless_height;
next->return_delay = s.return_delay;
next->pad_shortcuts = s.pad_shortcuts;
next->pad_menu = s.pad_menu;
next->hud = s.hud;
next->hud_jump = s.hud_jump;
next->logging = s.logging;
@@ -1366,7 +1483,18 @@ void build_view() {
next->map_note = s.map_file.note;
next->map_preset = s.map_file.preset.empty() ? std::string{} : s.map_file.preset_name.empty() ? "Map preset" : s.map_file.preset_name;
next->spots = s.map_file.spots;
next->rigs = s.rigs;
next->rigs_found = camera_rigs_summary();
next->camera_on = s.camera_on;
next->camera_board = s.camera_board;
next->camera_foot = s.camera_foot;
if (const auto profile = multiplayer::gameplay_camera_profile(); profile.samples >= 60)
next->camera_game = std::format("{:.1f} m behind, {:.1f} m up, tilt {:.0f} degrees, FOV {:.0f}", profile.distance, profile.height, profile.pitch, profile.fov);
next->open_serial = s.open_serial;
next->stock = !s.camera_on && s.rigs == std::array<RigSetting, camera_rigs>{} && !next->touched && s.active.empty() && std::ranges::none_of(s.entries, &Entry::frozen) &&
std::ranges::all_of(trick_names, [](std::string_view name) { return *trick_option(name) == 1.0f; });
next->share_text = s.share_text;
next->share_serial = s.share_serial;
next->open_tab = s.open_tab;
if (const auto found = s.maps.find(s.map); found != s.maps.end()) {
next->markers = found->second.markers;
@@ -1374,6 +1502,132 @@ void build_view() {
}
publish(std::move(next));
}
// ---- sharing presets ---------------------------------------------------------------------------
// One line of text, short enough to paste in a chat: "RST1:" and the preset's JSON in base64.
constexpr std::string_view share_tag = "RST1:";
constexpr std::string_view base64_digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64(std::string_view bytes) {
std::string out;
for (std::size_t i = 0; i < bytes.size(); i += 3) {
std::uint32_t chunk{};
const auto count = std::min<std::size_t>(3, bytes.size() - i);
for (std::size_t k = 0; k < 3; ++k) chunk = chunk << 8 | (k < count ? static_cast<unsigned char>(bytes[i + k]) : 0u);
for (std::size_t k = 0; k < 4; ++k) out += k <= count ? base64_digits[chunk >> (18 - 6 * k) & 63] : '=';
}
return out;
}
std::optional<std::string> unbase64(std::string_view text) {
std::string out;
std::uint32_t chunk{};
int bits{};
for (const char c : text) {
if (c == '=' || std::isspace(static_cast<unsigned char>(c))) continue;
const auto digit = base64_digits.find(c);
if (digit == std::string_view::npos) return std::nullopt;
chunk = chunk << 6 | static_cast<std::uint32_t>(digit);
if ((bits += 6) >= 8) out += static_cast<char>(chunk >> (bits -= 8) & 0xff);
}
return out;
}
std::filesystem::path shared_directory() {
const auto directory = data_directory();
return directory.empty() ? directory : directory / L"shared";
}
// File names from preset names: letters, digits, space, '-' and '_' only.
std::string file_stem(std::string_view name) {
std::string stem;
for (const char c : name) stem += std::isalnum(static_cast<unsigned char>(c)) || c == ' ' || c == '-' || c == '_' ? c : '_';
return stem.empty() ? "preset" : stem;
}
// `preset export <name>`: one of the player's presets, or "current" for what is changed right now.
std::string export_preset(const std::string &name) {
auto &s = state();
Json values = Json::object();
std::string title;
if (lower(name) == "current") {
for (const auto &e : s.entries)
if (e.touched) values[e.key] = e.value;
for (const auto trick : trick_names)
if (const auto *option = trick_option(trick); *option != 1.0f) values[std::string(trick_prefix) + std::string(trick)] = *option;
title = "My setup";
} else {
for (const auto &[user_name, saved] : s.user)
if (lower(user_name) == lower(name)) {
for (const auto &[key, value] : saved) values[key] = value;
title = user_name;
}
if (title.empty()) return "error: no preset of yours is called \"" + name + "\". Use \"current\" for what is changed right now.";
}
if (values.empty()) return "error: nothing is changed, so there is nothing to share.";
const auto count = values.size();
Json json = Json::object();
json["reskate_trainer"] = 1;
json["name"] = title;
json["values"] = std::move(values);
s.share_text = std::string(share_tag) + base64(json.dump());
++s.share_serial;
std::string where;
if (const auto directory = shared_directory(); !directory.empty()) {
std::error_code error;
std::filesystem::create_directories(directory, error);
std::ofstream file(directory / (file_stem(title) + ".json"), std::ios::binary | std::ios::trunc);
file << json.dump(2);
if (file) where = std::format(" Also saved as shared\\{}.json.", file_stem(title));
}
changed();
return std::format("\"{}\" copied to the clipboard as one line ({} values): paste it anywhere.{}", title, count, where);
}
// `preset import [file]`: the text the menu put in shared\clipboard.txt, or a file in shared\.
// Either the one-line form or the JSON itself. The preset joins the player's own; nothing is applied.
std::string import_preset(const std::string &file_name) {
auto &s = state();
const auto directory = shared_directory();
if (directory.empty()) return "error: the trainer's folder could not be found.";
const auto path = directory / (file_name.empty() ? "clipboard.txt" : file_stem(std::filesystem::path(file_name).stem().string()) + ".json");
std::string text;
{
std::ifstream file(path, std::ios::binary);
if (!file) return file_name.empty() ? "error: there is nothing to import." : "error: shared\\" + path.filename().string() + " does not exist.";
text.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
}
if (text.size() > 256 * 1024) return "error: that is too long to be a preset.";
if (const auto tag = text.find(share_tag); tag != std::string::npos) {
const auto start = tag + share_tag.size();
const auto end = text.find_first_of(" \t\r\n`\"'", start);
const auto decoded = unbase64(std::string_view(text).substr(start, end == std::string::npos ? end : end - start));
if (!decoded) return "error: that is not a whole preset line: copy all of it, from RST1: to the end.";
text = *decoded;
}
try {
const auto json = Json::parse(text);
if (!json.is_object() || !json.contains("values") || !json.at("values").is_object()) return "error: that is not a ReSkate Trainer preset.";
std::map<std::string, double, std::less<>> values;
std::size_t unknown{};
for (const auto &[key, value] : json.at("values").items()) {
if (!value.is_number() || values.size() >= 512) continue;
const auto id = lower(key);
const bool trick = id.starts_with(trick_prefix) && trick_option(std::string_view(id).substr(trick_prefix.size()));
if (!trick && !find_entry(id)) ++unknown; // kept: another build of the game may have it
values[id] = value.get<double>();
}
if (values.empty()) return "error: that preset holds no values.";
auto name = json.value("name", "Imported");
if (name.empty() || name.size() > 40) name = "Imported";
const auto taken = [&](const std::string &candidate) {
if (lower(candidate) == "stock" || lower(candidate) == "map" || s.user.contains(candidate)) return true;
return std::ranges::any_of(builtin_presets(), [&](const auto &preset) { return lower(preset.name) == lower(candidate); });
};
auto unique = name;
for (int n = 2; taken(unique); ++n) unique = std::format("{} {}", name, n);
const auto count = values.size();
s.user[unique] = std::move(values);
changed();
return std::format("Imported \"{}\" ({} values{}). Turn it on under Your presets.", unique, count,
unknown ? std::format(", {} of them unknown to this version", unknown) : "");
} catch (...) {
return "error: that is not a ReSkate Trainer preset.";
}
}
bool flag(const std::string &text, bool &out) {
const auto value = lower(text);
if (value == "1" || value == "on" || value == "true") { out = true; return true; }
@@ -1397,6 +1651,7 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s
s.base = base;
sync_session();
s.boosts = boosts_in_force();
drive_camera(playing);
if (const auto key = lower(playing ? level : std::string{}); key != s.map) enter_map(key);
if (playing) {
observe(base, client, now);
@@ -1451,6 +1706,111 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s
}
namespace {
// Every tick: the framing for what the skater is doing, or none.
void drive_camera(bool playing) {
auto &s = state();
multiplayer::CustomCamera camera;
const auto &set = s.telemetry.physics_state == addr::no_bail::offboard_physics_state ? s.camera_foot : s.camera_board;
camera.on = s.camera_on && playing && s.telemetry.skater;
camera.position = s.telemetry.position;
camera.heading = s.telemetry.heading;
camera.distance = set.distance;
camera.height = set.height;
camera.pitch = set.pitch;
camera.side = set.side;
camera.fov = set.fov;
camera.lag = set.lag;
multiplayer::set_custom_camera(camera);
want_camera_rigs(s.rigs);
if (playing && s.telemetry.skater) (void)apply_camera_rigs(GetTickCount64());
}
constexpr std::string_view rig_names[]{"low", "high", "foot"};
float *rig_field(RigSetting &rig, std::string_view name) {
return name == "distance" ? &rig.distance : name == "height" ? &rig.height : name == "raise" ? &rig.raise : name == "side" ? &rig.side : nullptr;
}
RigSetting sane_rig(RigSetting rig) {
const auto finite = [](float value, float low, float high, float otherwise) { return std::isfinite(value) ? std::clamp(value, low, high) : otherwise; };
rig.distance = finite(rig.distance, 0.1f, 10.0f, 1.0f);
rig.height = finite(rig.height, -3.0f, 10.0f, 0.0f);
rig.raise = finite(rig.raise, -3.0f, 10.0f, 0.0f);
rig.side = finite(rig.side, -5.0f, 5.0f, 1.0f);
return rig;
}
// The game's own cameras: `trainer camera rig low|high|foot <field> <number>`, or a preset for all three.
std::string rig_command(const std::vector<std::string> &a) {
auto &s = state();
const auto arg = [&](std::size_t i) { return i < a.size() ? lower(a[i]) : std::string{}; };
if (arg(1) == "stock") {
s.rigs = {};
changed();
return "The game's cameras are where the game puts them.";
}
if (arg(1) == "sunjay") {
// SunJay's Low Cam (thunderstore.io/c/reskate/p/SunJayTeam/SunJays_Low_Cam): both on-board
// cameras 1.2 m back, 0.4 m lower, turning about a point 0.4 m lower.
s.rigs[0] = {1.2f / 1.9f, -0.4f, -0.4f, 1.0f / 1.35f};
s.rigs[1] = {1.2f / 3.0f, -0.4f, -0.4f, 1.0f};
changed();
return "Both on-board cameras set like SunJay's Low Cam.";
}
if (arg(1) == "low+") {
s.rigs[0] = {0.8f, -0.25f, -0.15f, 0.85f};
changed();
return "Low camera: lower and closer, with more of the skater in view.";
}
const auto found = std::ranges::find(rig_names, arg(1));
auto *field = found != std::end(rig_names) ? rig_field(s.rigs[static_cast<std::size_t>(found - std::begin(rig_names))], arg(2)) : nullptr;
const auto value = number(arg(3));
if (!field || !value) return "error: usage: trainer camera rig low|high|foot distance|height|raise|side <number>, or camera rig stock|sunjay|low+";
*field = static_cast<float>(*value);
auto &rig = s.rigs[static_cast<std::size_t>(found - std::begin(rig_names))];
rig = sane_rig(rig);
changed();
return std::format("Camera {} {} = {:.3g}. {}", arg(1), arg(2), *field, camera_rigs_summary());
}
// trainer camera on|off | game | preset <name> | set board|foot <field> <number>
std::string camera_command(const std::vector<std::string> &a) {
auto &s = state();
const auto arg = [&](std::size_t i) { return i < a.size() ? lower(a[i]) : std::string{}; };
const auto action = arg(0);
if (action == "rig") return rig_command(a);
bool on{};
if (flag(action, on)) {
s.camera_on = on;
changed();
return on ? "Your camera is on. It does not avoid walls; switch it off to get the game's back." : "The game's own camera is back.";
}
if (action == "game") {
const auto profile = multiplayer::gameplay_camera_profile();
if (profile.samples < 60) return "error: skate for a few seconds with the game's own camera first, so it can be measured.";
s.camera_board = sane_camera({profile.distance, profile.height, profile.pitch, 0.0f, 0.0f, 0.1f});
changed();
return std::format("On board: the game's framing, {:.1f} m behind, {:.1f} m up, tilt {:.0f} degrees.", profile.distance, profile.height, profile.pitch);
}
if (action == "preset") {
std::string name;
for (std::size_t i = 1; i < a.size(); ++i) name += (i > 1 ? " " : "") + lower(a[i]);
for (const auto &preset : camera_presets)
if (lower(preset.name) == name) {
(preset.foot ? s.camera_foot : s.camera_board) = preset.set;
s.camera_on = true;
changed();
return std::format("Camera: {} ({}).", preset.name, preset.foot ? "on foot" : "on board");
}
return "error: no camera preset is called \"" + name + "\".";
}
if (action == "set") {
auto *set = arg(1) == "board" ? &s.camera_board : arg(1) == "foot" ? &s.camera_foot : nullptr;
auto *field = set ? camera_field(*set, arg(2)) : nullptr;
const auto value = number(arg(3));
if (!field || !value) return "error: usage: trainer camera set board|foot distance|height|pitch|side|fov|lag <number>";
*field = static_cast<float>(*value);
*set = sane_camera(*set);
changed();
return std::format("Camera {} {} = {:.3g}", arg(1), arg(2), *field);
}
return "error: usage: trainer camera on|off | game | preset <name> | set board|foot <field> <number>";
}
std::string run(std::string_view verb, const std::vector<std::string> &a) {
auto &s = state();
const auto v = lower(verb);
@@ -1496,6 +1856,8 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
}
s.saved.clear();
s.active.clear();
s.camera_on = false;
s.rigs = {};
s.hippy_height = s.nocomply_height = s.boneless_height = s.offboard_height = s.flip_speed = 1.0f;
changed();
return std::format("Everything is the game's own again: {} values put back, locks cleared, presets off, trick sliders at 1.", count);
@@ -1539,7 +1901,9 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
const auto action = lower(arg(0));
std::string name;
for (std::size_t i = 1; i < a.size(); ++i) name += (i > 1 ? " " : "") + a[i];
if (name.empty()) return "error: usage: trainer preset apply|remove|save|delete <name>";
if (action == "import") return import_preset(name);
if (name.empty()) return "error: usage: trainer preset apply|remove|save|delete|export <name>, or preset import [file]";
if (action == "export") return export_preset(name);
if (action == "remove") {
std::string why;
if (!editable(&why)) return "error: " + why;
@@ -1558,6 +1922,8 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
std::map<std::string, double, std::less<>> values;
for (const auto &e : s.entries)
if (e.touched) values[e.key] = e.value;
for (const auto trick : trick_names)
if (const auto *option = trick_option(trick); *option != 1.0f) values[std::string(trick_prefix) + std::string(trick)] = *option;
if (values.empty()) return "error: nothing is changed, so there is nothing to save.";
const auto count = values.size();
s.user[name] = std::move(values);
@@ -1572,6 +1938,7 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
}
return "error: usage: trainer preset apply|save|delete <name>";
}
if (v == "camera") return camera_command(a);
if (v == "slot") {
const auto slot = slot_argument(a, 0);
if (!slot) return std::format("error: slots are 1 to {}.", marker_slots);
@@ -1633,11 +2000,12 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
if (!value) return "error: return_delay needs seconds.";
s.return_delay = std::clamp(static_cast<float>(*value), 0.0f, 10.0f);
} else if (!flag(arg(1), on)) {
return "error: usage: trainer option hud|hud_jump|auto_return|pad|log 0|1";
return "error: usage: trainer option hud|hud_jump|auto_return|pad|pad_menu|log 0|1";
} else if (name == "hud") s.hud = on;
else if (name == "hud_jump") s.hud_jump = on;
else if (name == "auto_return") s.auto_return = on;
else if (name == "pad") s.pad_shortcuts = on;
else if (name == "pad_menu") s.pad_menu = on;
else if (name == "log") {
set_logging(on);
if (on && !s.logging) return "error: the telemetry file could not be created.";
@@ -1663,9 +2031,9 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
if (v == "open") {
const auto tab = lower(arg(0));
// The last three are the Tune tab on one of its lists.
const std::array<std::string_view, 7> tabs{"tune", "presets", "practice", "map", "realistic", "fun", "everything"};
const std::array<std::string_view, 8> tabs{"tune", "presets", "practice", "map", "realistic", "fun", "everything", "camera"};
const auto found = std::ranges::find(tabs, tab);
if (!tab.empty() && found == tabs.end()) return "error: usage: trainer open [tune|presets|practice|map|realistic|fun|everything]";
if (!tab.empty() && found == tabs.end()) return "error: usage: trainer open [tune|presets|practice|camera|map|realistic|fun|everything]";
s.open_tab = tab.empty() ? 1 : static_cast<int>(found - tabs.begin());
++s.open_serial;
s.view_due = true;
@@ -1719,6 +2087,19 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
}
} // namespace
bool stage_import(std::string_view text) noexcept {
try {
const auto directory = shared_directory();
if (directory.empty() || text.empty() || text.size() > 256 * 1024) return false;
std::error_code error;
std::filesystem::create_directories(directory, error);
std::ofstream file(directory / "clipboard.txt", std::ios::binary | std::ios::trunc);
file << text;
return static_cast<bool>(file);
} catch (...) {
return false;
}
}
std::string command(std::string_view verb, const std::vector<std::string> &arguments) {
auto result = run(verb, arguments);
// The menu shows the last answer, except for the ones a dragged slider sends every frame.
+22
View File
@@ -4,6 +4,7 @@
// The game thread owns the trainer's state (trainer.cpp). Every change is a `trainer ...`
// console command, so the menu page (presentation thread) only queues commands and reads
// the two snapshots below; nothing here touches the game from the UI.
#include "trainer_camera.h"
#include <array>
#include <cstdint>
#include <memory>
@@ -17,6 +18,8 @@ namespace dingosdk::trainer {
enum class Kind : std::uint8_t { real, integer, flag, curve, graph };
// The Tune tab's two short lists: which of them a value is on (Row::modes).
inline constexpr std::uint8_t mode_realistic = 1, mode_fun = 2;
// On a short list although the game was not seen reading it: named for players to try out.
inline constexpr std::uint8_t mode_trial = 4;
// One editable value of Gameplay/SkatePhysicsTuning. `id` is the name the game's data gives
// it ("PhysicsMode.JumpMaxHeight").
@@ -30,6 +33,7 @@ struct Row {
int rank{}; // its place on that list, from 1; 0: not on it
std::uint8_t modes{}; // which short lists it is on (trainer_presets.h: mode_realistic, mode_fun)
bool used{}; // the game's code was found to read it
std::string help; // what the value does, in a sentence; empty for most
};
struct PresetRow {
std::string name, note;
@@ -52,6 +56,11 @@ struct Spot {
inline constexpr std::size_t marker_slots = 5;
// Changes rarely: rebuilt when a command or a level load changes something.
// The Camera tab: one framing on the board and one on foot (Extension/Multiplayer/Hud/follow_camera.h).
struct CameraSet {
float distance{3.4f}, height{1.5f}, pitch{-9.0f}, side{}, fov{}, lag{0.08f};
bool operator==(const CameraSet &) const = default;
};
struct View {
std::uint64_t revision{};
bool ready{}; // the game's tuning is read and the running game's copy was found
@@ -63,12 +72,15 @@ struct View {
std::vector<std::string> groups;
std::vector<PresetRow> presets;
std::size_t touched{};
// Nothing the trainer does is in force: no changed value, lock, preset or trick slider.
bool stock{true};
// Practice
int slot{};
std::array<Marker, marker_slots> markers{};
bool auto_return{};
float return_delay{1.5f};
bool pad_shortcuts{};
bool pad_menu{true};
// Height of the hippy jump, which the game scripts instead of tuning (x of its own height).
float hippy_height{1};
// The same for the no comply and the boneless.
@@ -81,7 +93,14 @@ struct View {
std::string map, map_note, map_preset, profile_preset;
std::vector<Spot> spots;
// `trainer open <tab>`: each new serial opens the menu on the trainer page at that tab.
bool camera_on{};
CameraSet camera_board, camera_foot{1.9f, 1.6f, -8.0f, 0.4f, 0.0f, 0.06f};
std::string camera_game; // how the game itself frames the skater, measured
std::array<RigSetting, camera_rigs> rigs{}; // the game's own cameras, moved
std::string rigs_found; // what of their data was found
std::uint64_t open_serial{};
std::string share_text; // `preset export`: the line to put on the clipboard
std::uint64_t share_serial{};
int open_tab{};
};
@@ -117,6 +136,9 @@ void publish(const Telemetry &) noexcept;
// for a player who has a use for it: this is one. Any thread; the game thread takes the note.
void note_class_list_shown() noexcept;
bool take_class_list_shown() noexcept;
// The menu hands over text to import (a pasted preset line): written where `preset import`
// reads it. Any thread; touches no trainer state.
bool stage_import(std::string_view text) noexcept;
// Game thread (trainer.cpp).
// Each client tick; `playing` while a local skater can exist, `level` the loaded level asset.
+227
View File
@@ -0,0 +1,227 @@
#include "trainer_camera.h"
#include "Engine/Core/Log/logging.h"
#include <windows.h>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstring>
#include <format>
#include <mutex>
#include <thread>
#include <vector>
namespace dingosdk::trainer {
namespace {
// Game build 25414733. A camera framing set (CameraFramingSetAsset) holds its framings twice:
// as separate objects, which the running camera does not read, and compiled into its root as an
// array sorted by pitch, which it reads every frame (measured: writing the array moves the
// camera at once). One array element is 0x50 bytes: +0x00 offset x y z (right, up, back),
// +0x40 the camera pitch in radians that the framing is for.
// The root itself starts with the word below and holds, at +0x1c, the point the framings turn
// about (x y z), with its pitch limits 89 and 90 a little further on.
constexpr std::size_t element_size = 0x50, element_pitch = 0x40;
constexpr std::uint32_t root_word = 0x0006b100;
constexpr std::size_t pivot_offset = 0x1c, root_span = 0x90;
constexpr float degrees = 0.01745329252f;
struct Element {
float x, y, z, pitch; // pitch in degrees
};
struct Set {
CameraRig rig;
std::size_t count;
Element elements[3];
};
constexpr Set sets[]{
{CameraRig::low, 3, {{-1.35f, -0.1f, 2.0f, -45.0f}, {-1.35f, 0.0f, 1.9f, 0.0f}, {-1.35f, 0.2f, 2.0f, 45.0f}}}, // also the mega park's
{CameraRig::high, 1, {{-1.0f, 0.0f, 3.0f, -12.0f}}},
{CameraRig::high, 3, {{-1.0f, 0.0f, 3.0f, -75.0f}, {-1.0f, 0.0f, 3.0f, 0.0f}, {-1.0f, 0.0f, 3.0f, 75.0f}}}, // mega park
{CameraRig::foot, 3, {{0.0f, 0.75f, 2.5f, -75.0f}, {0.0f, 0.0f, 3.0f, 0.0f}, {0.0f, 0.45f, 1.25f, 75.0f}}},
};
struct Pivot {
CameraRig rig;
float y;
};
constexpr Pivot pivots[]{{CameraRig::low, 0.85f}, {CameraRig::high, 1.0f}, {CameraRig::high, 1.2f}};
struct Found {
std::uintptr_t address{}; // of the three numbers
CameraRig rig{};
bool pivot{};
std::array<float, 3> stock{};
float pitch{}; // radians: what a framing must still hold beside its numbers
};
struct State {
std::mutex mutex;
std::array<RigSetting, camera_rigs> wanted{};
std::vector<Found> found;
std::atomic<bool> searching{};
std::uint64_t next_search{};
int searches{};
};
State &state() {
static auto *value = new State;
return *value;
}
bool about(float a, float b) { return std::abs(a - b) < 2e-4f; }
template <class T> bool peek(std::uintptr_t address, T &value) noexcept {
__try {
std::memcpy(&value, reinterpret_cast<const void *>(address), sizeof(T));
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
bool put(std::uintptr_t address, float value) noexcept {
__try {
*reinterpret_cast<volatile float *>(address) = value;
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
bool element_at(const float *f, const Element &e) noexcept {
return about(f[0], e.x) && about(f[1], e.y) && about(f[2], e.z) && about(f[element_pitch / 4], e.pitch * degrees);
}
// One hit: what is at word `i` of a region, if anything known. Plain data only (it runs under __try).
struct Hit {
const Set *set{};
const Pivot *pivot{};
};
Hit classify(const std::uint32_t *words, const float *floats, std::size_t i) noexcept {
const auto first = words[i];
// The first number of every array is -1.35, -1 or 0; of a root, its word.
if (first == 0xbfaccccd || first == 0xbf800000 || first == 0) {
if (floats[i + 2] < 1.0f || floats[i + 2] > 3.5f) return {}; // no array's first distance
for (const auto &set : sets) {
bool all = true;
for (std::size_t k = 0; k < set.count && all; ++k) all = element_at(floats + i + k * element_size / 4, set.elements[k]);
if (all) return {&set, nullptr};
}
} else if (first == root_word && floats[i + pivot_offset / 4] == 0.0f && floats[i + pivot_offset / 4 + 2] == 0.0f) {
for (const auto &known : pivots) {
if (!about(floats[i + pivot_offset / 4 + 1], known.y)) continue;
for (std::size_t k = pivot_offset / 4 + 3; k + 1 < root_span / 4; ++k)
if (floats[i + k] == 89.0f && floats[i + k + 1] == 90.0f) return {nullptr, &known};
return {};
}
}
return {};
}
// Fills `hits` (word index and what it is) for one region; returns how many.
std::size_t search_region(std::uintptr_t start, std::size_t size, std::size_t *indices, Hit *hits, std::size_t capacity) noexcept {
std::size_t count{};
__try {
const auto *words = reinterpret_cast<const std::uint32_t *>(start);
const auto *floats = reinterpret_cast<const float *>(start);
const std::size_t total = size / 4, tail = 3 * element_size / 4 + root_span / 4;
for (std::size_t i = 0; i + tail < total && count < capacity; ++i) {
const auto first = words[i];
if (first != 0xbfaccccd && first != 0xbf800000 && first != 0 && first != root_word) continue;
if (const auto hit = classify(words, floats, i); hit.set || hit.pivot) {
indices[count] = i;
hits[count++] = hit;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {}
return count;
}
void search() {
auto &s = state();
std::vector<Found> fresh;
const auto started = GetTickCount64();
MEMORY_BASIC_INFORMATION info{};
std::array<std::size_t, 64> indices{};
std::array<Hit, 64> hits{};
for (std::uintptr_t at = 0x10000; at < 0x7fffffff0000ull && VirtualQuery(reinterpret_cast<void *>(at), &info, sizeof(info)) == sizeof(info);
at = reinterpret_cast<std::uintptr_t>(info.BaseAddress) + info.RegionSize) {
if (info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE || info.Type == MEM_IMAGE) continue;
const auto start = reinterpret_cast<std::uintptr_t>(info.BaseAddress);
const auto count = search_region(start, info.RegionSize, indices.data(), hits.data(), hits.size());
for (std::size_t h = 0; h < count && fresh.size() < 256; ++h) {
const auto address = start + indices[h] * 4;
if (const auto *set = hits[h].set)
for (std::size_t k = 0; k < set->count; ++k)
fresh.push_back({address + k * element_size, set->rig, false, {set->elements[k].x, set->elements[k].y, set->elements[k].z}, set->elements[k].pitch * degrees});
else if (const auto *pivot = hits[h].pivot)
fresh.push_back({address + pivot_offset, pivot->rig, true, {0.0f, pivot->y, 0.0f}, 0.0f});
}
}
{
std::lock_guard lock(s.mutex);
// What was already written no longer holds the game's numbers: it stays known.
for (const auto &old : s.found)
if (std::ranges::find(fresh, old.address, &Found::address) == fresh.end()) fresh.push_back(old);
s.found = std::move(fresh);
}
logging::write(logging::Level::info, logging::Channel::skater,
std::format("Trainer: camera framing data found: {} ({} ms).", camera_rigs_summary(), GetTickCount64() - started));
s.searching.store(false, std::memory_order_release);
}
} // namespace
void want_camera_rigs(const std::array<RigSetting, camera_rigs> &settings) noexcept {
auto &s = state();
std::lock_guard lock(s.mutex);
s.wanted = settings;
}
void forget_camera_rigs() noexcept {
auto &s = state();
std::lock_guard lock(s.mutex);
s.found.clear();
s.searches = 0;
s.next_search = 0;
}
std::size_t apply_camera_rigs(std::uint64_t now) noexcept {
auto &s = state();
std::size_t written{};
try {
std::lock_guard lock(s.mutex);
const bool any = std::ranges::any_of(s.wanted, [](const RigSetting &r) { return r != RigSetting{}; });
if (s.found.empty()) {
// The game's own numbers are in place until something else is wanted: only then look.
if (any && !s.searching.load(std::memory_order_acquire) && now >= s.next_search && s.searches < 4) {
++s.searches;
s.next_search = now + 15000;
s.searching.store(true, std::memory_order_release);
std::thread(search).detach();
}
return 0;
}
const auto finite = [](float value, float low, float high, float otherwise) { return std::isfinite(value) ? std::clamp(value, low, high) : otherwise; };
for (const auto &found : s.found) {
// The level may have been unloaded under us: write only into what is still a framing.
std::uint32_t word{};
float pitch{};
if (found.pivot ? !peek(found.address - pivot_offset, word) || word != root_word
: !peek(found.address + element_pitch, pitch) || !about(pitch, found.pitch))
continue;
const auto &want = s.wanted[static_cast<std::size_t>(found.rig)];
const bool foot = found.rig == CameraRig::foot;
const float distance = finite(want.distance, 0.1f, 10.0f, 1.0f), side = finite(want.side, -5.0f, 5.0f, 1.0f);
const float height = finite(want.height, -3.0f, 10.0f, 0.0f), raise = finite(want.raise, -3.0f, 10.0f, 0.0f);
std::array<float, 3> target = found.stock;
if (found.pivot) target[1] += height;
else {
target[0] = foot ? side - 1.0f : target[0] * side; // on foot the game centres the camera: metres to the right instead
target[1] += raise + (foot ? height : 0.0f); // the on-foot camera turns about the skater's root: nothing to move
target[2] *= distance;
}
for (std::size_t i = 0; i < 3; ++i) {
float current{};
if (peek(found.address + i * 4, current) && current != target[i] && put(found.address + i * 4, target[i])) ++written;
}
}
} catch (...) {}
return written;
}
std::string camera_rigs_summary() {
auto &s = state();
std::array<std::size_t, camera_rigs> framings{}, roots{};
{
std::unique_lock lock(s.mutex, std::try_to_lock); // also called with the lock held
if (lock.owns_lock())
for (const auto &found : s.found) ++(found.pivot ? roots : framings)[static_cast<std::size_t>(found.rig)];
}
return std::format("low camera {} framings and {} pivots, high camera {} and {}, on foot {} framings", framings[0], roots[0], framings[1], roots[1],
framings[2]);
}
} // namespace dingosdk::trainer
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// The game's own follow cameras, moved: where each one sits is a handful of numbers in the
// game's camera framing data (the offsets SunJay's Low Cam mod changes on disk). The trainer
// finds the loaded copies and writes the player's numbers into them, so the game's camera keeps
// its smoothing and wall avoidance and simply sits somewhere else.
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
namespace dingosdk::trainer {
// The three cameras a player sees: the low and the high one on the board, and the one on foot.
enum class CameraRig : std::uint8_t { low, high, foot, count };
inline constexpr std::size_t camera_rigs = static_cast<std::size_t>(CameraRig::count);
struct RigSetting {
float distance{1}; // x the game's own distance behind the skater
float height{}; // metres added to the point the camera turns about (camera and aim together)
float raise{}; // metres added to the camera alone: it looks further down, or up
float side{1}; // x the game's own sideways offset
bool operator==(const RigSetting &) const = default;
};
// What each camera should be. Any thread.
void want_camera_rigs(const std::array<RigSetting, camera_rigs> &settings) noexcept;
// Game thread, every tick while a level is loaded: starts the search for the framing data when
// something is wanted and none is known, and writes what is wanted. Returns the numbers written.
std::size_t apply_camera_rigs(std::uint64_t now) noexcept;
// A level change frees the framing data: forget it.
void forget_camera_rigs() noexcept;
// "low 6, high 8, foot 6 framings found" for the menu and the log.
std::string camera_rigs_summary();
} // namespace dingosdk::trainer
+2 -1
View File
@@ -17,7 +17,8 @@ void register_trainer_commands(Commands &registry) {
{"freeze", "Keep a value through presets and resets", "<value id> 0|1", true},
{"reset", "Put a value (or all) back to stock", "<value id>|all", false},
{"find", "List tuning values whose id contains the words", "<words>", false},
{"preset", "Apply, remove, save or delete a preset", "apply|remove|save|delete <name>", false},
{"preset", "Apply, remove, save or delete a preset", "apply|remove|save|delete|export <name> | import [file]", false},
{"camera", "Your own follow camera: distance, height, tilt, shoulder, FOV", "on|off | game | preset <name> | set board|foot <field> <number>", false},
{"dial", "Turn a built-in preset up or down: 1 is the game's own", "<multiplier> <preset name>", false},
{"slot", "Select a marker slot", "<1-5>", false},
{"marker", "Save, go to or clear a marker", "save|go|clear [slot]", false},
+3 -1
View File
@@ -51,7 +51,9 @@ void set_trajectory_hook(std::uintptr_t trajectory, float *parameters) {
watch.previous_seconds >= no_comply_crouch)
trick = Trick::no_comply;
const float up = parameters[velocity_y];
if (trick != Trick::none && std::isfinite(up) && up > 0.5f) {
// Any launch that still goes up: riding downhill takes most of the upward speed away, and
// those launches were being left alone.
if (trick != Trick::none && std::isfinite(up) && up > 0.05f) {
const float factor = (trick == Trick::boneless ? h.boneless : h.no_comply).load(std::memory_order_relaxed);
if (factor != 1 && up * factor < 200.0f) parameters[velocity_y] = up * factor;
std::uint32_t bits[2];
+165 -21
View File
@@ -33,9 +33,11 @@ struct Page {
float hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1};
bool hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{};
std::uint64_t open_serial{}; // the last `trainer open` acted on
std::uint64_t share_serial{}; // the last `preset export` put on the clipboard
bool share_seen{};
bool show_page{};
};
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view);
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words);
Page &page() {
static auto *value = new Page;
return *value;
@@ -70,19 +72,19 @@ void filter_rows(Page &p, const trainer::View &view) {
p.hidden_unused = 0;
const std::string *group = p.mode == 2 && p.group > 1 && static_cast<std::size_t>(p.group) <= view.groups.size() + 1
? &view.groups[static_cast<std::size_t>(p.group) - 2] : nullptr;
const bool essentials = p.mode < 2;
const bool essentials = p.mode < 2 && words.empty(); // searching looks through every value, whatever the list
const std::uint8_t list = p.mode == 0 ? trainer::mode_realistic : trainer::mode_fun;
for (std::size_t i = 0; i < view.rows.size(); ++i) {
const auto &row = view.rows[i];
if (essentials) {
// Only what the game was found or seen to read: a short list has no room for maybes.
if (!row.friendly.empty() && (row.modes & list) && row.used) p.shown.push_back(i);
if (!row.friendly.empty() && (row.modes & list) && (row.used || (row.modes & trainer::mode_trial))) p.shown.push_back(i);
continue;
}
if (row.detail && !p.graph_points && !row.touched) continue;
if (p.only_changed && !row.touched && !row.frozen) continue;
// Searching looks through every group.
if (words.empty() || p.mode < 2 ? (group && row.group != *group) : !contains_words(lower(row.id + " " + row.label + " " + row.friendly), words))
if (words.empty() ? (group && row.group != *group) : !contains_words(lower(row.id + " " + row.label + " " + row.friendly), words))
continue;
// A value nothing in the game reads would be a slider that does nothing.
if (!row.used && !p.show_unused && !row.touched && !row.frozen) {
@@ -109,7 +111,7 @@ void value_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tra
ImGui::TextUnformatted(row.used ? label.c_str() : (label + " (no use found)").c_str());
if (tinted) ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s\nStock: %s%s", row.id.c_str(), number(row.stock).c_str(),
ImGui::SetTooltip("%s%s%s\nStock: %s%s", row.help.c_str(), row.help.empty() ? "" : "\n", row.id.c_str(), number(row.stock).c_str(),
row.used ? "" : "\nThe game was not found (or seen) reading this value, so changing it may do nothing.");
ImGui::SameLine(column);
const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
@@ -229,16 +231,24 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
}
const std::uint8_t list = p.mode == 0 ? trainer::mode_realistic : p.mode == 1 ? trainer::mode_fun : std::uint8_t{0xff};
const bool can_edit = view.editable && callbacks.queue_console_command != nullptr;
// One search for the whole tab: dials, switches, trick sliders and every value.
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::InputTextWithHint("##search", "Search: dials, tricks and every value...", p.search.data(), p.search.size());
const auto words = lower(p.search.data());
const bool searching = !words.empty();
const auto listed = [&](const trainer::PresetRow &preset) {
return searching ? contains_words(lower(preset.title + " " + preset.name + " " + preset.note), words) : p.mode == 2 || (preset.modes & list) != 0;
};
begin_card(menu, "dials", p.mode == 0 ? "FEEL: TONE IT DOWN" : p.mode == 1 ? "FEEL: TURN IT UP" : "FEEL",
"1 is the game's own. A dial moves every value its preset moves.");
ImGui::BeginDisabled(!can_edit);
for (const auto &preset : view.presets)
if (preset.builtin && preset.dial && (p.mode == 2 || (preset.modes & list))) dial_row(menu, callbacks, p, preset);
if (preset.builtin && preset.dial && listed(preset)) dial_row(menu, callbacks, p, preset);
// The presets that are plain switches, and the ones that set several dials at once.
bool first = true;
for (const auto &preset : view.presets) {
if (!preset.builtin || preset.dial || preset.name == "Stock" || !(p.mode == 2 || (preset.modes & list))) continue;
if (!preset.builtin || preset.dial || preset.name == "Stock" || !listed(preset)) continue;
if (!first && ImGui::GetItemRectMax().x + ImGui::CalcTextSize(preset.name.c_str()).x + ImGui::GetStyle().FramePadding.x * 4 <
ImGui::GetWindowPos().x + ImGui::GetContentRegionMax().x)
ImGui::SameLine();
@@ -264,10 +274,10 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
end_card();
ImGui::BeginDisabled(!can_edit);
trick_heights(menu, callbacks, p, view);
trick_heights(menu, callbacks, p, view, words);
ImGui::EndDisabled();
if (p.mode == 2) {
if (p.mode == 2 && !searching) {
begin_card(menu, "preset-save", "YOUR PRESETS", "Save what is changed right now");
ImGui::BeginDisabled(!can_edit);
const float button = ImGui::CalcTextSize("Turn off").x + ImGui::GetStyle().FramePadding.x * 2;
@@ -286,9 +296,12 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
ImGui::TextUnformatted(preset.name.c_str());
ImGui::SameLine();
ImGui::TextDisabled("%s", preset.note.c_str());
ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - px(120));
ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - px(170));
if (ImGui::SmallButton("This map")) trainer_command(menu, callbacks, "profile set " + preset.name);
ImGui::SameLine();
if (ImGui::SmallButton("Share")) trainer_command(menu, callbacks, "preset export " + preset.name);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Copies this preset to the clipboard as one line of text. Paste it in a chat;\nanyone with the trainer can import it.");
ImGui::SameLine();
if (ImGui::SmallButton("Delete")) trainer_command(menu, callbacks, "preset delete " + preset.name);
ImGui::PopID();
}
@@ -301,6 +314,18 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
ImGui::BeginDisabled(!p.preset_name[0] || !view.touched);
if (ImGui::Button("Save", ImVec2(save, 0))) trainer_command(menu, callbacks, std::string("preset save ") + p.preset_name.data());
ImGui::EndDisabled();
// Sharing: a preset travels as one line of text on the clipboard.
ImGui::BeginDisabled(view.stock);
if (ImGui::Button("Share what is changed now")) trainer_command(menu, callbacks, "preset export current");
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Copies your current setup to the clipboard as one line of text.");
ImGui::SameLine();
if (ImGui::Button("Import from clipboard")) {
const char *text = ImGui::GetClipboardText();
if (text && trainer::stage_import(text)) trainer_command(menu, callbacks, "preset import");
else feedback(menu, "The clipboard holds no text.");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Copy a preset line someone shared (it starts with RST1:), then press this.\nIt is added to your presets; nothing changes until you turn it on.");
if (!view.map.empty()) {
info(menu, "This map applies", view.profile_preset.empty() ? "nothing" : view.profile_preset);
if (!view.profile_preset.empty() && ImGui::Button("Stop applying it on this map")) trainer_command(menu, callbacks, "profile clear");
@@ -317,9 +342,6 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
// before an edit to one does anything: with the list open, it looks now.
trainer::note_class_list_shown();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.42f);
ImGui::InputTextWithHint("##search", "Search every value...", p.search.data(), p.search.size());
ImGui::SameLine();
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.42f);
if (p.group < 1) p.group = 1;
const char *current = p.group > 1 && static_cast<std::size_t>(p.group) <= view.groups.size() + 1
? view.groups[static_cast<std::size_t>(p.group) - 2].c_str() : "Every group";
@@ -337,16 +359,19 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
ImGui::Checkbox("Values with no use found", &p.show_unused);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Also list the tuning values that the game was never found or seen reading.\nChanging those will probably do nothing.");
} else if (searching) {
trainer::note_class_list_shown();
} else {
note("The same values the dials move, one at a time. The box on the left locks a value against presets, dials and Reset values.");
}
filter_rows(p, view);
if (p.mode != 2 && searching) ImGui::TextDisabled("%zu values match", p.shown.size());
if (p.mode == 2) {
ImGui::SameLine();
if (p.hidden_unused) ImGui::TextDisabled("%zu shown, %zu changed, %zu hidden", p.shown.size(), view.touched, p.hidden_unused);
else ImGui::TextDisabled("%zu shown, %zu changed", p.shown.size(), view.touched);
}
const bool with_group = p.mode == 2 && (p.search[0] != 0 || p.group <= 1 || p.only_changed);
const bool with_group = searching || (p.mode == 2 && (p.group <= 1 || p.only_changed));
const bool friendly = p.mode < 2;
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(p.shown.size()));
@@ -356,10 +381,14 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
if (p.shown.empty()) note("Nothing matches.");
}
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words) {
static constexpr const char *labels[]{"Flip trick speed", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height"};
const auto wanted = [&](const char *label) { return words.empty() || contains_words(lower(std::string(label) + " tricks"), words); };
if (std::ranges::none_of(labels, wanted)) return;
begin_card(menu, "trick-heights", "TRICKS", "1.0 is the game's own");
// `limited`: the game itself stops at the slider's end, so a typed number is held to it too.
const auto slider = [&](const char *label, const char *option, float value, float &edit, bool &editing, float high = 50.0f, bool limited = false) {
if (!wanted(label)) return false;
field(menu, label);
ImGui::PushID(option);
float shown = editing ? edit : value;
@@ -381,9 +410,9 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
if (ImGui::IsItemHovered())
ImGui::SetTooltip(limited ? "Type a multiplier and press Enter." : "Type any multiplier and press Enter: the slider's end is not a limit.");
ImGui::PopID();
return true;
};
slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing, 3.0f, true);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Slows board flips. Above 1 the game's own limit on how fast a board turns takes over.");
if (slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing, 3.0f, true) && ImGui::IsItemHovered()) ImGui::SetTooltip("Slows board flips. Above 1 the game's own limit on how fast a board turns takes over.");
slider("No comply height", "nocomply_height", view.nocomply_height, p.nocomply_edit, p.nocomply_editing);
slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing);
slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing);
@@ -467,6 +496,10 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
bool pad = view.pad_shortcuts;
if (toggle_row(menu, "Controller shortcuts", "Hold LB + RB, then D-pad: up saves, down goes, left / right pick the slot.", pad))
trainer_command(menu, callbacks, std::format("option pad {}", pad ? 1 : 0));
bool pad_menu = view.pad_menu;
if (toggle_row(menu, "Open this menu from the controller",
"LB + RB + click the right stick opens and closes the menu. Inside: D-pad or left stick moves, A presses, B goes back, LB / RB change a slider in big steps.", pad_menu))
trainer_command(menu, callbacks, std::format("option pad_menu {}", pad_menu ? 1 : 0));
end_card();
begin_card(menu, "teleport", "TELEPORT");
@@ -498,6 +531,97 @@ void jump_lines(SkateMenu &menu, const trainer::Jump &jump) {
jump.landing[2]));
info(menu, "Rotation", std::format("spin {:.0f} degrees (peak {:.0f} per second), flip {:.0f} degrees", jump.spin, jump.spin_rate, jump.flip));
}
// The player's own follow camera, in place of the game's: one framing on the board, one on foot.
void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
begin_card(menu, "rigs", "THE GAME'S CAMERAS", "Move the game's own cameras: they keep their smoothing and stay out of walls");
if (ImGui::Button("SunJay's Low Cam")) trainer_command(menu, callbacks, "camera rig sunjay");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Both on-board cameras low and close, the numbers of SunJay's Low Cam mod.");
ImGui::SameLine();
if (ImGui::Button("Low, a little higher")) trainer_command(menu, callbacks, "camera rig low+");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("The low camera lower and closer, but with more of the skater in view.");
ImGui::SameLine();
if (ImGui::Button("The game's own")) trainer_command(menu, callbacks, "camera rig stock");
static constexpr const char *rig_ids[]{"low", "high", "foot"};
static constexpr const char *rig_titles[]{"Low camera (on board)", "High camera (on board)", "On foot"};
for (std::size_t i = 0; i < trainer::camera_rigs; ++i) {
ImGui::SeparatorText(rig_titles[i]);
const auto &rig = view.rigs[i];
const auto slider = [&](const char *label, const char *field, float value, float low, float high, const char *format, const char *tip) {
ImGui::PushID(static_cast<int>(i));
ImGui::PushID(field);
menu::field(menu, label);
const auto key = std::string("rig:") + rig_ids[i] + field;
float shown = p.active == key ? static_cast<float>(p.active_value) : value;
if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera rig {} {} {:.3f}", rig_ids[i], field, shown));
if (ImGui::IsItemActive()) {
p.active = key;
p.active_value = shown;
} else if (p.active == key) {
p.active.clear();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s\nCtrl + click to type a number.", tip);
ImGui::PopID();
ImGui::PopID();
};
slider("Distance", "distance", rig.distance, 0.3f, 3.0f, "x %.2f", "How far behind you the camera sits. 1 is the game's own.");
slider("Height", "height", rig.height, -1.0f, 2.0f, "%+.2f m", "Moves the camera and the point it looks at up or down together.");
slider("Camera only", "raise", rig.raise, -1.0f, 2.0f, "%+.2f m", "Moves the camera alone, so it looks more down at you (or up from below).");
if (i == 2) slider("Shoulder", "side", rig.side, 0.0f, 2.0f, "%.2f", "Over the shoulder: 1 is the game's own (centred), above 1 moves right, below 1 moves left, by that many metres.");
else slider("Side", "side", rig.side, -2.0f, 2.0f, "x %.2f", "The camera's sideways offset. 1 is the game's own, 0 is dead centre, negative is the other shoulder.");
}
note(("Found: " + view.rigs_found + ". A change shows at once, or the next time that camera starts.").c_str());
end_card();
begin_card(menu, "camera", "A CAMERA OF YOUR OWN (EXPERIMENTAL)", "Replaces the game's camera altogether");
bool on = view.camera_on;
if (toggle_row(menu, "Use my camera", "Replaces the game's camera with the one set below. It stays behind the way you travel and does not avoid walls.", on))
trainer_command(menu, callbacks, std::format("camera {}", on ? "on" : "off"));
static constexpr const char *presets[]{"Low", "Low plus", "Skate 3", "High", "Shoulder", "Shoulder left"};
static constexpr const char *notes[]{
"Close to the ground behind the board, looking slightly up: the Skate 1 feel.", "The low camera raised a little, so more of the skater shows.",
"Close chase camera with a wide lens.", "Further back and above, for lines and big gaps.",
"On foot: over the right shoulder.", "On foot: over the left shoulder."};
for (std::size_t i = 0; i < std::size(presets); ++i) {
if (i) ImGui::SameLine();
if (ImGui::Button(presets[i])) trainer_command(menu, callbacks, std::string("camera preset ") + presets[i]);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", notes[i]);
}
if (view.camera_game.empty()) note("Skate a few seconds with the game's own camera and its framing is measured here.");
else {
info(menu, "The game frames you from", view.camera_game);
if (ImGui::Button("Start from the game's framing")) trainer_command(menu, callbacks, "camera game");
}
end_card();
const auto set_card = [&](const char *id, const char *title, const char *which, const trainer::CameraSet &set) {
begin_card(menu, id, title);
const auto slider = [&](const char *label, const char *field, float value, float low, float high, const char *format, const char *tip) {
ImGui::PushID(field);
ImGui::PushID(which);
menu::field(menu, label);
const auto key = std::string("camera:") + which + field;
float shown = p.active == key ? static_cast<float>(p.active_value) : value;
if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera set {} {} {:.3f}", which, field, shown));
if (ImGui::IsItemActive()) {
p.active = key;
p.active_value = shown;
} else if (p.active == key) {
p.active.clear();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s\nCtrl + click to type a number.", tip);
ImGui::PopID();
ImGui::PopID();
};
slider("Distance", "distance", set.distance, 0.5f, 10.0f, "%.2f m", "How far behind the skater the camera sits.");
slider("Height", "height", set.height, -0.5f, 5.0f, "%.2f m", "How far above the skater's feet. Low numbers give the classic low camera.");
slider("Tilt", "pitch", set.pitch, -60.0f, 30.0f, "%.0f deg", "Negative looks down at the skater, positive looks up.");
slider("Shoulder", "side", set.side, -1.5f, 1.5f, "%.2f m", "Sideways offset: positive is over the right shoulder.");
slider("Field of view", "fov", set.fov, 0.0f, 120.0f, set.fov < 20.0f ? "the game's" : "%.0f deg", "How wide the lens is. All the way left keeps the game's own.");
slider("Follow lag", "lag", set.lag, 0.0f, 0.5f, "%.2f s", "How far the camera trails behind your movement. 0 is rigid.");
end_card();
};
set_card("camera-board", "ON THE BOARD", "board", view.camera_board);
set_card("camera-foot", "ON FOOT", "foot", view.camera_foot);
}
void map_tab(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::View &view) {
const auto telemetry = trainer::telemetry();
begin_card(menu, "hud", "HUD");
@@ -560,8 +684,8 @@ bool trainer_take_open() {
if (view->open_serial == p.open_serial) return false;
p.open_serial = view->open_serial;
// `trainer open` still takes "presets": they live on the Tune tab now.
p.tab = view->open_tab >= 4 ? 0 : std::clamp(view->open_tab - 1, 0, 2);
if (view->open_tab >= 4) p.mode = std::clamp(view->open_tab - 4, 0, 2);
p.tab = view->open_tab == 7 ? 2 : view->open_tab >= 4 ? 0 : view->open_tab == 3 ? 3 : std::clamp(view->open_tab - 1, 0, 1); // 3 was "map" before the Camera tab
if (view->open_tab >= 4 && view->open_tab < 7) p.mode = std::clamp(view->open_tab - 4, 0, 2);
p.show_page = true;
return true;
}
@@ -582,13 +706,24 @@ void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("The game as it shipped: every value, lock, preset and trick slider.");
category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "MAP & HUD"}, "trainer-tabs");
if (view->ready && view->stock) ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "STOCK: the game is exactly as it shipped. Nothing of the trainer's is on.");
else if (view->ready) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "MODIFIED: this button puts back every value, lock, preset and trick slider.");
else ImGui::TextDisabled("The game as it shipped: every value, lock, preset and trick slider.");
// `preset export` leaves its line here; the clipboard belongs to this thread.
if (!p.share_seen) {
p.share_seen = true;
p.share_serial = view->share_serial;
} else if (p.share_serial != view->share_serial) {
p.share_serial = view->share_serial;
ImGui::SetClipboardText(view->share_text.c_str());
}
category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "CAMERA", "MAP & HUD"}, "trainer-tabs");
ImGui::PushID(p.tab);
ImGui::BeginChild("trainer-tab", ImVec2(0, page_body_height(menu)));
switch (p.tab) {
case 0: tune_tab(menu, model, callbacks, p, *view); break;
case 1: practice_tab(menu, model, callbacks, p, *view); break;
case 2: camera_tab(menu, callbacks, p, *view); break;
default: map_tab(menu, callbacks, *view); break;
}
ImGui::EndChild();
@@ -598,6 +733,15 @@ void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
namespace dingosdk::overlay {
bool trainer_open_requested() { return menu::trainer_take_open(); }
bool trainer_pad_menu_pressed(bool menu_visible) {
static ControllerComboLatch latch;
ControllerInput input;
DingoSDKOverlayReadControllerInput(&input, true);
constexpr std::uint32_t combo = 0x100 | 0x200 | 0x80; // LB + RB + R3
if (!latch.update(combo, input, !trainer::view()->pad_menu)) return false;
if (!menu_visible) menu::page().show_page = true;
return true;
}
bool trainer_hud_pending() {
const auto view = trainer::view();
return (view->hud || view->hud_jump) && trainer::telemetry().skater;
+3
View File
@@ -14,5 +14,8 @@ namespace dingosdk::overlay {
bool trainer_hud_pending();
// True once per `trainer open`: the overlay should show the menu.
bool trainer_open_requested();
// True once per press of the controller's menu combo (LB + RB + R3): the overlay should show
// the menu on the trainer's page if `menu_visible` is false, and hide it otherwise.
bool trainer_pad_menu_pressed(bool menu_visible);
void draw_trainer_hud();
} // namespace dingosdk::overlay
+137 -1
View File
@@ -3,6 +3,8 @@
#include <algorithm>
#include <array>
#include <iterator>
#include <map>
#include <span>
#include <string_view>
#include <utility>
@@ -39,11 +41,45 @@ std::string_view essential_name(std::string_view key, int *rank, std::uint8_t *m
std::string_view key, label;
std::uint8_t modes;
};
constexpr std::uint8_t r = mode_realistic, f = mode_fun, both = mode_realistic | mode_fun;
constexpr std::uint8_t r = mode_realistic, f = mode_fun, both = mode_realistic | mode_fun, t = mode_trial;
static constexpr Name names[]{
{"physicsmode.jumpmaxheight", "Ollie height (max)", both},
{"physicsmode.jumpminheight", "Ollie height (min, light pop)", both},
{"physicsmode.grindjumpcommonmax", "Pop out of grinds (max)", both},
{"physicsmode.grindjumpcommonmin", "Pop out of grinds (min, a quick pop)", both},
{"physicsmode.grindjumpboardslidemax", "Pop out of board slides (max)", both},
{"physicsmode.grindjumpboardslidemin", "Pop out of board slides (min, a quick pop)", both},
{"physicsmode.grindjumptipslidemax", "Pop out of nose and tail slides (max)", both},
{"physicsmode.grindjumptipslidemin", "Pop out of nose and tail slides (min, a quick pop)", both},
{"physicsmode.jumpminheightmanual", "Pop out of a manual (min)", both | t},
{"physicsmode.jumpdeltaminheightmanual", "Pop out of a manual: extra for a held pop", both | t},
{"physicsonboardjumptuning.jumpadjustvelocityboostlimitground", "Ollie: speed the stick can add (m/s)", both},
{"physicsonboardjumptuning.jumpadjustvelocityboostlimitgrind", "Pop out of a grind: speed the stick can add (m/s)", both},
{"physicstrajectory.grindlandingvelscalar", "Grinds: speed kept landing on one", r},
{"onboard_speedmodel.gravityacceleration", "On board: gravity (9.81 m/s2)", both},
{"onboard_speedmodel.maxgravityacceleration", "On board: gravity limit (m/s2)", both},
{"speedmodel.maxpositiveacceleration", "On board: fastest speeding up (m/s2)", f},
{"speedmodel.maxnegativeacceleration", "On board: fastest slowing down (m/s2)", f},
{"onboard_speedmodel.noinputtime", "On board: seconds of rolling before friction grows", r},
{"physicsairstates.mountrunlaunchz", "Get on board from a jog: forward speed (m/s)", both | t},
{"physicsairstates.mountrunlaunchy", "Get on board from a jog: hop (m/s)", r | t},
{"physicsairstates.mountsprintlaunchz", "Get on board from a sprint: forward speed (m/s)", both | t},
{"physicsairstates.mountsprintlaunchy", "Get on board from a sprint: hop (m/s)", r | t},
{"physicsairstates.minbodyfliptimevsupy x", "Body flips: air time needed (x)", both | t},
{"physicsairstates.bodyflipmingrabtimefraction", "Body flips: share of the air time held in a grab", both | t},
{"physicsfootplant.leglengthonlanding", "Landing: leg length (lower crouches deeper)", both | t},
{"physicsfeet.maxlandingforce", "Landing: leg force limit", r | t},
{"physicsfeet.landingforcetime", "Landing: time the legs push back (s)", r | t},
{"physicsmode.wipeoutbadlandingscalar", "Bail on bad landings: strictness", both},
{"physicsfootplant.wipeoutmaxlandingspeeddown", "Bail: landing speed limit, downward (m/s)", both},
{"physicsfootplant.wipeoutmaxlandingspeedhoriz", "Bail: landing speed limit, sideways (m/s)", both},
{"onboard_powerslide.frictionscalar_revert", "Revert: friction", both},
{"onboard_powerslide.frictionscalar_autorevert", "Auto revert: friction", both},
{"onboard_powerslide.powerslide_forwardforcescalar_revert", "Revert: forward force", both},
{"onboard_powerslide.powerslide_forwardforcescalar_autorevert", "Auto revert: forward force", both},
{"physicsmode.pumpeffectfactor", "Pumping: strength", both},
{"physicsmode.pumpmaxacceleration", "Pumping: fastest speed gain (m/s2)", both},
{"physicsmode.unintentionalpumpscalar", "Pumping: speed gained without pumping", r},
{"physicsjump.jumpybonusmax", "Jump bonus (max)", f},
{"physicspush.maxpushablespeed", "Push speed (9.25 = the game's; scales every push)", both},
{"push.maxpushspeedlight", "Push: a tapped push settles at (m/s)", r},
@@ -97,6 +133,54 @@ std::string_view essential_name(std::string_view key, int *rank, std::uint8_t *m
return known ? found->label : std::string_view{};
}
// One plain sentence per named value. Kept beside the names so the two stay in step.
std::string_view essential_help(std::string_view key) {
struct Help {
std::string_view key, text;
};
static constexpr Help help[]{
{"physicsmode.jumpmaxheight", "How high the skater's body rises on a full, held ollie, in metres. 1.575 is the game's own; Skate 3 used 1.71."},
{"physicsmode.jumpminheight", "How high a quick tap of an ollie goes, in metres. Bring it close to the max and every ollie is the same height."},
{"physicsmode.grindjumpcommonmax", "The highest pop out of a truck grind (50-50, 5-0, nosegrind...), in metres."},
{"physicsmode.grindjumpcommonmin", "The lowest pop out of a truck grind: what a quick flick gives. A pop is never lower than this."},
{"physicsmode.grindjumpboardslidemax", "The highest pop out of a boardslide or lipslide, in metres."},
{"physicsmode.grindjumpboardslidemin", "The lowest pop out of a boardslide or lipslide: what a quick flick gives."},
{"physicsmode.grindjumptipslidemax", "The highest pop out of a noseslide or tailslide, in metres."},
{"physicsmode.grindjumptipslidemin", "The lowest pop out of a noseslide or tailslide: what a quick flick gives."},
{"physicsmode.jumpminheightmanual", "The pop height out of a manual, in metres. Not confirmed to do anything yet: try it and tell us."},
{"physicsonboardjumptuning.jumpadjustvelocityboostlimitground", "Pushing the left stick as you pop adds speed that way; this is the most it adds, in m/s."},
{"physicsonboardjumptuning.jumpadjustvelocityboostlimitgrind", "The same stick boost when popping out of a grind. It is lower than on the ground, which is why a pop out of a grind can feel slower."},
{"physicspush.maxpushablespeed", "The speed pushing can take you to, in m/s (1 m/s = 3.6 km/h). Every kind of push scales with it."},
{"onboard_speedmodel.gravityacceleration", "The pull of gravity the board's speed model uses, in m/s2. Higher should mean faster drops and less float; lower floats more."},
{"onboard_speedmodel.maxgravityacceleration", "The most gravity may speed the board up down a slope, in m/s2. Skate 3 capped this at 7."},
{"physicsairstates.mountrunlaunchz", "How fast you roll away after pressing Y to get on the board while jogging, in m/s. Not confirmed to do anything yet."},
{"physicsairstates.mountsprintlaunchz", "How fast you roll away after pressing Y to get on the board while sprinting, in m/s. Not confirmed to do anything yet."},
{"physicsairstates.minbodyfliptimevsupy x", "A body flip needs this much air time. x 0.5 halves it, so flips start from lower pops. Not confirmed to do anything yet."},
{"physicsreckoning.flipscalar", "How fast a front flip or back flip rotates. 1 is the game's own; Skate 3 used 1.25."},
{"physicsreckoning.flipmaxspeed", "The fastest a body flip may rotate. Skate 3 used 5, half of this game's 10."},
{"physicsairstates.maxspinspeed", "How fast the skater's body spins in the air, in degrees per second at full stick."},
{"physicsmode.speedwobblestartspeed", "The speed where the board starts to wobble, in m/s. Higher: stable for longer."},
{"physicsmode.grindlockdist", "How far from a rail or ledge the board still snaps onto it, in metres. Skate 3: 0.9 normal, 0.15 hardcore."},
{"physicsgrind.commonfrictionscalar", "How quickly a grind loses speed. Lower slides further."},
{"onboard_powerslide.frictionscalar_slide", "How hard a powerslide brakes. Higher stops sooner."},
{"onboard_powerslide.frictionscalar_revert", "How much speed a revert costs. Higher loses more; 0 loses none."},
{"onboard_powerslide.frictionscalar_autorevert", "How much speed the automatic revert after a spin landing costs."},
{"onboard_powerslide.powerslide_forwardforcescalar_revert", "A push forward during a revert. Higher gains speed out of reverts."},
{"physicsmode.pumpeffectfactor", "How much speed pumping a transition gives. 13 is the game's own; Skate 3 used 18."},
{"physicsmode.pumpmaxacceleration", "The fastest pumping may speed you up. 13 is the game's own; Skate 3 used 10."},
{"physicsfootplant.leglengthonlanding", "How straight the legs are when the board lands (1 = straight). Lower should crouch deeper on impact. Not confirmed yet."},
{"physicsmode.wipeoutbadlandingscalar", "How strict the game is about landing crooked. The switch above turns the check off altogether."},
{"physicsfootplant.wipeoutmaxlandingspeeddown", "Land falling faster than this (m/s) and you bail. Skate 3 used 10."},
{"physicsfootplant.wipeoutmaxlandingspeedhoriz", "Land with more sideways speed than this (m/s) and you bail. Skate 3 used 11."},
{"physicsmode.wipeout_groundxzacceleration", "How hard a sideways hit has to be to knock you off. Higher: harder to bail."},
{"jump.basejumpheight", "How high a jump on foot goes, in metres."},
{"locomotion.sprintspeed", "Sprinting speed on foot, in m/s."},
{"wipeout.spreadeaglegravity", "Gravity while gliding spread-eagle. -8 is the game's own; nearer 0 falls slower."},
};
const auto found = std::ranges::find(help, key, &Help::key);
return found == std::end(help) ? std::string_view{} : found->text;
}
PresetDial preset_dial(std::string_view name) {
struct Dial {
std::string_view name, title;
@@ -105,7 +189,15 @@ PresetDial preset_dial(std::string_view name) {
constexpr std::uint8_t r = mode_realistic, f = mode_fun, both = mode_realistic | mode_fun;
static constexpr Dial dials[]{
{"Super Ollie", "Ollie height", both},
{"Grind Pop", "Pop out of grinds and slides", both},
{"Fast", "Push speed", both},
{"Gravity", "On board: gravity (higher: less float)", both},
{"Board Mount", "Get-on-board speed (untested)", both},
{"Easy Body Flips", "Body flips: air time needed (untested)", both},
{"Soft Landings", "Landing speed you can take", both},
{"Deep Landings", "Landing: leg length (lower crouches deeper; untested)", both},
{"Revert Friction", "Revert friction (higher loses more speed)", both},
{"Pump Power", "Pumping strength", both},
{"Fast Flips", "Body flip speed", both},
{"Fast Spins", "Body spin speed", both},
{"Hard To Bail", "Bail resistance (higher: harder to bail)", both},
@@ -122,11 +214,33 @@ PresetDial preset_dial(std::string_view name) {
{"Auto Push", "", f},
{"Smooth Surfaces", "", f},
{"Long Wheelbase", "", both},
{"Skate 3", "", both},
{"Skate 3 Hardcore", "", both},
{"Skate 3 Easy", "", both},
};
const auto found = std::ranges::find(dials, name, &Dial::name);
return found == std::end(dials) ? PresetDial{} : PresetDial{found->title, found->modes};
}
// Skate 3's own tuning, for the values this game still has under the same class and field name
// and that differ: this game's tuning grew out of Skate 3's. Hardcore and Easy are Skate 3's
// difficulty settings on top of the same set.
namespace {
struct Skate3Value {
std::string_view id;
double value;
};
#include "trainer_skate3.inc"
std::vector<PresetRule> skate3_rules(std::span<const Skate3Value> difficulty) {
std::map<std::string_view, double> values;
for (const auto &row : skate3_normal) values[row.id] = row.value;
for (const auto &row : difficulty) values[row.id] = row.value;
std::vector<PresetRule> rules;
for (const auto &[id, value] : values) rules.push_back({id, false, value, false, true});
return rules;
}
} // namespace
// Patterns are matched against the lower-case ids the game's own data gives its tuning
// (run `trainer dump` for the list), so a preset reaches every value a pattern names in
// whatever build is running and simply skips the ones that build lacks.
@@ -141,6 +255,22 @@ const std::vector<BuiltinPreset> &builtin_presets() {
{{"physicsmode.jumpmaxheight", true, 3.0}, {"physicsmode.jumpminheight", true, 3.0},
{"physicsjump.absoluteminheight", true, 2.0}, {"physicsjump.jumpybonusmax", true, 3.0},
{"physicsmode.grindjump", true, 2.5}}},
{"Grind Pop", "Pops out of grinds, board slides and nose and tail slides go twice as high, quick pops included.",
{{"physicsmode.grindjump", true, 2.0}}},
{"Gravity", "The board falls and drops in faster: less float. Below 1 floats more.",
{{"onboard_speedmodel.gravityacceleration", true, 1.3}, {"onboard_speedmodel.maxgravityacceleration", true, 1.3}}},
{"Board Mount", "A calmer roll-away when you get on the board (Y) from a jog or a sprint.",
{{"physicsairstates.mount launchz", true, 0.5}}},
{"Easy Body Flips", "Front flips and back flips start from much lower pops.",
{{"physicsairstates.minbodyfliptimevsupy", true, 0.3, true}, {"physicsairstates.bodyflipmingrabtimefraction", true, 0.3}}},
{"Soft Landings", "Land from bigger drops and with more sideways speed before a bail.",
{{"physicsfootplant.wipeoutmaxlandingspeed", true, 1.5}, {"physicswipeout.maxspeedlandingonboard", true, 1.5}}},
{"Deep Landings", "The legs give more on impact, so big drops compress the skater further.",
{{"physicsfootplant.leglengthonlanding", true, 0.85}}},
{"Revert Friction", "Reverts and auto reverts scrub three times the speed, as in the older games.",
{{"onboard_powerslide.frictionscalar_revert", true, 3.0}, {"onboard_powerslide.frictionscalar_autorevert", true, 3.0}}},
{"Pump Power", "Pumping transitions gives 1.4 times the speed (Skate 3's strength).",
{{"physicsmode.pumpeffectfactor", true, 1.4}}},
{"Fast Flips", "Front flips and back flips rotate three times as fast.",
{{"physicsreckoning.flipscalar", true, 3.0}, {"physicsreckoning.flipmaxspeed", true, 3.0},
{"physicsmode.perfectbodyflips", false, 0.0}}},
@@ -185,6 +315,12 @@ const std::vector<BuiltinPreset> &builtin_presets() {
{"Smooth Surfaces", "Rough ground rides like polished concrete.", {{"physicsmode.makesurfacessmooth", false, 1.0}}},
{"Long Wheelbase", "Trucks 10 cm further out at both ends. Takes effect on the next respawn.",
{{"physicstrucks.truckzposfront", false, 0.0143}, {"physicstrucks.truckzposback", false, 0.0143}}},
{"Skate 3", "Skate 3's own numbers for everything this game still shares with it: pop, grind pops, pushing, pumping, "
"steering, manuals, body spins and flips, bails. Normal difficulty.",
skate3_rules({})},
{"Skate 3 Hardcore", "Skate 3 on Hardcore: lower pops, weaker pushes, tight grind lock-on, slow auto spins.",
skate3_rules(skate3_hardcore)},
{"Skate 3 Easy", "Skate 3 on Easy: full-height pops, strong pushes, generous grind lock-on.", skate3_rules(skate3_easy)},
};
return presets;
}
+3
View File
@@ -13,6 +13,7 @@ struct PresetRule {
bool multiply{true};
double amount{1};
bool curves{}; // the rule is for curve and graph multipliers instead of plain values
bool exact{}; // `pattern` is one whole id, and it may be a single graph point
};
struct BuiltinPreset {
std::string_view name, note;
@@ -21,6 +22,8 @@ struct BuiltinPreset {
// The plain name of a value on the Tune tab's short lists, by its lower-case id; empty for the
// rest. `modes`: which lists it is on (mode_realistic, mode_fun).
std::string_view essential_name(std::string_view key, int *rank = nullptr, std::uint8_t *modes = nullptr);
// What a named value does, in a plain sentence with its unit; empty when there is none.
std::string_view essential_help(std::string_view key);
// Whether the game's code was found to read the tuning value at this offset of the asset.
// False means "no use found": changing it will probably do nothing.
bool value_used(std::uint16_t offset);
+122
View File
@@ -0,0 +1,122 @@
// Generated by trainer-dev/s3parity/gen_preset.py from the name join of this game's tuning
// against Skate 3's (same class and field names). {id, Skate 3 value}; do not edit by hand.
static constexpr Skate3Value skate3_normal[]{
{"physicsairstates.naturalairmaxdist", 0.12},
{"physicsbodyspin.maxdeltaoppositedirection", 0.2},
{"physicsbodyspin.maxdeltavstime.y0", 0.992857},
{"physicsbodyspin.maxdeltavstime.y1", 0.7607145},
{"physicsbodyspin.maxdeltavstime.y2", 0.507143},
{"physicsbodyspin.minderivativescalar", 0.4},
{"physicsbodyspin.propbodyspinvstime.y0", 3.15},
{"physicsbodyspin.propbodyspinvstime.y1", 6.364285},
{"physicsbodyspin.propbodyspinvstime.y2", 7.778571},
{"physicsbodyspin.propbodyspinvstime.y3", 7.939286},
{"physicsbodyspin.propbodyspinvstime.y4", 7.714285},
{"physicsbodyspin.propbodyspinvstime.y5", 6.975},
{"physicsbodyspin.propbodyspinvstime.y6", 5.689285},
{"physicsbodyspin.propbodyspinvstime.y7", 3.664286},
{"physicsfeet.antifliptorquez.y2", 0.03857143},
{"physicsfeet.antifliptorquez.y3", 0.08285715},
{"physicsfeet.antifliptorquez.y4", 0.1971429},
{"physicsfeet.antifliptorquez.y5", 0.2942856},
{"physicsfeet.antifliptorquez.y6", 0.4},
{"physicsfeet.antifliptorquez.y7", 0.4},
{"physicsfeet.landingondeckeffectscalar", 0.8},
{"physicsfootplant.wipeoutmaxlandingspeeddown", 10},
{"physicsfootplant.wipeoutmaxlandingspeedhoriz", 11},
{"physicsfriction.frictionvsspeed_noinput.max_y", 0.5},
{"physicsfriction.noinputtime", 3},
{"physicsgrindsair.maxdistboardslide", 0.75},
{"physicsgrindsair.maxdisttipslide", 0.7},
{"physicsmanual.manualscalar_d", -0.8},
{"physicsmanual.manualscalar_i", 0.03},
{"physicsmanual.manualscalar_p", 0.05},
{"physicsmanual.maxanglediff", 0.1},
{"physicsmanual.powerscalar_d", -0.4},
{"physicsmanual.powerscalar_i", 0.04},
{"physicsmanual.powerscalar_p", 0.03},
{"physicsmanual.starttorquescalar", 0},
{"physicsmode.grindjumpboardslidemax", 2.61},
{"physicsmode.grindjumpboardslidemin", 2.61},
{"physicsmode.grindjumpcommonmax", 2.5},
{"physicsmode.grindjumpcommonmin", 2.5},
{"physicsmode.grindjumptipslidemax", 2.61},
{"physicsmode.grindjumptipslidemin", 2.61},
{"physicsmode.jumpmaxheight", 1.71},
{"physicsmode.jumpminheight", 1.33},
{"physicsmode.jumpminheightmanual", 1.33},
{"physicsmode.maxpushdvend", 0.5},
{"physicsmode.maxpushdvstart", 0.6},
{"physicsmode.nosemanualbraketriggersbail", 1},
{"physicsmode.nosemanualbraketriggerspeed", 5},
{"physicsmode.pumpeffectfactor", 18},
{"physicsmode.pumpmaxacceleration", 10},
{"physicsmode.speedwobbleeffectscalar", 0.5},
{"physicsmode.speedwobblestartspeed", 15},
{"physicsmode.unintentionalpumpscalar", 0.7},
{"physicsmode.wipeout_groundxzacceleration", 7.1},
{"physicspumping.pumpangle", 50},
{"physicspumping.pumpminfactor", 0.01},
{"physicspush.maxpushablespeed", 8.5},
{"physicsreckoning.flipmaxspeed", 5},
{"physicsreckoning.flipscalar", 1.25},
{"physicsreckoning.vertjumpalignmaxgroundnormaly", 0.2},
{"physicssteering.damping", 0.7},
{"physicssteering.generalscalar", 0.6},
{"physicssteering.hardturnincrease", 1},
{"physicssteering.pushscalardecriment", 0.015},
{"physicssteering.pushscalarincriment", 0.05},
{"physicssteering.pushscalarmin", 0.5},
{"physicssteering.steeringtiltblending", 0.2},
{"physicssteering.steeringvsspeed.y6", 0.28},
{"physicssteering.steeringvsspeed.y7", 0.27},
{"physicssteering.steeringvsspeedmaxspeed", 50},
{"physicswipeout.wipeout_airskeletonmaxcontact", 3.5},
{"physicswipeout.wipeout_airskeletonmaxcontactarms", 20},
{"physicswipeout.wipeout_groundmaxsquash", 0.6},
{"physicswipeout.wipeout_groundskeletonmaxcontact", 8.5},
{"physicswipeout.wipeout_groundskeletonmaxcontactarms", 30},
{"smoothsurface.brakingscalar", 1},
{"smoothsurface.sidefriction_scalar", 70},
};
static constexpr Skate3Value skate3_hardcore[]{
{"physicsmode.grindjumpboardslidemax", 2},
{"physicsmode.grindjumpboardslidemin", 1.61},
{"physicsmode.grindjumpcommonmax", 2},
{"physicsmode.grindjumpcommonmin", 0.5},
{"physicsmode.grindjumptipslidemax", 2},
{"physicsmode.grindjumptipslidemin", 1},
{"physicsmode.grindlockdist", 0.15},
{"physicsmode.jumpheightoverrideenabled", 0},
{"physicsmode.jumpminheight", 1.21},
{"physicsmode.maxautobodyspinspeed", 0.5},
{"physicsmode.maxpushdvend", 0.25},
{"physicsmode.maxpushdvstart", 0.5},
{"physicsmode.nosemanualbraketriggerspeed", 2},
{"physicsmode.pumpeffectfactorabsorption", 9},
{"physicsmode.pumpmaxacceleration", 8},
{"physicsmode.pumpmaxdeceleration", 8},
{"physicsmode.speedwobbleeffectscalar", 1},
{"physicsmode.speedwobblestartspeed", 12},
{"physicsmode.unintentionalpumpscalar", 0},
};
static constexpr Skate3Value skate3_easy[]{
{"physicsmode.autopushenabled", 1},
{"physicsmode.easybodyspins", 1},
{"physicsmode.grindlockdist", 1.35},
{"physicsmode.jumpminheight", 1.71},
{"physicsmode.jumpminheightmanual", 1.53},
{"physicsmode.makesurfacessmooth", 1},
{"physicsmode.maxpushdvend", 1.5},
{"physicsmode.maxpushdvstart", 1.5},
{"physicsmode.nosemanualbraketriggersbail", 0},
{"physicsmode.nosemanualbraketriggerspeed", 4},
{"physicsmode.perfectbodyflips", 1},
{"physicsmode.speedwobbleeffectscalar", 0.25},
{"physicsmode.speedwobblestartspeed", 22},
{"physicsmode.unintentionalpumpscalar", 1},
{"physicsmode.wipeout_groundxzacceleration", 9},
{"physicsmode.wipeout_upsidedown", 0},
{"physicsmode.wipeoutbadlandingscalar", 1.5},
{"physicsmode.wipeoutcheckforsquashedskeleton", 0},
};
+2 -1
View File
@@ -280,7 +280,7 @@ bool setup_graphics() {
ImGui::SetCurrentContext(s.context);
ImGui::GetIO().IniFilename = nullptr;
ImGui::GetIO().LogFilename = nullptr;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad;
ImGui::StyleColorsDark();
dingosdk::overlay::load_skate_fonts(s.menu);
// Thumbnails are read from the game's own data at startup; the read is
@@ -455,6 +455,7 @@ void render(IDXGISwapChain* presented, UINT flags) {
const bool nametag_frame = nametags_pending();
const bool perf_frame = perf_hud_pending() || trainer_hud_pending();
if (trainer_open_requested()) s.visible.store(true);
if (trainer_pad_menu_pressed(s.visible.load())) s.visible.store(!s.visible.load());
const bool menu_frame = interactive_visible(s);
if (!menu_frame) {
if (s.ui_was_interactive) {
+1 -1
View File
@@ -278,7 +278,7 @@ ImFont* embedded_font(const wchar_t* name, float size, const ImWchar* ranges) {
enum Page { map, world, build, skater, training, multiplayer, progress, mods, settings, special, developer, page_count };
constexpr std::array<const char*, page_count> page_names{
"MAP", "WORLD", "BUILD", "SKATER", "TRAINER", "MULTIPLAYER", "PROGRESS", "MODS", "SETTINGS", "SPECIAL", "DEVELOPER"};
"MAP", "WORLD", "BUILD", "SKATER", "PHYSICS", "MULTIPLAYER", "PROGRESS", "MODS", "SETTINGS", "SPECIAL", "DEVELOPER"};
constexpr std::array<const char*, page_count> page_subtitles{
"Pick your spot.", "Set the vibe.", "Make the park yours.", "Ride it your way.", "Tune it. Drill it. Measure it.",
"Bring your crew.", "Pick up where you want.", "Bring your own.", "Your controls, your screen.", "Not everyone gets this page.",
+20
View File
@@ -1009,6 +1009,26 @@ void tick(std::uintptr_t client, std::uintptr_t update) {
dingosdk::multiplayer::tick_native_party_actions(r.base, client,
r.spectate_ready && r.spectate_client == client && (state == 13 || state == 21), camera_phase);
}
{
// The player's own camera (the trainer's Camera tab) takes the same camera lease as
// Spectate, which goes first.
DINGO_PROFILE_ZONE("tick/own camera");
static bool owned = false;
static std::string last_detail;
const bool in_play = state == 13 || state == 21;
float fov{};
std::optional<std::array<float, 16>> camera;
if (in_play && !dingosdk::multiplayer::spectating_party_member()) camera = dingosdk::multiplayer::custom_camera(fov);
if (camera || owned) {
std::string detail;
const bool applied = dingosdk::update_party_camera(r.base, client, in_play, camera_phase, camera ? &*camera : nullptr, detail, fov);
owned = applied ? camera.has_value() : owned || camera.has_value();
if (!detail.empty() && detail != last_detail) {
last_detail = detail;
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::ui, "Own camera: {}", detail);
}
}
}
if (state == 13 || state == 21) {
DINGO_PROFILE_ZONE("tick/camera view");
// The camera ReSkate's nametags start from, and the spectate camera's lesson in how
+1
View File
@@ -113,6 +113,7 @@ add_library(dingosdk_runtime SHARED
Extension/Trainer/trainer.cpp
Extension/Trainer/trainer_presets.cpp
Extension/Trainer/trainer_jump.cpp
Extension/Trainer/trainer_camera.cpp
Extension/Trainer/trainer_classes.cpp
Extension/Trainer/trainer_commands.cpp
Extension/Trainer/trainer_session.cpp