Trainer 0.4.2: flip trick speed scales only the stick-speed curve (the pop keeps its height); the slow-flip switch says when a session keeps it off

This commit is contained in:
andrewnakas committed 2026-10-06 19:50:09 -06:00
1 parent 097d306a15
commit 6fcd427bf6
6 files changed
+55 -18

No files matched your search

+9
View File
@@ -10,6 +10,15 @@
AutoRevertBoost. Settings saved by 0.4.1 with the old 12 are moved to off. Turn the rule on
yourself and a landing that counts by it alone gets the auto revert boost at most. Found and
measured by AutoRevertBoost's authors (Sivaes, jaq and OVM).
- **Flip trick speed no longer lowers your ollie.** The slider used to scale seven of the game's
flip curves and shrink the range each one is read over; three of those are what a flick adds
to the height of the pop, so a slow flip came with a low ollie. It now scales only the one
curve that turns the stick's speed into the flip's speed. Measured on flat ground at x 0.3:
kickflips at about half the game's turn rate, plain ollies the same height and air time as
with the slider untouched.
- **Let slow flips stay slow says when it is off.** In a session whose host enforces physics or
has turned trick settings off, the switch cannot act (the likely reason it seemed to work only on
the main map); a line under it now says so.
## v0.4.1 - 2026-10-06
+2
View File
@@ -1541,6 +1541,7 @@ void build_view() {
next->flip_speed = s.flip_speed;
next->flip_gate = s.flip_gate;
next->flip_gate_found = flip_gates() != 0;
next->flip_gate_blocked = !s.flip_gate && (s.enforced || (multiplayer_session_active() && !session_boosts_allowed()));
next->boneless_height = s.boneless_height;
next->revert_boost = s.revert_boost;
next->revert = s.revert;
@@ -1758,6 +1759,7 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s
find_classes();
}
}
flip_speed_in_use(s.boosts.flip != 1.0f);
want_flip_speed(s.boosts.flip);
// The player's own choice, and only where their own trick settings count: not under a
// host's enforced physics or in a session that has turned boosts off.
+1
View File
@@ -92,6 +92,7 @@ struct View {
float flip_speed{1}; // board flip tricks: x of the game's own speed
bool flip_gate{true}; // the game brings a flip round before the landing; false: slow flips stay slow
bool flip_gate_found{}; // the rule's number was found in memory (the switch can act)
bool flip_gate_blocked{}; // ticked, but this session's rules keep the game's own flips
// HUD
bool hud{}, hud_jump{}, logging{};
// The loaded map and what its author ships for the trainer (Mods/<mod>/trainer.json).
+37 -16
View File
@@ -65,7 +65,7 @@ int mirror_field(const MirrorSlot &slot) noexcept {
struct FlipCurve {
std::uintptr_t curve{}, points{};
std::vector<float> outputs;
float low{}, high{}; // the bounds the curve clamps its output to (+0x20, +0x24)
float low{}, high{}; // the input range the curve is read over (MinX, MaxX at +0x20, +0x24): never written
};
constexpr std::size_t curve_point = 0x1c, curve_x = 0xc, curve_y = 0x14, curve_points_at = 0x18;
// The point that tells what a curve holds: its largest output (some curves start at 0).
@@ -81,6 +81,7 @@ struct Found {
std::mutex mutex;
std::vector<FlipCurve> flip;
float flip_wanted{1}, flip_written{1};
bool flip_used{}; // a flip speed other than the game's is set, though not written at this moment
std::vector<std::uintptr_t> gates; // where the flick warp limit's number lives
bool gate_kept{true}; // false: the trainer holds it off
Copies copies;
@@ -232,6 +233,14 @@ void collect_eights(std::uintptr_t start, std::size_t size, const std::vector<st
}
} __except (EXCEPTION_EXECUTE_HANDLER) {}
}
// The curves the flip speed slider scales, a bit per curve in memory order: only curve 4,
// GestureSpeedToFlipSpeed, the stick's speed to the speed of the flip. Measured with the owner on
// flat ground, 2026-10-06 (outputs x0.3, nothing else touched): kickflips at about half the
// game's turn rate, plain ollies 0.39 to 0.40 m with 0.62 s of air, the same as untouched.
// 0.4.0 to 0.4.2 also scaled curves 3, 6 and 7 (what a flick adds to the HEIGHT of the pop) and
// every curve's input range at +0x20 / +0x24 (MinX and MaxX, not output bounds as was thought):
// that made a slow flip a low ollie. Curves 0 and 1 changed neither the flip nor the pop.
std::atomic<std::uint32_t> flip_curve_mask{0x10};
// One curve's points: false unless it has `count` points starting at (x0, y0).
bool read_curve(std::uintptr_t curve, std::uint32_t count, float x0, float y0, FlipCurve &out) noexcept {
std::uintptr_t points{};
@@ -265,18 +274,24 @@ std::vector<FlipCurve> find_flip_curves(std::uintptr_t curve_type) {
for (const auto run : runs) {
std::array<std::uintptr_t, 8> curves{};
if (!peek(run, curves)) continue;
// In memory order: 0 the flip speed against the flick, 1 the same for the legacy controls,
// 2 a multiplier for a high flick (1 to 2: left alone), 3, 6 and 7 the speed a flick's height
// and the stick sensitivity add (0 to 2.75), 4 flick speed to flip speed, 5 ollie speed to
// flip speed. Scaling only 0, 1, 4 and 5 slowed a flip by a third at x0.4: the rest is 3, 6, 7.
// In memory order: 0 and 1 a speed against the flick (new controls and legacy), 2 a
// multiplier for a high flick (1 to 2), 3, 6 and 7 what a flick adds to the height of the
// pop (0 to 2.75 over the stick's speed), 4 the stick's speed to the flip speed, 5 the
// ollie's speed to the flip speed (flat 1). All seven are checked to know the run is the
// flip trick tuning; flip_curve_mask says which are scaled.
std::array<FlipCurve, 7> speed;
if (!read_curve(curves[0], 3, 0.0f, 0.4f, speed[0]) || !read_curve(curves[1], 3, 0.35f, 0.65f, speed[1]) ||
!read_curve(curves[4], 3, 0.0f, 0.05f, speed[2]) || !read_curve(curves[5], 2, 0.0f, 1.0f, speed[3]) ||
!read_curve(curves[3], 4, 0.0f, 0.0f, speed[4]) || !read_curve(curves[6], 4, 0.0f, 0.0f, speed[5]) ||
!read_curve(curves[7], 4, 0.0f, 0.0f, speed[6]))
continue;
for (auto &curve : speed)
if (std::ranges::none_of(result, [&](const FlipCurve &known) { return known.points == curve.points; })) result.push_back(std::move(curve));
// Which of them the flip speed scales: a bit per curve, in memory order (see flip_curve_mask).
static constexpr std::size_t memory_index[]{0, 1, 4, 5, 3, 6, 7};
const auto mask = flip_curve_mask.load(std::memory_order_relaxed);
for (std::size_t i = 0; i < speed.size(); ++i)
if (auto &curve = speed[i]; ((mask >> memory_index[i]) & 1u) &&
std::ranges::none_of(result, [&](const FlipCurve &known) { return known.points == curve.points; }))
result.push_back(std::move(curve));
}
return result;
}
@@ -414,8 +429,6 @@ DWORD WINAPI search(void *) noexcept {
if (std::ranges::any_of(f.flip, [&](const FlipCurve &known) { return known.points == curve.points; })) continue;
if (f.flip_written != 1) {
for (std::size_t i = 0; i < curve.outputs.size(); ++i) (void)put(curve.points + i * curve_point + curve_y, curve.outputs[i] * f.flip_written);
(void)put(curve.curve + 0x20, curve.low > 0 ? curve.low * f.flip_written : curve.low);
(void)put(curve.curve + 0x24, curve.high > 0 ? curve.high * f.flip_written : curve.high);
}
f.flip.push_back(std::move(curve));
}
@@ -458,9 +471,6 @@ std::size_t write_gates(Found &f) noexcept {
}
void write_flip_curve(const FlipCurve &curve, float factor) noexcept {
for (std::size_t i = 0; i < curve.outputs.size(); ++i) (void)put(curve.points + i * curve_point + curve_y, curve.outputs[i] * factor);
// The bounds move with the outputs, or the curve clamps the scaled values back.
(void)put(curve.curve + 0x20, curve.low > 0 ? curve.low * factor : curve.low);
(void)put(curve.curve + 0x24, curve.high > 0 ? curve.high * factor : curve.high);
}
} // namespace
@@ -475,6 +485,15 @@ bool refresh_classes(std::uint64_t now) noexcept {
std::size_t repaired{};
repaired += write_gates(f);
lost = lost || (!f.gate_kept && f.gates.empty());
// On the ground the curves hold the game's own (the flip speed is written in the air only):
// one that holds something else is someone else's memory now.
if (f.flip_used && f.flip_written == 1) {
std::erase_if(f.flip, [&](const FlipCurve &curve) {
float y{};
return curve.outputs.empty() || !peek(probe_address(curve), y) || !same(y, probe_output(curve));
});
lost = lost || f.flip.empty();
}
if (f.flip_written != 1) {
std::erase_if(f.flip, [&](const FlipCurve &curve) {
float y{};
@@ -567,7 +586,7 @@ void want_class_value(std::size_t field, float value) noexcept {
bool classes_wanted() noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
if (f.flip_wanted != 1 || !f.gate_kept) return true;
if (f.flip_wanted != 1 || f.flip_used || !f.gate_kept) return true;
for (std::size_t i = 0; i < class_field_count; ++i)
if (f.wanted[i] != class_fields[i].stock) return true;
return false;
@@ -587,9 +606,6 @@ std::size_t apply_classes() noexcept {
for (const auto &curve : f.flip) {
for (std::size_t i = 0; i < curve.outputs.size(); ++i)
if (put(curve.points + i * curve_point + curve_y, curve.outputs[i] * f.flip_wanted)) ++count;
// The bounds move with the outputs, or the curve clamps the scaled values back.
(void)put(curve.curve + 0x20, curve.low > 0 ? curve.low * f.flip_wanted : curve.low);
(void)put(curve.curve + 0x24, curve.high > 0 ? curve.high * f.flip_wanted : curve.high);
}
f.flip_written = f.flip_wanted;
}
@@ -631,6 +647,11 @@ void want_flip_speed(float factor) noexcept {
f.dirty = true;
}
}
void flip_speed_in_use(bool used) noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
f.flip_used = used;
}
void want_flip_gate(bool kept) noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
+3
View File
@@ -51,6 +51,9 @@ std::string classes_summary();
// (speed against the flick, against the ollie...); the search finds it as eight curve references
// in a row and apply_classes multiplies the outputs of its four speed curves by this.
void want_flip_speed(float factor) noexcept;
// The player has a flip speed of their own set, whether or not it is written right now (it is
// written only while the board is in the air): the curves must be found either way.
void flip_speed_in_use(bool used) noexcept;
// How many flip trick speed curves were found (0: the multiplier does nothing yet).
std::size_t flip_curves() noexcept;
// The game hurries a flip trick so that it is round 1/6 s before the landing it predicts, however
+3 -2
View File
@@ -474,7 +474,7 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
return true;
};
slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing,
"How fast the board turns in a flip trick. Below 1 a flip is slower and gets that much longer to come round. On its own the game still "
"How fast the board turns in a flip trick. It does not change how high you pop. Below 1 a flip is slower. On its own the game still "
"hurries a slow flip so it is finished before you land: tick the switch below to stop that. Above 1 the game's own limit on how fast a "
"board turns takes over.",
3.0f);
@@ -485,7 +485,8 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
wrapped_tooltip("%s", "The game speeds a flip up so the board is round 1/6 of a second before the landing it predicts, however slow the flip speed "
"is set. That is why a slowed flip still comes round on a small pop. Ticked: that rule is off. The board turns as "
"slowly as you set it and lands however far round it got, so a flip you start too late or too low is a bail.");
if (slow && !view.flip_gate_found) ImGui::TextDisabled("Not found in the game yet: it takes effect a few seconds after a level loads.");
if (view.flip_gate_blocked) ImGui::TextDisabled("Off in this session: the host's physics rules apply here, so flips finish before landing as usual.");
else if (slow && !view.flip_gate_found) ImGui::TextDisabled("Not found in the game yet: it takes effect a few seconds after a level loads.");
}
slider("No comply height", "nocomply_height", view.nocomply_height, p.nocomply_edit, p.nocomply_editing, "How high a no comply pops. 1 is the game's own.");
slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing, "How high a boneless pops. 1 is the game's own.");