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;