From c2d86f2ffa35d029ab83de8a976f69a83eb53df1 Mon Sep 17 00:00:00 2001 From: andrewnakas Date: Mon, 19 Oct 2026 23:20:59 -0600 Subject: [PATCH] Trainer 0.8.2: Underflip speed, Underflip part only, a speed for each underflip - Underflip speed: the game holds the underflip's two playback rate controllers (cdbchooser_onboard_underflips_(pbrate) 1-1.2, ..._toair(pbrate) 0.6-1.2) to a band it copies into each controller as it is made; the trainer scales that band, and the state copy that starts the underflip, so the slider sets the whole underflip. - Underflip part only: the band spans the flip's speed and Underflip speed from the start; where the board turns back the trainer scales the rate the game computes for its rate controllers every tick (0x10dd2f0, called by their update 0x10de730) until the landing. - A speed for each underflip at the bottom of Advanced trick speed (kickflip, heelflip, pop shuvit, FS pop shuvit and the four impossible underflips), times Underflip speed, and the Late flip slider beside them. - Late flip speed in the TRICKS card; dial sliders reach 0; Underflip speed named on the MODIFIED line and shared by a session's host (session extras version 2). Co-Authored-By: Claude Opus 5.5 (1M context) --- Extension/Trainer/CHANGELOG.md | 21 + .../Trainer/Test/session_extras_tests.cpp | 24 ++ Extension/Trainer/trainer.cpp | 183 ++++++++- Extension/Trainer/trainer.h | 4 +- Extension/Trainer/trainer_gamestate.cpp | 358 +++++++++++++++++- Extension/Trainer/trainer_gamestate.h | 43 ++- Extension/Trainer/trainer_page.cpp | 67 +++- Extension/Trainer/trainer_session.cpp | 32 +- Extension/Trainer/trainer_session.h | 5 + 9 files changed, 690 insertions(+), 47 deletions(-) diff --git a/Extension/Trainer/CHANGELOG.md b/Extension/Trainer/CHANGELOG.md index 1bb61c06..7b036f3e 100644 --- a/Extension/Trainer/CHANGELOG.md +++ b/Extension/Trainer/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## v0.8.2 - 2026-10-19 + +Built on ReSkate 2.1.0, like 0.8.0. + +- **Underflip speed.** A slider under Flip trick speed (POP AND TRICKS > TRICKS) for every underflip. You flick and the + underflip triggers as always. The game held underflips to 1 to 1.2 times its speed whatever the flip speed was; the + trainer moves that range with the slider. By itself it sets the whole underflip, and Flip trick speed does not reach + it. `trainer option underflip_speed `; presets carry it (`trick.underflip_speed`) and RESET EVERYTHING puts it back. + A session's host shares it with the other physics. +- **Underflip part only.** A switch under Underflip speed: the underflip's flip turns at Flip trick speed and Underflip + speed takes over where the board starts to turn back. The game sets the rate of the clips playing the turn back as + the underflip starts, so from that point the trainer scales the rate the game plays them at every tick, until the + landing. `trainer option underflip_split 1`, preset key `trick.underflip_split`. +- **A speed for each underflip.** At the bottom of Advanced trick speed: kickflip, heelflip, pop shuvit, FS pop shuvit, + impossible, front foot impossible, nollie impossible and nollie back foot impossible underflips each have a speed, + times Underflip speed (nollie underflips use their regular trick's, but for the impossibles). The Late flip slider is + there too (the same setting as Late flip speed). Presets carry them like the other trick speeds. +- **Late flip speed in the TRICKS card.** The late flip's own speed has a slider beside Flip trick speed and counts + without Advanced trick speed ticked (it moved out of the Advanced list). +- **Dial sliders reach 0.** Grind lock-on and the other dials go down to 0 on the slider. + ## v0.8.1 - 2026-10-18 Built on ReSkate 2.1.0, like 0.8.0. diff --git a/Extension/Trainer/Test/session_extras_tests.cpp b/Extension/Trainer/Test/session_extras_tests.cpp index e03e7446..e2e1a752 100644 --- a/Extension/Trainer/Test/session_extras_tests.cpp +++ b/Extension/Trainer/Test/session_extras_tests.cpp @@ -19,6 +19,7 @@ void check(bool condition, const std::string &message) { } // The encoding's layout, for the cases that break it: version, table, six numbers, count, rows. constexpr std::size_t table_at = 1, numbers_at = 5, count_at = 29, rows_at = 31; +constexpr std::size_t header_at_split = 33; // version 2: Underflip speed at 29, then the under part byte template void poke(std::vector &bytes, std::size_t at, T value) { std::memcpy(bytes.data() + at, &value, sizeof(T)); } std::pair row(std::size_t index, float value) { return {static_cast(index), value}; } } @@ -63,6 +64,29 @@ int main() { const auto only = decode_session_extras(encode_session_extras(heights)); check(only && *only == heights && encode_session_extras(heights).size() == rows_at, "multipliers alone travel without rows"); + // Underflip speed: version 2 only when it is not 1, so an older guest still reads a host who leaves it alone. + check(bytes[0] == 1, "a host with the game's own underflip speed writes version 1"); + SessionExtras underflip = host; + underflip.underflip_speed = 0.25f; + const auto underflip_bytes = encode_session_extras(underflip); + check(underflip_bytes[0] == 2 && underflip_bytes.size() == bytes.size() + 5, "a changed underflip speed writes version 2, 5 bytes longer"); + const auto underflip_guest = decode_session_extras(underflip_bytes); + check(underflip_guest && *underflip_guest == underflip, "a guest reads the host's underflip speed"); + SessionExtras split = host; + split.underflip_split = true; + const auto split_bytes = encode_session_extras(split); + const auto split_guest = decode_session_extras(split_bytes); + check(split_bytes[0] == 2 && split_guest && *split_guest == split, "the under part only setting alone travels too (version 2)"); + { + auto odd = split_bytes; + odd[header_at_split] = 2; + check(!decode_session_extras(odd), "an under part byte that is not 0 or 1 is refused"); + } + SessionExtras underflip_only; + underflip_only.underflip_speed = 3.0f; + check(!underflip_only.stock() && !encode_session_extras(underflip_only).empty(), "an underflip speed alone is not the game's own"); + check(!decode_session_extras(std::vector(underflip_bytes.begin(), underflip_bytes.end() - 1)), "a version 2 encoding cut short is refused"); + // Not an encoding. const auto refused = [&](std::vector broken) { return !decode_session_extras(broken); }; check(refused({1}), "a lone byte is refused"); diff --git a/Extension/Trainer/trainer.cpp b/Extension/Trainer/trainer.cpp index 1a2af923..c93e4514 100644 --- a/Extension/Trainer/trainer.cpp +++ b/Extension/Trainer/trainer.cpp @@ -140,6 +140,11 @@ 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}; + float underflip_speed{1}; // the board while the game says an underflip is turning, times the flip speed + // Off: the whole underflip (its flip too) turns at Underflip speed. On: the flip turns at Flip trick speed and only + // the under part, from where the board turns back, at Underflip speed. + bool underflip_split{}; + std::array board_spin{}; // the board's angular velocity at the last tick in the air, deg/s // "Advanced trick speed": each flip trick also turns at a speed of its own (flip_tricks, trainer_gamestate.h). bool flip_advanced{}; bool flip_pop{}; // a slowed flip trick pops higher, so it has the air time it needs @@ -158,7 +163,21 @@ struct State { float clamped{}; // the impossible's own speed state as the flight began float factor{1}; // the flip speed times the turning trick's own std::uint32_t trick{}; // the trick it was last driven for + std::int8_t underflip_anim{-1}, underflip_state{-1}, underflip_window{-1}; // as last logged + // UnderflipTimeScale, which the underflip clips play at: the game's own number and what the trainer made of it. + bool timescale_active{}; + bool window_closed{}; // the flick's underflip window opened and closed in this flight: an underflip may start + float timescale_game{}, timescale_written{}; + int timescale_sets{}; // how often the game set its own number this flight (logged up to 8) + // "Underflip part only": the way the board turned in the underflip's flip (summed while it turned), how many + // ticks went into it, and how many ticks in a row it has turned back against it. Two back and the under part + // has started. + float flip_way[3]{}; + int flip_way_ticks{}, turned_back{}; + bool under_part{}; + int underflip_ticks{}; // ticks since the underflip started (for the log) } flip_drive; + int underflip_bands{}; // copies of the underflip's rate controllers found float flip_game_seen{}; // the game's own number in the last driven flight (for the flight log) float flip_clamped_seen{-1}; // and what the impossible's own speed state read (-1: not found) bool flip_live{}; @@ -425,6 +444,8 @@ void load_store() { s.foot_setting[i] = static_cast(std::clamp(o.value("foot.set." + std::string(foot_settings[i].key), -1), -1, 1)); 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.underflip_speed = std::clamp(o.value("underflip_speed", 1.0f), flip_low, flip_high); + s.underflip_split = o.value("underflip_split", false); s.flip_advanced = o.value("flip_advanced", false); s.flip_pop = o.value("flip_pop", false); s.flip_flick = o.value("flip_flick", false); @@ -522,6 +543,8 @@ void save_store() { if (s.foot_setting[i] >= 0) options["foot.set." + std::string(foot_settings[i].key)] = static_cast(s.foot_setting[i]); options["offboard_height"] = s.offboard_height; options["flip_speed"] = s.flip_speed; + options["underflip_speed"] = s.underflip_speed; + options["underflip_split"] = s.underflip_split; options["flip_advanced"] = s.flip_advanced; options["flip_pop"] = s.flip_pop; options["flip_flick"] = s.flip_flick; @@ -664,13 +687,17 @@ bool extras_blocked(); // so the flip speed the trainer gives other flips never reaches it (measured 2026-10-15: x0.5, 1 and 2 all turned a late kickflip // about 260 deg at 1155 deg/s). Those two ends follow the flip speed and the Late flip per-trick speed instead. bool late_flip_speed(std::string_view key) { return key == "lateflip.maxspeed" || key == "lateflip.minspeed"; } +// The Late flip's own speed sits beside Flip trick speed and always counts; the other tricks' own speeds count with +// Advanced trick speed ticked. +float own_trick_speed(int index) { + auto &s = state(); + if (index < 0 || (!s.flip_advanced && static_cast(index) != late_flip_slot)) return 1.0f; + return s.flip_trick[static_cast(index)]; +} float late_flip_factor() { auto &s = state(); if (extras_blocked()) return 1.0f; - float factor = s.boosts.flip; - if (s.flip_advanced) - if (const auto index = flip_trick_index(40); index >= 0) factor *= s.flip_trick[static_cast(index)]; // LateFlipTrick - return factor; + return s.boosts.flip * own_trick_speed(flip_trick_index(40)); // LateFlipTrick } bool slowed_catch_time(std::string_view key) { return key.starts_with("heldflip.") && key.ends_with("catchtime"); @@ -915,6 +942,19 @@ Boosts boosts_in_force() { 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()}; } +// Underflip speed as it acts now: the host's in a session where the host sets the physics, 1 where the player's own +// extras do not count, otherwise the player's. +float underflip_in_force() { + auto &s = state(); + if (s.enforced) return s.host.underflip_speed; + return extras_blocked() ? 1.0f : s.underflip_speed; +} +// "Underflip part only", the same way. +bool underflip_split_in_force() { + auto &s = state(); + if (s.enforced) return s.host.underflip_split; + return !extras_blocked() && s.underflip_split; +} // The player's own, for a session they host to share with its guests. void publish_extras() { auto &s = state(); @@ -926,6 +966,8 @@ void publish_extras() { own.boneless_height = s.boneless_height; own.offboard_height = s.offboard_height; own.cruise = own_cruise(); + own.underflip_speed = s.underflip_speed; + own.underflip_split = s.underflip_split; for (const auto index : s.class_entries) if (const auto &e = s.entries[index]; e.touched) own.classes.emplace_back(static_cast(e.class_field), static_cast(e.value)); @@ -1095,7 +1137,7 @@ 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 + return name == "flip_speed" ? &s.flip_speed : name == "underflip_speed" ? &s.underflip_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 : name == "pump_power" ? &s.pump_power : name == "manual_height" ? &s.manual_height : nullptr; } @@ -1136,6 +1178,7 @@ const std::vector &setups() { all->back().high = 1; }; real("trick.flip_speed", topic_pop, &s.flip_speed, 1.0f, flip_low, flip_high); + real("trick.underflip_speed", topic_pop, &s.underflip_speed, 1.0f, flip_low, flip_high); real("trick.manual_height", topic_pop, &s.manual_height, 1.0f, height_low, height_high); real("trick.gravity", topic_pop, &s.gravity, 1.0f, gravity_low, gravity_high); real("trick.nocomply_height", topic_pop, &s.nocomply_height, 1.0f, height_low, height_high); @@ -1147,10 +1190,11 @@ const std::vector &setups() { toggle("trick.flip_advanced", topic_pop, &s.flip_advanced); toggle("trick.flip_pop", topic_pop, &s.flip_pop); toggle("trick.flip_flick", topic_pop, &s.flip_flick); + toggle("trick.underflip_split", topic_pop, &s.underflip_split); for (std::size_t i = 0; i < flip_tricks.size(); ++i) { real("trick." + std::string(flip_tricks[i].key), topic_pop, &s.flip_trick[i], 1.0f, flip_low, flip_high); - all->back().title = std::string(flip_tricks[i].label) + ": flip speed"; - all->back().in_force = [&s] { return s.flip_advanced; }; + all->back().title = std::string(flip_tricks[i].label) + (i >= first_underflip_slot ? ": underflip speed" : ": flip speed"); + if (i != late_flip_slot) all->back().in_force = [&s] { return s.flip_advanced; }; } real("grind.side", topic_grinds, &s.grind_side, 1.0f, 0.0f, 1.0e6f); real("grind.push", topic_grinds, &s.grind_push, 0.0f, -1.0e6f, 1.0e6f); @@ -1193,10 +1237,11 @@ const std::vector &setups() { all->back().high = high; } constexpr std::pair titles[]{ - {"trick.flip_speed", "Flip trick speed"}, {"trick.manual_height", "Manual pop height"}, {"trick.gravity", "Gravity"}, + {"trick.flip_speed", "Flip trick speed"}, {"trick.underflip_speed", "Underflip speed"}, {"trick.manual_height", "Manual pop height"}, {"trick.gravity", "Gravity"}, {"trick.nocomply_height", "No comply height"}, {"trick.boneless_height", "Boneless height"}, {"trick.hippy_height", "Hippy jump height"}, {"trick.offboard_height", "Off-board jump height"}, {"trick.pump_power", "Pump power"}, {"trick.revert_boost", "Board bending boost"}, {"trick.flip_advanced", "Advanced trick speed"}, {"trick.flip_pop", "Slow flips pop higher"}, {"trick.flip_flick", "Flick tricks at full speed"}, + {"trick.underflip_split", "Underflip speed: under part only"}, {"grind.side", "Grind pop: side force"}, {"grind.push", "Grind pop: push with stick (m/s)"}, {"grind.range", "Grinds: left stick turn range (deg)"}, {"transition.lean", "Transition: lean into the ramp"}, {"transition.carry", "Transition: roll off at your angle"}, {"transition.drop", "Transition: roll-off drop speed (m/s)"}, {"transition.unstick", "Transition: no hang-ups"}, @@ -1698,6 +1743,20 @@ void drive_flip_speed(std::uintptr_t base, bool airborne, float to_landing) { auto &drive = s.flip_drive; const auto now = read_flip_state(base, static_cast(s.entity)); s.flip_live = static_cast(now); + // The underflip's rate band follows the speed the underflip should turn at: without it the game holds every underflip + // to 1 to 1.2. + // Underflip speed times the underflipped trick's own (Advanced trick speed). + const float underflip = underflip_in_force() * (now && !extras_blocked() ? own_trick_speed(underflip_trick_index(now.trick)) : 1.0f); + const bool split = underflip_split_in_force(); + // The band is copied into each controller as it is made (0x10d9e40), so with "Underflip part only" it spans both the + // flip's speed and the under part's from the start. + const auto band = [&s, split, underflip](float factor) { + const float low = split ? std::min(factor, underflip) : factor, high = split ? std::max(factor, underflip) : factor; + if (const int bands = scale_underflip_bands(low, high); bands != s.underflip_bands) { + s.underflip_bands = bands; + say(logging::Level::info, std::format("Trainer: {} underflip rate controller(s) found.", bands)); + } + }; set_launch_trick(now ? now.trick : 0xffu, now ? now.trick_at : 0); // for the launch hook: the trick the game has in mind this tick // A plain ollie (trick 1) plays the same clip and reads the same number: slowed, it hangs in // the air (measured: 0.60 s of air at 1, 0.65 s at 0.5, 0.77 to 0.88 s at 0.25). Only a flip @@ -1708,13 +1767,74 @@ void drive_flip_speed(std::uintptr_t base, bool airborne, float to_landing) { (void)write_flip_speed(now, drive.game); if (now.clamped_at) (void)write_state_number(now.clamped_at, drive.clamped); } + if (now && drive.timescale_active && now.underflip_timescale == drive.timescale_written) + (void)write_state_number(now.underflip_timescale_at, drive.timescale_game); + (void)hold_underflip_timescale(0, 1.0f); + arm_underflip_flip_speed(0, 1.0f); + scale_under_part(0); + band(split ? (extras_blocked() ? 1.0f : s.boosts.flip) : underflip); + if (s.logging && drive.window_closed) + say(logging::Level::info, std::format("Trainer underflip flight ended: {}.", underflip_timescale_callers())); 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)]; + if (!extras_blocked()) factor *= own_trick_speed(flip_trick_index(now.trick)); + if (s.logging && (now.underflip_anim != drive.underflip_anim || now.underflip_state != drive.underflip_state || + now.underflip_window != drive.underflip_window)) + say(logging::Level::info, std::format("Trainer underflip states: trick {} ({}), Is.Underflip anim {} state {}, window {}, game's speed {:.3f}, " + "timescale {:.3f} (game's {:.3f}), factor {:.2f}.", + now.trick, flip_trick_name(now.trick), now.underflip_anim, now.underflip_state, now.underflip_window, now.speed, + now.underflip_timescale_at ? now.underflip_timescale : -1.0f, drive.timescale_game, + factor)); + if (s.logging && drive.underflip_anim == 1 && now.underflip_anim == 0) + say(logging::Level::info, std::format("Trainer underflip timescale asked by: {}.", underflip_timescale_callers())); + if (drive.underflip_window == 1 && now.underflip_window == 0) drive.window_closed = true; + const bool in_underflip = now.underflip_anim == 1 || now.underflip_state == 1; + // "Underflip part only": the underflip's own flip turns one way, the under part back the other. The way it turned + // is summed from the third tick of the underflip (the first two read a jolt as the clip changes, measured + // 2026-10-19), and two ticks in a row against it start the under part. + if (split && in_underflip && !drive.under_part) { + const auto &w = s.board_spin; + const float rate = std::sqrt(w[0] * w[0] + w[1] * w[1] + w[2] * w[2]); + if (++drive.underflip_ticks > 2 && rate > 200.0f) { + const float way = std::sqrt(drive.flip_way[0] * drive.flip_way[0] + drive.flip_way[1] * drive.flip_way[1] + drive.flip_way[2] * drive.flip_way[2]); + const float along = way > 0 ? (w[0] * drive.flip_way[0] + w[1] * drive.flip_way[1] + w[2] * drive.flip_way[2]) / way : 0.0f; + if (drive.flip_way_ticks >= 4 && along < -150.0f) { + if (++drive.turned_back >= 2) drive.under_part = true; + } else { + drive.turned_back = 0; + if (along >= 0 || drive.flip_way_ticks < 4) { + for (std::size_t k = 0; k < 3; ++k) drive.flip_way[k] += w[k]; + ++drive.flip_way_ticks; + } + } + } + if (drive.under_part) { + // The copy that started the underflip was scaled by the flip's factor: from here the under part's. + const float to = std::clamp(now.speed / std::max(factor, flip_low) * underflip, 0.0f, 1000.0f); + if (s.logging) + say(logging::Level::info, std::format("Trainer underflip: under part from tick {} of the underflip, speed {:.3f} -> {:.3f} (x{:.2f}).", + drive.underflip_ticks, now.speed, to, underflip)); + (void)write_flip_speed(now, to); + } + } + // The under part plays on rate controllers that took their rate as the underflip started: each one's rate is scaled + // from here to the landing. + scale_under_part(split && in_underflip && drive.under_part ? underflip : 0.0f); + // What the underflip turns at: Underflip speed for all of it, or the flip's own speed until the under part. + const float underflip_rate = split && !drive.under_part ? factor : underflip; + band(underflip_rate); + // The game sets TrickFlipSpeed afresh as the underflip starts, a tick after the window closes: that set is scaled. + if (drive.window_closed && underflip_rate != 1.0f) arm_underflip_flip_speed(now.speed_at, underflip_rate); + else arm_underflip_flip_speed(0, 1.0f); + drive.underflip_anim = now.underflip_anim; + drive.underflip_state = now.underflip_state; + drive.underflip_window = now.underflip_window; + // The underflip itself turns at the game's speed times that factor alone (the game's copy that starts it was scaled + // as it was made): the trainer leaves it there. + if (in_underflip) return; // "Slow flips pop higher" for a trick the game chose after the pop (seen out of manuals and grinds). if (s.flip_pop && !extras_blocked() && (!drive.active || drive.trick != now.trick)) { float was{}, time{}; @@ -1730,11 +1850,32 @@ void drive_flip_speed(std::uintptr_t base, bool airborne, float to_landing) { if (!drive.active) drive.clamped = now.clamped; drive.active = true; drive.factor = factor; - const bool held_up = s.flip_flick && !extras_blocked(); + const bool held_up = s.flip_flick && !extras_blocked() && !in_underflip; // the floor would undo Underflip speed s.flip_game_seen = drive.game; s.flip_clamped_seen = now.clamped_at ? now.clamped : -1.0f; drive.written = std::clamp((held_up ? std::max(drive.game, flip_least_game) : drive.game) * factor, 0.0f, 1000.0f); if (now.speed != drive.written || (now.clamped_at && now.clamped != drive.written)) (void)write_flip_speed(now, drive.written); + // The underflip does not turn at that speed but at UnderflipTimeScale: Underflip speed (times the flip speed) goes there + // for the whole flight, so the number is in place whenever the game's underflip reads it, and the flip before it is + // left alone. + if (now.underflip_timescale_at) { + const float timescale = factor * underflip; + if (hold_underflip_timescale(now.underflip_timescale_at, 1.0f)) { // read and logged only: it is not the underflip's rate + // Held in the game's lookup: what the tick reads is already the trainer's. + if (s.logging && now.underflip_anim == 1 && ++drive.timescale_sets <= 12) + say(logging::Level::info, std::format("Trainer underflip timescale: game's {:.3f}, read {:.3f}, held x{:.2f}.", underflip_timescale_game(), + now.underflip_timescale, timescale)); + drive.timescale_active = false; + } else if (!drive.timescale_active || now.underflip_timescale != drive.timescale_written) { + if (s.logging && drive.timescale_active && ++drive.timescale_sets <= 8) + say(logging::Level::info, std::format("Trainer underflip timescale: the game set {:.3f} (the trainer had {:.3f}), Is.Underflip {}, window {}.", + now.underflip_timescale, drive.timescale_written, now.underflip_anim, now.underflip_window)); + drive.timescale_game = now.underflip_timescale; + } + drive.timescale_active = true; + drive.timescale_written = std::clamp(drive.timescale_game * timescale, 0.0f, 1000.0f); + if (now.underflip_timescale != drive.timescale_written) (void)write_state_number(now.underflip_timescale_at, drive.timescale_written); + } } void observe(std::uintptr_t base, std::uintptr_t client, std::uint64_t now) { auto &s = state(); @@ -1885,10 +2026,13 @@ void observe(std::uintptr_t base, std::uintptr_t client, std::uint64_t now) { if (!bodies.parts.empty() && memory::peek(bodies.parts[0] + 0x90, spin)) { board_turn = std::sqrt(spin[0] * spin[0] + spin[1] * spin[1] + spin[2] * spin[2]) * 180.0f / std::numbers::pi_v; if (!std::isfinite(board_turn) || board_turn > 1.0e5f) board_turn = 0; + else + for (std::size_t k = 0; k < 3; ++k) s.board_spin[k] = spin[k] * 180.0f / std::numbers::pi_v; } } catch (...) {} if (m.airborne) m.board_turn = std::max(m.board_turn, board_turn); } + if (!m.airborne) s.board_spin = {}; if (s.logging && s.log) { s.log << std::format("{:.4f},{:.3f},{:.3f},{:.3f},{:.3f},{:.3f},{:.1f},{},{},{:.0f}\n", clock_seconds(stamp - s.log_start), position[0], position[1], position[2], t.speed, t.vertical, t.heading, t.airborne ? 1 : 0, t.physics_state, board_turn); @@ -2328,6 +2472,8 @@ void build_view() { next->tr_option = s.tr_option; next->offboard_height = s.offboard_height; next->flip_speed = s.flip_speed; + next->underflip_speed = s.underflip_speed; + next->underflip_split = s.underflip_split; next->flip_advanced = s.flip_advanced; next->flip_pop = s.flip_pop; next->flip_flick = s.flip_flick; @@ -2566,7 +2712,8 @@ 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 || s.flip_advanced || s.flip_flick); + flip_speed_in_use(s.boosts.flip != 1.0f || s.flip_advanced || s.flip_trick[late_flip_slot] != 1.0f || s.flip_flick || underflip_in_force() != 1.0f || + underflip_split_in_force()); // 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); @@ -2635,10 +2782,7 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s for (std::uint32_t trick = 0; trick < 64; ++trick) { float up = 0; if (s.flip_pop && !extras_blocked() && (!plain_pop(trick) || ollie_pop(trick))) { - float factor = s.boosts.flip; - if (s.flip_advanced) - if (const auto index = flip_trick_index(trick); index >= 0) factor *= s.flip_trick[static_cast(index)]; - up = slow_flip_up(trick, factor); + up = slow_flip_up(trick, s.boosts.flip * own_trick_speed(flip_trick_index(trick))); } set_flip_launch(trick, up); } @@ -3286,7 +3430,7 @@ 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" || name.starts_with("flip.")) { + if (name == "flip_speed" || name == "underflip_speed" || name.starts_with("flip.")) { const auto value = number(arg(1)); 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."; @@ -3322,7 +3466,7 @@ std::string run(std::string_view verb, const std::vector &a) { if (!value) return "error: return_delay needs seconds."; s.return_delay = std::clamp(static_cast(*value), 0.0f, 3600.0f); } else if (!flag(arg(1), on)) { - return "error: usage: trainer option hud|hud_jump|auto_return|pad|flip_advanced|flip_pop|flip_flick|log 0|1"; + return "error: usage: trainer option hud|hud_jump|auto_return|pad|flip_advanced|flip_pop|flip_flick|underflip_split|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; @@ -3339,6 +3483,9 @@ std::string run(std::string_view verb, const std::vector &a) { } else if (name == "flip_advanced") { if (on && !editable(&why)) return "error: " + why; s.flip_advanced = on; + } else if (name == "underflip_split") { + if (on && !editable(&why)) return "error: " + why; + s.underflip_split = on; } else if (name == "log") { set_logging(on); diff --git a/Extension/Trainer/trainer.h b/Extension/Trainer/trainer.h index daafea67..2f6dd130 100644 --- a/Extension/Trainer/trainer.h +++ b/Extension/Trainer/trainer.h @@ -141,7 +141,9 @@ struct View { 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 + float flip_speed{1}; + float underflip_speed{1}; // while an underflip turns, times the flip speed // board flip tricks: x of the game's own speed + bool underflip_split{}; // Underflip speed for the under part only; the underflip's flip at Flip trick speed bool flip_advanced{}; // each flip trick also has a speed of its own bool flip_pop{}; // a slowed flip trick pops higher bool flip_flick{}; // a slow flick turns the board as fast as a full one diff --git a/Extension/Trainer/trainer_gamestate.cpp b/Extension/Trainer/trainer_gamestate.cpp index bb53f1ec..28ce4bc9 100644 --- a/Extension/Trainer/trainer_gamestate.cpp +++ b/Extension/Trainer/trainer_gamestate.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace dingosdk::trainer { @@ -59,6 +60,36 @@ StateObject flip_clamped_object{{0x36, 0xac, 0x11, 0x7c, 0x47, 0xc8, 0x20, 0x44, "Animation/Dingo/Float.Anim.TrickFlipSpeedClamped"}; 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"}; +// The underflip: CurrentFlipTrick stays the flipped trick through it (owner's round, 2026-10-18); these say when one is +// turning (Underflip speed acts while they do). +StateObject underflip_anim_object{{0xa7, 0xcb, 0xd5, 0xbb, 0x63, 0x03, 0x34, 0x4b, 0xbb, 0xd5, 0xcb, 0xa7, 0x03, 0x63, 0x4b, 0x34}, + "Animation/Dingo/Bool.Anim.Is.Underflip"}; +StateObject underflip_state_object{{0x60, 0x0f, 0xe7, 0x0f, 0x33, 0x04, 0x06, 0xb2, 0x0f, 0xe7, 0x0f, 0x60, 0x04, 0x33, 0xb2, 0x06}, + "Animation/Dingo/Bool.State.Is.Underflip"}; +StateObject underflip_window_object{{0xb9, 0xfe, 0x99, 0x99, 0xe7, 0x7e, 0x13, 0xbd, 0x99, 0x99, 0xfe, 0xb9, 0x7e, 0xe7, 0xbd, 0x13}, + "Animation/Dingo/Bool.Anim.IsInUnderflipWindow"}; +// What the underflip plays at: all 48 underflip sequences (seq_c_proto_onb_trick_*_uflip) read it where every other +// trick's sequence reads TrickFlipSpeed (the game's clip data, 2026-10-19), so TrickFlipSpeed never reached them. +StateObject underflip_timescale_object{{0xba, 0xd6, 0x9c, 0xbf, 0x33, 0x37, 0x71, 0x75, 0xbf, 0x9c, 0xd6, 0xba, 0x37, 0x33, 0x75, 0x71}, + "Animation/Dingo/Float.Anim.UnderflipTimeScale"}; +// The underflip's playback rate controllers (PlaybackRateControllerAsset): they play the underflip at TrickFlipSpeed +// (and the part into the air at an expression of it and UnderflipTimeScale), held between MinScale and MaxScale. The +// game's band is 1 to 1.2 and 0.6 to 1.2 (the game's data, 2026-10-19), so no slower speed ever reached an underflip. +// Underflip speed scales the band; the copies of each asset in memory are all kept. +struct RateBand { + std::uint8_t id[16]; + std::string_view name; + float stock_min, stock_max; + std::atomic at[4]{}; +}; +constexpr std::uintptr_t rate_min_scale = 0x74, rate_max_scale = 0x78; // from the object (after its id) +RateBand underflip_bands[]{ + {{0xbf, 0x2e, 0xfa, 0xb7, 0x63, 0x8e, 0x74, 0xe3, 0xb7, 0xfa, 0x2e, 0xbf, 0x8e, 0x63, 0xe3, 0x74}, + "Animation/Dingo/cdbchooser_onboard_underflips_(pbrate)", 1.0f, 1.2f}, + {{0x35, 0x45, 0x95, 0xf4, 0x69, 0xe1, 0xe6, 0xc9, 0xf4, 0x95, 0x45, 0x35, 0xe1, 0x69, 0xc9, 0xe6}, + "Animation/Dingo/cdbchooser_onboard_underflips_toair(pbrate)", 0.6f, 1.2f}, +}; +std::atomic band_factor{1}, band_high_factor{1}; 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"}; @@ -87,7 +118,8 @@ StateObject grind_control_objects[]{ {{0xef, 0xb2, 0xb6, 0x2b, 0x9a, 0xee, 0xc0, 0x5d, 0x2b, 0xb6, 0xb2, 0xef, 0xee, 0x9a, 0x5d, 0xc0}, "Animation/Dingo/Bool.Intent.GrindControlHasActiveInput"}, }; StateObject *const objects[]{&manual_object, &flip_speed_object, &flip_trick_object, &pumping_object, &pump_strength_object, &pump_assist_object, - &body_flip_speed_object, &body_flip_rate_object, &flip_clamped_object, &grind_nudge_object}; + &body_flip_speed_object, &body_flip_rate_object, &flip_clamped_object, &grind_nudge_object, + &underflip_anim_object, &underflip_state_object, &underflip_window_object, &underflip_timescale_object}; StateObject grind_objects[]{ {{0xda, 0x09, 0x5b, 0x51, 0xf4, 0xff, 0x36, 0x3e, 0x51, 0x5b, 0x09, 0xda, 0xff, 0xf4, 0x3e, 0x36}, "Animation/Dingo/Bool.Intent.NoPopGrindExit"}, @@ -123,15 +155,16 @@ bool put_flag(std::uintptr_t address, bool value) noexcept { } // 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 { +bool is_named(const std::uint8_t (&id)[16], std::string_view wanted, std::uintptr_t id_at) noexcept { __try { - if (std::memcmp(reinterpret_cast(id_at), object.id, sizeof(object.id)) != 0) return false; + if (std::memcmp(reinterpret_cast(id_at), id, sizeof(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; + return _strnicmp(reinterpret_cast(name), wanted.data(), wanted.size()) == 0 && + *reinterpret_cast(name + wanted.size()) == 0; } __except (1) { return false; } } +bool is_object(const StateObject &object, std::uintptr_t id_at) noexcept { return is_named(object.id, object.name, id_at); } // 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); @@ -207,6 +240,12 @@ const std::array &state_id_filter() noexcept { for (const auto &object : grind_objects) mark(object); for (const auto &object : grind_control_objects) mark(object); for (const auto &object : transition_objects) mark(object); + for (const auto &band : underflip_bands) { + std::uint32_t first; + std::memcpy(&first, band.id, 4); + const auto h = (first * 0x9e3779b1u) >> 20; + result[h >> 6] |= std::uint64_t{1} << (h & 63); + } return result; }(); return bits; @@ -236,9 +275,46 @@ void find_state_objects(std::uintptr_t start, std::size_t size) noexcept { for (auto &object : grind_objects) look(object, i); for (auto &object : grind_control_objects) look(object, i); for (auto &object : transition_objects) look(object, i); + for (auto &band : underflip_bands) { + std::uint32_t first; + std::memcpy(&first, band.id, 4); + if (words[i] != first || !is_named(band.id, band.name, start + i * 4)) continue; + const auto here = start + i * 4 + object_after_id; + // The band as the game made it, or as the trainer last set it: anything else is not this layout. + float low{}, high{}; + const float f = band_factor.load(std::memory_order_relaxed), g = band_high_factor.load(std::memory_order_relaxed); + if (!peek(here + rate_min_scale, low) || !peek(here + rate_max_scale, high) || + !((low == band.stock_min && high == band.stock_max) || (low == band.stock_min * f && high == band.stock_max * g))) + continue; + for (auto &slot : band.at) { + auto known = slot.load(std::memory_order_relaxed); + if (known == here) break; + if (!known && slot.compare_exchange_strong(known, here, std::memory_order_relaxed)) break; + } + } } } __except (1) {} } +int scale_underflip_bands(float low_factor, float high_factor) noexcept { + band_factor.store(low_factor, std::memory_order_relaxed); + band_high_factor.store(high_factor, std::memory_order_relaxed); + int found = 0; + for (auto &band : underflip_bands) + for (auto &slot : band.at) { + const auto at = slot.load(std::memory_order_relaxed); + if (!at) continue; + float low{}, high{}; + if (!peek(at + rate_min_scale, low) || !peek(at + rate_max_scale, high)) { + slot.store(0, std::memory_order_relaxed); // freed with its level + continue; + } + const float want_low = band.stock_min * low_factor, want_high = band.stock_max * high_factor; + if (low != want_low) (void)put(at + rate_min_scale, want_low); + if (high != want_high) (void)put(at + rate_max_scale, want_high); + ++found; + } + return found; +} bool flip_states_found() noexcept { return flip_speed_object.at.load(std::memory_order_relaxed) && flip_trick_object.at.load(std::memory_order_relaxed); } @@ -254,6 +330,17 @@ FlipState read_flip_state(std::uintptr_t base, std::uintptr_t entity) noexcept { state.speed_at = speed_at; state.trick_at = trick_at; if (const auto at = value_address(flip_clamped_object, block); at && peek(at, state.clamped) && std::isfinite(state.clamped)) state.clamped_at = at; + const auto flag = [&](StateObject &object) -> std::int8_t { + std::uint8_t value{}; + const auto at = value_address(object, block); + return at && peek(at, value) ? static_cast(value != 0) : std::int8_t{-1}; + }; + state.underflip_anim = flag(underflip_anim_object); + state.underflip_state = flag(underflip_state_object); + state.underflip_window = flag(underflip_window_object); + if (const auto at = value_address(underflip_timescale_object, block); at && peek(at, state.underflip_timescale) && + std::isfinite(state.underflip_timescale)) + state.underflip_timescale_at = at; return state; } PumpState read_pump_state(std::uintptr_t base, std::uintptr_t entity) noexcept { @@ -376,7 +463,163 @@ std::atomic lookup_original{}; std::atomic holds_until{}; bool holds_alive() noexcept { return GetTickCount64() < holds_until.load(std::memory_order_relaxed); } std::atomic lookup_attempted{}, lookup_ready{}; +std::atomic module_base{}; +// Underflip speed, held in the lookup: the game sets UnderflipTimeScale every tick, and its clips had read it before the +// trainer's tick wrote it (owner's round of 2026-10-19: written 0.1, the next ticks read 1.000, 0.973, 0.949 and the +// underflip turned at full speed). Each lookup of the local skater's value scales what the game last put there, so +// whoever reads it after the game's write gets the scaled number. +struct HeldScale { + std::atomic at{}; // the local skater's value; 0: not held + std::atomic factor{1}, raw{}, scaled{}; + std::atomic fresh{}; + std::atomic caller[8]{}; // who asked (offsets in the game) and how often, for the log + std::atomic calls[8]{}; +} held_timescale; +void note_timescale_caller(std::uintptr_t from) noexcept { + const auto base = module_base.load(std::memory_order_relaxed); + const auto offset = base && from > base ? from - base : from; + for (std::size_t i = 0; i < std::size(held_timescale.caller); ++i) { + auto known = held_timescale.caller[i].load(std::memory_order_relaxed); + if (!known && held_timescale.caller[i].compare_exchange_strong(known, offset, std::memory_order_relaxed)) known = offset; + if (known == offset) { + held_timescale.calls[i].fetch_add(1, std::memory_order_relaxed); + return; + } + } +} +void scale_timescale(std::uintptr_t at) noexcept { + float value{}; + if (!peek(at, value) || !std::isfinite(value)) return; + // A number the trainer did not put there is the game's. + if (!held_timescale.fresh.load(std::memory_order_relaxed) || value != held_timescale.scaled.load(std::memory_order_relaxed)) + held_timescale.raw.store(value, std::memory_order_relaxed); + const float out = held_timescale.raw.load(std::memory_order_relaxed) * held_timescale.factor.load(std::memory_order_relaxed); + held_timescale.scaled.store(out, std::memory_order_relaxed); + held_timescale.fresh.store(true, std::memory_order_relaxed); + if (value != out) (void)put(at, out); +} +// The game's setter of a number state (a FloatAsset's virtual, `self` = object + 0x60, the value in the third argument): +// it finds the value with 0x11dcff0 and stores it. The underflip's clips take their rate as they start, in the tick the +// game sets UnderflipTimeScale, so scaling on later lookups came too late (owner's round of 2026-10-19: the one reader +// through the lookup got the scaled number, the underflip still turned at full speed). Scaled as the game sets it. +constexpr std::uintptr_t float_setter = 0x11dd7d0; +constexpr unsigned char float_setter_bytes[]{0x48, 0x83, 0xec, 0x38, 0x48, 0x8b, 0xc2, 0x41, 0xb0, 0x01, 0x48, 0x8d, 0x51, 0xa0}; +using FloatSetter = void (*)(std::uintptr_t self, std::uintptr_t context, float value); +std::atomic setter_original{}; +std::atomic timescale_sets{}, timescale_sets_scaled{}; +// Underflip speed: the underflip's playback rate is TrickFlipSpeed (cdbchooser_onboard_underflips_(pbrate)), set afresh as +// the underflip starts and taken by its clips then. While the trainer's tick says an underflip may start (the flick's +// window has closed in the air), that set is scaled. UnderflipTimeScale is not it: held at x0.01 from the game's first +// set, the underflip turned as fast (owner's round of 2026-10-19). +std::atomic underflip_flip_at{}; +std::atomic underflip_flip_factor{1}; +std::atomic flip_sets{}, flip_sets_scaled{}; +void float_setter_hook(std::uintptr_t self, std::uintptr_t context, float value) { + const auto object = self - 0x60; + if (object == flip_speed_object.at.load(std::memory_order_relaxed) && std::isfinite(value)) { + const auto armed = underflip_flip_at.load(std::memory_order_relaxed); + const auto original = lookup_original.load(std::memory_order_acquire); + if (armed && original && original(object, context) == armed && holds_alive()) { + flip_sets.fetch_add(1, std::memory_order_relaxed); + value *= underflip_flip_factor.load(std::memory_order_relaxed); + flip_sets_scaled.fetch_add(1, std::memory_order_relaxed); + } + } + if (object == underflip_timescale_object.at.load(std::memory_order_relaxed) && std::isfinite(value)) { + timescale_sets.fetch_add(1, std::memory_order_relaxed); + const auto original = lookup_original.load(std::memory_order_acquire); + const auto at = original ? original(object, context) : 0; + if (at && at == held_timescale.at.load(std::memory_order_relaxed) && holds_alive()) { + const float out = value * held_timescale.factor.load(std::memory_order_relaxed); + held_timescale.raw.store(value, std::memory_order_relaxed); + held_timescale.scaled.store(out, std::memory_order_relaxed); + held_timescale.fresh.store(true, std::memory_order_relaxed); + timescale_sets_scaled.fetch_add(1, std::memory_order_relaxed); + value = out; + } + } + setter_original.load(std::memory_order_acquire)(self, context, value); +} +// The game's lookup of a state's value for writing (context, object, create) -> where to write. Hooked only to see who +// writes TrickFlipSpeed: the underflip's fresh 1.000 did not come through the number setter (owner's round of +// 2026-10-19: armed, nothing scaled). Two tables: writes while the trainer is armed for an underflip, and the others. +constexpr std::uintptr_t state_write_lookup = 0x11dcff0; +constexpr unsigned char state_write_lookup_bytes[]{0x48, 0x89, 0x6c, 0x24, 0x18, 0x48, 0x89, 0x74, 0x24, 0x20, 0x57, 0x41, 0x56, 0x41, 0x57}; +using StateWriteLookup = std::uintptr_t (*)(std::uintptr_t context, std::uintptr_t object, bool create); +std::atomic write_lookup_original{}; +struct CallerTable { + std::atomic caller[8]{}; + std::atomic calls[8]{}; + void note(std::uintptr_t from) noexcept { + const auto base = module_base.load(std::memory_order_relaxed); + const auto offset = base && from > base ? from - base : from; + for (std::size_t i = 0; i < std::size(caller); ++i) { + auto known = caller[i].load(std::memory_order_relaxed); + if (!known && caller[i].compare_exchange_strong(known, offset, std::memory_order_relaxed)) known = offset; + if (known == offset) { + calls[i].fetch_add(1, std::memory_order_relaxed); + return; + } + } + } + std::string take() noexcept { + std::string text; + for (std::size_t i = 0; i < std::size(caller); ++i) + if (const auto from = caller[i].load(std::memory_order_relaxed)) + if (const auto n = calls[i].exchange(0, std::memory_order_relaxed)) + text += std::format("{}0x{:x} x{}", text.empty() ? "" : ", ", from, n); + return text.empty() ? "none" : text; + } +} flip_writers_armed, flip_writers; +std::uintptr_t state_write_lookup_hook(std::uintptr_t context, std::uintptr_t object, bool create) { + if (object && object == flip_speed_object.at.load(std::memory_order_relaxed)) { + const auto from = reinterpret_cast(_ReturnAddress()); + (underflip_flip_at.load(std::memory_order_relaxed) ? flip_writers_armed : flip_writers).note(from); + } + return write_lookup_original.load(std::memory_order_acquire)(context, object, create); +} +// The game's copy of one state into another (destination object and context, source context, source object or 0 for the +// destination's own): it finds both values and has the destination's type copy them. This is how TrickFlipSpeed is set, +// at the flick and again as an underflip starts (the only writer of it, owner's round of 2026-10-19: 0x11d9a7b is its +// call of the write lookup). Underflip speed scales the copy right after it, before anything reads it. +constexpr std::uintptr_t state_copy = 0x11d9a40; +constexpr unsigned char state_copy_bytes[]{0x40, 0x53, 0x55, 0x56, 0x57, 0x48, 0x83, 0xec, 0x28, 0x4d, 0x85, 0xc9, 0x48, 0x8b, 0xd9}; +using StateCopy = void (*)(std::uintptr_t object, std::uintptr_t context, std::uintptr_t source_context, std::uintptr_t source_object); +std::atomic copy_original{}; +void state_copy_hook(std::uintptr_t object, std::uintptr_t context, std::uintptr_t source_context, std::uintptr_t source_object) { + copy_original.load(std::memory_order_acquire)(object, context, source_context, source_object); + if (!object || object != flip_speed_object.at.load(std::memory_order_relaxed)) return; + const auto armed = underflip_flip_at.load(std::memory_order_relaxed); + const auto original = lookup_original.load(std::memory_order_acquire); + if (!armed || !original || !holds_alive() || original(object, context) != armed) return; + flip_sets.fetch_add(1, std::memory_order_relaxed); + float value{}; + if (!peek(armed, value) || !std::isfinite(value)) return; + if (put(armed, value * underflip_flip_factor.load(std::memory_order_relaxed))) flip_sets_scaled.fetch_add(1, std::memory_order_relaxed); +} +// A rate controller's rate for this tick: the game's per-tick update of each one (0x10de730) asks this function +// (0x10dd2f0) for its time scale clamped to the min and max it copied from its asset when it was made, and plays its +// clips at that rate. The under part of an underflip runs on controllers made before it starts (or as it starts), so +// "Underflip part only" scales the rate of every controller asked during it (measured 2026-10-19: one to three, the +// underflip's own among them; the owner saw the under part turn at Underflip speed). +constexpr std::uintptr_t rate_compute = 0x10dd2f0; +constexpr unsigned char rate_compute_bytes[]{0x40, 0x53, 0x48, 0x83, 0xec, 0x30, 0x83, 0xb9, 0xac, 0x00, 0x00, 0x00, 0x00}; +using RateCompute = float (*)(std::uintptr_t self); +std::atomic rate_original{}; +std::atomic under_part_rate{0}; +float rate_compute_hook(std::uintptr_t self) { + const float rate = rate_original.load(std::memory_order_acquire)(self); + const float scale = under_part_rate.load(std::memory_order_relaxed); + return scale > 0 && holds_alive() ? rate * scale : rate; +} std::uintptr_t state_lookup_hook(std::uintptr_t object, std::uintptr_t context) { + if (object && object == underflip_timescale_object.at.load(std::memory_order_relaxed)) { + note_timescale_caller(reinterpret_cast(_ReturnAddress())); + const auto original = lookup_original.load(std::memory_order_acquire); + const auto found = original ? original(object, context) : 0; + if (found && found == held_timescale.at.load(std::memory_order_relaxed) && holds_alive()) scale_timescale(found); + return found; + } for (auto &held : held_switches) { if (held.object.load(std::memory_order_relaxed) != object) continue; held.asked.fetch_add(1, std::memory_order_relaxed); @@ -403,10 +646,100 @@ bool start_state_holds(std::uintptr_t base) noexcept { void *original{}; if (hook_prepare(target, reinterpret_cast(&state_lookup_hook), &original) != HookOk || !original) return false; lookup_original.store(reinterpret_cast(original), std::memory_order_release); + module_base.store(base, std::memory_order_relaxed); if (hook_enable(target) != HookOk) return false; lookup_ready.store(true, std::memory_order_relaxed); + // The state copy: Underflip speed acts there. + bool copy_known{}; + __try { + copy_known = std::memcmp(reinterpret_cast(base + state_copy), state_copy_bytes, sizeof(state_copy_bytes)) == 0; + } __except (1) {} + if (copy_known) { + void *copy{}; + if (hook_prepare(reinterpret_cast(base + state_copy), reinterpret_cast(&state_copy_hook), ©) == HookOk && copy) { + copy_original.store(reinterpret_cast(copy), std::memory_order_release); + (void)hook_enable(reinterpret_cast(base + state_copy)); + } + } + // The rate controllers' rate: "Underflip part only" acts there. + bool rate_known{}; + __try { + rate_known = std::memcmp(reinterpret_cast(base + rate_compute), rate_compute_bytes, sizeof(rate_compute_bytes)) == 0; + } __except (1) {} + if (rate_known) { + void *rate{}; + if (hook_prepare(reinterpret_cast(base + rate_compute), reinterpret_cast(&rate_compute_hook), &rate) == HookOk && rate) { + rate_original.store(reinterpret_cast(rate), std::memory_order_release); + (void)hook_enable(reinterpret_cast(base + rate_compute)); + } + } + // Who writes TrickFlipSpeed (for the log). + bool write_lookup_known{}; + __try { + write_lookup_known = std::memcmp(reinterpret_cast(base + state_write_lookup), state_write_lookup_bytes, sizeof(state_write_lookup_bytes)) == 0; + } __except (1) {} + if (write_lookup_known) { + void *writer{}; + if (hook_prepare(reinterpret_cast(base + state_write_lookup), reinterpret_cast(&state_write_lookup_hook), &writer) == HookOk && writer) { + write_lookup_original.store(reinterpret_cast(writer), std::memory_order_release); + (void)hook_enable(reinterpret_cast(base + state_write_lookup)); + } + } + // The number setter: only Underflip speed needs it, so the holds work without it. + bool setter_known{}; + __try { + setter_known = std::memcmp(reinterpret_cast(base + float_setter), float_setter_bytes, sizeof(float_setter_bytes)) == 0; + } __except (1) {} + if (setter_known) { + void *setter{}; + if (hook_prepare(reinterpret_cast(base + float_setter), reinterpret_cast(&float_setter_hook), &setter) == HookOk && setter) { + setter_original.store(reinterpret_cast(setter), std::memory_order_release); + (void)hook_enable(reinterpret_cast(base + float_setter)); + } + } return true; } +bool hold_underflip_timescale(std::uintptr_t at, float factor) noexcept { + const auto was = held_timescale.at.load(std::memory_order_relaxed); + if (!at) { + // Let go: the game's own number back where the trainer's is still there. + float value{}; + if (was && held_timescale.fresh.load(std::memory_order_relaxed) && peek(was, value) && value == held_timescale.scaled.load(std::memory_order_relaxed)) + (void)put(was, held_timescale.raw.load(std::memory_order_relaxed)); + held_timescale.at.store(0, std::memory_order_relaxed); + held_timescale.fresh.store(false, std::memory_order_relaxed); + return lookup_ready.load(std::memory_order_relaxed); + } + if (!lookup_ready.load(std::memory_order_relaxed)) return false; + held_timescale.factor.store(factor, std::memory_order_relaxed); + if (was != at) { + held_timescale.fresh.store(false, std::memory_order_relaxed); + held_timescale.at.store(at, std::memory_order_relaxed); + } + scale_timescale(at); // the tick's own read: the scaled number is there before the next lookup too + return true; +} +void scale_under_part(float factor) noexcept { under_part_rate.store(factor, std::memory_order_relaxed); } +void arm_underflip_flip_speed(std::uintptr_t at, float factor) noexcept { + underflip_flip_factor.store(factor, std::memory_order_relaxed); + underflip_flip_at.store(at, std::memory_order_relaxed); +} +float underflip_timescale_game() noexcept { return held_timescale.raw.load(std::memory_order_relaxed); } +std::string underflip_timescale_callers() noexcept { + std::string text; + for (std::size_t i = 0; i < std::size(held_timescale.caller); ++i) { + const auto from = held_timescale.caller[i].load(std::memory_order_relaxed); + if (!from) continue; + text += std::format("{}0x{:x} x{}", text.empty() ? "" : ", ", from, held_timescale.calls[i].exchange(0, std::memory_order_relaxed)); + } + text = text.empty() ? "nobody" : text; + text += std::format("; the game set it {} times, {} scaled; TrickFlipSpeed scaled for the underflip {} times", + timescale_sets.exchange(0, std::memory_order_relaxed), timescale_sets_scaled.exchange(0, std::memory_order_relaxed), + flip_sets_scaled.exchange(0, std::memory_order_relaxed)); + flip_sets.store(0, std::memory_order_relaxed); + text += std::format("; TrickFlipSpeed written by {} (armed for an underflip), {} (otherwise)", flip_writers_armed.take(), flip_writers.take()); + return text; +} void keep_holds_alive() noexcept { holds_until.store(GetTickCount64() + 500, std::memory_order_relaxed); } int holds_in_force() noexcept { if (!holds_alive()) return 0; @@ -524,5 +857,20 @@ bool write_flip_speed(const FlipState &state, float speed) noexcept { return put(state.speed_at, speed); } int flip_trick_index(std::uint32_t trick) noexcept { return trick < trick_count ? trick_slider[trick] : -1; } +int underflip_trick_index(std::uint32_t trick) noexcept { + int slot = -1; + switch (trick) { + case 2: case 17: case 43: case 47: slot = 0; break; // kickflip, nollie kickflip, their underflips + case 3: case 18: case 44: case 48: slot = 1; break; // heelflip + case 4: case 19: case 45: case 49: slot = 2; break; // pop shuvit + case 7: case 22: case 46: case 50: slot = 3; break; // FS pop shuvit + case 36: case 55: slot = 4; break; // impossible + case 37: case 56: slot = 5; break; // front foot impossible + case 38: case 57: slot = 6; break; // nollie impossible + case 39: case 58: slot = 7; break; // nollie back foot impossible + default: return -1; + } + return static_cast(first_underflip_slot) + slot; +} 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 index 09e33c5a..d6fd6ba1 100644 --- a/Extension/Trainer/trainer_gamestate.h +++ b/Extension/Trainer/trainer_gamestate.h @@ -21,6 +21,11 @@ struct FlipState { // The impossible's clips (and only they) play at Float.Anim.TrickFlipSpeedClamped instead: 0 when not found. std::uintptr_t clamped_at{}; float clamped{}; + // Bool.Anim.Is.Underflip, Bool.State.Is.Underflip, Bool.Anim.IsInUnderflipWindow: 1 or 0, -1 when not found. + std::int8_t underflip_anim{-1}, underflip_state{-1}, underflip_window{-1}; + // Float.Anim.UnderflipTimeScale, what the underflip clips play at: 0 when not found. + std::uintptr_t underflip_timescale_at{}; + float underflip_timescale{}; explicit operator bool() const noexcept { return speed_at != 0; } }; FlipState read_flip_state(std::uintptr_t base, std::uintptr_t entity) noexcept; @@ -67,13 +72,15 @@ GrindControl read_grind_control(std::uintptr_t base, std::uintptr_t entity) noex std::int32_t read_manual_intent(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, but for the impossibles, which are different tricks. Underflips, held flips, hospital and -// flower flips have none: their clips play at no speed state. +// its regular trick's speed, but for the impossibles, which are different tricks. Held flips have none: their clips play +// at no speed state. Underflips turn at Underflip speed times a speed of their own (the last eight): CurrentFlipTrick +// stays the flipped trick through an underflip, so the kickflip's underflip is told apart from the heelflip's by it +// (nollies share their regular trick's, but for the impossibles). struct FlipTrick { std::string_view key; // the option and preset name std::string_view label; // in the menu }; -inline constexpr std::array flip_tricks{{ +inline constexpr std::array flip_tricks{{ {"flip.kickflip", "Kickflip"}, {"flip.heelflip", "Heelflip"}, {"flip.popshuvit", "Pop shuvit"}, @@ -94,9 +101,21 @@ inline constexpr std::array flip_tricks{{ {"flip.nollieimpossible", "Nollie impossible"}, {"flip.nolliebackfootimpossible", "Nollie back foot impossible"}, {"flip.lateflip", "Late flip"}, + {"flip.kickflipunderflip", "Kickflip underflip"}, + {"flip.heelflipunderflip", "Heelflip underflip"}, + {"flip.popshuvitunderflip", "Pop shuvit underflip"}, + {"flip.fspopshuvitunderflip", "FS pop shuvit underflip"}, + {"flip.impossibleunderflip", "Impossible underflip"}, + {"flip.frontfootimpossibleunderflip", "Front foot impossible underflip"}, + {"flip.nollieimpossibleunderflip", "Nollie impossible underflip"}, + {"flip.nolliebackfootimpossibleunderflip", "Nollie back foot impossible underflip"}, }}; -// Which of those a CurrentFlipTrick value belongs to; -1: none (an ollie, a wallie, an underflip). +inline constexpr std::size_t late_flip_slot = 19, first_underflip_slot = 20; +static_assert(flip_tricks[late_flip_slot].key == "flip.lateflip" && flip_tricks[first_underflip_slot].key == "flip.kickflipunderflip"); +// Which of those a CurrentFlipTrick value belongs to; -1: none (a wallie, a held flip). int flip_trick_index(std::uint32_t trick) noexcept; +// Which underflip slot of those a CurrentFlipTrick value turns into when it is underflipped; -1: none. +int underflip_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; @@ -283,5 +302,21 @@ void grind_switch_asked(std::size_t index, std::uint32_t &asked, std::uint32_t & // Why a switch reads nothing: 0 its state object is not found, 1 found but its handle is not valid, 2 found but the // skater has no page or no value for it, 3 it has a value. `handle` is the object's handle word. int grind_switch_status(std::uintptr_t base, std::uintptr_t entity, std::size_t index, std::uint32_t &handle) noexcept; +// Underflip speed: the local skater's UnderflipTimeScale at `at` is scaled by `factor` at every lookup of it (the hook +// above), since the game sets it every tick before its clips read it. `at` 0 lets go and puts the game's number back. +// False while the hook is not running. +bool hold_underflip_timescale(std::uintptr_t at, float factor) noexcept; +// Underflip speed: while `at` (the local skater's TrickFlipSpeed) is armed, the game's own set of it, which starts an +// underflip, is scaled by `factor` as the game makes it. `at` 0 disarms. +void arm_underflip_flip_speed(std::uintptr_t at, float factor) noexcept; +// The underflip's playback rate band (MinScale, MaxScale of its two rate controllers): the game's own times `factor`, on +// every copy found by the memory search. Returns how many copies there are (0: not found yet). +int scale_underflip_bands(float low_factor, float high_factor) noexcept; +// The rate of every animation rate controller the game updates while it is set (0: off): the under part of an underflip. +void scale_under_part(float factor) noexcept; +// The game's own last UnderflipTimeScale, as the hold saw it. +float underflip_timescale_game() noexcept; +// Who looked UnderflipTimeScale up since last asked: "offset xcount, ..." (offsets in the game). +std::string underflip_timescale_callers() noexcept; void hold_grind_switches(std::uintptr_t base, std::uintptr_t entity, const std::int8_t *modes, std::int8_t *live) noexcept; } diff --git a/Extension/Trainer/trainer_page.cpp b/Extension/Trainer/trainer_page.cpp index 05050693..615b789e 100644 --- a/Extension/Trainer/trainer_page.cpp +++ b/Extension/Trainer/trainer_page.cpp @@ -44,7 +44,7 @@ struct Page { std::array preset_name{}; std::array teleport{}; double speed_edit{-1}, speed_until{}; - float manual_edit{1}, hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1}, pump_edit{1}; + float manual_edit{1}, hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1}, pump_edit{1}, underflip_edit{1}; std::array trick_edit{}; // the speed of each flip trick std::array trick_editing{}; float grind_side_edit{1}, grind_push_edit{}; @@ -57,7 +57,7 @@ struct Page { // Rows of the value table that cards show by name, for one snapshot (place_rows). std::array, 4> trick_rows; std::array, 6> transition_rows; - bool manual_editing{}, hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{}, pump_editing{}; + bool manual_editing{}, hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{}, pump_editing{}, underflip_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{}; @@ -99,7 +99,8 @@ void card_end(Page &p) { // A slider shows what the player is dragging until the trainer's own number catches up; after a reset there is nothing // to catch up with. void clear_edits(Page &p) { - p.manual_editing = p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.pump_editing = p.revert_editing = false; + p.manual_editing = p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.pump_editing = p.revert_editing = + p.underflip_editing = false; p.grind_side_editing = p.grind_push_editing = p.grind_range_editing = p.gravity_editing = false; p.trick_editing = {}; p.active.clear(); @@ -350,7 +351,8 @@ void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trai const auto hint = std::format("{} 1 is the game's own; \"{}\" is x {}.", preset.note, preset.name, number(preset.amount)); field(menu, std::abs(preset.factor - 1.0) > 1e-4 ? (preset.title + " *").c_str() : preset.title.c_str(), hint.c_str()); double value = preset.factor; - const double low = preset.amount < 1 ? 0.05 : 0.2, high = preset.amount < 1 ? 3.0 : 10.0; + // From 0: Grind lock-on at 0 is no lock-on (the owner's ask, 2026-10-19). + const double low = 0.0, high = preset.amount < 1 ? 3.0 : 10.0; if (slider_double("##dial", &value, low, high, "x %.2f", ImGuiSliderFlags_Logarithmic) && value >= 0) trainer_command(menu, callbacks, std::format("dial {} {}", number(value), preset.name)); ImGui::PopID(); @@ -371,11 +373,11 @@ void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trai 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; - // A preset that turns its value down gets a dial with more room below 1. - const double low = preset.amount < 1 ? 0.05 : 0.2, high = preset.amount < 1 ? 3.0 : 10.0; + // A preset that turns its value down gets a dial with less room above 1. + const double low = 0.0, high = preset.amount < 1 ? 3.0 : 10.0; // from 0, as the console menu's dial const float box = px(64); ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - box - button - reset - ImGui::GetStyle().ItemSpacing.x * 3)); - if (ImGui::SliderScalar("##dial", ImGuiDataType_Double, &value, &low, &high, "x %.2f", ImGuiSliderFlags_Logarithmic) && value > 0) + if (ImGui::SliderScalar("##dial", ImGuiDataType_Double, &value, &low, &high, "x %.2f", ImGuiSliderFlags_Logarithmic) && value >= 0) trainer_command(menu, callbacks, std::format("dial {} {}", number(value), preset.name)); if (ImGui::IsItemActive()) { p.active = id; @@ -1195,7 +1197,7 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, // jump on foot (ON FOOT). Pump power is on PUMPING and the board bending boost on TRICKLINING. void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words, int part) { 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[]{"Gravity", "Flip trick speed", advanced_label, "Pump power (pumping transitions)", "Manual pop height", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height", + static constexpr const char *labels[]{"Gravity", "Flip trick speed", "Underflip speed", "Late flip speed", advanced_label, "Pump power (pumping transitions)", "Manual pop height", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height", "Board bending boost (revert speed)"}; const auto wanted = [&](const char *label) { const std::string_view name(label); @@ -1208,7 +1210,9 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const // `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, bool always = false) { if (!always && !wanted(label)) return false; - field(menu, label, help); + // A value that is not the game's own (1) is marked, like a moved dial. + const auto shown_label = std::abs(value - 1.0f) > 1e-4f ? std::string(label) + " *" : std::string(label); + field(menu, shown_label.c_str(), help); ImGui::PushID(option); if (console_style()) { float shown = editing ? edit : value; @@ -1256,6 +1260,27 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const "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 (slider("Underflip speed", "underflip_speed", view.underflip_speed, p.underflip_edit, p.underflip_editing, + "How fast the board turns in an underflip (the kickflip, heelflip, shuvit and impossible underflips, the hospital and flower " + "flips). You flick and trigger the underflip as always. 1 is the game's own. By itself it sets the whole underflip, its flip " + "too, and Flip trick speed does not reach it; with 'Underflip part only' below the flip turns at Flip trick speed and this " + "sets the part where the board turns back.", + 3.0f)) { + bool split = view.underflip_split; + if (ImGui::Checkbox("Underflip part only (the flip at Flip trick speed)", &split)) + trainer_command(menu, callbacks, std::format("option underflip_split {}", split ? 1 : 0)); + if (ImGui::IsItemHovered()) + wrapped_tooltip("%s", "Off: Underflip speed sets the whole underflip. On: the underflip's flip turns at Flip trick speed (and the " + "trick's own speed under Advanced trick speed), and Underflip speed takes over where the board starts to " + "turn back. The trainer sees that from the board itself, so it starts a tick or two after the turn back."); + } + { + constexpr std::size_t late = trainer::late_flip_slot; + slider("Late flip speed", "flip.lateflip", view.flip_trick[late], p.trick_edit[late], p.trick_editing[late], + "How fast the board turns in a late flip (a flick after the pop, in the air), times the flip trick speed. 1 is the game's own. " + "It counts with or without Advanced trick speed.", + 3.0f); + } if (!view.flip_live && (view.flip_speed != 1.0f || view.flip_advanced || view.flip_flick) && wanted("Flip trick speed")) ImGui::TextDisabled("Finding the flip animations: flip speeds take hold about 15 seconds after a level loads."); if (wanted("Flip trick speed") || wanted("Slow flips pop higher") || wanted("Flick tricks at full speed")) { @@ -1280,17 +1305,33 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const 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 (the nollie impossibles have their own). The ollie's speed only acts " + "A trick turns at the speed above times its own. Nollie tricks use their regular trick's speed (the nollie impossibles have their own). Each underflip has a speed of its own at the bottom, times Underflip speed above. The ollie's speed only acts " "with 'Slow flips pop higher' on. 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) { + for (std::size_t i = 0; i < trainer::late_flip_slot; ++i) { // the Late flip has its slider above 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::SeparatorText("Underflips"); + for (std::size_t i = trainer::first_underflip_slot; 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 underflip, times Underflip speed above (with 'Underflip part only', the part where " + "the board turns back). Nollie underflips use their regular trick's speed, but for the nollie impossibles. 1 leaves " + "it at Underflip speed.", + 3.0f, false, true); + } + ImGui::SeparatorText("Late flips"); + slider("Late flip", "flip.lateflip", view.flip_trick[trainer::late_flip_slot], p.trick_edit[trainer::late_flip_slot], + p.trick_editing[trainer::late_flip_slot], + "How fast the board turns in a late flip, times the flip trick speed above: the same setting as Late flip speed above. " + "The game tells every late flip apart only as 'a late flip', so they share this one speed.", + 3.0f, false, true); ImGui::TreePop(); } } @@ -2122,6 +2163,10 @@ void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callba {trainer::topic_pumping, "pumping settings"}, {trainer::topic_foot, "on-foot jump"}}; for (const auto &[topic, name] : kinds) if (view->settings[topic]) add(name); + // Named on its own: it changes the underflips the game scores, which "trick settings" does not say. + if (std::abs(view->underflip_speed - 1.0f) > 1e-4f) + add(std::format("underflip speed x{:.2f}{}", view->underflip_speed, view->underflip_split ? " (under part)" : "")); + else if (view->underflip_split) add("underflip flip at flip trick speed"); } const auto cheats = cheat_lines(model); for (const auto &line : cheats) add(line.name); diff --git a/Extension/Trainer/trainer_session.cpp b/Extension/Trainer/trainer_session.cpp index 40e37b46..fecfd428 100644 --- a/Extension/Trainer/trainer_session.cpp +++ b/Extension/Trainer/trainer_session.cpp @@ -9,10 +9,11 @@ namespace dingosdk::trainer { namespace { -// version, table id, six multipliers, count; then an index and a value per class field. -constexpr std::uint8_t version = 1; -constexpr std::size_t header = 1 + 4 + 6 * 4 + 2, row = 2 + 4; -static_assert(header + class_field_count * row <= max_physics_extras, "every class value must fit in a session's physics extras"); +// version, table id, six multipliers (in version 2 then Underflip speed and a byte: 1 for the under part only), count; +// then an index and a value per class field. +constexpr std::uint8_t version = 1, version_underflip = 2; +constexpr std::size_t header = 1 + 4 + 6 * 4 + 2, header_underflip = header + 4 + 1, row = 2 + 4; +static_assert(header_underflip + class_field_count * row <= max_physics_extras, "every class value must fit in a session's physics extras"); static_assert(class_field_count <= 0xffff); constexpr float class_value_high = 1.0e6f; // what the trainer holds its own edits to @@ -53,12 +54,17 @@ std::uint32_t class_table_id() noexcept { std::vector encode_session_extras(const SessionExtras &extras) { std::vector out; if (extras.stock()) return out; - out.reserve(header + extras.classes.size() * row); - put(out, version); + const bool underflip = extras.underflip_speed != 1.0f || extras.underflip_split; + out.reserve((underflip ? header_underflip : header) + extras.classes.size() * row); + put(out, underflip ? version_underflip : version); put(out, class_table_id()); for (const float value : {extras.flip_speed, extras.hippy_height, extras.nocomply_height, extras.boneless_height, extras.offboard_height, extras.cruise}) put(out, value); + if (underflip) { + put(out, extras.underflip_speed); + put(out, static_cast(extras.underflip_split ? 1 : 0)); + } auto classes = extras.classes; std::erase_if(classes, [](const auto &entry) { return entry.first >= class_field_count || !std::isfinite(entry.second); }); std::ranges::stable_sort(classes, {}, [](const auto &entry) { return entry.first; }); @@ -74,7 +80,9 @@ std::vector encode_session_extras(const SessionExtras &extras) { std::optional decode_session_extras(std::span bytes) { SessionExtras extras; if (bytes.empty()) return extras; - if (bytes.size() < header || bytes.size() > max_physics_extras || bytes[0] != version) return std::nullopt; + const bool underflip = bytes[0] == version_underflip; + const std::size_t head = underflip ? header_underflip : header; + if (bytes.size() < head || bytes.size() > max_physics_extras || (bytes[0] != version && !underflip)) return std::nullopt; std::size_t at = 1; const auto table = take(bytes, at); std::array numbers; @@ -88,8 +96,16 @@ std::optional decode_session_extras(std::span extras.boneless_height = std::clamp(numbers[3], height_low, height_high); extras.offboard_height = std::clamp(numbers[4], height_low, height_high); extras.cruise = numbers[5] > 0 && numbers[5] < cruise_high ? numbers[5] : 0.0f; + if (underflip) { + const auto value = take(bytes, at); + if (!std::isfinite(value)) return std::nullopt; + extras.underflip_speed = std::clamp(value, flip_low, flip_high); + const auto split = take(bytes, at); + if (split > 1) return std::nullopt; + extras.underflip_split = split == 1; + } const std::size_t count = take(bytes, at); - if (bytes.size() != header + count * row) return std::nullopt; + if (bytes.size() != head + count * row) return std::nullopt; const bool same_table = table == class_table_id(); int previous = -1; for (std::size_t i = 0; i < count; ++i) { diff --git a/Extension/Trainer/trainer_session.h b/Extension/Trainer/trainer_session.h index b5d52780..1ee322e9 100644 --- a/Extension/Trainer/trainer_session.h +++ b/Extension/Trainer/trainer_session.h @@ -25,6 +25,11 @@ struct SessionExtras { float flip_speed{1}, hippy_height{1}, nocomply_height{1}, boneless_height{1}, offboard_height{1}; // Auto push: the speed a rolling skater is carried up to, m/s; 0: off. float cruise{}; + // x of the game's own while an underflip turns, and whether only its under part does (its flip then at the flip + // speed). Sent only when not the game's own (version 2), so a guest on an older trainer still reads a host who + // leaves them alone. + float underflip_speed{1}; + bool underflip_split{}; // Values of the game's tuning classes that are not the game's own: an index into // class_fields (trainer_classes.h) and the value, by ascending index. std::vector> classes;