diff --git a/Extension/Skater/GRIND_EXITS.md b/Extension/Skater/GRIND_EXITS.md index 42814ce..b4a58b8 100644 --- a/Extension/Skater/GRIND_EXITS.md +++ b/Extension/Skater/GRIND_EXITS.md @@ -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. diff --git a/Extension/Skater/grind_exit_commands.cpp b/Extension/Skater/grind_exit_commands.cpp index 2d28786..ec15867 100644 --- a/Extension/Skater/grind_exit_commands.cpp +++ b/Extension/Skater/grind_exit_commands.cpp @@ -13,6 +13,7 @@ void register_grind_exit_commands(Commands ®istry) { {"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)", ""}, diff --git a/Extension/Skater/grind_exit_control.cpp b/Extension/Skater/grind_exit_control.cpp index 4dd2c72..ad4af40 100644 --- a/Extension/Skater/grind_exit_control.cpp +++ b/Extension/Skater/grind_exit_control.cpp @@ -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 base{}; std::atomic started{}, enabled{}, guard{true}, lean_guard{true}, hooked{}; std::atomic side_speed{gx::measured_side_speed}, prewindow{0.4f}; + std::atomic strength{100}; // pop-out strength, percent of the sideways speed (10-100) std::atomic stance{static_cast(Stance::unset)}, sides{static_cast(Sides::toeheel)}; std::atomic 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(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(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(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(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(c.stance.load()); const bool toeheel = static_cast(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(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& 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& 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 , sidespeed "; + return "Usage: grindexit exits on|off|status, stance goofy|regular, sides toeheel|travel, strength <10-100>, guard on|off, leanguard on|off, prewindow , sidespeed "; } } // namespace dingosdk::grind_exit_control diff --git a/Extension/Skater/grind_exit_control.h b/Extension/Skater/grind_exit_control.h index f81cd9e..d8d164e 100644 --- a/Extension/Skater/grind_exit_control.h +++ b/Extension/Skater/grind_exit_control.h @@ -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 , sidespeed . std::string command(const std::vector& 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) } diff --git a/Extension/UI/NativeMenu/Test/native_tools_view_tests.cpp b/Extension/UI/NativeMenu/Test/native_tools_view_tests.cpp index 0f0344c..a4f03e4 100644 --- a/Extension/UI/NativeMenu/Test/native_tools_view_tests.cpp +++ b/Extension/UI/NativeMenu/Test/native_tools_view_tests.cpp @@ -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); diff --git a/Extension/UI/NativeMenu/native_tools_view.h b/Extension/UI/NativeMenu/native_tools_view.h index b013b62..1ccf69c 100644 --- a/Extension/UI/NativeMenu/native_tools_view.h +++ b/Extension/UI/NativeMenu/native_tools_view.h @@ -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", diff --git a/Extension/UI/Overlay/hub_page.cpp b/Extension/UI/Overlay/hub_page.cpp index 92b1c9f..6aefeeb 100644 --- a/Extension/UI/Overlay/hub_page.cpp +++ b/Extension/UI/Overlay/hub_page.cpp @@ -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"); diff --git a/Extension/UI/Overlay/overlay.h b/Extension/UI/Overlay/overlay.h index cafb7ff..fc0adb3 100644 --- a/Extension/UI/Overlay/overlay.h +++ b/Extension/UI/Overlay/overlay.h @@ -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 { diff --git a/Extension/UI/Overlay/skate_menu_skater.cpp b/Extension/UI/Overlay/skate_menu_skater.cpp index cafc769..baa033f 100644 --- a/Extension/UI/Overlay/skate_menu_skater.cpp +++ b/Extension/UI/Overlay/skate_menu_skater.cpp @@ -1,5 +1,6 @@ #include "skate_menu_internal.h" +#include #include #include @@ -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"); diff --git a/Runtime/client_tick.cpp b/Runtime/client_tick.cpp index a87cff4..e48d74f 100644 --- a/Runtime/client_tick.cpp +++ b/Runtime/client_tick.cpp @@ -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();