Grind exits: pop-out strength slider

A Pop-out strength setting (10-100%, kept) scales the sideways speed of
trigger-set grind exits, for rails next to a wall, where the full exit
carries the skater into it. 100% is the default and exactly the previous
behaviour.

- ReSkate menu: a slider in the GRIND EXITS card, sent when it is let go,
  in steps of 5.
- skate.'s Mod Options and the tools menu: a row stepping 100 / 75 / 50 /
  25%.
- Console: grindexit strength <10-100>; kept in control.txt with the
  other grind exit settings.
- Below 100%, an exit the game was already sending toward the chosen side
  is scaled too, so the setting applies to every trigger-set exit.
This commit is contained in:
Belcross committed 2026-10-13 07:59:13 +01:00
1 parent 30a320ff62
commit 64bfefbf68
10 files changed
+78 -9

No files matched your search

+3
View File
@@ -26,6 +26,9 @@ ReSkate menu's skater page, or the console (`~`) with `grindexit exits on|off`).
grind, the stick's left / right reaches the game at the pop instead of before it (it cannot
lean-balance the grind meanwhile), so the game's own lean exit cannot drag the board toward the
spin and into the rail (`grindexit leanguard on|off`).
- **Pop-out strength** (`grindexit strength 10-100`, kept): how far to the side a trigger exit
pops, as a share of the full exit (100%, the default, is the game's own sideways speed). Lower it
for rails next to a wall. Very low values leave little clearance on exits over the rail.
- **Multiplayer**: works in any session or server, whatever the host's settings, throwdowns
included. It adds no speed or height (the sideways speed is the game's own); it only changes which
input picks the exit side. Server checks do not see it, since it changes no game data.
+1
View File
@@ -13,6 +13,7 @@ void register_grind_exit_commands(Commands &registry) {
{"exits", "Grind exit triggers: LT/RT pressed while grinding pick the side you pop out to (kept)", "on|off|status"},
{"stance", "Grind exits: your skater's stance, for toe/heel sides (kept)", "goofy|regular"},
{"sides", "Grind exits: LT toe / RT heel, or LT left / RT right of travel (kept)", "toeheel|travel"},
{"strength", "Grind exits: how far to the side a trigger exit pops, percent of the full exit (kept; 100 is the game's own)", "<10-100>"},
{"guard", "Grind exits: hide a trigger used to pick the side from the game until released (no accidental grab)", "on|off"},
{"leanguard", "Grind exits: the left stick's left/right reaches the game at the pop while a trigger picks the side", "on|off"},
{"prewindow", "Grind exits: seconds a trigger may be pressed before the grind connects and still count (0.4)", "<seconds>"},
+29 -7
View File
@@ -32,6 +32,7 @@ constexpr double release_grace_seconds = 0.1; // a trigger let g
constexpr double axes_fresh_seconds = 0.25; // the skater's facing, sampled on the client tick
constexpr std::size_t rail_steps = 6; // grind steps the rail's direction is averaged over
constexpr float min_rail_speed = 0.5f, min_kept_side_speed = 1.0f;
constexpr int min_strength = 10; // pop-out strength floor, percent: below it a trigger barely picks a side
constexpr float min_toe_across = 0.35f; // below this the toe points along the rail (slides): left/right of travel instead
enum class Stance : int { unset, regular, goofy };
@@ -78,6 +79,7 @@ struct Control {
std::atomic<std::uintptr_t> base{};
std::atomic<bool> started{}, enabled{}, guard{true}, lean_guard{true}, hooked{};
std::atomic<float> side_speed{gx::measured_side_speed}, prewindow{0.4f};
std::atomic<int> strength{100}; // pop-out strength, percent of the sideways speed (10-100)
std::atomic<int> stance{static_cast<int>(Stance::unset)}, sides{static_cast<int>(Sides::toeheel)};
std::atomic<bool> wanted{}; // the player's on/off choice, kept; `enabled` is whether it is installed now
// Physics thread -> the launch hook and the pad filter.
@@ -155,6 +157,11 @@ void load_settings(Control& c) {
else if (line == "sides=toeheel") c.sides.store(static_cast<int>(Sides::toeheel));
else if (line == "enabled=1") c.wanted.store(true);
else if (line == "enabled=0") c.wanted.store(false);
else if (line.starts_with("strength=")) {
int value{};
const auto parsed = std::from_chars(line.data() + 9, line.data() + line.size(), value);
if (parsed.ec == std::errc{} && value >= min_strength && value <= 100) c.strength.store(value);
}
}
}
void save_settings(Control& c) {
@@ -166,6 +173,7 @@ void save_settings(Control& c) {
if (stance != Stance::unset) out << "stance=" << (stance == Stance::goofy ? "goofy" : "regular") << "\n";
out << "sides=" << (static_cast<Sides>(c.sides.load()) == Sides::travel ? "travel" : "toeheel") << "\n";
out << "enabled=" << (c.wanted.load() ? 1 : 0) << "\n";
out << "strength=" << c.strength.load() << "\n";
}
// ---- physics side --------------------------------------------------------------------------
@@ -338,9 +346,13 @@ void modify_launch(std::uintptr_t caller_rva, std::uintptr_t, float (&v)[3]) noe
if (d.trigger == 0) { d.reason = both ? Reason::both_triggers : Reason::no_trigger; record(c, d); return; }
d.toward = toward_side(c, d.trigger, nx, nz, now, d);
const float side = static_cast<float>(d.toward);
if (d.before_side * side >= min_kept_side_speed) { d.reason = Reason::kept; record(c, d); return; }
const float magnitude = std::abs(d.before_side) >= min_kept_side_speed ? std::abs(d.before_side)
: c.side_speed.load(std::memory_order_relaxed);
// Pop-out strength scales the sideways speed of every trigger-set exit; at 100% (the default)
// the game's own exit toward the chosen side is kept as it is.
const int strength = c.strength.load(std::memory_order_relaxed);
if (strength >= 100 && d.before_side * side >= min_kept_side_speed) { d.reason = Reason::kept; record(c, d); return; }
const float full = std::abs(d.before_side) >= min_kept_side_speed ? std::abs(d.before_side)
: c.side_speed.load(std::memory_order_relaxed);
const float magnitude = full * static_cast<float>(strength) / 100.0f;
const float change = side * magnitude - d.before_side;
v[0] += change * nx;
v[2] += change * nz;
@@ -361,11 +373,11 @@ std::string status_text(Control& c) {
const auto stance = static_cast<Stance>(c.stance.load());
const bool toeheel = static_cast<Sides>(c.sides.load()) == Sides::toeheel;
std::string text = std::format(
"Grind exit control: {}. Sides: {}. Stance: {}. Grab guard {}, lean guard {}, pre-press window {:.2f} s, side speed {:.2f} m/s. "
"Pops out of grinds seen: {}, sides set: {}.",
"Grind exit control: {}. Sides: {}. Stance: {}. Pop-out strength {}%. Grab guard {}, lean guard {}, pre-press window {:.2f} s, "
"side speed {:.2f} m/s. Pops out of grinds seen: {}, sides set: {}.",
on ? "ON" : "off", toeheel ? "LT toe / RT heel" : "LT left / RT right of travel",
stance == Stance::goofy ? "goofy" : stance == Stance::regular ? "regular" : "NOT SET (type: grindexit stance goofy|regular)",
c.guard.load() ? "on" : "off", c.lean_guard.load() ? "on" : "off", c.prewindow.load(), c.side_speed.load(), c.pops.load(),
c.strength.load(), c.guard.load() ? "on" : "off", c.lean_guard.load() ? "on" : "off", c.prewindow.load(), c.side_speed.load(), c.pops.load(),
c.applied.load());
if (on && toeheel && stance == Stance::unset) text += " Until the stance is set, LT/RT are left/right of travel.";
if (on && !c.hooked.load()) text += " The launch hook is not ready yet (it installs once the game's code is recognised).";
@@ -398,6 +410,7 @@ bool available() noexcept { return control().started.load(std::memory_order_acqu
bool enabled() noexcept { return control().enabled.load(std::memory_order_acquire); }
bool toe_heel() noexcept { return static_cast<Sides>(control().sides.load()) == Sides::toeheel; }
int stance() noexcept { return control().stance.load(); }
int strength() noexcept { return control().strength.load(); }
bool start(std::uintptr_t image_base) noexcept {
auto& c = control();
@@ -492,6 +505,15 @@ std::string command(const std::vector<std::string>& words) {
try { save_settings(c); } catch (...) {}
return status_text(c);
}
if (verb == "strength") {
int value{};
const auto parsed = std::from_chars(arg.data(), arg.data() + arg.size(), value);
if (parsed.ec != std::errc{} || parsed.ptr != arg.data() + arg.size() || value < min_strength || value > 100)
return "Usage: grindexit strength <10 to 100> (percent of the full pop-out to the side; 100 is the game's own)";
c.strength.store(value);
try { save_settings(c); } catch (...) {}
return status_text(c);
}
if (verb == "guard" || verb == "leanguard") {
if (arg != "on" && arg != "off") return "Usage: grindexit " + verb + " on|off";
(verb == "guard" ? c.guard : c.lean_guard).store(arg == "on");
@@ -510,6 +532,6 @@ std::string command(const std::vector<std::string>& words) {
c.side_speed.store(value);
return status_text(c);
}
return "Usage: grindexit exits on|off|status, stance goofy|regular, sides toeheel|travel, guard on|off, leanguard on|off, prewindow <s>, sidespeed <m/s>";
return "Usage: grindexit exits on|off|status, stance goofy|regular, sides toeheel|travel, strength <10-100>, guard on|off, leanguard on|off, prewindow <s>, sidespeed <m/s>";
}
} // namespace dingosdk::grind_exit_control
+4 -1
View File
@@ -20,6 +20,8 @@
// sideways speed when it had one, mirrored to the chosen side, else `sidespeed` (1.8 m/s, the
// game's own). Along the rail, the pop's height, the trick, the flip and the body's rotation
// are the game's.
// - Pop-out strength (10-100%, kept): scales the sideways speed of every trigger-set exit, for
// rails next to a wall. 100% is the behaviour above, the game's own full exit.
// - Grab guard: a claimed trigger is hidden from the game until it is let go, so holding it
// through the pop does not start a grab; press it again in the air to grab.
// - Lean guard: while a claimed trigger is held in a grind, the left stick's left/right is
@@ -40,7 +42,7 @@ bool start(std::uintptr_t image_base) noexcept;
// Client thread, every tick, with the local client: keeps the launch hook ready, samples the
// skater's facing, writes the physics thread's decisions to the log.
void on_client_tick(std::uintptr_t client) noexcept;
// The console verbs: exits on|off|status, stance goofy|regular, sides toeheel|travel,
// The console verbs: exits on|off|status, stance goofy|regular, sides toeheel|travel, strength <10-100>,
// guard on|off, leanguard on|off, prewindow <seconds>, sidespeed <m/s>.
std::string command(const std::vector<std::string>& words);
@@ -49,4 +51,5 @@ bool available() noexcept;
bool enabled() noexcept;
bool toe_heel() noexcept; // else left/right of travel
int stance() noexcept; // 0 not set, 1 regular, 2 goofy
int strength() noexcept; // pop-out strength, percent of the sideways speed (10-100; 100 the full exit)
}
@@ -177,6 +177,16 @@ int main() {
"which needs no stance");
activate(state, model, cb, "grind-exits", "");
check(capture.command == "grindexit exits off", "and the triggers turn off");
// Pop-out strength steps down from the full exit and wraps back to it.
check(grind_row("grind-exit-strength").title == "Pop-out strength: 100%", "Pop-out strength shows the full exit");
activate(state, model, cb, "grind-exit-strength", "");
check(capture.command == "grindexit strength 75", "and steps down");
model.grind_exit.strength = 40;
activate(state, model, cb, "grind-exit-strength", "");
check(capture.command == "grindexit strength 25", "a slider value steps to the next lower step");
model.grind_exit.strength = 25;
activate(state, model, cb, "grind-exit-strength", "");
check(capture.command == "grindexit strength 100", "and the lowest wraps to the full exit");
model.multiplayer.local_name = "steam_name";
model.player_card = {true, "Card Name", {}};
const auto card_page = render(state, model, cb, player_section);
@@ -219,6 +219,11 @@ inline void activate(State& s, const overlay::Model& m, const overlay::Callbacks
} else if (command == "grind-exit-stance") {
// Not set or regular -> goofy -> regular.
console(m.grind_exit.stance == 2 ? "grindexit stance regular" : "grindexit stance goofy", m.grind_exit.available);
} else if (command == "grind-exit-strength") {
// 100 -> 75 -> 50 -> 25 -> 100 (the ReSkate menu's slider sets any value from 10 to 100).
const int strength = m.grind_exit.strength;
const int next = strength > 75 ? 75 : strength > 50 ? 50 : strength > 25 ? 25 : 100;
console("grindexit strength " + std::to_string(next), m.grind_exit.available);
} else if (command == "board-wear-reset") {
console("boardwear reset", m.offline.board_wear.available && m.offline.board_wear.effective);
} else if (command == "challenges") {
@@ -331,6 +336,8 @@ inline Page render(State& s, const overlay::Model& m, const overlay::CallbacksV3
button(p.main, "grind-exit-stance",
std::string("Your stance: ") + (grind.stance == 2 ? "Goofy" : grind.stance == 1 ? "Regular" : "Not set"),
"grind-exit-stance", grind_console);
button(p.main, "grind-exit-strength", "Pop-out strength: " + std::to_string(grind.strength) + "%", "grind-exit-strength",
grind_console);
toggle(p.main, "board-wear", "Board wear", m.offline.board_wear.effective,
m.offline.board_wear.available && cb.queue_offline_feature);
button(p.main, "board-wear-reset", "Reset board wear", "board-wear-reset",
+1
View File
@@ -1175,6 +1175,7 @@ void tools_page(Ui &ui, ImVec2 a, ImVec2 z) {
if (grind.toe_heel &&
list.setting("Your stance", grind.stance == 2 ? "Goofy" : grind.stance == 1 ? "Regular" : "Not set", grind.available && console))
act("grind-exit-stance");
if (list.setting("Pop-out strength", std::to_string(grind.strength) + "%", grind.available && console)) act("grind-exit-strength");
const auto &wear = m.offline.board_wear;
if (list.setting("Board wear", on_off(wear.effective), wear.available && cb.queue_offline_feature)) act("board-wear");
if (list.setting("Reset board wear", {}, wear.available && wear.effective && console)) act("board-wear-reset");
+1
View File
@@ -239,6 +239,7 @@ struct BoardBreakModel {
struct GrindExitModel {
bool available = false, enabled = false, toe_heel = true;
int stance = 0;
int strength = 100; // pop-out strength, percent (10-100)
};
struct Model {
@@ -1,5 +1,6 @@
#include "skate_menu_internal.h"
#include <algorithm>
#include <array>
#include <cmath>
@@ -207,6 +208,25 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
send_console(menu, callbacks, grind_stance == 1 ? "grindexit stance goofy" : "grindexit stance regular");
if (grind.stance == 0) warn("Pick your stance. Until then LT / RT are left / right of travel.");
}
{
// Sent once the slider is let go (not every frame of a drag); the dragged value shows
// until the setting comes back, in steps of 5.
static int edited = -1;
static double edited_at = 0;
int strength = edited >= 0 ? edited : grind.strength;
field(menu, "Pop-out strength", "How far to the side a trigger exit pops. 100% is the full exit; lower it for rails next to a wall.");
ImGui::BeginDisabled(!grind_console);
if (ImGui::SliderInt("##grind-strength", &strength, 10, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
edited = std::clamp((strength + 2) / 5 * 5, 10, 100);
edited_at = ImGui::GetTime();
}
if (ImGui::IsItemDeactivatedAfterEdit() && edited >= 0) {
send_console(menu, callbacks, "grindexit strength " + std::to_string(edited));
edited_at = ImGui::GetTime();
}
if (edited >= 0 && !ImGui::IsItemActive() && (grind.strength == edited || ImGui::GetTime() - edited_at > 2.0)) edited = -1;
ImGui::EndDisabled();
}
end_card();
begin_card(menu, "boosts", "BOOSTS", "Buttons are set in Settings > Controls");
+2 -1
View File
@@ -962,7 +962,8 @@ void update_model(std::uintptr_t client, TickState& frame) {
model.board_break = {dingosdk::board_break::available(), dingosdk::board_break::enabled(), !breakable_deck.empty(),
dingosdk::board_break::deck_label(breakable_deck)};
model.grind_exit = {dingosdk::grind_exit_control::available(), dingosdk::grind_exit_control::enabled(),
dingosdk::grind_exit_control::toe_heel(), dingosdk::grind_exit_control::stance()};
dingosdk::grind_exit_control::toe_heel(), dingosdk::grind_exit_control::stance(),
dingosdk::grind_exit_control::strength()};
model.bindings = dingosdk::local_profile_controller_bindings();
model.parks = dingosdk::local_profile_parks();
model.world = dingosdk::local_profile_world_layers();