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) <noreply@anthropic.com>
This commit is contained in:
andrewnakasandClaude Opus 5.5 committed 2026-10-19 23:20:59 -06:00
1 parent f9c6dbdda2
commit c2d86f2ffa
9 files changed
+690 -47

No files matched your search

+21
View File
@@ -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 <x>`; 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.
@@ -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 <class T> void poke(std::vector<std::uint8_t> &bytes, std::size_t at, T value) { std::memcpy(bytes.data() + at, &value, sizeof(T)); }
std::pair<std::uint16_t, float> row(std::size_t index, float value) { return {static_cast<std::uint16_t>(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<std::uint8_t>(underflip_bytes.begin(), underflip_bytes.end() - 1)), "a version 2 encoding cut short is refused");
// Not an encoding.
const auto refused = [&](std::vector<std::uint8_t> broken) { return !decode_session_extras(broken); };
check(refused({1}), "a lone byte is refused");
+165 -18
View File
@@ -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<float, 3> 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::int8_t>(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<int>(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<std::size_t>(index) != late_flip_slot)) return 1.0f;
return s.flip_trick[static_cast<std::size_t>(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<std::size_t>(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<std::uint16_t>(e.class_field), static_cast<float>(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<Setup> &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<Setup> &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<Setup> &setups() {
all->back().high = high;
}
constexpr std::pair<std::string_view, std::string_view> 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<std::uintptr_t>(s.entity));
s.flip_live = static_cast<bool>(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<std::size_t>(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<float>;
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<float>;
}
} 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<std::size_t>(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<std::string> &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<std::string> &a) {
if (!value) return "error: return_delay needs seconds.";
s.return_delay = std::clamp(static_cast<float>(*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<std::string> &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);
+3 -1
View File
@@ -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
+353 -5
View File
@@ -5,6 +5,7 @@
#include <cmath>
#include <cstring>
#include <format>
#include <intrin.h>
#include <string>
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<std::uintptr_t> 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<float> 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<const void *>(id_at), object.id, sizeof(object.id)) != 0) return false;
if (std::memcmp(reinterpret_cast<const void *>(id_at), id, sizeof(id)) != 0) return false;
const auto name = *reinterpret_cast<const std::uintptr_t *>(id_at + object_after_id + object_name);
if (name <= id_at || name > id_at + 0x400) return false;
return _strnicmp(reinterpret_cast<const char *>(name), object.name.data(), object.name.size()) == 0 &&
*reinterpret_cast<const char *>(name + object.name.size()) == 0;
return _strnicmp(reinterpret_cast<const char *>(name), wanted.data(), wanted.size()) == 0 &&
*reinterpret_cast<const char *>(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<std::uint64_t, 64> &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<std::int8_t>(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<StateLookup> lookup_original{};
std::atomic<std::uint64_t> holds_until{};
bool holds_alive() noexcept { return GetTickCount64() < holds_until.load(std::memory_order_relaxed); }
std::atomic<bool> lookup_attempted{}, lookup_ready{};
std::atomic<std::uintptr_t> 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<std::uintptr_t> at{}; // the local skater's value; 0: not held
std::atomic<float> factor{1}, raw{}, scaled{};
std::atomic<bool> fresh{};
std::atomic<std::uintptr_t> caller[8]{}; // who asked (offsets in the game) and how often, for the log
std::atomic<std::uint32_t> 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<FloatSetter> setter_original{};
std::atomic<std::uint32_t> 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<std::uintptr_t> underflip_flip_at{};
std::atomic<float> underflip_flip_factor{1};
std::atomic<std::uint32_t> 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<StateWriteLookup> write_lookup_original{};
struct CallerTable {
std::atomic<std::uintptr_t> caller[8]{};
std::atomic<std::uint32_t> 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<std::uintptr_t>(_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<StateCopy> 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<RateCompute> rate_original{};
std::atomic<float> 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<std::uintptr_t>(_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<void *>(&state_lookup_hook), &original) != HookOk || !original) return false;
lookup_original.store(reinterpret_cast<StateLookup>(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<const void *>(base + state_copy), state_copy_bytes, sizeof(state_copy_bytes)) == 0;
} __except (1) {}
if (copy_known) {
void *copy{};
if (hook_prepare(reinterpret_cast<void *>(base + state_copy), reinterpret_cast<void *>(&state_copy_hook), &copy) == HookOk && copy) {
copy_original.store(reinterpret_cast<StateCopy>(copy), std::memory_order_release);
(void)hook_enable(reinterpret_cast<void *>(base + state_copy));
}
}
// The rate controllers' rate: "Underflip part only" acts there.
bool rate_known{};
__try {
rate_known = std::memcmp(reinterpret_cast<const void *>(base + rate_compute), rate_compute_bytes, sizeof(rate_compute_bytes)) == 0;
} __except (1) {}
if (rate_known) {
void *rate{};
if (hook_prepare(reinterpret_cast<void *>(base + rate_compute), reinterpret_cast<void *>(&rate_compute_hook), &rate) == HookOk && rate) {
rate_original.store(reinterpret_cast<RateCompute>(rate), std::memory_order_release);
(void)hook_enable(reinterpret_cast<void *>(base + rate_compute));
}
}
// Who writes TrickFlipSpeed (for the log).
bool write_lookup_known{};
__try {
write_lookup_known = std::memcmp(reinterpret_cast<const void *>(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<void *>(base + state_write_lookup), reinterpret_cast<void *>(&state_write_lookup_hook), &writer) == HookOk && writer) {
write_lookup_original.store(reinterpret_cast<StateWriteLookup>(writer), std::memory_order_release);
(void)hook_enable(reinterpret_cast<void *>(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<const void *>(base + float_setter), float_setter_bytes, sizeof(float_setter_bytes)) == 0;
} __except (1) {}
if (setter_known) {
void *setter{};
if (hook_prepare(reinterpret_cast<void *>(base + float_setter), reinterpret_cast<void *>(&float_setter_hook), &setter) == HookOk && setter) {
setter_original.store(reinterpret_cast<FloatSetter>(setter), std::memory_order_release);
(void)hook_enable(reinterpret_cast<void *>(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<int>(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{}; }
}
+39 -4
View File
@@ -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<FlipTrick, 20> flip_tricks{{
inline constexpr std::array<FlipTrick, 28> flip_tricks{{
{"flip.kickflip", "Kickflip"},
{"flip.heelflip", "Heelflip"},
{"flip.popshuvit", "Pop shuvit"},
@@ -94,9 +101,21 @@ inline constexpr std::array<FlipTrick, 20> 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;
}
+56 -11
View File
@@ -44,7 +44,7 @@ struct Page {
std::array<char, 49> preset_name{};
std::array<float, 3> 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<float, trainer::flip_tricks.size()> trick_edit{}; // the speed of each flip trick
std::array<bool, trainer::flip_tricks.size()> 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<std::vector<std::size_t>, 4> trick_rows;
std::array<std::vector<std::size_t>, 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);
+24 -8
View File
@@ -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<std::uint8_t> encode_session_extras(const SessionExtras &extras) {
std::vector<std::uint8_t> 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<std::uint8_t>(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<std::uint8_t> encode_session_extras(const SessionExtras &extras) {
std::optional<SessionExtras> decode_session_extras(std::span<const std::uint8_t> 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<std::uint32_t>(bytes, at);
std::array<float, 6> numbers;
@@ -88,8 +96,16 @@ std::optional<SessionExtras> decode_session_extras(std::span<const std::uint8_t>
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<float>(bytes, at);
if (!std::isfinite(value)) return std::nullopt;
extras.underflip_speed = std::clamp(value, flip_low, flip_high);
const auto split = take<std::uint8_t>(bytes, at);
if (split > 1) return std::nullopt;
extras.underflip_split = split == 1;
}
const std::size_t count = take<std::uint16_t>(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) {
+5
View File
@@ -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<std::pair<std::uint16_t, float>> classes;