Files
AyeZeeBB 4c07f2493b Menu: a full screen layout for controllers beside the window, and the pages regrouped
The ReSkate menu now has two styles, switched with View / F11 or the buttons in its top bar
and saved with the profile (`ui fullscreen`):
- Window: the mouse menu as before, with a side rail of pages in groups.
- Full screen: made for a controller, in the pause menu's look. Pages across the top (LB / RB),
  each page's sections as the ONLINE page's tabs (LT / RT), button prompts at the bottom, the
  pause menu's sounds, and the pause menu's own Multiplayer pages as ONLINE.

The pages are regrouped: HOME (quick switches and a tile for each page), MAP, BUILD, SKATER
(freecam, first person, cheats), SANDBOX (once Trainer), ADDONS (drone, Board Break, Road
Rash, Hall of Meat, grind exits, music), ONLINE, PROGRESS, MODS, SETTINGS, DEVELOPER.

Full screen, every shared control is drawn for a pad: switches and option rows changed with
left / right one step a press, sliders as steppers that repeat when held, rows of buttons,
and pages that scroll with the right stick. Search boxes and typed values stay in the window.
A pad press takes the menu's focus back and hides the pointer until the mouse moves.

Also: map pictures in the map picker (official loading screens, a mod's icon) with a search
box in the window, Splash and Tutorial Island left out of it; number boxes beside the
window's sliders; the binds in groups, with an action bind to open the menu (`menu`); a menu
size for each style (`ui scalefull`); the time of day stepping from the one last asked for.
2026-10-16 23:37:33 -05:00

347 lines
22 KiB
C++

#include "skate_menu_internal.h"
#include <algorithm>
#include <array>
#include <cmath>
// The SKATER page.
namespace dingosdk::overlay::menu {
namespace {
// A slider row with a trailing button (e.g. "Default") after a field() label.
float trailing_width(const char* label) {
return ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2;
}
}
void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "freecam", "FREECAM");
bool flight = debug.free_camera;
if (toggle_row(menu, "Freecam", "Detach the camera and explore.", flight,
debug.available && debug.camera_available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_free_camera, flight});
bool freecam_controller = model.bindings.freecam_controller;
if (toggle_row(menu, "Block Input(Enable Controller Support)", "Block player input and use the controller for the Freecam.", freecam_controller,
model.bindings.available && callbacks.queue_debug)) {
std::array<char, 512> result{};
callbacks.queue_console_command(callbacks.user, freecam_controller ? "freecam_controller true" : "freecam_controller false", result.data(), result.size());
}
field(menu, "Teleport to Freecam");
ImGui::BeginDisabled(!debug.free_camera || !debug.camera_position_valid || !callbacks.queue_console_command);
if (ImGui::Button("Teleport", ImVec2(-1, 0))) {
std::array<char, 512> result{};
callbacks.queue_console_command(callbacks.user, "tp_to_freecam", result.data(), result.size());
}
ImGui::EndDisabled();
{
field(menu, "Field of view");
const bool custom = debug.free_camera_fov > 0;
int free_fov = static_cast<int>(std::lround(custom ? debug.free_camera_fov : debug.camera_fov));
const float reset = trailing_width("Default");
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - reset - ImGui::GetStyle().ItemSpacing.x);
ImGui::BeginDisabled(!callbacks.queue_debug);
if (::dingosdk::overlay::menu::slider_int("##free-camera-fov", &free_fov, 40, 120, custom ? "%d degrees" : "%d degrees (game)",
ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_free_camera_fov, false, static_cast<float>(free_fov)});
ImGui::SameLine();
ImGui::BeginDisabled(!custom);
if (ImGui::Button("Default", ImVec2(reset, 0)))
debug_request(menu, callbacks, {DebugAction::set_free_camera_fov, false, 0.0f});
ImGui::EndDisabled();
ImGui::EndDisabled();
}
field(menu, "Flight speed", "Also used by Noclip.");
ImGui::BeginDisabled((!debug.free_camera && !debug.noclip) || !debug.camera_available || !callbacks.queue_debug);
constexpr std::array<float, 7> speeds{0.6f, 3, 5, 15, 60, 300, 1500};
constexpr std::array<const char*, 7> labels{"Precise", "Slow", "Cruise", "Default", "Fast", "Travel", "Maximum"};
int selected = 3;
for (int i = 0; i < static_cast<int>(speeds.size()); ++i)
if (std::abs(debug.camera_speed - speeds[i]) < .01f) selected = i;
if (::dingosdk::overlay::menu::slider_int("##flight-speed", &selected, 0, 6, labels[selected], ImGuiSliderFlags_NoInput))
debug_request(menu, callbacks, {DebugAction::set_camera_speed, false, speeds[selected]});
ImGui::EndDisabled();
if (debug.free_camera) note("Close the menu to fly. WASD / Q E move; hold the right mouse button to look.");
if (!debug.camera_available) warn(debug.camera_unavailable.c_str());
end_card();
}
void first_person_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "first-person", "FIRST PERSON");
bool first_person = debug.first_person;
if (toggle_row(menu, "First person", "See the world through the skater's eyes.", first_person,
debug.available && debug.camera_available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_first_person, first_person});
field(menu, "Field of view");
int fov = static_cast<int>(std::lround(debug.first_person_fov > 0 ? debug.first_person_fov : debug.camera_fov));
ImGui::BeginDisabled(!debug.first_person || !callbacks.queue_debug);
if (::dingosdk::overlay::menu::slider_int("##first-person-fov", &fov, 40, 120, "%d degrees", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_first_person_fov, false, static_cast<float>(fov)});
ImGui::EndDisabled();
{
auto settings = debug.first_person_arm;
const bool can_edit = debug.available && callbacks.queue_debug;
if (toggle_row(menu, "True first person",
"Keep the horizon level and the view steady; the head's heading is still followed.", settings.stabilize, can_edit))
debug_request(menu, callbacks, {DebugAction::set_first_person_stabilize, settings.stabilize});
ImGui::BeginDisabled(!settings.stabilize || !can_edit);
const auto percent = [&](const char* label, const char* id, float value, DebugAction action) {
field(menu, label);
if (::dingosdk::overlay::menu::slider_float(id, &value, 0, dingosdk::first_person::strength_limit, "%.0f%%", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {action, false, value});
};
percent("Smoothing", "##fp-smoothing", settings.smoothing, DebugAction::set_first_person_smoothing);
percent("Head nod (up / down)", "##fp-head-pitch", settings.head_pitch, DebugAction::set_first_person_head_pitch);
percent("Head tilt (horizon roll)", "##fp-head-roll", settings.head_roll, DebugAction::set_first_person_head_roll);
percent("Head bob", "##fp-bob", settings.bob, DebugAction::set_first_person_bob);
if (toggle_row(menu, "Follow flips", "Turn the view with the skater during flips and bails.", settings.follow_flips,
can_edit && settings.stabilize))
debug_request(menu, callbacks, {DebugAction::set_first_person_follow_flips, settings.follow_flips});
ImGui::EndDisabled();
if (toggle_row(menu, "Third person on foot",
"Use the game's camera while walking; first person comes back when you get on the board.",
settings.board_only, can_edit))
debug_request(menu, callbacks, {DebugAction::set_first_person_board_only, settings.board_only});
if (toggle_row(menu, "First person in the camera cycle",
"Clicking the right stick (R3) on the board goes high camera, low camera, first person, then high again. "
"Needs Camera height control on in the game's settings (Gameplay > Advanced Camera).",
settings.camera_cycle, can_edit))
debug_request(menu, callbacks, {DebugAction::set_first_person_camera_cycle, settings.camera_cycle});
note("Nod, tilt and bob are the share of the head's own movement kept. Use Spring arm > Pitch to look "
"further down at the board.");
}
if (ImGui::TreeNode("Spring arm")) {
namespace fp = dingosdk::first_person;
auto settings = debug.first_person_arm;
if (toggle_row(menu, "Spring", "Let the camera lag behind and soften animated head movement.", settings.enabled,
debug.available && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_first_person_spring, settings.enabled});
ImGui::BeginDisabled(!debug.available || !callbacks.queue_debug);
const auto slider = [&](const char* label, float value, float low, float high, const char* format, DebugAction action) {
field(menu, label);
ImGui::PushID(label);
if (::dingosdk::overlay::menu::slider_float("##arm-value", &value, low, high, format, ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {action, false, value});
ImGui::PopID();
};
note("Position offset, metres from the head");
slider("Right", settings.offset[0], -fp::offset_limit, fp::offset_limit, "%.3f", DebugAction::set_first_person_offset_x);
slider("Up", settings.offset[1], -fp::offset_limit, fp::offset_limit, "%.3f", DebugAction::set_first_person_offset_y);
slider("Forward", settings.offset[2], -fp::offset_limit, fp::offset_limit, "%.3f", DebugAction::set_first_person_offset_z);
note("Rotation offset, degrees");
slider("Pitch", settings.rotation[0], -fp::rotation_limit, fp::rotation_limit, "%.1f", DebugAction::set_first_person_pitch);
slider("Yaw", settings.rotation[1], -fp::rotation_limit, fp::rotation_limit, "%.1f", DebugAction::set_first_person_yaw);
slider("Roll", settings.rotation[2], -fp::rotation_limit, fp::rotation_limit, "%.1f", DebugAction::set_first_person_roll);
note("Smoothing strength");
slider("Moving up", settings.up, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_up);
slider("Moving down", settings.down, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_down);
slider("Moving left", settings.left, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_left);
slider("Moving right", settings.right, 0, fp::strength_limit, "%.0f%%", DebugAction::set_first_person_spring_right);
if (ImGui::Button("Reset arm settings", ImVec2(-FLT_MIN, 0)))
debug_request(menu, callbacks, {DebugAction::reset_first_person_arm});
ImGui::EndDisabled();
note("Higher smoothing follows more slowly; 0% follows directly. Offsets work with Spring off. "
"Depth and roll use the average strength.");
ImGui::TreePop();
}
end_card();
}
// Board Break's settings (Extension/BoardBreak/board_break_feel.h): how a board breaks, wears and
// sounds. Each is a console verb, kept with the profile.
void board_break_settings_card(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& bb = model.board_break;
if (!bb.available) return;
const bool console = bb.enabled && callbacks.queue_console_command;
std::string subtitle = !bb.has_deck ? "No Board Break deck is in the game"
: "Your board: " + std::to_string(static_cast<int>(bb.worn * 100.0f + 0.5f)) + "% worn (" + bb.condition + ")";
begin_card(menu, "board-break-settings", "BOARD BREAK SETTINGS", subtitle.c_str());
if (!bb.enabled) note("Switch Board Break on to change these.");
const auto flag = [&](const char* label, const char* hint, bool on, const char* verb) {
bool value = on;
if (toggle_row(menu, label, hint, value, console)) send_console(menu, callbacks, std::string("boardbreak ") + verb + (value ? " 1" : " 0"));
};
flag("Always break", "Every slam breaks the board, and every slide that comes down onto a rail: for replay clips.", bb.always, "always");
flag("Rail snaps", "A boardslide or darkslide that comes down too hard onto a rail or ledge can snap the board, and you go down with it.",
bb.rails, "rails");
flag("Wear and tear", "Every hit wears your board. A worn board turns lazier, wobbles and breaks more easily; worn out, it can give way. "
"Turn on Board wear (Mod Options) to see the scratches.", bb.wear, "wear");
flag("Break sounds", "The crack of a break, and metal when it breaks on metal.", bb.crack_sounds, "cracks");
flag("Body hit sounds", "Each hit of your body in a bail, as hard as it hits.", bb.body_sounds, "bodysounds");
// Sent once a slider is let go; the dragged value shows until the setting comes back.
const auto percent = [&](const char* id, const char* label, const char* hint, int value, int low, int high, int step, const char* verb) {
static std::array<int, 3> edited{-1, -1, -1};
static std::array<double, 3> edited_at{};
const auto slot = std::string_view(id) == "bb-sensitivity" ? 0u : std::string_view(id) == "bb-joined" ? 1u : 2u;
int shown = edited[slot] >= 0 ? edited[slot] : value;
field(menu, label, hint);
ImGui::BeginDisabled(!console);
if (::dingosdk::overlay::menu::slider_int((std::string("##") + id).c_str(), &shown, low, high, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
edited[slot] = std::clamp((shown + step / 2) / step * step, low, high);
edited_at[slot] = ImGui::GetTime();
}
if (ImGui::IsItemDeactivatedAfterEdit() && edited[slot] >= 0) {
send_console(menu, callbacks, std::string("boardbreak ") + verb + " " + std::to_string(edited[slot]));
edited_at[slot] = ImGui::GetTime();
}
if (edited[slot] >= 0 && !ImGui::IsItemActive() && (value == edited[slot] || ImGui::GetTime() - edited_at[slot] > 2.0)) edited[slot] = -1;
ImGui::EndDisabled();
};
percent("bb-sensitivity", "Break sensitivity", "How easily a board breaks. 200% takes half the force, 50% twice as much.", bb.sensitivity, 10, 400, 10,
"sensitivity");
percent("bb-joined", "Grip tape holds", "How many breaks the grip tape holds together instead of a piece coming off.", bb.joined, 0, 100, 5, "joined");
percent("bb-wear-rate", "Wear speed", "How fast boards wear. 0% keeps a board as it is.", bb.wear_rate, 0, 500, 10, "wearrate");
if (console_style()) {
if (button_row(menu, "bb-new", {{"NEW BOARD", console && bb.has_deck, false, "Your board as good as new: no wear."}}) == 0)
send_console(menu, callbacks, "boardbreak worn 0");
} else {
ImGui::BeginDisabled(!console || !bb.has_deck);
if (ImGui::Button("New board", ImVec2(-FLT_MIN, 0))) send_console(menu, callbacks, "boardbreak worn 0");
ImGui::EndDisabled();
}
end_card();
}
// Skater > CHEATS: flying through things, never bailing, and the boosts.
void cheats_tab(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "cheats", "NOCLIP AND NO BAIL");
bool noclip = debug.noclip;
if (toggle_row(menu, "Noclip", "Fly with the normal player camera. Includes No Bail; uses the Freecam flight speed.", noclip,
(debug.noclip_available || debug.noclip) && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_noclip, noclip});
if (!debug.noclip_available && !debug.noclip) note(debug.noclip_unavailable.c_str());
bool no_bail = debug.no_bail;
const char* bail_help = debug.noclip && debug.no_bail_active
? "Protection is automatic during noclip. Enable this to keep it when flight ends."
: debug.no_bail && !debug.no_bail_active
? "Enabled; waiting for an active local skater."
: "Prevent new wipeouts. Recover from any current bail before enabling.";
if (toggle_row(menu, "No Bail", bail_help, no_bail,
(debug.no_bail_available || debug.no_bail) && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_no_bail, no_bail});
end_card();
begin_card(menu, "boosts", "BOOSTS", "Buttons are set in Settings > Controls");
ImGui::BeginDisabled(!callbacks.queue_debug);
field(menu, "Forward boost");
float forward_velocity = debug.forward_velocity_speed;
if (slider_float("##forward-velocity", &forward_velocity, 1.0f, 300.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_forward_velocity_speed, false, forward_velocity});
field(menu, "Up boost");
float up_velocity = debug.up_velocity_speed;
if (slider_float("##up-velocity", &up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_up_velocity_speed, false, up_velocity});
field(menu, "Off-board up boost");
float offboard_up_velocity = debug.offboard_up_velocity_speed;
if (slider_float("##offboard-up-velocity", &offboard_up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_offboard_up_velocity_speed, false, offboard_up_velocity});
ImGui::EndDisabled();
note("Controller: left stick moves, right stick looks, RT / LT rise and fall, click the left stick to boost.");
note("Keyboard: WASD / Q E, Shift to boost. Close the menu to fly.");
end_card();
}
// Addons > ROAD RASH: bails leave marks.
void road_rash_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "road-rash", "ROAD RASH");
bool road_rash = model.road_rash.enabled;
const bool rash_console = model.road_rash.available && callbacks.queue_console_command;
if (toggle_row(menu, "Road Rash", "Bails leave marks that build up: dust, scrapes, bruises, wounds. Needs the Road Rash item pack.",
road_rash, rash_console))
send_console(menu, callbacks, road_rash ? "roadrash 1" : "roadrash 0");
bool rash_blood = model.road_rash.blood;
if (toggle_row(menu, "Road Rash blood", "Fresh blood on the worst wounds. Off, they are dry and scabbed.", rash_blood,
rash_console && model.road_rash.enabled))
send_console(menu, callbacks, rash_blood ? "roadrashblood 1" : "roadrashblood 0");
end_card();
}
// Addons > HALL OF MEAT: bails show the bones they hurt and score a Meat card.
void hall_of_meat_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "hall-of-meat", "HALL OF MEAT");
bool hall_of_meat = model.hall_of_meat.enabled;
// No Bail (and noclip, which includes it) stops the bails Hall of Meat scores.
const bool bails_off = debug.no_bail || debug.no_bail_active;
const char* meat_help = hall_of_meat && bails_off
? "On, but No Bail is stopping your bails, so nothing shows. Turn No Bail (and noclip) off in Skater > Cheats."
: bails_off
? "Your bails show the bones they hurt and score a Meat card. Needs No Bail (and noclip) off."
: "Your bails show the bones they hurt and score a Meat card. A break slows the game in single player.";
if (toggle_row(menu, "Hall of Meat", meat_help, hall_of_meat, model.hall_of_meat.available && callbacks.queue_console_command))
send_console(menu, callbacks, hall_of_meat ? "hallofmeat 1" : "hallofmeat 0");
end_card();
}
// Addons > BOARD BREAK: the switch, the deck in the game, and how boards break, wear and sound.
void board_break_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "board-break", "BOARD BREAK");
bool board_break = model.board_break.enabled;
if (toggle_row(menu, "Board Break", model.board_break.has_deck
? "A hard slam can snap your board: at the nose, in the middle or at the tail. Whole again once you pick it up."
: "A hard slam can snap your board. Needs one of the Board Break deck packs.",
board_break, model.board_break.available && callbacks.queue_console_command))
send_console(menu, callbacks, board_break ? "boardbreak 1" : "boardbreak 0");
end_card();
board_break_deck_card(menu, model, callbacks);
board_break_settings_card(menu, model, callbacks);
}
// Addons > GRIND EXITS: grind exit control (Extension/Skater/grind_exit_control.h).
void grind_exits_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& grind = model.grind_exit;
const bool grind_console = grind.available && callbacks.queue_console_command;
begin_card(menu, "grind-exits", "GRIND EXITS", "Choose the side you pop out of a grind using LT/RT");
bool grind_on = grind.enabled;
if (toggle_row(menu, "Grind exit triggers", "Press LT or RT while grinding to pick the side you pop out to.",
grind_on, grind_console))
send_console(menu, callbacks, grind_on ? "grindexit exits on" : "grindexit exits off");
field(menu, "Input Mode");
int grind_sides = grind.toe_heel ? 0 : 1;
if (choice(menu, "grind-sides", grind_sides, {"Toe / Heel", "Left / Right"}, grind_console, 0.0f,
{"Direction is based on your skater's toe/heel side", "Direction is based on left or right of your direction of travel"}))
send_console(menu, callbacks, grind_sides == 0 ? "grindexit sides toeheel" : "grindexit sides travel");
if (grind.toe_heel) {
field(menu, "Your stance");
int grind_stance = grind.stance == 2 ? 1 : grind.stance == 1 ? 0 : -1;
if (choice(menu, "grind-stance", grind_stance, {"Regular", "Goofy"}, grind_console))
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 (slider_int("##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();
}
void skater_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
// (Grind exits are an addon: Addons > Grind Exits.)
category_tabs(menu, menu.skater_tab, {"FREECAM", "FIRST PERSON", "CHEATS"}, "skater-tabs");
ImGui::PushID(menu.skater_tab);
ImGui::BeginChild("skater-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
switch (menu.skater_tab) {
case 0: camera_controls(menu, model, callbacks); break;
case 1: first_person_controls(menu, model, callbacks); break;
default: cheats_tab(menu, model, callbacks); break;
}
ImGui::EndChild();
ImGui::PopID();
}
}