Trainer 0.4.2: flip trick speed through the game's flip state with a speed for each trick, pump power, slow flips stay slow by themselves; the slow-flip switch, the camera tab and two dead dials are gone; far fewer memory searches

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
andrewnakasandClaude Opus 5.5 committed 2026-10-07 02:07:04 -06:00
1 parent 6fcd427bf6
commit 614f07e819
24 files changed
+839 -816

No files matched your search

@@ -34,12 +34,6 @@ 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;
@@ -132,67 +126,4 @@ 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.0f, 10000.0f), height = std::clamp(c.height, -10000.0f, 10000.0f);
const float side = std::clamp(c.side, -10000.0f, 10000.0f), pitch = std::clamp(c.pitch, -89.0f, 89.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 + distance) s.custom_eye = target;
else {
const float k = 1.0f - std::exp(-dt / std::min(c.lag, 60.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 >= 1.0f && c.fov <= 179.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,7 +3,6 @@
#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
@@ -19,30 +18,4 @@ 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,8 +34,6 @@ 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,10 +809,6 @@ 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;
+124 -5
View File
@@ -239,6 +239,7 @@ struct PushSpeed {
// Physics thread only.
int pushing{}, since_push{hold_steps};
bool carried{}; // a held push is being carried past the game's last step
float reached{}; // the speed pushing has been carried to past the game's own ceiling (0: none)
};
PushSpeed& push_speed() { static auto* value = new PushSpeed; return *value; }
void trainer_push_speed(std::uintptr_t core) noexcept {
@@ -282,11 +283,19 @@ void trainer_push_speed(std::uintptr_t core) noexcept {
if (braking || !at_last_step) p.carried = false;
else if (p.pushing >= held_steps) p.carried = true;
float change = 0;
// The push class's speeds (the trainer raises those) are what a push aims for, but the
// game's speed model settles near 10 to 11 m/s however high they are (measured with them at
// 360 to 860 m/s: its own target stayed at 9.6 to 10 m/s). Past the stock ceiling a push is
// carried on here, at the game's own gain, to the stock ceiling x factor, and that speed
// is kept for as long as the game keeps a pushed speed.
const float ceiling = stock * factor;
if (braking || factor <= 1 || p.since_push >= hold_steps || speed < stock * 0.85f) p.reached = 0;
if (braking) {
} else if (pushed && factor > 1 && p.carried) {
change = std::min(push_gain, target * factor - speed);
} else if (pushed && factor > 1 && speed >= tap_speed * 0.85f && speed < tap_speed * factor) {
change = std::min(push_gain, tap_speed * factor - speed);
} else if (factor > 1 && p.pushing > 0 && speed >= stock * 0.85f && speed < ceiling) {
change = std::min(push_gain, ceiling - speed);
p.reached = std::max(p.reached, speed + change);
} else if (factor > 1 && p.reached > speed && p.since_push < hold_steps) {
change = std::min(push_gain, p.reached - speed);
} else if (pushed && factor < 1 && speed > target * factor && speed <= target * 1.1f) {
change = -std::min(push_gain, speed - target * factor); // pushed speed only: a hill's is faster than the target
} else if (cruise > 0 && speed >= 1.0f && speed < cruise) {
@@ -303,6 +312,101 @@ void trainer_push_speed(std::uintptr_t core) noexcept {
}
} catch (...) {}
}
// ---- pump power --------------------------------------------------------------------------------
// What a pump gains, times a factor. The game's trick scripts decide when the skater is pumping
// (Bool.Intent.Pumping, read by the trainer) and the game's physics then speeds the skater up;
// which numbers size that has not been found. Measured on a 12 m ramp (the skater's energy, speed
// and height, after each physics step against the step before): a pump of the game's own gains
// about 0.3 to 1 m/s over at most 28 steps, and the game gives less the faster the skater already
// is, so a share of the game's gain fades to nothing at speed (x 100 felt like x 1). The trainer's
// share is therefore its own: for every physics step of a pump it adds pump_rate x (factor - 1)
// along the direction of travel, and under 1 takes that much away. The game takes about half of
// an addition back while the pump lasts (measured at x 10 and x 100; PumpMaxDeceleration is not
// what does it), so the rate is twice what one more of the game's pumps would need at x 2.
// Owner, 2026-10-07: x 100 is strong on a small quarter and on a 12 m half pipe.
constexpr float pump_gravity = 9.81f; // m/s2, as flights measure it
constexpr float pump_rate = 0.03f; // m/s each physics step
constexpr float pump_step = 3.0f; // m/s the trainer may add or take in one physics step
struct PumpPower {
std::atomic<float> factor{1};
std::atomic<bool> pumping{}, measure{};
std::uintptr_t client{}, entity{};
std::atomic<ULONGLONG> expires{};
std::atomic<std::uint32_t> pumps{};
std::atomic<float> gained{}, given{}, speed{};
std::atomic<std::uint32_t> steps{};
// Physics thread only.
std::uint32_t count{};
bool primed{}, running{};
float energy{}, sum{}, credit{};
};
PumpPower& pump_power() { static auto* value = new PumpPower; return *value; }
void trainer_pump_power(std::uintptr_t core) noexcept {
auto& p = pump_power();
const float factor = p.factor.load(std::memory_order_relaxed);
const auto stop = [&] { p.primed = p.running = false; };
if ((factor == 1 && !p.measure.load(std::memory_order_relaxed)) || GetTickCount64() >= p.expires.load(std::memory_order_acquire)) return stop();
const auto watch = watched_physics_state();
if (!watch.valid || (watch.state != 100 && watch.state != 103)) return stop(); // riding, standing or crouched
SourceLastError error;
auto& state = source_state();
if (!state.initialized.load(std::memory_order_acquire) || state.busy.test_and_set(std::memory_order_acquire)) return;
SourceBusyScope scope{state.busy};
try {
const auto bodies = debug_noclip_bodies(state.trial.base, p.client, p.entity);
if (bodies.core != core || bodies.offboard || bodies.parts.empty()) return stop();
SourceReader reader;
std::array<std::array<float, 3>, 32> velocities{};
std::array<std::uint32_t, 32> flags{};
const auto count = std::min<std::size_t>(bodies.parts.size(), velocities.size());
for (std::size_t i = 0; i < count; ++i) {
velocities[i] = reader.value<std::array<float, 3>>(bodies.parts[i], 0x70);
flags[i] = reader.value<std::uint32_t>(bodies.parts[i], 0x60);
}
reader.verify();
const auto& v = velocities[0];
const float squared = v[0] * v[0] + v[1] * v[1] + v[2] * v[2], speed = std::sqrt(squared);
const float height = bodies.root[1];
if (!(speed >= 1.0f) || !(speed < 1000.0f) || !std::isfinite(height)) return stop();
const float energy = 0.5f * squared + pump_gravity * height;
if (!p.primed || !p.pumping.load(std::memory_order_relaxed)) { // coasting: only keep up
p.primed = true;
p.running = false;
p.energy = energy;
return;
}
if (!p.running) {
p.running = true;
p.sum = p.credit = 0;
p.count = 0;
p.pumps.fetch_add(1, std::memory_order_relaxed);
}
const float step = energy - p.energy;
p.energy = energy;
if (std::abs(step) > speed * 2.0f + 2.0f) return; // a teleport or a hit, not a pump
p.sum += step;
float change = 0;
if (factor != 1) change = std::clamp((factor - 1) * pump_rate, -pump_step, pump_step);
if (speed + change < 1.0f || speed + change > 300.0f) change = 0;
++p.count;
if (std::abs(change) >= 0.002f) {
const float scale = (speed + change) / speed;
// Like the native velocity writers: XYZ at +70 and the dirty bit 8 at +60.
for (std::size_t i = 0; i < count; ++i) {
for (auto& axis : velocities[i]) axis *= scale;
body_write(bodies.parts[i] + 0x70, velocities[i]);
body_write(bodies.parts[i] + 0x60, flags[i] | 8u);
}
const float given = 0.5f * ((speed + change) * (speed + change) - squared);
p.credit += given;
p.energy += given; // the trainer's own share is not the game's gain
}
p.gained.store(p.sum, std::memory_order_relaxed);
p.given.store(p.credit, std::memory_order_relaxed);
p.speed.store(speed, std::memory_order_relaxed);
p.steps.store(p.count, std::memory_order_relaxed);
} catch (...) {}
}
// ---- revert boost ------------------------------------------------------------------------------
// When the game auto reverts a landing the board is still well out of line with the body and the
// game swings it round. Earlier builds of the game gained speed there; this gives it back: a
@@ -428,6 +532,7 @@ void noclip_physics_update(std::uintptr_t core) {
noclip_apply_velocity(core);
trainer_apply_jump_scale(core);
trainer_push_speed(core);
trainer_pump_power(core);
trainer_revert_boost(core);
}
bool noclip_motion_target(std::uintptr_t rig, std::uintptr_t context,
@@ -515,9 +620,23 @@ void set_push_speed(std::uintptr_t client, std::uintptr_t entity, float factor,
p.entity = entity;
p.cruise.store(cruise > 0 && cruise < 100 ? cruise : 0.0f, std::memory_order_relaxed);
p.stock.store(stock > 1 && stock < 100 ? stock : 9.25f, std::memory_order_relaxed);
p.factor.store(factor > 0.02f && factor <= 50 ? factor : 1.0f, std::memory_order_relaxed);
p.factor.store(factor > 0.02f && factor <= 1.0e6f ? factor : 1.0f, std::memory_order_relaxed);
p.expires.store(GetTickCount64() + 500, std::memory_order_release);
}
void set_pump_power(std::uintptr_t client, std::uintptr_t entity, float factor, bool pumping, bool measure) noexcept {
auto& p = pump_power();
p.client = client;
p.entity = entity;
p.factor.store(factor >= 0 && factor <= 1.0e6f ? factor : 1.0f, std::memory_order_relaxed);
p.pumping.store(pumping, std::memory_order_relaxed);
p.measure.store(measure, std::memory_order_relaxed);
p.expires.store(GetTickCount64() + 500, std::memory_order_release);
}
PumpPowerReport pump_power_report() noexcept {
auto& p = pump_power();
return {p.pumps.load(std::memory_order_relaxed), p.gained.load(std::memory_order_relaxed), p.given.load(std::memory_order_relaxed),
p.speed.load(std::memory_order_relaxed), p.steps.load(std::memory_order_relaxed)};
}
void set_revert_boost(std::uintptr_t client, std::uintptr_t entity, float strength, const RevertTuning &tuning) noexcept {
auto& r = revert_boost();
AcquireSRWLockExclusive(&r.report_lock);
+14
View File
@@ -25,6 +25,20 @@ JumpScaleResult take_jump_scale_result() noexcept;
// rolling skater that is not braking gains speed up to it (m/s). Publish every client tick: it
// expires after 500 ms.
void set_push_speed(std::uintptr_t client, std::uintptr_t entity, float factor, float stock, float cruise) noexcept;
// The trainer's pump power: speed added for as long as the skater pumps a transition, sized so
// that `factor` 2 is about one more of the game's own pumps (1 leaves the game alone, under 1
// takes speed away). `pumping` is the game's own word on whether the skater is pumping this tick; with
// `measure` the gain is measured even at factor 1. Publish every client tick: it expires after
// 500 ms.
void set_pump_power(std::uintptr_t client, std::uintptr_t entity, float factor, bool pumping, bool measure) noexcept;
// The pump now running, or the last one: the energy the game's pump gained (J per kg; negative: the
// skater lost speed while pumping) and what the trainer added to that.
struct PumpPowerReport {
std::uint32_t pumps{}; // one more per pump
float gained{}, given{}, speed{};
std::uint32_t steps{}; // physics steps it has lasted
};
PumpPowerReport pump_power_report() noexcept;
// The trainer's revert boost: speed given back on a landing the game auto reverts (client_noclip.cpp).
// `strength` multiplies the boost (1: about +1.5 to +2.6 m/s a landing), 0 switches it off. Call
// every tick: it lapses half a second after the last call.
+1
View File
@@ -102,6 +102,7 @@ bool fingerprint(std::uintptr_t address, const std::array<unsigned char, 24>& ex
actual.size(), &count) && count == actual.size() && actual == expected;
}
}
std::uint64_t skater_creations() noexcept { return observer().counts[CreateEntered].load(); }
bool start_skater_observer(std::uintptr_t base) noexcept {
try {
auto& o = observer();
+2
View File
@@ -20,4 +20,6 @@ SkaterComponentSample read_skater_component(HANDLE process, std::uintptr_t image
// Requires the validated build and an initialized Detours hook service. No native calls are
// made by the observer except forwarding each intercepted call exactly once.
bool start_skater_observer(std::uintptr_t image_base) noexcept;
// How many times the game has built a skater since start (0 while the observer is not running).
std::uint64_t skater_creations() noexcept;
}
+34 -10
View File
@@ -1,7 +1,40 @@
# Changelog
## v0.4.2 - 2026-10-06
## v0.4.2 - 2026-10-07
- **Flip trick speed really sets the speed, both ways, and each trick can have its own.** The
slider now multiplies the speed the trick's animation plays at while the board is in the air
(the game state Float.Anim.TrickFlipSpeed), not the curves in front of it: x 0.5 is half
speed, x 2 is double (faster than the game's own never worked before), for every flip trick,
shuvits, 360 flips and varials included, and the pop is untouched. **Advanced trick speed**
adds a speed for each of 16 tricks (kickflip, heelflip, the shuvits, varials, 360 flip, laser
flip, hardflip, inward heelflip, impossibles; a nollie uses its regular trick's), multiplied
with the main one. Presets carry them (`trick.flip.<trick>`, `trick.flip_advanced`).
- **A slow flip is slow from the start.** Below x 1 the game's finish-before-landing rule is held
off by itself, so the board is not hurried round: with too little air you land on a board that
is still turning. The switch "Let slow flips stay slow" of 0.4.1 is gone: this does its job
(a preset that carries it still loads).
- **Pump power.** A new slider under TRICKS sizes what pumping a transition gives: 1 is the
game's own, x 2 is about one more of the game's pumps on top of each of yours, x 10 and up is
a rocket, under 1 takes speed away. The game's own pumping state (Bool.Intent.Pumping) says
when you are pumping, and for every physics step it does the trainer adds speed along your
travel (`trainer option pump_power <x>`, preset key `trick.pump_power`). Like the flip speed
it is found about 15 seconds after a level loads. Off under a host's enforced physics or with
boosts off. It replaces the dial "Pumping strength" (Pump Power), which changed nothing: the
game's own gain is small (0.3 to 1 m/s a pump, measured) and fades the faster you already go.
- **The camera tab is gone.** The camera controls did not work reliably, so they are out of this
version: the CAMERA tab, `trainer camera` and the saved camera numbers (ignored if present).
- **The dial "On foot: flip and roll speed" is gone.** It changed nothing in the game. The three
values are still listed under EVERYTHING.
- **Far fewer memory searches.** Every bail builds a new skater and each one started a full search
of the game's memory (about 13 seconds of a background thread), so a session of slow flips
searched over and over. A search that a new skater asked for and that found nothing new now
doubles the wait before the next (20 s up to 10 minutes); a level load or a real change puts it
back.
- **It takes about 15 seconds after every level load.** The game loads its flip animation states
with the level, and the trainer has to find them in memory again; until it has, the main
slider falls back to the old, weaker way and the per-trick speeds do nothing. The TRICKS card
says so while it is looking.
- **Board bending boost: no more speed from ordinary 180s.** 0.4.1 also counted a landing whose
board and body were together 12 degrees or more out of line with the travel. The game
straightens plain short or crooked 180s (flip trick 180s too) that way, so they got a boost,
@@ -10,15 +43,6 @@
AutoRevertBoost. Settings saved by 0.4.1 with the old 12 are moved to off. Turn the rule on
yourself and a landing that counts by it alone gets the auto revert boost at most. Found and
measured by AutoRevertBoost's authors (Sivaes, jaq and OVM).
- **Flip trick speed no longer lowers your ollie.** The slider used to scale seven of the game's
flip curves and shrink the range each one is read over; three of those are what a flick adds
to the height of the pop, so a slow flip came with a low ollie. It now scales only the one
curve that turns the stick's speed into the flip's speed. Measured on flat ground at x 0.3:
kickflips at about half the game's turn rate, plain ollies the same height and air time as
with the slider untouched.
- **Let slow flips stay slow says when it is off.** In a session whose host enforces physics or
has turned trick settings off, the switch cannot act (the likely reason it seemed to work only on
the main map); a line under it now says so.
## v0.4.1 - 2026-10-06
+7 -6
View File
@@ -1,18 +1,19 @@
# ReSkate Trainer
The PHYSICS page in the ReSkate menu (Insert): live physics tuning, tricklining, Skate 3 presets, cameras, practice markers and a
The PHYSICS page in the ReSkate menu (Insert): live physics tuning, tricklining, Skate 3 presets, practice markers and a
telemetry HUD. It ships no game data: the list of values is built at run time from the player's own
`Gameplay/SkatePhysicsTuning`.
## What it does
**Slow flips that stay slow.** The game hurries a flip trick so the board is round 1/6 s before the landing it predicts. "Let slow flips stay slow" (TUNE > TRICKS, under Flip trick speed) switches that off: the board turns as slowly as you set it and lands however far round it got.
**Flip trick speed, for all tricks or one by one.** "Flip trick speed" (TUNE > TRICKS) makes every flip trick turn slower or faster without changing your pop; "Advanced trick speed" under it gives each of 16 tricks a speed of its own. Below 1 a flip is slow from the start and is not hurried round before you land. **Give it about 15 seconds after every level load**: the trainer has to find the game's flip animations again before the speeds take hold, and the card says so while it looks.
**Pump power.** "Pump power" (TUNE > TRICKS) sizes what pumping a transition gives: 1 is the game's own, 2 is about one more of the game's pumps on top of each of yours, 10 and up is a rocket, under 1 takes speed away. The game still decides when you are pumping. Like the flip speed it needs about 15 seconds after a level load.
| Tab | What you get |
|---|---|
| **TUNE** | Four lists: REALISTIC, FUN, TRICKLINING and EVERYTHING, under a "Play like" choice (skate., Skate 3 Easy, Skate 3, Skate 3 Hardcore). TRICKLINING gathers what a line needs: the board bending boost (speed out of a landing with the board out of line, as in Skate 3) with its rules, revert and powerslide friction, pumping, pops, spins and flips, manuals and grinds, each with a line on what it is for. 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 a revert speed boost: speed back on a landing the game auto reverts, from AutoRevertBoost by Sivaes, jaq and OVM) 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. |
| **TUNE** | Four lists: REALISTIC, FUN, TRICKLINING and EVERYTHING, under a "Play like" choice (skate., Skate 3 Easy, Skate 3, Skate 3 Hardcore). TRICKLINING gathers what a line needs: the board bending boost (speed out of a landing with the board out of line, as in Skate 3) with its rules, revert and powerslide friction, pumping, pops, spins and flips, manuals and grinds, each with a line on what it is for. Each opens on the dials: every built-in preset as one slider (Ollie height, pop out of grinds, on-board gravity, landings, revert friction, 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, pump power, no comply, boneless, hippy and off-board jump height, and a revert speed boost: speed back on a landing the game auto reverts, from AutoRevertBoost by Sivaes, jaq and OVM) 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,
@@ -22,8 +23,8 @@ Controller: **LB + RB + click the right stick** opens and closes the menu (D-pad
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|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>`,
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|export <name>`, `trainer preset import`, `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`.
+239 -243
View File
@@ -1,9 +1,9 @@
#include "trainer.h"
#include "trainer_classes.h"
#include "trainer_gamestate.h"
#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"
@@ -16,6 +16,7 @@
#include "Extension/Skater/client_source_spawn_internal.h"
#include "Extension/Skater/no_bail.h"
#include "Extension/Skater/physics_tuning.h"
#include "Extension/Skater/skater_observer.h"
#include "Extension/UI/Overlay/overlay.h"
#include <windows.h>
#include <shlobj.h>
@@ -30,7 +31,6 @@
#include <numbers>
#include <optional>
// Game thread only. See trainer.h for how the menu reaches this.
namespace dingosdk::trainer {
namespace {
@@ -94,6 +94,8 @@ struct Motion {
struct Boosts {
float flip{1}, hippy{1}, nocomply{1}, boneless{1}, offboard{1}, cruise{};
};
// How long a new skater alone waits for a memory search: doubled while searches find nothing new.
constexpr std::uint64_t class_skater_wait_low = 20000, class_skater_wait_high = 600000;
struct State {
bool loaded{}; // the store was read
std::shared_ptr<const tuning::Model> model;
@@ -116,9 +118,6 @@ struct State {
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};
@@ -126,12 +125,31 @@ struct State {
float offboard_height{1};
// Board flip tricks: a multiplier on the game's flip speed curves (trainer_classes.h).
float flip_speed{1};
bool flip_gate{true}; // false: the game's finish-before-landing rule for flip tricks is held off
// "Advanced trick speed": each flip trick also turns at a speed of its own (flip_tricks, trainer_gamestate.h).
bool flip_advanced{};
std::array<float, flip_tricks.size()> flip_trick = [] {
std::array<float, flip_tricks.size()> ones{};
ones.fill(1.0f);
return ones;
}();
// The flip speed in the air now: the game's own number, what the trainer made of it, and whether it can be reached at all.
struct FlipDrive {
bool active{};
float game{}, written{};
float factor{1}; // the flip speed times the turning trick's own
} flip_drive;
bool flip_live{};
std::uint32_t flip_trick_seen{}; // the last trick that turned (CurrentFlipTrick)
bool catch_at{}; // flips are caught at catch_percent of the jump's air time
float catch_percent{70};
float catch_seconds{flip_gate_stock}; // what that comes to for the jump in the air now, as seconds before landing
bool offboard_grounded{}; // off the board and not moving up or down at the last tick
const char *boost_name{""};
// The no comply and the boneless are launched by the trick scripts too (trainer_jump.cpp).
float nocomply_height{1}, boneless_height{1};
float revert_boost{}; // 0: off
float pump_power{1}; // x what pumping a transition gains
bool pump_live{}; // the game's pumping states are found
RevertTuning revert;
std::uint64_t revert_boosts_seen{};
std::uint32_t last_state{};
@@ -146,6 +164,13 @@ struct State {
float flip_applied{1}; // the flip speed the catch times were last set for
std::uint64_t class_entity{}; // the skater the last search was for
std::uint64_t class_research_after{}; // no new search before this, however many skaters come and go
std::uint64_t class_creations{}; // the skaters the game had built at the last look
bool class_research_due{}; // a new skater came while a search could not start: look when it can
bool class_skater_due{}; // the same, for a new skater alone (nothing looked lost)
bool class_skater_judged{true}; // the search a new skater asked for has been compared with what was known
std::uint64_t class_skater_after{}; // no search for a new skater alone before this
std::uint64_t class_skater_wait{class_skater_wait_low};
std::uint64_t class_found{}; // class_search_found() before that search
std::vector<std::size_t> class_entries; // the entries that are fields of those classes
// A session whose host sets everyone's physics (session_physics.h): what the host shares
// beyond its tuning, and what each class field holds for it. The player's own stand down.
@@ -238,50 +263,6 @@ 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.0f, 10000.0f, 3.4f);
set.height = finite(set.height, -10000.0f, 10000.0f, 1.5f);
set.pitch = finite(set.pitch, -89.0f, 89.0f, -9.0f);
set.side = finite(set.side, -10000.0f, 10000.0f, 0.0f);
set.fov = set.fov < 1.0f ? 0.0f : finite(set.fov, 1.0f, 179.0f, 0.0f);
set.lag = finite(set.lag, 0.0f, 60.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])}); }
struct RevertField {
@@ -304,7 +285,6 @@ RevertTuning sane_revert(RevertTuning tuning) {
}
return tuning;
}
RigSetting sane_rig(RigSetting rig);
void load_store() {
auto &s = state();
s.loaded = true;
@@ -323,9 +303,14 @@ void load_store() {
s.nocomply_height = std::clamp(o.value("nocomply_height", 1.0f), height_low, height_high);
s.offboard_height = std::clamp(o.value("offboard_height", 1.0f), height_low, height_high);
s.flip_speed = std::clamp(o.value("flip_speed", 1.0f), flip_low, flip_high);
s.flip_gate = o.value("flip_gate", true);
s.flip_advanced = o.value("flip_advanced", false);
for (std::size_t i = 0; i < flip_tricks.size(); ++i)
s.flip_trick[i] = std::clamp(o.value(std::string(flip_tricks[i].key), 1.0f), flip_low, flip_high);
s.catch_at = o.value("catch_at", false);
s.catch_percent = std::clamp(o.value("catch_percent", 70.0f), catch_low, catch_high);
s.boneless_height = std::clamp(o.value("boneless_height", 1.0f), height_low, height_high);
s.revert_boost = std::clamp(o.value("revert_boost", 0.0f), 0.0f, revert_boost_high);
s.pump_power = std::clamp(o.value("pump_power", 1.0f), 0.0f, pump_high);
s.slot = std::clamp(o.value("slot", 0), 0, static_cast<int>(marker_slots) - 1);
}
if (json->contains("values") && json->at("values").is_object())
@@ -341,7 +326,8 @@ 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());
// (Fast Parkour Flips and Pump Power were presets of earlier versions that changed nothing.)
if (name.is_string() && name.string() != "Fast Parkour Flips" && name.string() != "Pump Power") s.active.push_back(name.string());
if (json->contains("revert") && json->at("revert").is_object()) {
for (const auto &field : revert_fields)
if (const auto name = std::string(field.name); json->at("revert").contains(name) && json->at("revert").at(name).is_number())
@@ -351,25 +337,6 @@ void load_store() {
if (!json->at("revert").contains("rules") && s.revert.min_slip == 12.0f) s.revert.min_slip = RevertTuning{}.min_slip;
s.revert = sane_revert(s.revert);
}
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] = sane_rig(rig);
}
}
if (json->contains("maps") && json->at("maps").is_object())
for (const auto &[level, row] : json->at("maps").items()) {
if (!row.is_object()) continue;
@@ -400,9 +367,14 @@ void save_store() {
options["nocomply_height"] = s.nocomply_height;
options["offboard_height"] = s.offboard_height;
options["flip_speed"] = s.flip_speed;
options["flip_gate"] = s.flip_gate;
options["flip_advanced"] = s.flip_advanced;
for (std::size_t i = 0; i < flip_tricks.size(); ++i)
if (s.flip_trick[i] != 1.0f) options[std::string(flip_tricks[i].key)] = s.flip_trick[i];
options["catch_at"] = s.catch_at;
options["catch_percent"] = s.catch_percent;
options["boneless_height"] = s.boneless_height;
options["revert_boost"] = s.revert_boost;
options["pump_power"] = s.pump_power;
options["slot"] = s.slot;
json["options"] = std::move(options);
Json values = Json::object();
@@ -439,21 +411,6 @@ void save_store() {
for (const auto &field : revert_fields) revert[std::string(field.name)] = s.revert.*field.member;
revert["rules"] = 2;
json["revert"] = std::move(revert);
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;
@@ -736,13 +693,12 @@ 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
// over either would only fight the session, so the player's own stand down.
if (session_tuning_enforced()) {
if (why) *why = "This session's host sets everyone's physics: you skate with the host's.";
if (why) *why = "You are in a multiplayer game where the host controls physics. This menu will not work in this multiplayer game.";
return false;
}
return true;
@@ -755,12 +711,17 @@ float own_cruise() {
// The host's where a session's host sets everyone's physics, whatever the boosts switch says
// (that switch is about a guest's own); otherwise the player's, and the game's own in a
// session that has turned boosts off.
// A session where the player's own trick extras do not count: its host sets the physics, or has turned boosts off.
bool extras_blocked() { return state().enforced || (multiplayer_session_active() && !session_boosts_allowed()); }
// With the catch set by the air time, the flip itself has to be slow enough for that limit to be
// what ends it (the game's own flip is over in a third of a second).
constexpr float catch_flip_speed = 0.25f;
Boosts boosts_in_force() {
auto &s = state();
if (s.enforced)
return {s.host.flip_speed, s.host.hippy_height, s.host.nocomply_height, s.host.boneless_height, s.host.offboard_height, s.host.cruise};
if (multiplayer_session_active() && !session_boosts_allowed()) return {};
return {s.flip_speed, s.hippy_height, s.nocomply_height, s.boneless_height, s.offboard_height, own_cruise()};
return {s.catch_at ? std::min(s.flip_speed, catch_flip_speed) : s.flip_speed, s.hippy_height, s.nocomply_height, s.boneless_height, s.offboard_height, own_cruise()};
}
// The player's own, for a session they host to share with its guests.
void publish_extras() {
@@ -932,25 +893,41 @@ std::string apply_dial(std::string_view name, double factor) {
}
// 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", "revert_boost"};
// What a trick slider is when the game is left alone: the multipliers 1, the revert boost off.
constexpr std::string_view flip_gate_name = "flip_gate";
constexpr float trick_stock(std::string_view name) { return name == "revert_boost" ? 0.0f : 1.0f; }
constexpr std::string_view trick_sliders[]{"flip_speed", "hippy_height", "nocomply_height", "boneless_height", "offboard_height", "revert_boost", "pump_power"};
// Those and the speed of each flip trick.
constexpr auto trick_names = [] {
std::array<std::string_view, std::size(trick_sliders) + flip_tricks.size()> names{};
std::size_t at{};
for (const auto name : trick_sliders) names[at++] = name;
for (const auto &trick : flip_tricks) names[at++] = trick.key;
return names;
}();
constexpr std::string_view flip_gate_name = "flip_gate", catch_at_name = "catch_at", catch_percent_name = "catch_percent", flip_advanced_name = "flip_advanced";
bool trick_switch(std::string_view name) { return name == flip_gate_name || name == catch_at_name || name == catch_percent_name || name == flip_advanced_name; }
// What a trick slider or switch is when the game is left alone: the multipliers 1, the revert boost and the switches off.
constexpr float trick_stock(std::string_view name) {
return name == "revert_boost" || name == catch_at_name || name == flip_advanced_name ? 0.0f : name == catch_percent_name ? 70.0f : 1.0f;
}
float *trick_option(std::string_view name) {
auto &s = state();
for (std::size_t i = 0; i < flip_tricks.size(); ++i)
if (name == flip_tricks[i].key) return &s.flip_trick[i];
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
: name == "revert_boost" ? &s.revert_boost : nullptr;
: name == "revert_boost" ? &s.revert_boost : name == "pump_power" ? &s.pump_power : 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());
// The slow-flip switch rides along as a number: 0 holds the game's finish-before-landing rule off.
if (name == flip_gate_name) state().flip_gate = value != 0;
// (flip_gate, a switch of earlier versions, still comes in old presets: a slow flip stays slow by itself now.)
if (name == catch_at_name) state().catch_at = value != 0;
if (name == flip_advanced_name) state().flip_advanced = value != 0;
if (name == catch_percent_name) state().catch_percent = std::clamp(static_cast<float>(value), catch_low, catch_high);
if (auto *option = trick_option(name))
*option = name == "flip_speed" ? std::clamp(static_cast<float>(value), flip_low, flip_high)
*option = name == "flip_speed" || name.starts_with("flip.") ? std::clamp(static_cast<float>(value), flip_low, flip_high)
: name == "revert_boost" ? std::clamp(static_cast<float>(value), 0.0f, revert_boost_high)
: name == "pump_power" ? std::clamp(static_cast<float>(value), 0.0f, pump_high)
: std::clamp(static_cast<float>(value), height_low, height_high);
return true;
}
@@ -1105,7 +1082,6 @@ 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 = {};
@@ -1118,6 +1094,10 @@ void enter_map(const std::string &level) {
s.class_search_at = level.empty() ? 0 : GetTickCount64() + 10000;
s.class_search_tries = 0;
s.class_search_seen = class_searches();
s.class_skater_wait = class_skater_wait_low;
s.class_skater_after = 0;
s.class_skater_due = false;
s.class_skater_judged = true;
s.class_list_wanted = false;
if (!level.empty()) {
const auto found = s.maps.find(level);
@@ -1195,6 +1175,31 @@ void finish_jump(const Vec3 &landing, float landing_speed, std::int64_t landed_a
jump.landing_speed * 3.6f, landing[0], landing[1], landing[2], jump.spin, jump.spin_rate, jump.flip, jump.board_turn, m.ground_state,
m.air_state));
}
// Board flip tricks turn at the game's speed times the player's. The trick scripts set
// Float.Anim.TrickFlipSpeed when the board is flicked; while the skater is in the air the
// trainer keeps it at that number times the flip speed (and, with "Advanced trick speed", times
// the speed of the trick that is turning). On the ground the game's own number is put back.
void drive_flip_speed(std::uintptr_t base, bool airborne) {
auto &s = state();
auto &drive = s.flip_drive;
const auto now = read_flip_state(base, static_cast<std::uintptr_t>(s.entity));
s.flip_live = static_cast<bool>(now);
if (!now || !airborne) {
if (now && drive.active && now.speed == drive.written && drive.written != drive.game) (void)write_flip_speed(now, drive.game);
drive = {};
return;
}
if (now.trick) s.flip_trick_seen = now.trick;
float factor = s.boosts.flip;
if (s.flip_advanced && !extras_blocked())
if (const auto index = flip_trick_index(now.trick); index >= 0) factor *= s.flip_trick[static_cast<std::size_t>(index)];
// A number that is not the trainer's own is the game's: the flight's first, or a new trick's.
if (!drive.active || now.speed != drive.written) drive.game = now.speed;
drive.active = true;
drive.factor = factor;
drive.written = std::clamp(drive.game * factor, 0.0f, 1000.0f);
if (now.speed != drive.written) (void)write_flip_speed(now, drive.written);
}
void observe(std::uintptr_t base, std::uintptr_t client, std::uint64_t now) {
auto &s = state();
auto &t = s.telemetry;
@@ -1306,9 +1311,29 @@ void observe(std::uintptr_t base, std::uintptr_t client, std::uint64_t now) {
m.takeoff_stamp = m.stamp;
m.apex = std::max(m.position[1], position[1]);
}
// On the ground the limit is the game's own: the number left by the last flight means nothing for the next.
if (!m.airborne) state().catch_seconds = flip_gate_stock;
drive_flip_speed(base, m.airborne);
if (m.airborne) {
m.apex = std::max(m.apex, position[1]);
t.air_time = static_cast<float>(clock_seconds(stamp - m.takeoff_stamp));
if (auto &own = state(); own.catch_at && flying) {
// The whole flight, guessed from where the skater is and how fast they rise: the time
// so far plus the time back down to the height they left from. Right over flat ground,
// short over a drop. The game has a flip round `seconds` before the landing it
// predicts, so the catch falls at the asked part of the flight.
const auto *gravity = find_entry("onboard_speedmodel.gravityacceleration");
const float g = std::max(1.0f, gravity ? static_cast<float>(gravity->touched ? gravity->value : gravity->stock) : 9.81f);
const float rise = velocity[1], above = std::max(0.0f, position[1] - m.takeoff[1]);
const float whole = t.air_time + (rise + std::sqrt(rise * rise + 2 * g * above)) / g;
// A catch asked for earlier than a board can come round (a flick late in a short
// jump) would make the limit negative, which the game reads as no limit at all and
// the flip crawls (measured at 10%: big pops turned at 450 deg/s to the ground). Such a
// flip is asked to come round as fast as the game turns one, about a fifth of a second.
constexpr float fastest_flip = 0.2f;
if (whole > 0.05f && whole < 60.0f)
own.catch_seconds = std::min((1 - own.catch_percent / 100) * whole, whole - t.air_time - fastest_flip);
}
t.height = m.apex - m.takeoff[1];
if (!flying)
finish_jump(position, std::sqrt(m.velocity[0] * m.velocity[0] + m.velocity[1] * m.velocity[1] + m.velocity[2] * m.velocity[2]),
@@ -1488,6 +1513,8 @@ void build_view() {
next->status = s.status;
next->last = s.last;
next->editable = editable(&next->blocked);
next->session_enforced = s.enforced;
next->boosts_blocked = !s.enforced && multiplayer_session_active() && !session_boosts_allowed();
next->rows.reserve(s.entries.size());
for (const auto &e : s.entries) {
int rank{};
@@ -1539,11 +1566,19 @@ void build_view() {
next->nocomply_height = s.nocomply_height;
next->offboard_height = s.offboard_height;
next->flip_speed = s.flip_speed;
next->flip_gate = s.flip_gate;
next->flip_advanced = s.flip_advanced;
next->flip_trick = s.flip_trick;
next->flip_advanced_blocked = s.flip_advanced && extras_blocked();
next->flip_live = s.flip_live;
next->flip_gate_found = flip_gates() != 0;
next->flip_gate_blocked = !s.flip_gate && (s.enforced || (multiplayer_session_active() && !session_boosts_allowed()));
next->flip_gate_blocked = s.catch_at && extras_blocked();
next->catch_at = s.catch_at;
next->catch_percent = s.catch_percent;
next->boneless_height = s.boneless_height;
next->revert_boost = s.revert_boost;
next->pump_power = s.pump_power;
next->pump_live = s.pump_live;
next->pump_blocked = s.pump_power != 1.0f && extras_blocked();
next->revert = s.revert;
next->return_delay = s.return_delay;
next->pad_shortcuts = s.pad_shortcuts;
@@ -1555,16 +1590,9 @@ 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.revert == RevertTuning{} && !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) == trick_stock(name); }) && s.flip_gate;
next->stock = s.revert == RevertTuning{} && !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) == trick_stock(name); }) && !s.catch_at && !s.flip_advanced;
next->share_text = s.share_text;
next->share_serial = s.share_serial;
next->open_tab = s.open_tab;
@@ -1621,7 +1649,11 @@ std::string export_preset(const std::string &name) {
if (e.touched) values[e.key] = e.value;
for (const auto trick : trick_names)
if (const auto *option = trick_option(trick); *option != trick_stock(trick)) values[std::string(trick_prefix) + std::string(trick)] = *option;
if (!s.flip_gate) values[std::string(trick_prefix) + std::string(flip_gate_name)] = 0;
if (s.flip_advanced) values[std::string(trick_prefix) + std::string(flip_advanced_name)] = 1;
if (s.catch_at) {
values[std::string(trick_prefix) + std::string(catch_at_name)] = 1;
values[std::string(trick_prefix) + std::string(catch_percent_name)] = s.catch_percent;
}
title = "My setup";
} else {
for (const auto &[user_name, saved] : s.user)
@@ -1680,7 +1712,7 @@ std::string import_preset(const std::string &file_name) {
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())) ||
std::string_view(id).substr(trick_prefix.size()) == flip_gate_name);
trick_switch(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>();
}
@@ -1725,7 +1757,6 @@ 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);
@@ -1759,11 +1790,16 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s
find_classes();
}
}
flip_speed_in_use(s.boosts.flip != 1.0f);
want_flip_speed(s.boosts.flip);
flip_speed_in_use(s.boosts.flip != 1.0f || s.flip_advanced);
// The speed itself goes straight to the trick in the air (drive_flip_speed); the flip curves
// are only scaled where that number cannot be reached.
want_flip_speed(s.flip_live ? 1.0f : s.boosts.flip);
// The player's own choice, and only where their own trick settings count: not under a
// host's enforced physics or in a session that has turned boosts off.
want_flip_gate(s.flip_gate || s.enforced || (multiplayer_session_active() && !session_boosts_allowed()));
// A flip asked to be slow is slow from the start: the game's rule would hurry it round before
// the landing, so it is held off for as long as the speed in force is under the game's own.
const bool slow = (s.flip_drive.active ? s.flip_drive.factor : s.boosts.flip) < 1.0f;
want_flip_gate(extras_blocked() ? flip_gate_stock : s.catch_at ? s.catch_seconds : slow ? flip_gate_off : flip_gate_stock);
if (s.boosts.flip != s.flip_applied) {
// The catch times follow the flip speed (want_class).
s.flip_applied = s.boosts.flip;
@@ -1776,20 +1812,61 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s
{
const bool lost = refresh_classes(now);
const auto entity = static_cast<std::uint64_t>(s.entity);
const bool new_skater = entity && s.class_entity && entity != s.class_entity;
bool new_skater = entity && s.class_entity && entity != s.class_entity;
if (entity) s.class_entity = entity;
if ((lost || new_skater) && classes_wanted() && !s.class_search_at && now >= s.class_research_after && !finding_classes()) {
// A respawn can keep the entity and still fill new copies (the push speeds' native
// one among them): the old ones go on holding what was written, so nothing looks lost.
if (const auto creations = skater_creations(); creations != s.class_creations) {
new_skater = new_skater || s.class_creations != 0;
s.class_creations = creations;
}
// A bail builds a new skater too, and most of those bring nothing new: a search that a
// new skater asked for and that found only what was known doubles the wait before the
// next one (a search reads all of the game's writable memory, for seconds).
if (!s.class_skater_judged && !s.class_search_at && !finding_classes()) {
s.class_skater_judged = true;
s.class_skater_wait = class_search_found() == s.class_found ? std::min(s.class_skater_wait * 2, class_skater_wait_high) : class_skater_wait_low;
s.class_skater_after = now + s.class_skater_wait;
}
s.class_research_due = s.class_research_due || lost;
s.class_skater_due = s.class_skater_due || new_skater;
if (!classes_wanted()) s.class_research_due = s.class_skater_due = false;
const bool for_lost = s.class_research_due && now >= s.class_research_after;
const bool for_skater = s.class_skater_due && now >= s.class_skater_after && now >= s.class_research_after;
if ((for_lost || for_skater) && !s.class_search_at && !finding_classes()) {
s.class_research_due = s.class_skater_due = false;
s.class_search_seen = class_searches();
s.class_search_tries = 0;
s.class_search_at = now + 2000;
s.class_search_at = now + 3000;
s.class_research_after = now + 20000;
if (!for_lost) {
s.class_skater_judged = false;
s.class_found = class_search_found();
s.class_skater_after = now + s.class_skater_wait;
}
}
}
set_trick_heights(s.boosts.nocomply, s.boosts.boneless);
// The speeds pushes aim for are the push class's (value_links ties them to the tuning's top
// pushing speed, which only gates whether a push may start).
const auto *top_speed = find_entry("physicspush.maxpushablespeed");
set_push_speed(client, s.entity, 1.0f, top_speed ? static_cast<float>(top_speed->stock) : 0.0f, s.boosts.cruise);
// Past the game's own ceiling the push is carried by the trainer (see trainer_push_speed):
// the player's own setting only, like the other speed the trainer adds.
const float push_factor = top_speed && top_speed->touched && top_speed->stock > 0 && !extras_blocked()
? std::max(1.0f, static_cast<float>(top_speed->value / top_speed->stock)) : 1.0f;
set_push_speed(client, s.entity, push_factor, top_speed ? static_cast<float>(top_speed->stock) : 0.0f, s.boosts.cruise);
// Pumping: the game says when (its pumping state), the trainer sizes the gain on the physics
// step (trainer_pump_power). The player's own setting only, like the push speed.
{
const auto pump = read_pump_state(base, static_cast<std::uintptr_t>(s.entity));
if (pump.found != s.pump_live) {
s.pump_live = pump.found;
s.view_due = true;
}
bool measure = false;
states_in_use(s.pump_power != 1.0f || measure);
set_pump_power(client, s.entity, extras_blocked() ? 1.0f : s.pump_power, pump.found && pump.pumping, measure);
}
// The revert boost adds speed, so it keeps to the session's rule for boosts and to a host's physics.
set_revert_boost(client, s.entity, s.enforced || (multiplayer_session_active() && !session_boosts_allowed()) ? 0.0f : s.revert_boost, s.revert);
if (const auto report = revert_boost_report(); report.sequence != s.revert_boosts_seen) {
@@ -1817,36 +1894,6 @@ 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, -1000.0f, 1000.0f, 1.0f);
rig.height = finite(rig.height, -1000.0f, 1000.0f, 0.0f);
rig.raise = finite(rig.raise, -1000.0f, 1000.0f, 0.0f);
rig.side = finite(rig.side, -1000.0f, 1000.0f, 1.0f);
return rig;
}
// ---- tricklining -------------------------------------------------------------------------------
// trainer revert <field> <number> | reset: what counts as a revert and what it is worth.
std::string revert_command(const std::vector<std::string> &a) {
@@ -1882,7 +1929,10 @@ std::string feel_command(const std::vector<std::string> &a) {
if (!editable(&why)) return "error: " + why;
const auto found = std::ranges::find(feels, which, [](const auto &feel) { return feel.first; });
if (found == std::end(feels) && which != "stock" && which != "skate") return "error: usage: trainer feel stock|easy|normal|hardcore";
for (const auto &feel : feels) (void)remove_preset(feel.second);
// Only a feel that is on is taken off: taking off one that is not would put back values the
// player set themselves that it happens to share (the push speed dial, for one).
for (const auto &feel : feels)
if (std::ranges::find(state().active, feel.second) != state().active.end()) (void)remove_preset(feel.second);
if (found != std::end(feels)) (void)apply_preset(found->second);
changed();
return found == std::end(feels) ? "Plays like skate.: the game's own tuning." : std::format("Plays like {}: its tuning for everything this game shares with it.", found->second);
@@ -1901,81 +1951,6 @@ std::string trickline_command(const std::vector<std::string> &a) {
changed();
return on ? "Tricklining extras on: the board bending boost, heavier revert and powerslide friction." : "Tricklining extras off.";
}
// 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);
@@ -1996,8 +1971,10 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
: std::format("{} values put back.", count);
}
if (v == "reset" && lower(arg(0)) == "tricks") {
s.hippy_height = s.nocomply_height = s.boneless_height = s.offboard_height = s.flip_speed = 1.0f;
s.flip_gate = true;
s.hippy_height = s.nocomply_height = s.boneless_height = s.offboard_height = s.flip_speed = s.pump_power = 1.0f;
s.catch_at = false;
s.flip_advanced = false;
s.flip_trick.fill(1.0f);
s.revert_boost = 0.0f;
changed();
return "Trick heights and flip trick speed are the game's own again.";
@@ -2023,10 +2000,10 @@ 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;
s.flip_gate = true;
s.hippy_height = s.nocomply_height = s.boneless_height = s.offboard_height = s.flip_speed = s.pump_power = 1.0f;
s.catch_at = false;
s.flip_advanced = false;
s.flip_trick.fill(1.0f);
s.revert_boost = 0.0f;
s.revert = {};
changed();
@@ -2094,7 +2071,11 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
if (e.touched) values[e.key] = e.value;
for (const auto trick : trick_names)
if (const auto *option = trick_option(trick); *option != trick_stock(trick)) values[std::string(trick_prefix) + std::string(trick)] = *option;
if (!s.flip_gate) values[std::string(trick_prefix) + std::string(flip_gate_name)] = 0;
if (s.flip_advanced) values[std::string(trick_prefix) + std::string(flip_advanced_name)] = 1;
if (s.catch_at) {
values[std::string(trick_prefix) + std::string(catch_at_name)] = 1;
values[std::string(trick_prefix) + std::string(catch_percent_name)] = s.catch_percent;
}
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);
@@ -2109,7 +2090,6 @@ 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 == "revert") return revert_command(a);
if (v == "trickline") return trickline_command(a);
if (v == "feel") return feel_command(a);
@@ -2158,36 +2138,52 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
const auto name = lower(arg(0));
bool on{};
std::string why;
if (name == "flip_speed") {
if (name == "flip_speed" || name.starts_with("flip.")) {
const auto value = number(arg(1));
if (!value) return "error: flip_speed needs a multiplier, 1 = the game's own speed.";
auto *option = trick_option(name);
if (!option) return "error: no such flip trick. flip_speed is all of them; flip.kickflip, flip.360flip ... are one each.";
if (!value) return "error: " + name + " needs a multiplier, 1 = the game's own speed.";
if (!editable(&why)) return "error: " + why;
s.flip_speed = std::clamp(static_cast<float>(*value), flip_low, flip_high);
*option = std::clamp(static_cast<float>(*value), flip_low, flip_high);
} else if (name == "hippy_height" || name == "nocomply_height" || name == "boneless_height" || name == "offboard_height") {
const auto value = number(arg(1));
if (!value) return "error: " + name + " needs a multiplier, 1 = the game's own height.";
if (!editable(&why)) return "error: " + why;
(name == "hippy_height" ? s.hippy_height : name == "nocomply_height" ? s.nocomply_height : name == "offboard_height" ? s.offboard_height : s.boneless_height) =
std::clamp(static_cast<float>(*value), height_low, height_high);
} else if (name == "pump_power") {
const auto value = number(arg(1));
if (!value) return "error: pump_power needs a multiplier, 1 = what the game's own pump gains.";
if (!editable(&why)) return "error: " + why;
s.pump_power = std::clamp(static_cast<float>(*value), 0.0f, pump_high);
} else if (name == "revert_boost") {
const auto value = number(arg(1));
if (!value) return "error: revert_boost needs a strength: 0 is off, 1 gives back about 2 m/s on an auto revert.";
if (!editable(&why)) return "error: " + why;
s.revert_boost = std::clamp(static_cast<float>(*value), 0.0f, revert_boost_high);
} else if (name == "catch_percent") {
const auto value = number(arg(1));
if (!value) return "error: catch_percent needs a percent of the air time.";
if (!editable(&why)) return "error: " + why;
s.catch_percent = std::clamp(static_cast<float>(*value), catch_low, catch_high);
} else if (name == "return_delay") {
const auto value = number(arg(1));
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|pad_menu|flip_gate|log 0|1";
return "error: usage: trainer option hud|hud_jump|auto_return|pad|pad_menu|flip_advanced|catch_at|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 == "flip_gate") {
if (!on && !editable(&why)) return "error: " + why;
s.flip_gate = on;
else if (name == "flip_gate") return "That switch is gone: a flip set slower than the game's own stays slow by itself.";
else if (name == "catch_at") {
if (on && !editable(&why)) return "error: " + why;
s.catch_at = on;
} else if (name == "flip_advanced") {
if (on && !editable(&why)) return "error: " + why;
s.flip_advanced = on;
}
else if (name == "log") {
set_logging(on);
@@ -2216,7 +2212,7 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
// The last three are the Tune tab on one of its lists.
const std::array<std::string_view, 9> tabs{"tune", "presets", "practice", "map", "realistic", "fun", "everything", "camera", "trickline"};
const auto found = std::ranges::find(tabs, tab);
if (!tab.empty() && found == tabs.end()) return "error: usage: trainer open [tune|trickline|practice|camera|map|realistic|fun|everything]";
if (!tab.empty() && found == tabs.end()) return "error: usage: trainer open [tune|trickline|practice|map|realistic|fun|everything]";
s.open_tab = tab.empty() ? 1 : static_cast<int>(found - tabs.begin());
++s.open_serial;
s.view_due = true;
+12 -12
View File
@@ -4,7 +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 "trainer_gamestate.h"
#include "Extension/Skater/client_source_spawn.h"
#include <array>
#include <cstdint>
@@ -57,11 +57,6 @@ 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
@@ -69,6 +64,8 @@ struct View {
std::string last; // what the last command answered
bool editable{}; // false while a session's host sets everyone's physics (the host's apply)
std::string blocked; // the reason shown on locked rows
bool session_enforced{}; // a guest where the host (or the server) sets everyone's physics
bool boosts_blocked{}; // a session whose host turned boosts off: the trick extras are the game's own
std::vector<Row> rows;
std::vector<std::string> groups;
std::vector<PresetRow> presets;
@@ -87,23 +84,26 @@ struct View {
// The same for the no comply and the boneless.
float nocomply_height{1}, boneless_height{1};
float revert_boost{}; // strength of the speed given back on an auto revert; 0: off
float pump_power{1}; // x what pumping a transition gains
bool pump_live{}; // the game's pumping states are found: the pump power acts
bool pump_blocked{}; // set, but this session's rules keep the game's own pumping
RevertTuning revert; // what counts as a revert and what it is worth
float offboard_height{1}; // a jump on foot
float flip_speed{1}; // board flip tricks: x of the game's own speed
bool flip_gate{true}; // the game brings a flip round before the landing; false: slow flips stay slow
bool flip_advanced{}; // each flip trick also has a speed of its own
std::array<float, flip_tricks.size()> flip_trick{}; // those, in the order of flip_tricks
bool flip_advanced_blocked{}; // ticked, but this session's rules keep the game's own flips
bool flip_live{}; // the game's flip states are found: the speeds act
bool flip_gate_found{}; // the rule's number was found in memory (the switch can act)
bool flip_gate_blocked{}; // ticked, but this session's rules keep the game's own flips
bool catch_at{}; // flips are caught at a set part of the jump
float catch_percent{70}; // which part: percent of the air time
// HUD
bool hud{}, hud_jump{}, logging{};
// The loaded map and what its author ships for the trainer (Mods/<mod>/trainer.json).
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{};
-227
View File
@@ -1,227 +0,0 @@
#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, -1000.0f, 1000.0f, 1.0f), side = finite(want.side, -1000.0f, 1000.0f, 1.0f);
const float height = finite(want.height, -1000.0f, 1000.0f, 0.0f), raise = finite(want.raise, -1000.0f, 1000.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
@@ -1,31 +0,0 @@
#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
+60 -17
View File
@@ -1,4 +1,5 @@
#include "trainer_classes.h"
#include "trainer_gamestate.h"
#include "Engine/Core/Log/logging.h"
#include <windows.h>
#include <algorithm>
@@ -82,8 +83,9 @@ struct Found {
std::vector<FlipCurve> flip;
float flip_wanted{1}, flip_written{1};
bool flip_used{}; // a flip speed other than the game's is set, though not written at this moment
bool states_used{}; // the game states are wanted for something else
std::vector<std::uintptr_t> gates; // where the flick warp limit's number lives
bool gate_kept{true}; // false: the trainer holds it off
float gate_wanted{flip_gate_stock}; // the seconds before landing a flip must be round by
Copies copies;
MirrorCopies mirror_copies;
std::array<std::array<int, 15>, mirror_count> mirror_fields{};
@@ -300,12 +302,19 @@ std::vector<FlipCurve> find_flip_curves(std::uintptr_t curve_type) {
// left before landing less one number: 1/6 s as shipped. That number sits in the expression's
// kernel between words that do not change; far below zero, the limit never binds and a slow flip
// stays slow all the way to the ground.
constexpr float gate_stock = 1.0f / 6.0f, gate_off = -1000.0f;
bool is_gate(std::uintptr_t address, std::uintptr_t low, std::uintptr_t high) noexcept {
constexpr float gate_stock = flip_gate_stock, gate_off = flip_gate_off;
// The words around the number: enough to know a place already found is still the rule.
bool gate_shape(std::uintptr_t address, std::uintptr_t low, std::uintptr_t high) noexcept {
if (address < low + 12 || address + 12 > high) return false;
const auto *words = reinterpret_cast<const std::uint32_t *>(address);
return words[2] == 0x2419u && words[-3] == 0x00010300u && !words[-2] && !words[-1] && !words[1] &&
(words[0] == bits(gate_stock) || words[0] == bits(gate_off));
return words[2] == 0x2419u && words[-3] == 0x00010300u && !words[-2] && !words[-1] && !words[1];
}
// A search also wants the number itself (the game's, or the trainer's "off"): other expressions
// share the shape.
bool is_gate(std::uintptr_t address, std::uintptr_t low, std::uintptr_t high) noexcept {
if (!gate_shape(address, low, high)) return false;
const auto word = *reinterpret_cast<const std::uint32_t *>(address);
return word == bits(gate_stock) || word == bits(gate_off);
}
void collect_gates(std::uintptr_t start, std::size_t size, std::vector<std::uintptr_t> &gates) noexcept {
__try {
@@ -320,7 +329,7 @@ bool gate_still_there(std::uintptr_t address) noexcept {
if (VirtualQuery(reinterpret_cast<void *>(address), &info, sizeof(info)) != sizeof(info) || info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE)
return false;
const auto low = reinterpret_cast<std::uintptr_t>(info.BaseAddress);
return is_gate(address, low, low + info.RegionSize);
return gate_shape(address, low, low + info.RegionSize);
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
@@ -355,7 +364,7 @@ DWORD WINAPI search(void *) noexcept {
stock.data());
bytes += info.RegionSize;
}
std::size_t classes{}, copies{};
std::size_t classes{}, copies{}, native{};
{
std::lock_guard lock(f.mutex);
for (std::size_t c = 0; c < class_count; ++c) {
@@ -387,7 +396,7 @@ DWORD WINAPI search(void *) noexcept {
(void)put(base + i * 4, f.written[index]);
kept.insert(kept.end(), fresh_mirrors[m].begin(), fresh_mirrors[m].end());
f.mirror_copies[m] = std::move(kept);
copies += f.mirror_copies[m].size();
native += f.mirror_copies[m].size();
}
f.ready = true;
f.dirty = true;
@@ -435,17 +444,22 @@ DWORD WINAPI search(void *) noexcept {
flip_found = f.flip.size();
f.dirty = true;
}
// The flip states' objects (trainer_gamestate.cpp): the flip speed is driven through them.
each_region([](std::uintptr_t start, std::size_t size) { find_state_objects(start, size); });
std::size_t gates_found{};
{
std::vector<std::uintptr_t> gates;
each_region([&](std::uintptr_t start, std::size_t size) { collect_gates(start, size, gates); });
std::lock_guard lock(f.mutex);
// One already known may hold a number of the trainer's own that a search does not look for.
for (const auto known : f.gates)
if (std::ranges::find(gates, known) == gates.end() && gate_still_there(known)) gates.push_back(known);
f.gates = std::move(gates);
gates_found = f.gates.size();
}
logging::write(logging::Level::info, logging::Channel::skater,
std::format("Trainer: found {} of {} game tuning classes ({} copies), {} flip trick speed curves and {} flip landing limit in {} MB, {} ms.",
classes, class_count, copies, flip_found, gates_found, bytes >> 20, GetTickCount64() - started));
std::format("Trainer: found {} of {} game tuning classes ({} copies, {} native), {} flip trick speed curves, {} flip landing limit and the flip states{} in {} MB, {} ms.",
classes, class_count, copies, native, flip_found, gates_found, flip_states_found() ? "" : " (not found)", bytes >> 20, GetTickCount64() - started));
} catch (...) {}
f.searches.fetch_add(1, std::memory_order_release);
f.searching.store(false, std::memory_order_release);
@@ -462,7 +476,7 @@ bool same(float a, float b) noexcept { return std::abs(a - b) <= 1e-5f * std::ma
std::size_t write_gates(Found &f) noexcept {
std::erase_if(f.gates, [](std::uintptr_t address) { return !gate_still_there(address); });
std::size_t count{};
const float want = f.gate_kept ? gate_stock : gate_off;
const float want = f.gate_wanted;
for (const auto address : f.gates) {
float now{};
if (peek(address, now) && now != want && put(address, want)) ++count;
@@ -484,7 +498,7 @@ bool refresh_classes(std::uint64_t now) noexcept {
bool lost = false;
std::size_t repaired{};
repaired += write_gates(f);
lost = lost || (!f.gate_kept && f.gates.empty());
lost = lost || (f.gate_wanted != gate_stock && f.gates.empty());
// On the ground the curves hold the game's own (the flip speed is written in the air only):
// one that holds something else is someone else's memory now.
if (f.flip_used && f.flip_written == 1) {
@@ -568,6 +582,29 @@ void find_classes() noexcept {
}
bool finding_classes() noexcept { return found().searching.load(std::memory_order_acquire); }
std::uint64_t class_searches() noexcept { return found().searches.load(std::memory_order_acquire); }
std::uint64_t class_search_found() noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
std::uint64_t all{};
const auto add = [&](std::uintptr_t address) { all = (all ^ address) * 0x100000001b3ull; };
for (const auto &list : f.copies) {
std::uint64_t sum{}; // the order within a class is the search's, not the game's
for (const auto copy : list) sum += copy.base;
add(sum + list.size());
}
for (const auto &list : f.mirror_copies) {
std::uint64_t sum{};
for (const auto base : list) sum += base;
add(sum + list.size());
}
std::uint64_t sum{};
for (const auto address : f.gates) sum += address;
add(sum + f.gates.size());
sum = 0;
for (const auto &curve : f.flip) sum += curve.points;
add(sum + f.flip.size());
return all;
}
std::size_t class_copies(std::size_t index) noexcept {
auto &f = found();
if (index >= class_count) return 0;
@@ -586,7 +623,7 @@ void want_class_value(std::size_t field, float value) noexcept {
bool classes_wanted() noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
if (f.flip_wanted != 1 || f.flip_used || !f.gate_kept) return true;
if (f.flip_wanted != 1 || f.flip_used || f.states_used || f.gate_wanted != gate_stock) return true;
for (std::size_t i = 0; i < class_field_count; ++i)
if (f.wanted[i] != class_fields[i].stock) return true;
return false;
@@ -652,11 +689,17 @@ void flip_speed_in_use(bool used) noexcept {
std::lock_guard lock(f.mutex);
f.flip_used = used;
}
void want_flip_gate(bool kept) noexcept {
void states_in_use(bool used) noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
if (f.gate_kept == kept) return;
f.gate_kept = kept;
f.states_used = used;
}
void want_flip_gate(float seconds) noexcept {
auto &f = found();
if (!(seconds == seconds)) return;
std::lock_guard lock(f.mutex);
if (f.gate_wanted == seconds) return;
f.gate_wanted = seconds;
(void)write_gates(f);
}
std::size_t flip_gates() noexcept {
@@ -697,7 +740,7 @@ std::string classes_summary() {
result += std::format("{}{}:{}+{}", c ? ", " : "", class_specs[c].key, f.copies[c].size() - static_cast<std::size_t>(slots), slots);
}
for (std::size_t m = 0; m < mirror_count; ++m) result += std::format(", native copy {}:{}", m, f.mirror_copies[m].size());
result += std::format(", flip trick speed curves:{}, flip landing limit:{}{}", f.flip.size(), f.gates.size(), f.gate_kept ? "" : " (held off)");
result += std::format(", flip trick speed curves:{}, flip landing limit:{}{}", f.flip.size(), f.gates.size(), f.gate_wanted == gate_stock ? std::string{} : std::format(" (held at {:.3f} s)", f.gate_wanted));
return result;
}
} // namespace dingosdk::trainer
+8 -1
View File
@@ -34,6 +34,8 @@ void find_classes() noexcept;
bool finding_classes() noexcept;
// How many searches have finished (0: none yet).
std::uint64_t class_searches() noexcept;
// A number that changes when a search finds something it did not know (or loses something).
std::uint64_t class_search_found() noexcept;
// Copies of class `index` found.
std::size_t class_copies(std::size_t index) noexcept;
// What field `field` (an index into class_fields) should hold. Written by apply_classes.
@@ -60,7 +62,12 @@ std::size_t flip_curves() noexcept;
// slow the flip speed is set. `kept` false holds that rule off: the board turns as slowly as set
// and lands however far round it got. The search finds the rule's number; flip_gates says whether
// it did (0: nothing to switch yet).
void want_flip_gate(bool kept) noexcept;
// `seconds`: how long before the landing it predicts the game has a flip round. The game's own is
// flip_gate_stock; flip_gate_off never hurries a flip; anything else moves the catch.
inline constexpr float flip_gate_stock = 1.0f / 6.0f, flip_gate_off = -1000.0f;
void want_flip_gate(float seconds) noexcept;
// Something else needs the game states the search finds (the pump power): a reason to search.
void states_in_use(bool used) noexcept;
std::size_t flip_gates() noexcept;
// Where field `field` (an index into class_fields) lives: each copy's address and kind ('c' the
// class's own layout, 's' the eight-byte slots, 'n' native code's copy). Returns how many.
-1
View File
@@ -21,7 +21,6 @@ void register_trainer_commands(Commands &registry) {
{"revert", "What counts as a revert and what its speed boost is worth", "<field> <number> | reset", false},
{"feel", "Which game the skating plays like", "stock|easy|normal|hardcore", false},
{"trickline", "Skate 3's tricklining extras: the board bending boost and heavier revert friction", "extras on|off", 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},
+187
View File
@@ -0,0 +1,187 @@
#include "trainer_gamestate.h"
#include <atomic>
#include <cmath>
#include <cstring>
namespace dingosdk::trainer {
namespace {
// Game build 20260929.
//
// A game state is an asset object in memory: its 16-byte asset id, then the object, with its
// name at + 0x18 and at + 0x28 the handle the game finds its value by (0x11dcd40): the low 12
// bits pick one of the skater's state pages, the next 11 the value's place in that page, and a
// bit map at page + 0x71c says whether the page holds it. The handles differ from level to
// level, so they are read from the objects each time; the objects are found by their asset id
// in the memory search (trainer_classes.cpp).
//
// The skater's pages hang off its AntDrivenComponent: + 0x68 is the animatable, which leads to
// the block of pages (known by its vtable) two ways, + 0xe8 -> + 0x8 and + 0x1d8 -> + 0x248;
// page n is at block + 0x60 + n * 8.
constexpr std::uintptr_t ant_driven_vtable = 0x63aaee8, states_block_vtable = 0x61a8bd8;
constexpr std::uintptr_t component_collection = 0x70, collection_count = 0x8, collection_first = 0x20, collection_step = 0x20;
constexpr std::uintptr_t driven_animatable = 0x68, block_pages = 0x60, page_map = 0x71c;
constexpr std::uintptr_t block_ways[2][2]{{0xe8, 0x8}, {0x1d8, 0x248}};
constexpr std::uintptr_t object_after_id = 0x10, object_name = 0x18, object_handle = 0x28;
constexpr std::uint32_t trick_count = 59;
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 (1) { return false; }
}
bool put(std::uintptr_t address, float value) noexcept {
__try {
*reinterpret_cast<volatile float *>(address) = value;
return true;
} __except (1) { return false; }
}
bool pointer(std::uintptr_t value) noexcept { return value > 0x10000 && value < 0x7fffffffffffull && !(value & 3); }
// The two states a flip trick's speed is driven by: the speed its clip plays at, which the game
// copies from the trick scripts' Float.Intent.TrickFlipSpeed once when the board is flicked
// (writing the copy in the air slows or speeds the flip; writing the scripts' number then does
// nothing, and nothing that sets the pop reads either), and the trick that is turning.
struct StateObject {
std::uint8_t id[16];
std::string_view name;
std::atomic<std::uintptr_t> at{};
};
StateObject flip_speed_object{{0xb0, 0x7b, 0x6c, 0x08, 0x42, 0xe7, 0xef, 0xc7, 0x08, 0x6c, 0x7b, 0xb0, 0xe7, 0x42, 0xc7, 0xef},
"Animation/Dingo/Float.Anim.TrickFlipSpeed"};
StateObject flip_trick_object{{0x17, 0x54, 0x40, 0x3c, 0xa7, 0x06, 0x61, 0x3d, 0x3c, 0x40, 0x54, 0x17, 0x06, 0xa7, 0x3d, 0x61},
"Animation/Dingo/EnumGS.Anim.CurrentFlipTrick"};
StateObject pumping_object{{0xab, 0x5b, 0xd5, 0x21, 0x9f, 0xa4, 0xb0, 0xfb, 0x21, 0xd5, 0x5b, 0xab, 0xa4, 0x9f, 0xfb, 0xb0}, "Animation/Dingo/Bool.Intent.Pumping"};
StateObject pump_strength_object{{0x54, 0xc6, 0x59, 0x6a, 0xbb, 0xad, 0xfc, 0x05, 0x6a, 0x59, 0xc6, 0x54, 0xad, 0xbb, 0x05, 0xfc},
"Animation/Dingo/Float.Intent.UserTriggeredPumpStrength"};
StateObject pump_assist_object{{0xcb, 0xdf, 0x5b, 0x53, 0x14, 0x53, 0xc8, 0x29, 0x53, 0x5b, 0xdf, 0xcb, 0x53, 0x14, 0x29, 0xc8},
"Animation/Dingo/Float.Anim.PumpingAssistAmount"};
StateObject *const objects[]{&flip_speed_object, &flip_trick_object, &pumping_object, &pump_strength_object, &pump_assist_object};
// The object behind an asset id seen at `id_at`, when it is that state's (its name says so).
bool is_object(const StateObject &object, std::uintptr_t id_at) noexcept {
__try {
if (std::memcmp(reinterpret_cast<const void *>(id_at), object.id, sizeof(object.id)) != 0) return false;
const auto name = *reinterpret_cast<const std::uintptr_t *>(id_at + object_after_id + object_name);
if (name <= id_at || name > id_at + 0x400) return false;
return _strnicmp(reinterpret_cast<const char *>(name), object.name.data(), object.name.size()) == 0 &&
*reinterpret_cast<const char *>(name + object.name.size()) == 0;
} __except (1) { return false; }
}
// Where the skater's value of that state is; 0 when it has none.
std::uintptr_t value_address(StateObject &object, std::uintptr_t block) noexcept {
const auto at = object.at.load(std::memory_order_relaxed);
if (!at) return 0;
if (!is_object(object, at - object_after_id)) {
object.at.store(0, std::memory_order_relaxed); // unloaded with its level
return 0;
}
std::uint32_t handle{}, map{};
std::uintptr_t page{};
if (!peek(at + object_handle, handle) || !(handle >> 24)) return 0;
const std::uint32_t index = handle & 0xfff, place = (handle >> 12) & 0x7ff;
if (index >= 16 || !peek(block + block_pages + index * 8, page) || !pointer(page) || !peek(page + page_map + (place >> 5) * 4, map) ||
!(map >> (place & 31) & 1))
return 0;
return page + place;
}
std::uintptr_t states_block(std::uintptr_t base, std::uintptr_t entity) noexcept {
std::uintptr_t collection{}, owner{};
std::uint8_t count{};
if (!entity || !peek(entity + component_collection, collection) || !pointer(collection) || !peek(collection, owner) || owner != entity ||
!peek(collection + collection_count, count) || count > 128)
return 0;
for (unsigned i = 0; i < count; ++i) {
std::uintptr_t component{}, type{}, animatable{};
if (!peek(collection + collection_first + i * collection_step, component) || !pointer(component) || !peek(component, type) ||
type != base + ant_driven_vtable)
continue;
if (!peek(component + driven_animatable, animatable) || !pointer(animatable)) return 0;
for (const auto &way : block_ways) {
std::uintptr_t holder{}, block{};
if (peek(animatable + way[0], holder) && pointer(holder) && peek(holder + way[1], block) && pointer(block) && peek(block, type) &&
type == base + states_block_vtable)
return block;
}
return 0;
}
return 0;
}
// EnumGS.Anim.CurrentFlipTrick, as the game names its values.
constexpr std::string_view game_trick_names[trick_count]{
"None", "Ollie", "Kickflip", "Heelflip", "PopShuvit", "VarialKickflip", "InwardHeelflip", "FSPopShuvit", "VarialHeelflip", "Hardflip",
"360PopShuvit", "360Flip", "360InwardHeelflip", "FS360PopShuvit", "Laserflip", "360Hardflip", "Nollie", "N_Kickflip", "N_Heelflip",
"N_PopShuvit", "N_Hardflip", "N_VarialHeelflip", "N_FSPopShuvit", "N_VarialKickflip", "N_InwardHeelflip", "N_FS360PopShuvit",
"N_360Hardflip", "N_Laserflip", "N_360PopShuvit", "N_360Flip", "N_360InwardHeelflip", "OlliePop", "N_OlliePop", "Wallie", "FSWallie",
"BSWallie", "Impossible", "FrontFoot_Impossible", "N_Impossible", "N_BackFoot_Impossible", "LateFlipTrick", "HeldFlipTrick", "BonedOllie",
"KickflipUnderflip", "HeelflipUnderflip", "BSPopShuvitUnderflip", "FSPopShuvitUnderflip", "NollieKickflipUnderflip",
"NollieHeelflipUnderflip", "NollieBSPopShuvitUnderflip", "NollieFSPopShuvitUnderflip", "HospitalFlip", "FlowerFlip", "NollieHospitalFlip",
"NollieFlowerFlip", "ImpossibleUnderflip", "FrontFootImpossibleUnderflip", "NollieImpossibleUnderflip", "NollieBackFootImpossibleUnderflip"};
// Index into flip_tricks for each value, -1 where a trick has no speed of its own.
constexpr std::int8_t trick_slider[trick_count]{
-1, -1, 0, 1, 2, 6, 11, 3, 7, 10,
4, 8, 13, 5, 9, 12, -1, 0, 1,
2, 10, 7, 3, 6, 11, 5,
12, 9, 4, 8, 13, -1, -1, -1, -1,
-1, 14, 15, 14, 15, -1, -1, -1,
-1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1};
}
void find_state_objects(std::uintptr_t start, std::size_t size) noexcept {
__try {
const auto *words = reinterpret_cast<const std::uint32_t *>(start);
for (std::size_t i = 0; i + 0x20 < size / 4; ++i)
for (auto *object : objects) {
std::uint32_t first;
std::memcpy(&first, object->id, 4);
if (words[i] == first && !object->at.load(std::memory_order_relaxed) && is_object(*object, start + i * 4))
object->at.store(start + i * 4 + object_after_id, std::memory_order_relaxed);
}
} __except (1) {}
}
bool flip_states_found() noexcept {
return flip_speed_object.at.load(std::memory_order_relaxed) && flip_trick_object.at.load(std::memory_order_relaxed);
}
FlipState read_flip_state(std::uintptr_t base, std::uintptr_t entity) noexcept {
if (!flip_states_found()) return {};
const auto block = states_block(base, entity);
if (!block) return {};
FlipState state;
const auto speed_at = value_address(flip_speed_object, block), trick_at = value_address(flip_trick_object, block);
if (!speed_at || !trick_at || !peek(trick_at, state.trick) || !peek(speed_at, state.speed)) return {};
// Anything else there means this is not the layout this was written for.
if (state.trick >= trick_count || !std::isfinite(state.speed) || state.speed < 0 || state.speed > 1000) return {};
state.speed_at = speed_at;
return state;
}
PumpState read_pump_state(std::uintptr_t base, std::uintptr_t entity) noexcept {
PumpState state;
if (!pumping_object.at.load(std::memory_order_relaxed) && !pump_strength_object.at.load(std::memory_order_relaxed)) return state;
const auto block = states_block(base, entity);
if (!block) return state;
std::uint8_t flag{};
float number{};
if (const auto at = value_address(pumping_object, block); at && peek(at, flag)) {
state.found = true;
state.pumping = flag != 0;
}
if (const auto at = value_address(pump_strength_object, block); at && peek(at, number) && std::isfinite(number)) state.strength = number;
if (const auto at = value_address(pump_assist_object, block); at && peek(at, number) && std::isfinite(number)) state.assist = number;
return state;
}
std::array<std::uintptr_t, 6> state_pages(std::uintptr_t base, std::uintptr_t entity) noexcept {
std::array<std::uintptr_t, 6> pages{};
if (const auto block = states_block(base, entity))
for (std::size_t k = 0; k < pages.size(); ++k)
if (!peek(block + block_pages + k * 8, pages[k]) || !pointer(pages[k])) pages[k] = 0;
return pages;
}
bool write_flip_speed(const FlipState &state, float speed) noexcept {
return state && std::isfinite(speed) && put(state.speed_at, speed);
}
int flip_trick_index(std::uint32_t trick) noexcept { return trick < trick_count ? trick_slider[trick] : -1; }
std::string_view flip_trick_name(std::uint32_t trick) noexcept { return trick < trick_count ? game_trick_names[trick] : std::string_view{}; }
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <array>
#include <cstdint>
#include <string_view>
namespace dingosdk::trainer {
// The animation system's game states the trainer reads and writes for the local skater.
//
// A flip trick's clip plays at Float.Anim.TrickFlipSpeed, a number the game sets once when the
// board is flicked. The trainer multiplies that number while the trick is in the air: nothing
// that sets the pop reads it, and it works for every trick (EnumGS.Anim.CurrentFlipTrick says
// which one is turning).
struct FlipState {
std::uintptr_t speed_at{}; // where the live flip speed is; 0: not found
std::uint32_t trick{}; // CurrentFlipTrick
float speed{};
explicit operator bool() const noexcept { return speed_at != 0; }
};
FlipState read_flip_state(std::uintptr_t base, std::uintptr_t entity) noexcept;
// The state objects are loaded with a level: the memory search (trainer_classes.cpp) hands
// every region it reads to this, which keeps the ones it knows. True once both are known.
void find_state_objects(std::uintptr_t start, std::size_t size) noexcept;
bool flip_states_found() noexcept;
// The skater's pages of game states (0 where there is none).
std::array<std::uintptr_t, 6> state_pages(std::uintptr_t base, std::uintptr_t entity) noexcept;
bool write_flip_speed(const FlipState &state, float speed) noexcept;
// Pumping a transition, as the game's trick scripts see it: whether the skater counts as pumping
// this tick and how hard (Bool.Intent.Pumping, Float.Intent.UserTriggeredPumpStrength,
// Float.Anim.PumpingAssistAmount).
struct PumpState {
bool found{}; // the states are known and the skater has them
bool pumping{};
float strength{}, assist{};
};
PumpState read_pump_state(std::uintptr_t base, std::uintptr_t entity) noexcept;
// The tricks that have a speed of their own under "Advanced trick speed". A nollie turns at
// its regular trick's speed.
struct FlipTrick {
std::string_view key; // the option and preset name
std::string_view label; // in the menu
};
inline constexpr std::array<FlipTrick, 16> flip_tricks{{
{"flip.kickflip", "Kickflip"},
{"flip.heelflip", "Heelflip"},
{"flip.popshuvit", "Pop shuvit"},
{"flip.fspopshuvit", "FS pop shuvit"},
{"flip.360popshuvit", "360 pop shuvit"},
{"flip.fs360popshuvit", "FS 360 pop shuvit"},
{"flip.varialkickflip", "Varial kickflip"},
{"flip.varialheelflip", "Varial heelflip"},
{"flip.360flip", "360 flip"},
{"flip.laserflip", "Laser flip"},
{"flip.hardflip", "Hardflip"},
{"flip.inwardheelflip", "Inward heelflip"},
{"flip.360hardflip", "360 hardflip"},
{"flip.360inwardheelflip", "360 inward heelflip"},
{"flip.impossible", "Impossible"},
{"flip.frontfootimpossible", "Front foot impossible"},
}};
// Which of those a CurrentFlipTrick value belongs to; -1: none (an ollie, a wallie, an underflip).
int flip_trick_index(std::uint32_t trick) noexcept;
// The name the game gives a CurrentFlipTrick value ("Kickflip", "N_360Flip"); empty when unknown.
std::string_view flip_trick_name(std::uint32_t trick) noexcept;
}
+81 -134
View File
@@ -31,13 +31,15 @@ struct Page {
std::array<char, 49> preset_name{};
std::array<float, 3> teleport{};
double speed_edit{-1}, speed_until{};
float hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1};
float hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1}, pump_edit{1};
std::array<float, trainer::flip_tricks.size()> trick_edit{}; // the speed of each flip trick
std::array<bool, trainer::flip_tricks.size()> trick_editing{};
float revert_edit{};
bool revert_editing{};
// The Tricklining list: rows of the value table per group, for one snapshot.
std::array<std::vector<std::size_t>, 5> trick_rows;
std::uint64_t trick_revision{~0ull};
bool hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{};
bool hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{}, pump_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{};
@@ -196,7 +198,7 @@ void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trai
if (ImGui::IsItemHovered())
wrapped_tooltip("%s\n1 is the game's own; \"%s\" is x %s.", preset.note.c_str(), preset.name.c_str(), number(preset.amount).c_str());
ImGui::SameLine(column);
const float button = ImGui::CalcTextSize("Fast Parkour Flips").x + ImGui::GetStyle().FramePadding.x * 2;
const float button = ImGui::CalcTextSize("Easy Body Flips ").x + ImGui::GetStyle().FramePadding.x * 2;
const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
const auto id = "dial:" + preset.name;
double value = p.active == id ? p.active_value : preset.factor;
@@ -245,7 +247,6 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
end_card();
return;
}
if (!view.editable) warn(view.blocked.c_str());
// Two short lists, the trick line section and the whole table (modes 0, 1, 3 and 2).
{
static constexpr std::pair<const char *, int> modes[]{{"REALISTIC", 0}, {"FUN", 1}, {"TRICKLINING", 3}, {"EVERYTHING", 2}};
@@ -441,14 +442,15 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
}
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", "Let slow flips stay slow (no finish before landing)", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height",
static constexpr const char *advanced_label = "Advanced trick speed (a speed for each flip trick: kickflip, heelflip, 360 flip, varial, shuvit, hardflip, laser flip, impossible)";
static constexpr const char *labels[]{"Flip trick speed", advanced_label, "Pump power (pumping transitions)", "Catch flips at a set point of the jump (realism, percent of air time)", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height",
"Board bending boost (revert speed)"};
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, const char *help, float high = 50.0f, bool limited = false) {
if (!wanted(label)) return false;
const auto slider = [&](const char *label, const char *option, float value, float &edit, bool &editing, const char *help, float high = 50.0f, bool limited = false, bool always = false) {
if (!always && !wanted(label)) return false;
field(menu, label);
ImGui::PushID(option);
float shown = editing ? edit : value;
@@ -474,24 +476,60 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
return true;
};
slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing,
"How fast the board turns in a flip trick. It does not change how high you pop. Below 1 a flip is slower. On its own the game still "
"hurries a slow flip so it is finished before you land: tick the switch below to stop that. Above 1 the game's own limit on how fast a "
"board turns takes over.",
"How fast the board turns in every flip trick. It does not change how high you pop. Below 1 flips are slower, above 1 faster. A slow "
"flip is slow from the start and is not hurried round before you land: with too little air you land on a board that is still turning. "
"It takes about 15 seconds to take hold after every level load: the trainer has to find the game's flip animations again first.",
3.0f);
if (wanted("Let slow flips stay slow (no finish before landing)")) {
bool slow = !view.flip_gate;
if (ImGui::Checkbox("Let slow flips stay slow", &slow)) trainer_command(menu, callbacks, std::format("option flip_gate {}", slow ? 0 : 1));
if (!view.flip_live && (view.flip_speed != 1.0f || view.flip_advanced) && wanted("Flip trick speed"))
ImGui::TextDisabled("Finding the flip animations: flip speeds take hold about 15 seconds after a level loads.");
if (wanted(advanced_label)) {
bool on = view.flip_advanced;
if (ImGui::Checkbox("Advanced trick speed", &on)) trainer_command(menu, callbacks, std::format("option flip_advanced {}", on ? 1 : 0));
tip("Gives every flip trick a speed of its own, on top of the flip trick speed above: a kickflip at 1.0 and a 360 flip at 0.6, say. "
"A trick turns at the speed above times its own. Nollie tricks use their regular trick's speed. Pop height is not changed. "
"Unticked, the speeds below are kept but not used. Like the speed above, they take hold about 15 seconds after every level load.");
if (view.flip_advanced_blocked) ImGui::TextDisabled("Off in this multiplayer game: the host's rules apply here.");
if (on && ImGui::TreeNodeEx("Speed of each flip trick", ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_DefaultOpen)) {
for (std::size_t i = 0; i < trainer::flip_tricks.size(); ++i) {
const auto &trick = trainer::flip_tricks[i];
const std::string label(trick.label), option(trick.key);
slider(label.c_str(), option.c_str(), view.flip_trick[i], p.trick_edit[i], p.trick_editing[i],
"How fast the board turns in this trick, times the flip trick speed above. 1 leaves it at that speed.", 3.0f, false, true);
}
ImGui::TreePop();
}
}
if (wanted("Catch flips at a set point of the jump (realism, percent of air time)")) {
bool on = view.catch_at;
if (ImGui::Checkbox("Catch flips at a set point of the jump", &on)) trainer_command(menu, callbacks, std::format("option catch_at {}", on ? 1 : 0));
if (ImGui::IsItemHovered())
wrapped_tooltip("%s", "The game speeds a flip up so the board is round 1/6 of a second before the landing it predicts, however slow the flip speed "
"is set. That is why a slowed flip still comes round on a small pop. Ticked: that rule is off. The board turns as "
"slowly as you set it and lands however far round it got, so a flip you start too late or too low is a bail.");
if (view.flip_gate_blocked) ImGui::TextDisabled("Off in this session: the host's physics rules apply here, so flips finish before landing as usual.");
else if (slow && !view.flip_gate_found) ImGui::TextDisabled("Not found in the game yet: it takes effect a few seconds after a level loads.");
wrapped_tooltip("%s", "For realism. Every flip trick turns so that the board is round, and caught, at the percent of the jump's air time "
"you set: 70 means the catch comes 70% of the way from the pop to the landing, on a small ollie and a big one alike. "
"While this is on the trainer sets the flip speed and the finish-before-landing rule itself, so the flip speed "
"above stands aside. The air time is worked out from the pop over level ground: off a drop or a gap the catch "
"comes earlier than asked. 100 or close to it means you land still flipping.");
ImGui::SameLine();
float percent = view.catch_percent;
ImGui::SetNextItemWidth(px(64));
if (ImGui::InputFloat("##catch-percent", &percent, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue))
trainer_command(menu, callbacks, std::format("option catch_percent {}", percent));
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", "Percent of the air time at which the flip is caught. Type a number from 1 to 100 and press Enter.");
ImGui::SameLine();
ImGui::TextUnformatted("% of the air time");
if (view.catch_at && view.flip_gate_blocked) ImGui::TextDisabled("Off in this multiplayer game: the host's rules apply here.");
else if (on && !view.flip_gate_found) ImGui::TextDisabled("Not found in the game yet: it takes effect a few seconds after a level loads.");
}
slider("No comply height", "nocomply_height", view.nocomply_height, p.nocomply_edit, p.nocomply_editing, "How high a no comply pops. 1 is the game's own.");
slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing, "How high a boneless pops. 1 is the game's own.");
slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing, "How high you jump off the board in a hippy jump. 1 is the game's own.");
slider("Off-board jump height", "offboard_height", view.offboard_height, p.offboard_edit, p.offboard_editing, "How high you jump on foot. 1 is the game's own.");
if (slider("Pump power (pumping transitions)", "pump_power", view.pump_power, p.pump_edit, p.pump_editing,
"How much speed pumping a transition gives. 1 is the game's own; 2 gives about one more of the game's pumps on top of each "
"of yours, 10 about nine more, and under 1 takes speed away. The game still decides when you are pumping: the extra comes "
"for as long as it counts you as pumping. It is found in the game about 15 seconds after a level loads.")) {
if (view.pump_blocked) ImGui::TextDisabled("Off in this multiplayer game: the host's rules apply here.");
else if (view.pump_power != 1.0f && !view.pump_live) ImGui::TextDisabled("Finding the game's pumping in memory: it acts about 15 seconds after a level loads.");
}
if (wanted("Board bending boost (revert speed)")) {
// Not a multiplier: 0 is the game's own (no boost).
field(menu, "Board bending boost");
@@ -522,7 +560,8 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
}
if (ImGui::Button("Reset tricks")) {
p.revert_editing = false;
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false;
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.pump_editing = false;
p.trick_editing = {};
trainer_command(menu, callbacks, "reset tricks");
}
tip("Puts every slider and switch on this card back to the game's own.");
@@ -809,114 +848,6 @@ void trickline_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, c
ImGui::EndDisabled();
}
// 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()) wrapped_tooltip("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()) wrapped_tooltip("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");
tip("Puts all three of the game's cameras back as they shipped.");
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;
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x));
if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera rig {} {} {:.3f}", rig_ids[i], field, shown));
if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) wrapped_tooltip("%s", tip);
ImGui::SameLine();
float typed = value;
ImGui::SetNextItemWidth(px(64));
if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue))
trainer_command(menu, callbacks, std::format("camera {} {} {} {}", "rig", rig_ids[i], field, typed));
if (ImGui::IsItemHovered()) wrapped_tooltip("Type any number and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemActive()) {
p.active = key;
p.active_value = shown;
} else if (p.active == key) {
p.active.clear();
}
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()) wrapped_tooltip("%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");
tip("Copies the framing measured from the game's camera into your camera's sliders below.");
}
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;
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x));
if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera set {} {} {:.3f}", which, field, shown));
if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) wrapped_tooltip("%s", tip);
ImGui::SameLine();
float typed = value;
ImGui::SetNextItemWidth(px(64));
if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue))
trainer_command(menu, callbacks, std::format("camera {} {} {} {}", "set", which, field, typed));
if (ImGui::IsItemHovered()) wrapped_tooltip("Type any number and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemActive()) {
p.active = key;
p.active_value = shown;
} else if (p.active == key) {
p.active.clear();
}
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();
@@ -982,7 +913,7 @@ 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 == 7 ? 2 : view->open_tab == 3 ? 3 : view->open_tab == 2 ? 1 : 0; // by name, see `trainer open`
p.tab = view->open_tab == 3 ? 2 : view->open_tab == 2 ? 1 : 0; // by name, see `trainer open`
if (view->open_tab == 8) p.mode = 3; // the trick line section of the Tune tab
if (view->open_tab >= 4 && view->open_tab < 7) p.mode = std::clamp(view->open_tab - 4, 0, 2);
p.show_page = true;
@@ -997,16 +928,21 @@ bool trainer_page_wanted() {
void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) {
auto &p = page();
const auto view = trainer::view();
ImGui::BeginDisabled(!callbacks.queue_console_command);
// A guest under the host's physics would only wipe a setup they cannot see at work here.
ImGui::BeginDisabled(!callbacks.queue_console_command || view->session_enforced);
if (ImGui::Button("RESET EVERYTHING")) {
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.revert_editing = false;
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.pump_editing = p.revert_editing = false;
p.trick_editing = {};
trainer_command(menu, callbacks, "reset everything");
}
tip("Puts back every value, lock, preset, dial, trick setting and camera: the game exactly as it shipped.");
tip("Puts back every value, lock, preset, dial, trick setting: the game exactly as it shipped.");
ImGui::EndDisabled();
if (view->session_enforced && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
wrapped_tooltip("%s", "Off in this multiplayer game: the host controls physics here. Your own setup is kept for when you leave.");
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
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.");
if (view->session_enforced) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "HOST'S PHYSICS: your own setup is kept for when you leave.");
else 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.
@@ -1017,13 +953,24 @@ void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
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");
// Above the tabs, so every tab says why its controls do nothing in this session.
if (view->session_enforced) {
begin_card(menu, "session-note", "MULTIPLAYER: THE HOST CONTROLS PHYSICS");
warn("You are in a multiplayer game where the host controls physics. This menu will not work in this multiplayer game.");
note("You skate with the host's setup. HUD and markers still work. Your own setup comes back when you leave.");
end_card();
} else if (view->boosts_blocked) {
begin_card(menu, "session-note", "MULTIPLAYER: BOOSTS ARE OFF");
warn("The host has turned boosts off in this multiplayer game: trick sliders, flip speed, auto push and the board bending boost will not work here.");
note("The physics values still apply. The rest comes back when the host allows boosts or you leave.");
end_card();
}
category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "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();
-6
View File
@@ -211,7 +211,6 @@ PresetDial preset_dial(std::string_view name) {
{"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},
@@ -219,7 +218,6 @@ PresetDial preset_dial(std::string_view name) {
{"Slick Grinds", "Grind friction", both},
{"Moon Jump", "On foot: jump height", both},
{"Fast On Foot", "On foot: sprint speed", both},
{"Fast Parkour Flips", "On foot: flip and roll speed", f},
{"Super Glide", "Glide: gravity (lower falls slower)", f},
{"Torpedo Boost", "Torpedo and falling: steering", f},
{"Realistic", "", r},
@@ -283,8 +281,6 @@ const std::vector<BuiltinPreset> &builtin_presets() {
{{"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}}},
@@ -308,8 +304,6 @@ const std::vector<BuiltinPreset> &builtin_presets() {
{{"jump.basejumpheight", true, 3.0}, {"jump.maxjumpheight", true, 3.0}, {"jump.maxjumpvelocity", true, 2.0}}},
{"Fast On Foot", "On foot: sprint nearly twice as fast.",
{{"locomotion.sprintspeed", true, 1.8}, {"locomotion.sprintspeedboost", true, 1.8}}},
{"Fast Parkour Flips", "On foot: flips and rolls rotate and travel much faster.",
{{"flumping.flumptuckmaxrotationvelocity", true, 2.5}, {"flumping.rollmaxvelocity", true, 2.0}, {"flumping.rollvelocitymultiplier", true, 1.5}}},
{"Super Glide", "Spread-eagle falls slowly and steers hard.",
{{"wipeout.spreadeaglegravity", true, 0.35}, {"wipeout.spreadeagleaircontrolmax", true, 2.0}, {"wipeout.spreadeagleaircontrolmin", true, 2.0}}},
{"Torpedo Boost", "Torpedo steers and carries much harder, and so does a plain fall.",
+2
View File
@@ -13,7 +13,9 @@
namespace dingosdk::trainer {
// What the trick multipliers may be, here and in the trainer's own options.
inline constexpr float height_low = 0.001f, height_high = 1.0e6f; // heights; the sliders stop far short, a typed number need not
inline constexpr float catch_low = 1.0f, catch_high = 100.0f; // where in a jump a flip is caught, percent of the air time
inline constexpr float flip_low = 0.01f, flip_high = 100.0f; // board flip speed; the slider stops far short, a typed number need not
inline constexpr float pump_high = 1000.0f; // pump power; 1 is the game's own
inline constexpr float revert_boost_high = 1000.0f; // revert boost strength; 0 is off
inline constexpr float cruise_high = 100.0f; // auto push, m/s
-20
View File
@@ -1009,26 +1009,6 @@ 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 -1
View File
@@ -113,8 +113,8 @@ 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_gamestate.cpp
Extension/Trainer/trainer_commands.cpp
Extension/Trainer/trainer_session.cpp
Extension/Skater/offboard_flight.cpp