From 614f07e819a6bd21bfe110d765a294be30b83005 Mon Sep 17 00:00:00 2001 From: andrewnakas Date: Wed, 7 Oct 2026 02:07:04 -0600 Subject: [PATCH] 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) --- Extension/Multiplayer/Hud/follow_camera.cpp | 69 --- Extension/Multiplayer/Hud/follow_camera.h | 27 - Extension/Multiplayer/Hud/native_party.h | 2 - .../Multiplayer/Hud/native_party_hooks.cpp | 4 - Extension/Skater/client_noclip.cpp | 129 ++++- Extension/Skater/client_source_spawn.h | 14 + Extension/Skater/skater_observer.cpp | 1 + Extension/Skater/skater_observer.h | 2 + Extension/Trainer/CHANGELOG.md | 44 +- Extension/Trainer/README.md | 13 +- Extension/Trainer/trainer.cpp | 482 +++++++++--------- Extension/Trainer/trainer.h | 24 +- Extension/Trainer/trainer_camera.cpp | 227 --------- Extension/Trainer/trainer_camera.h | 31 -- Extension/Trainer/trainer_classes.cpp | 77 ++- Extension/Trainer/trainer_classes.h | 9 +- Extension/Trainer/trainer_commands.cpp | 1 - Extension/Trainer/trainer_gamestate.cpp | 187 +++++++ Extension/Trainer/trainer_gamestate.h | 67 +++ Extension/Trainer/trainer_page.cpp | 215 +++----- Extension/Trainer/trainer_presets.cpp | 6 - Extension/Trainer/trainer_session.h | 2 + Runtime/client_tick.cpp | 20 - cmake/Runtime.cmake | 2 +- 24 files changed, 839 insertions(+), 816 deletions(-) delete mode 100644 Extension/Trainer/trainer_camera.cpp delete mode 100644 Extension/Trainer/trainer_camera.h create mode 100644 Extension/Trainer/trainer_gamestate.cpp create mode 100644 Extension/Trainer/trainer_gamestate.h diff --git a/Extension/Multiplayer/Hud/follow_camera.cpp b/Extension/Multiplayer/Hud/follow_camera.cpp index e40b37e6..71e056ac 100644 --- a/Extension/Multiplayer/Hud/follow_camera.cpp +++ b/Extension/Multiplayer/Hud/follow_camera.cpp @@ -34,12 +34,6 @@ struct State { Clock::time_point at; float hx{}, hz{1}; // direction of travel, eased std::array last{}; - // The player's own camera. - CustomCamera custom; - Clock::time_point custom_at; - float custom_hx{}, custom_hz{1}; - std::array custom_last{}, custom_eye{}; - bool custom_running{}; }; State &state() { static auto *value = new State; @@ -132,67 +126,4 @@ std::array 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> 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 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 back{-hx * cp, -sp, -hz * cp}; - std::array 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 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{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 diff --git a/Extension/Multiplayer/Hud/follow_camera.h b/Extension/Multiplayer/Hud/follow_camera.h index 11ccbbd6..9faf0f3e 100644 --- a/Extension/Multiplayer/Hud/follow_camera.h +++ b/Extension/Multiplayer/Hud/follow_camera.h @@ -3,7 +3,6 @@ #include "Extension/Multiplayer/Net/protocol.h" #include #include -#include // 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 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 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> 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 diff --git a/Extension/Multiplayer/Hud/native_party.h b/Extension/Multiplayer/Hud/native_party.h index bf2d687d..856e908b 100644 --- a/Extension/Multiplayer/Hud/native_party.h +++ b/Extension/Multiplayer/Hud/native_party.h @@ -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(); diff --git a/Extension/Multiplayer/Hud/native_party_hooks.cpp b/Extension/Multiplayer/Hud/native_party_hooks.cpp index dff2db4b..f74c4192 100644 --- a/Extension/Multiplayer/Hud/native_party_hooks.cpp +++ b/Extension/Multiplayer/Hud/native_party_hooks.cpp @@ -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; diff --git a/Extension/Skater/client_noclip.cpp b/Extension/Skater/client_noclip.cpp index c94961bb..a9869468 100644 --- a/Extension/Skater/client_noclip.cpp +++ b/Extension/Skater/client_noclip.cpp @@ -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 factor{1}; + std::atomic pumping{}, measure{}; + std::uintptr_t client{}, entity{}; + std::atomic expires{}; + std::atomic pumps{}; + std::atomic gained{}, given{}, speed{}; + std::atomic 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, 32> velocities{}; + std::array flags{}; + const auto count = std::min(bodies.parts.size(), velocities.size()); + for (std::size_t i = 0; i < count; ++i) { + velocities[i] = reader.value>(bodies.parts[i], 0x70); + flags[i] = reader.value(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); diff --git a/Extension/Skater/client_source_spawn.h b/Extension/Skater/client_source_spawn.h index 3e91b957..9befcec2 100644 --- a/Extension/Skater/client_source_spawn.h +++ b/Extension/Skater/client_source_spawn.h @@ -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. diff --git a/Extension/Skater/skater_observer.cpp b/Extension/Skater/skater_observer.cpp index 9f8fee92..b1e436a7 100644 --- a/Extension/Skater/skater_observer.cpp +++ b/Extension/Skater/skater_observer.cpp @@ -102,6 +102,7 @@ bool fingerprint(std::uintptr_t address, const std::array& 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(); diff --git a/Extension/Skater/skater_observer.h b/Extension/Skater/skater_observer.h index 78d015df..f1b8195f 100644 --- a/Extension/Skater/skater_observer.h +++ b/Extension/Skater/skater_observer.h @@ -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; } diff --git a/Extension/Trainer/CHANGELOG.md b/Extension/Trainer/CHANGELOG.md index 7755f0b8..2248e39c 100644 --- a/Extension/Trainer/CHANGELOG.md +++ b/Extension/Trainer/CHANGELOG.md @@ -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.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 `, 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 diff --git a/Extension/Trainer/README.md b/Extension/Trainer/README.md index fe66858e..51055387 100644 --- a/Extension/Trainer/README.md +++ b/Extension/Trainer/README.md @@ -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 `, -`trainer find `, `trainer preset apply|remove|export `, `trainer preset import`, `trainer camera rig low|high|foot distance|height|raise|side `, `trainer dial `, +Everything is also a console command (`~`): `trainer open [tune|practice|map|realistic|fun|everything]`, `trainer status`, `trainer set `, +`trainer find `, `trainer preset apply|remove|export `, `trainer preset import`, `trainer dial `, `trainer reset |all|tricks|presets|everything`, `trainer marker save|go|clear [slot]`, `trainer tp `, `trainer where`, `trainer jumps`, `trainer dump`, `trainer selftest`. diff --git a/Extension/Trainer/trainer.cpp b/Extension/Trainer/trainer.cpp index 86ceb958..15adbdf4 100644 --- a/Extension/Trainer/trainer.cpp +++ b/Extension/Trainer/trainer.cpp @@ -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 #include @@ -30,7 +31,6 @@ #include #include - // 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 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 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 flip_trick = [] { + std::array 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 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 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(); - 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(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 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(value), catch_low, catch_high); if (auto *option = trick_option(name)) - *option = name == "flip_speed" ? std::clamp(static_cast(value), flip_low, flip_high) + *option = name == "flip_speed" || name.starts_with("flip.") ? std::clamp(static_cast(value), flip_low, flip_high) : name == "revert_boost" ? std::clamp(static_cast(value), 0.0f, revert_boost_high) + : name == "pump_power" ? std::clamp(static_cast(value), 0.0f, pump_high) : std::clamp(static_cast(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(s.entity)); + s.flip_live = static_cast(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(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(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(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{} && !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(); } @@ -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(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(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(top_speed->value / top_speed->stock)) : 1.0f; + set_push_speed(client, s.entity, push_factor, top_speed ? static_cast(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(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 | reset: what counts as a revert and what it is worth. std::string revert_command(const std::vector &a) { @@ -1882,7 +1929,10 @@ std::string feel_command(const std::vector &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 &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 `, or a preset for all three. -std::string rig_command(const std::vector &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(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 , or camera rig stock|sunjay|low+"; - *field = static_cast(*value); - auto &rig = s.rigs[static_cast(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 | set board|foot -std::string camera_command(const std::vector &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 "; - *field = static_cast(*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 | set board|foot "; -} std::string run(std::string_view verb, const std::vector &a) { auto &s = state(); const auto v = lower(verb); @@ -1996,8 +1971,10 @@ std::string run(std::string_view verb, const std::vector &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 &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 &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 &a) { } return "error: usage: trainer preset apply|save|delete "; } - 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 &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(*value), flip_low, flip_high); + *option = std::clamp(static_cast(*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(*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(*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(*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(*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(*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 &a) { // The last three are the Tune tab on one of its lists. const std::array 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(found - tabs.begin()); ++s.open_serial; s.view_due = true; diff --git a/Extension/Trainer/trainer.h b/Extension/Trainer/trainer.h index 7207e5d4..00ba63c2 100644 --- a/Extension/Trainer/trainer.h +++ b/Extension/Trainer/trainer.h @@ -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 #include @@ -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 rows; std::vector groups; std::vector 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 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//trainer.json). std::string map, map_note, map_preset, profile_preset; std::vector spots; // `trainer open `: 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 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{}; diff --git a/Extension/Trainer/trainer_camera.cpp b/Extension/Trainer/trainer_camera.cpp deleted file mode 100644 index 6e670b29..00000000 --- a/Extension/Trainer/trainer_camera.cpp +++ /dev/null @@ -1,227 +0,0 @@ -#include "trainer_camera.h" -#include "Engine/Core/Log/logging.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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 stock{}; - float pitch{}; // radians: what a framing must still hold beside its numbers -}; -struct State { - std::mutex mutex; - std::array wanted{}; - std::vector found; - std::atomic 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 bool peek(std::uintptr_t address, T &value) noexcept { - __try { - std::memcpy(&value, reinterpret_cast(address), sizeof(T)); - return true; - } __except (EXCEPTION_EXECUTE_HANDLER) { - return false; - } -} -bool put(std::uintptr_t address, float value) noexcept { - __try { - *reinterpret_cast(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(start); - const auto *floats = reinterpret_cast(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 fresh; - const auto started = GetTickCount64(); - MEMORY_BASIC_INFORMATION info{}; - std::array indices{}; - std::array hits{}; - for (std::uintptr_t at = 0x10000; at < 0x7fffffff0000ull && VirtualQuery(reinterpret_cast(at), &info, sizeof(info)) == sizeof(info); - at = reinterpret_cast(info.BaseAddress) + info.RegionSize) { - if (info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE || info.Type == MEM_IMAGE) continue; - const auto start = reinterpret_cast(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 &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(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 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 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(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 diff --git a/Extension/Trainer/trainer_camera.h b/Extension/Trainer/trainer_camera.h deleted file mode 100644 index d723ddcb..00000000 --- a/Extension/Trainer/trainer_camera.h +++ /dev/null @@ -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 -#include -#include -#include - -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(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 &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 diff --git a/Extension/Trainer/trainer_classes.cpp b/Extension/Trainer/trainer_classes.cpp index bf6d129b..b955c414 100644 --- a/Extension/Trainer/trainer_classes.cpp +++ b/Extension/Trainer/trainer_classes.cpp @@ -1,4 +1,5 @@ #include "trainer_classes.h" +#include "trainer_gamestate.h" #include "Engine/Core/Log/logging.h" #include #include @@ -82,8 +83,9 @@ struct Found { std::vector 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 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, mirror_count> mirror_fields{}; @@ -300,12 +302,19 @@ std::vector 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(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(address); + return word == bits(gate_stock) || word == bits(gate_off); } void collect_gates(std::uintptr_t start, std::size_t size, std::vector &gates) noexcept { __try { @@ -320,7 +329,7 @@ bool gate_still_there(std::uintptr_t address) noexcept { if (VirtualQuery(reinterpret_cast(address), &info, sizeof(info)) != sizeof(info) || info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE) return false; const auto low = reinterpret_cast(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 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(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 diff --git a/Extension/Trainer/trainer_classes.h b/Extension/Trainer/trainer_classes.h index 21e2388a..db4bcee6 100644 --- a/Extension/Trainer/trainer_classes.h +++ b/Extension/Trainer/trainer_classes.h @@ -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. diff --git a/Extension/Trainer/trainer_commands.cpp b/Extension/Trainer/trainer_commands.cpp index 0e1f4371..6c9a7439 100644 --- a/Extension/Trainer/trainer_commands.cpp +++ b/Extension/Trainer/trainer_commands.cpp @@ -21,7 +21,6 @@ void register_trainer_commands(Commands ®istry) { {"revert", "What counts as a revert and what its speed boost is worth", " | 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 | set board|foot ", false}, {"dial", "Turn a built-in preset up or down: 1 is the game's own", " ", false}, {"slot", "Select a marker slot", "<1-5>", false}, {"marker", "Save, go to or clear a marker", "save|go|clear [slot]", false}, diff --git a/Extension/Trainer/trainer_gamestate.cpp b/Extension/Trainer/trainer_gamestate.cpp new file mode 100644 index 00000000..0e1b5d91 --- /dev/null +++ b/Extension/Trainer/trainer_gamestate.cpp @@ -0,0 +1,187 @@ +#include "trainer_gamestate.h" +#include +#include +#include + +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 bool peek(std::uintptr_t address, T &value) noexcept { + __try { + std::memcpy(&value, reinterpret_cast(address), sizeof(T)); + return true; + } __except (1) { return false; } +} +bool put(std::uintptr_t address, float value) noexcept { + __try { + *reinterpret_cast(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 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(id_at), object.id, sizeof(object.id)) != 0) return false; + const auto name = *reinterpret_cast(id_at + object_after_id + object_name); + if (name <= id_at || name > id_at + 0x400) return false; + return _strnicmp(reinterpret_cast(name), object.name.data(), object.name.size()) == 0 && + *reinterpret_cast(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(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 state_pages(std::uintptr_t base, std::uintptr_t entity) noexcept { + std::array 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{}; } +} diff --git a/Extension/Trainer/trainer_gamestate.h b/Extension/Trainer/trainer_gamestate.h new file mode 100644 index 00000000..f752c992 --- /dev/null +++ b/Extension/Trainer/trainer_gamestate.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +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 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 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; +} diff --git a/Extension/Trainer/trainer_page.cpp b/Extension/Trainer/trainer_page.cpp index ff026133..9446d5ea 100644 --- a/Extension/Trainer/trainer_page.cpp +++ b/Extension/Trainer/trainer_page.cpp @@ -31,13 +31,15 @@ struct Page { std::array preset_name{}; std::array 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 trick_edit{}; // the speed of each flip trick + std::array trick_editing{}; float revert_edit{}; bool revert_editing{}; // The Tricklining list: rows of the value table per group, for one snapshot. std::array, 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 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(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(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(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(); diff --git a/Extension/Trainer/trainer_presets.cpp b/Extension/Trainer/trainer_presets.cpp index 53f37d0b..beadd5a7 100644 --- a/Extension/Trainer/trainer_presets.cpp +++ b/Extension/Trainer/trainer_presets.cpp @@ -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 &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 &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.", diff --git a/Extension/Trainer/trainer_session.h b/Extension/Trainer/trainer_session.h index e6fc0f56..89ab4bd5 100644 --- a/Extension/Trainer/trainer_session.h +++ b/Extension/Trainer/trainer_session.h @@ -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 diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index 60702833..7d3f17a7 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -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> 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 diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index ea7b5896..cf618742 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -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