#include "trainer_page.h" #include "trainer.h" #include "Extension/UI/Overlay/skate_menu_internal.h" #include "Extension/UI/skate_theme.h" #include #include #include #include #include #include #include // The TRAINER page and the skating HUD. Presentation thread: reads the trainer's snapshots // and queues `trainer ...` console commands; it never touches the game. namespace dingosdk::overlay::menu { namespace { struct Page { int tab{}; // opens on TUNE: the dials and switches std::array search{}; int mode{1}; // the Tune tab: 0 Realistic, 1 Fun (two short lists), 2 Everything int group{1}; // Everything: 1: every group, 2..: one of the view's groups bool only_changed{}, graph_points{}, show_unused{}; std::size_t hidden_unused{}; // rows the filters would show but for "no use found" // The rows the filters leave, rebuilt when the snapshot or a filter changes. std::vector shown; std::uint64_t shown_revision{}; std::string shown_key; // The value under the pointer while it is dragged: the snapshot lags a frame or two. std::string active; double active_value{}; std::array preset_name{}; std::array teleport{}; double speed_edit{-1}, speed_until{}; float hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1}; float revert_edit{}; bool revert_editing{}; // The Tricklining list: rows of the value table per group, for one snapshot. std::array, 5> trick_rows; std::uint64_t trick_revision{~0ull}; bool hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_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{}; bool show_page{}; }; void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words); void feel_buttons(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::View &view); void trickline_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view); Page &page() { static auto *value = new Page; return *value; } // A tooltip that wraps at a readable width instead of running off the screen as one line. void wrapped_tooltip(const char *format, ...) { if (!ImGui::BeginTooltip()) return; ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f); va_list arguments; va_start(arguments, format); ImGui::TextV(format, arguments); va_end(arguments); ImGui::PopTextWrapPos(); ImGui::EndTooltip(); } // What the control just drawn does, shown while the pointer (or the controller's focus) is on it. void tip(const char *text) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) wrapped_tooltip("%s", text); } std::string lower(std::string_view text) { std::string result(text); std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return result; } bool contains_words(const std::string &haystack, const std::string &words) { std::size_t start = 0; while (start < words.size()) { auto end = words.find(' ', start); if (end == std::string::npos) end = words.size(); if (end > start && haystack.find(words.substr(start, end - start)) == std::string::npos) return false; start = end + 1; } return true; } std::string number(double value) { return std::format("{:.6g}", value); } void trainer_command(SkateMenu &menu, const CallbacksV3 &callbacks, const std::string &text) { send_console(menu, callbacks, "trainer " + text); } void filter_rows(Page &p, const trainer::View &view) { const auto words = lower(p.search.data()); const auto key = std::format("{}|{}|{}|{}|{}|{}", words, p.group, p.only_changed, p.graph_points, p.show_unused, p.mode); if (p.shown_revision == view.revision && p.shown_key == key) return; p.shown_revision = view.revision; p.shown_key = key; p.shown.clear(); p.hidden_unused = 0; const std::string *group = p.mode == 2 && p.group > 1 && static_cast(p.group) <= view.groups.size() + 1 ? &view.groups[static_cast(p.group) - 2] : nullptr; const bool essentials = p.mode < 2 && words.empty(); // searching looks through every value, whatever the list const std::uint8_t list = p.mode == 0 ? trainer::mode_realistic : trainer::mode_fun; for (std::size_t i = 0; i < view.rows.size(); ++i) { const auto &row = view.rows[i]; if (essentials) { // Only what the game was found or seen to read: a short list has no room for maybes. if (!row.friendly.empty() && (row.modes & list) && (row.used || (row.modes & trainer::mode_trial))) p.shown.push_back(i); continue; } if (row.detail && !p.graph_points && !row.touched) continue; if (p.only_changed && !row.touched && !row.frozen) continue; // Searching looks through every group. if (words.empty() ? (group && row.group != *group) : !contains_words(lower(row.id + " " + row.label + " " + row.friendly), words)) continue; // A value nothing in the game reads would be a slider that does nothing. if (!row.used && !p.show_unused && !row.touched && !row.frozen) { ++p.hidden_unused; continue; } p.shown.push_back(i); } if (essentials) std::ranges::sort(p.shown, {}, [&](std::size_t i) { return view.rows[i].rank; }); } void value_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const trainer::Row &row, bool with_group, bool friendly) { ImGui::PushID(row.id.c_str()); bool frozen = row.frozen; if (ImGui::Checkbox("##freeze", &frozen)) trainer_command(menu, callbacks, std::format("freeze {} {}", row.id, frozen ? 1 : 0)); if (ImGui::IsItemHovered()) wrapped_tooltip("Lock: presets and Reset values leave this value alone.\nReset everything (top of the page) clears locks too.\nYou do not need it for a change to apply."); ImGui::SameLine(); const float column = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.52f; const auto label = friendly && !row.friendly.empty() ? row.friendly : with_group ? row.group + " / " + row.label : row.label; const bool tinted = row.touched || !row.used; if (tinted) ImGui::PushStyleColor(ImGuiCol_Text, row.used ? skate_theme::blue : IM_COL32(150, 150, 150, 255)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(row.used ? label.c_str() : (label + " (no use found)").c_str()); if (tinted) ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) wrapped_tooltip("%s%s%s\nStock: %s%s", row.help.c_str(), row.help.empty() ? "" : "\n", row.id.c_str(), number(row.stock).c_str(), row.used ? "" : "\nThe game was not found (or seen) reading this value, so changing it may do nothing."); ImGui::SameLine(column); const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2; ImGui::BeginDisabled(!view.editable || !callbacks.queue_console_command); double value = p.active == row.id ? p.active_value : row.value; bool edited = false; if (row.kind == trainer::Kind::flag) { bool on = value != 0; if (ImGui::Checkbox("##value", &on)) { value = on ? 1 : 0; edited = true; } } else { ImGui::SetNextItemWidth(std::max(px(90), ImGui::GetContentRegionAvail().x - reset - ImGui::GetStyle().ItemSpacing.x)); const bool scale = row.kind == trainer::Kind::curve || row.kind == trainer::Kind::graph; const bool whole = row.kind == trainer::Kind::integer; const double low = scale ? -1.0e9 : -1.0e12, high = scale ? 1.0e9 : 1.0e12; // none of the trainer's own const auto speed = whole ? 0.1f : scale ? 0.01f : static_cast(std::max({std::abs(row.stock), std::abs(value), 0.01}) * 0.004); edited = ImGui::DragScalar("##value", ImGuiDataType_Double, &value, speed, &low, &high, whole ? "%.0f" : scale ? "x %.2f" : "%.4g", ImGuiSliderFlags_AlwaysClamp); if (ImGui::IsItemActive()) { p.active = row.id; p.active_value = value; } else if (p.active == row.id) { p.active.clear(); } } if (!ImGui::IsItemActive()) tip(row.kind == trainer::Kind::flag ? "Tick to switch this on, untick to switch it off." : "Drag left or right to change it, or double-click to type a number. Blue means changed from the game's own."); if (edited) trainer_command(menu, callbacks, std::format("set {} {}", row.id, number(value))); if (row.touched && row.kind != trainer::Kind::flag) { ImGui::SameLine(); if (ImGui::Button("Reset", ImVec2(reset, 0))) trainer_command(menu, callbacks, "reset " + row.id); tip("Puts this one value back to the game's own."); } ImGui::EndDisabled(); ImGui::PopID(); } // A built-in preset as a switch: blue while every value it sets holds what it sets. void preset_switch(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::PresetRow &preset) { if (preset.active) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue); const bool pressed = ImGui::Button(preset.name.c_str()); if (preset.active) ImGui::PopStyleColor(); if (pressed) trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name); if (ImGui::IsItemHovered() && !preset.note.empty()) wrapped_tooltip("%s", preset.note.c_str()); } // A built-in preset as a dial: one multiplier that moves everything the preset moves. void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::PresetRow &preset) { ImGui::PushID(preset.name.c_str()); const float column = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.40f; const bool moved = std::abs(preset.factor - 1.0) > 1e-4; if (moved) ImGui::PushStyleColor(ImGuiCol_Text, skate_theme::blue); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(preset.title.c_str()); if (moved) ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) wrapped_tooltip("%s\n1 is the game's own; \"%s\" is x %s.", preset.note.c_str(), preset.name.c_str(), number(preset.amount).c_str()); ImGui::SameLine(column); const float button = ImGui::CalcTextSize("Fast Parkour Flips").x + ImGui::GetStyle().FramePadding.x * 2; 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; 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) trainer_command(menu, callbacks, std::format("dial {} {}", number(value), preset.name)); if (ImGui::IsItemActive()) { p.active = id; p.active_value = value; } else if (p.active == id) { p.active.clear(); } if (!ImGui::IsItemActive() && ImGui::IsItemHovered()) wrapped_tooltip("%s 1 is the game's own.", preset.note.c_str()); ImGui::SameLine(); // Any number at all: the slider only covers the sensible range. double typed = preset.factor; ImGui::SetNextItemWidth(box); if (ImGui::InputDouble("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue) && typed > 0) trainer_command(menu, callbacks, std::format("dial {} {}", number(typed), preset.name)); if (ImGui::IsItemHovered()) wrapped_tooltip("Type any multiplier and press Enter: the slider's ends are not a limit."); ImGui::SameLine(); if (preset.active) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue); const bool pressed = ImGui::Button(preset.name.c_str(), ImVec2(button, 0)); if (preset.active) ImGui::PopStyleColor(); if (pressed) trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name); if (ImGui::IsItemHovered()) wrapped_tooltip("%s", preset.note.c_str()); if (moved) { ImGui::SameLine(); if (ImGui::Button("Reset", ImVec2(reset, 0))) trainer_command(menu, callbacks, "dial 1 " + preset.name); tip("Puts this dial back to 1, the game's own."); } ImGui::PopID(); } // One screen for everything that changes how the game plays: the presets as dials and // switches, the trick multipliers, the player's own presets and the values themselves. All of // them show the same values, so none can disagree with another. void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) { if (!view.ready) { begin_card(menu, "tune-wait", "PHYSICS TUNING"); note(view.status.c_str()); note("The values appear once a level is loaded."); end_card(); return; } if (!view.editable) warn(view.blocked.c_str()); // Two short lists, the trick line section and the whole table (modes 0, 1, 3 and 2). { static constexpr std::pair modes[]{{"REALISTIC", 0}, {"FUN", 1}, {"TRICKLINING", 3}, {"EVERYTHING", 2}}; const float width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 3) / 4; bool first = true; for (const auto &[label, mode] : modes) { if (!first) ImGui::SameLine(); first = false; const bool on = p.mode == mode; if (on) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue); if (ImGui::Button(label, ImVec2(width, 0))) p.mode = mode; tip(mode == 0 ? "A short list for toning the game down: lower pops, slower pushes, harder bails." : mode == 1 ? "A short list for turning the game up: bigger pops, faster pushes, moon jumps." : mode == 3 ? "Everything that makes a trick line in one place: board bending, reverts, pumping, pops, spins, manuals and grinds." : "Every dial, your own presets and every tuning value the trainer knows."); if (on) ImGui::PopStyleColor(); } } const std::uint8_t list = p.mode == 0 ? trainer::mode_realistic : p.mode == 1 ? trainer::mode_fun : std::uint8_t{0xff}; const bool can_edit = view.editable && callbacks.queue_console_command != nullptr; // One search for the whole tab: dials, switches, trick sliders and every value. ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); ImGui::InputTextWithHint("##search", "Search: dials, tricks and every value...", p.search.data(), p.search.size()); tip("Type a word to find dials, trick sliders and tuning values on every list at once. Clear the box to go back to the list."); const auto words = lower(p.search.data()); const bool searching = !words.empty(); const auto listed = [&](const trainer::PresetRow &preset) { return searching ? contains_words(lower(preset.title + " " + preset.name + " " + preset.note), words) : p.mode == 2 || (preset.modes & list) != 0; }; if (!searching) { begin_card(menu, "feel", "PLAY LIKE", "The game's own tuning, or Skate 3's on one of its difficulties"); if (p.mode == 3) note("A Skate 3 choice sets Skate 3's own numbers for everything the two games share (pop, grind pops, pushing, pumping, steering, " "manuals, spins, bails), and for grinds the way they were before skate.: the older grind friction, Skate 3's lock-on distances, wider slide " "angles, and no automatic turning between grinds. What a number cannot change stays skate.'s: animations and which stick " "motion asks for which grind."); ImGui::BeginDisabled(!can_edit); feel_buttons(menu, callbacks, view); ImGui::EndDisabled(); end_card(); } if (p.mode == 3 && !searching) { trickline_section(menu, callbacks, p, view); return; } begin_card(menu, "dials", p.mode == 0 ? "FEEL: TONE IT DOWN" : p.mode == 1 ? "FEEL: TURN IT UP" : "FEEL", "1 is the game's own. A dial moves every value its preset moves."); ImGui::BeginDisabled(!can_edit); for (const auto &preset : view.presets) if (preset.builtin && preset.dial && listed(preset)) dial_row(menu, callbacks, p, preset); // The presets that are plain switches, and the ones that set several dials at once. bool first = true; for (const auto &preset : view.presets) { if (!preset.builtin || preset.dial || preset.name == "Stock" || preset.name.starts_with("Skate 3") || !listed(preset)) continue; if (!first && ImGui::GetItemRectMax().x + ImGui::CalcTextSize(preset.name.c_str()).x + ImGui::GetStyle().FramePadding.x * 4 < ImGui::GetWindowPos().x + ImGui::GetContentRegionMax().x) ImGui::SameLine(); first = false; preset_switch(menu, callbacks, preset); } ImGui::EndDisabled(); if (p.mode != 0) { // The game's own bail protection, the same switch as Skater > Movement. const auto &debug = model.debug; bool no_bail = debug.no_bail; if (toggle_row(menu, "Never bail", "No wipeouts at all. Recover from a bail before switching it on.", no_bail, (debug.no_bail_available || debug.no_bail) && callbacks.queue_debug != nullptr)) debug_request(menu, callbacks, {DebugAction::set_no_bail, no_bail}); } ImGui::BeginDisabled(!can_edit || !view.touched); if (ImGui::Button("Reset values")) trainer_command(menu, callbacks, "reset all"); if (ImGui::IsItemHovered()) wrapped_tooltip("Every tuning value back to the game's own. Locked values stay."); ImGui::EndDisabled(); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("%zu values changed", view.touched); end_card(); ImGui::BeginDisabled(!can_edit); trick_heights(menu, callbacks, p, view, words); ImGui::EndDisabled(); if (p.mode == 2 && !searching) { begin_card(menu, "preset-save", "YOUR PRESETS", "Save what is changed right now"); ImGui::BeginDisabled(!can_edit); const float button = ImGui::CalcTextSize("Turn off").x + ImGui::GetStyle().FramePadding.x * 2; for (const auto &preset : view.presets) { if (preset.builtin) continue; ImGui::PushID(preset.name.c_str()); if (preset.active) { ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue); if (ImGui::Button("Turn off", ImVec2(button, 0))) trainer_command(menu, callbacks, "preset remove " + preset.name); ImGui::PopStyleColor(); } else if (ImGui::Button("Turn on", ImVec2(button, 0))) { trainer_command(menu, callbacks, "preset apply " + preset.name); } tip("Turn on sets every value saved in this preset. Turn off puts those values back to the game's own."); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(preset.name.c_str()); ImGui::SameLine(); ImGui::TextDisabled("%s", preset.note.c_str()); ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - px(170)); if (ImGui::SmallButton("This map")) trainer_command(menu, callbacks, "profile set " + preset.name); tip("Applies this preset by itself every time this map loads."); ImGui::SameLine(); if (ImGui::SmallButton("Share")) trainer_command(menu, callbacks, "preset export " + preset.name); if (ImGui::IsItemHovered()) wrapped_tooltip("Copies this preset to the clipboard as one line of text. Paste it in a chat;\nanyone with the trainer can import it."); ImGui::SameLine(); if (ImGui::SmallButton("Delete")) trainer_command(menu, callbacks, "preset delete " + preset.name); tip("Removes this preset from your list."); ImGui::PopID(); } ImGui::EndDisabled(); field(menu, "Name"); const float save = ImGui::CalcTextSize("Save").x + ImGui::GetStyle().FramePadding.x * 2; ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - save - ImGui::GetStyle().ItemSpacing.x); ImGui::InputText("##preset-name", p.preset_name.data(), p.preset_name.size()); tip("A name for the preset you are about to save."); ImGui::SameLine(); ImGui::BeginDisabled(!p.preset_name[0] || !view.touched); if (ImGui::Button("Save", ImVec2(save, 0))) trainer_command(menu, callbacks, std::string("preset save ") + p.preset_name.data()); tip("Saves every value, dial and trick setting that is changed right now as a preset under this name. Share it from its row afterwards."); ImGui::EndDisabled(); // Sharing: a preset travels as one line of text on the clipboard. ImGui::BeginDisabled(view.stock); if (ImGui::Button("Share what is changed now")) trainer_command(menu, callbacks, "preset export current"); ImGui::EndDisabled(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) wrapped_tooltip("Copies your current setup to the clipboard as one line of text."); ImGui::SameLine(); if (ImGui::Button("Import from clipboard")) { const char *text = ImGui::GetClipboardText(); if (text && trainer::stage_import(text)) trainer_command(menu, callbacks, "preset import"); else feedback(menu, "The clipboard holds no text."); } if (ImGui::IsItemHovered()) wrapped_tooltip("Copy a preset line someone shared (it starts with RST1:), then press this.\nIt is added to your presets; nothing changes until you turn it on."); if (!view.map.empty()) { info(menu, "This map applies", view.profile_preset.empty() ? "nothing" : view.profile_preset); if (!view.profile_preset.empty() && ImGui::Button("Stop applying it on this map")) trainer_command(menu, callbacks, "profile clear"); note("\"This map\" beside one of your presets applies it every time this map loads."); } end_card(); } // The values themselves: the short list's, or all of them. ImGui::Spacing(); ImGui::SeparatorText(p.mode == 2 ? "EVERY VALUE" : "FINE TUNING"); if (p.mode == 2) { // The list includes the game's tuning classes, which the trainer has to find in memory // before an edit to one does anything: with the list open, it looks now. trainer::note_class_list_shown(); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.42f); if (p.group < 1) p.group = 1; const char *current = p.group > 1 && static_cast(p.group) <= view.groups.size() + 1 ? view.groups[static_cast(p.group) - 2].c_str() : "Every group"; if (ImGui::BeginCombo("##group", current)) { if (ImGui::Selectable("Every group", p.group == 1)) p.group = 1; for (std::size_t i = 0; i < view.groups.size(); ++i) if (ImGui::Selectable(view.groups[i].c_str(), p.group == static_cast(i) + 2)) p.group = static_cast(i) + 2; ImGui::EndCombo(); } tip("Shows one group of tuning values at a time."); ImGui::Checkbox("Only what I changed", &p.only_changed); tip("Lists only the values that differ from the game's own."); ImGui::SameLine(); ImGui::Checkbox("Graph points", &p.graph_points); if (ImGui::IsItemHovered()) wrapped_tooltip("Show every point of the tuning graphs, not just one multiplier per graph."); ImGui::SameLine(); ImGui::Checkbox("Values with no use found", &p.show_unused); if (ImGui::IsItemHovered()) wrapped_tooltip("Also list the tuning values that the game was never found or seen reading.\nChanging those will probably do nothing."); } else if (searching) { trainer::note_class_list_shown(); } else { note("The same values the dials move, one at a time. The box on the left locks a value against presets, dials and Reset values."); } filter_rows(p, view); if (p.mode != 2 && searching) ImGui::TextDisabled("%zu values match", p.shown.size()); if (p.mode == 2) { ImGui::SameLine(); if (p.hidden_unused) ImGui::TextDisabled("%zu shown, %zu changed, %zu hidden", p.shown.size(), view.touched, p.hidden_unused); else ImGui::TextDisabled("%zu shown, %zu changed", p.shown.size(), view.touched); } const bool with_group = searching || (p.mode == 2 && (p.group <= 1 || p.only_changed)); const bool friendly = p.mode < 2; ImGuiListClipper clipper; clipper.Begin(static_cast(p.shown.size())); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) value_row(menu, callbacks, p, view, view.rows[p.shown[static_cast(i)]], with_group, friendly); if (p.shown.empty()) note("Nothing matches."); } void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words) { static constexpr const char *labels[]{"Flip trick speed", "Let slow flips stay slow (no finish before landing)", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height", "Board bending boost (revert speed)"}; const auto wanted = [&](const char *label) { return words.empty() || contains_words(lower(std::string(label) + " tricks"), words); }; if (std::ranges::none_of(labels, wanted)) return; begin_card(menu, "trick-heights", "TRICKS", "1.0 is the game's own"); // `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) { if (!wanted(label)) return false; field(menu, label); ImGui::PushID(option); float shown = editing ? edit : value; const float box = px(64); ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - box - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::SliderFloat("##height", &shown, 0.1f, high, "x %.2f", (limited ? ImGuiSliderFlags_AlwaysClamp : 0) | ImGuiSliderFlags_Logarithmic)) { edit = shown; editing = true; } if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option {} {:.2f}", option, edit)); if (!ImGui::IsItemActive() && editing && std::abs(value - edit) < 0.005f * std::max(1.0f, std::abs(edit))) editing = false; if (!ImGui::IsItemActive()) tip(help); ImGui::SameLine(); float typed = value; ImGui::SetNextItemWidth(box); if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue) && typed > 0) { editing = false; trainer_command(menu, callbacks, std::format("option {} {}", option, typed)); } if (ImGui::IsItemHovered()) wrapped_tooltip(limited ? "Type a multiplier and press Enter." : "Type any multiplier and press Enter: the slider's end is not a limit."); ImGui::PopID(); 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. 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); if (wanted("Let slow flips stay slow (no finish before landing)")) { bool slow = !view.flip_gate; if (ImGui::Checkbox("Let slow flips stay slow", &slow)) trainer_command(menu, callbacks, std::format("option flip_gate {}", slow ? 0 : 1)); if (ImGui::IsItemHovered()) 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 (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."); slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing, "How high you jump off the board in a hippy jump. 1 is the game's own."); slider("Off-board jump height", "offboard_height", view.offboard_height, p.offboard_edit, p.offboard_editing, "How high you jump on foot. 1 is the game's own."); if (wanted("Board bending boost (revert speed)")) { // Not a multiplier: 0 is the game's own (no boost). field(menu, "Board bending boost"); ImGui::PushID("revert_boost"); float shown = p.revert_editing ? p.revert_edit : view.revert_boost; const float box = px(64); ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - box - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::SliderFloat("##strength", &shown, 0.0f, 5.0f, shown <= 0.0f ? "off" : "x %.2f")) { p.revert_edit = shown; p.revert_editing = true; } if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option revert_boost {:.2f}", p.revert_edit)); if (!ImGui::IsItemActive() && p.revert_editing && std::abs(view.revert_boost - p.revert_edit) < 0.006f) p.revert_editing = false; if (ImGui::IsItemHovered()) wrapped_tooltip("Board bending: land with the board turned out of line and the game snaps it straight. With this on, that snap\n" "throws you forward, as it did in Skate 3: chain bends to keep and build speed without pushing.\n" "x 1 gives about +1.5 to +2.6 m/s a landing. 0 is off, the game's own. The Tricklining list has the details.\n" "From AutoRevertBoost by Sivaes, jaq and OVM."); ImGui::SameLine(); float typed = view.revert_boost; ImGui::SetNextItemWidth(box); if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue) && typed >= 0) { p.revert_editing = false; trainer_command(menu, callbacks, std::format("option revert_boost {}", typed)); } if (ImGui::IsItemHovered()) wrapped_tooltip("Type any strength and press Enter: the slider's end is not a limit."); ImGui::PopID(); } if (ImGui::Button("Reset tricks")) { p.revert_editing = false; p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false; trainer_command(menu, callbacks, "reset tricks"); } tip("Puts every slider and switch on this card back to the game's own."); note("Not tuning values: the game sets these in its trick scripts and flip curves. Type in the box to go past a slider's end."); end_card(); } void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) { const auto telemetry = trainer::telemetry(); begin_card(menu, "speed", "GAME SPEED", "Slow motion for learning a line"); { const NamedSettingModel *setting = nullptr; for (const auto &row : model.engine_settings) if (row.name == "SimulationTime.TimeScale") { setting = &row; break; } if (!setting || !setting->available) { note(setting && !setting->reason.empty() ? setting->reason.c_str() : "Game speed is unavailable right now."); // The engine resolves a setting the first time it is asked for: ask. if (setting && callbacks.queue_console_command && ImGui::GetTime() >= p.speed_until) { p.speed_until = ImGui::GetTime() + 2.0; p.speed_edit = -1; std::array ignored{}; callbacks.queue_console_command(callbacks.user, "reset SimulationTime.TimeScale", ignored.data(), ignored.size()); } } else { double current = 1; try { const auto first = setting->value.find_first_of("-.0123456789"); if (first != std::string::npos) current = std::stod(setting->value.substr(first)); } catch (...) {} if (p.speed_edit >= 0 && ImGui::GetTime() < p.speed_until) current = p.speed_edit; const auto set = [&](double speed) { p.speed_edit = speed; p.speed_until = ImGui::GetTime() + 1.0; send_console(menu, callbacks, speed == 1.0 ? "reset SimulationTime.TimeScale" : "SimulationTime.TimeScale " + number(speed)); }; field(menu, "Speed"); auto value = static_cast(current); if (ImGui::SliderFloat("##game-speed", &value, 0.05f, 2.0f, current == 0.0 ? "paused" : "%.2fx", ImGuiSliderFlags_AlwaysClamp)) set(value); if (!ImGui::IsItemActive()) tip("Slows the whole game down or speeds it up. 1x is normal speed."); if (ImGui::Button(current == 0.0 ? "Resume" : "Pause")) set(current == 0.0 ? 1.0 : 0.0); tip("Freezes the game where it is. Press again to carry on."); for (const auto quick : {0.1, 0.25, 0.5, 0.75, 1.0}) { ImGui::SameLine(); if (ImGui::Button(std::format("{}x", number(quick)).c_str())) set(quick); tip("Sets the game speed to this straight away."); } note("The jump read-out measures real time, so it reads low while the game is slowed."); } } end_card(); begin_card(menu, "markers", "MARKERS", "Saved for each map"); int slot = view.slot; if (choice(menu, "marker-slot", slot, {"1", "2", "3", "4", "5"})) trainer_command(menu, callbacks, std::format("slot {}", slot + 1)); const auto &marker = view.markers[static_cast(std::clamp(view.slot, 0, static_cast(trainer::marker_slots) - 1))]; info(menu, "Selected", marker.set ? std::format("{:.1f}, {:.1f}, {:.1f}", marker.position[0], marker.position[1], marker.position[2]) : "empty"); ImGui::BeginDisabled(view.map.empty()); ImGui::BeginDisabled(!telemetry.skater); if (ImGui::Button("Save here")) trainer_command(menu, callbacks, "marker save"); tip("Saves where you are standing in the selected slot, for this map."); ImGui::EndDisabled(); ImGui::SameLine(); ImGui::BeginDisabled(!marker.set); if (ImGui::Button("Go")) trainer_command(menu, callbacks, "marker go"); tip("Puts you back at the selected marker."); ImGui::SameLine(); if (ImGui::Button("Clear")) trainer_command(menu, callbacks, "marker clear"); tip("Empties the selected slot."); ImGui::EndDisabled(); ImGui::EndDisabled(); bool auto_return = view.auto_return; if (toggle_row(menu, "Return after a bail", "Go back to the selected marker when the skater wipes out.", auto_return)) trainer_command(menu, callbacks, std::format("option auto_return {}", auto_return ? 1 : 0)); field(menu, "Return delay"); float delay = view.return_delay; ImGui::SliderFloat("##return-delay", &delay, 0.0f, 10.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp); if (!ImGui::IsItemActive()) tip("How long after a bail before you are put back at the marker."); if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option return_delay {:.1f}", delay)); bool pad = view.pad_shortcuts; if (toggle_row(menu, "Controller shortcuts", "Hold LB + RB, then D-pad: up saves, down goes, left / right pick the slot.", pad)) trainer_command(menu, callbacks, std::format("option pad {}", pad ? 1 : 0)); bool pad_menu = view.pad_menu; if (toggle_row(menu, "Open this menu from the controller", "LB + RB + click the right stick opens and closes the menu. Inside: D-pad or left stick moves, A presses, B goes back, LB / RB change a slider in big steps.", pad_menu)) trainer_command(menu, callbacks, std::format("option pad_menu {}", pad_menu ? 1 : 0)); end_card(); begin_card(menu, "teleport", "TELEPORT"); if (telemetry.skater) info(menu, "You are at", std::format("{:.2f}, {:.2f}, {:.2f}", telemetry.position[0], telemetry.position[1], telemetry.position[2])); field(menu, "X, Y, Z"); ImGui::InputFloat3("##teleport", p.teleport.data(), "%.2f"); tip("A position in the game's own coordinates: X, height, Z."); ImGui::BeginDisabled(!telemetry.skater); if (ImGui::Button("Here")) p.teleport = telemetry.position; tip("Fills the boxes with where you are now."); ImGui::SameLine(); if (ImGui::Button("Go##teleport")) trainer_command(menu, callbacks, std::format("tp {:.3f} {:.3f} {:.3f}", p.teleport[0], p.teleport[1], p.teleport[2])); tip("Moves you to the position in the boxes. You arrive on foot."); ImGui::SameLine(); if (ImGui::Button("Copy position")) ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", telemetry.position[0], telemetry.position[1], telemetry.position[2]).c_str()); tip("Copies where you are to the clipboard as X, Y, Z."); ImGui::SameLine(); if (ImGui::Button("Copy for Blender")) ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", telemetry.position[0], -telemetry.position[2], telemetry.position[1]).c_str()); tip("Copies where you are in Blender's axes (Z up), for map makers."); ImGui::EndDisabled(); end_card(); } void jump_lines(SkateMenu &menu, const trainer::Jump &jump) { info(menu, "Takeoff", std::format("{:.1f} km/h at {:.1f} degrees", jump.takeoff_speed * 3.6f, jump.takeoff_angle)); info(menu, "Air time", std::format("{:.2f} s", jump.air_time)); info(menu, "Height", std::format("{:.2f} m", jump.height)); info(menu, "Distance", std::format("{:.2f} m, {:.2f} m drop", jump.distance, jump.drop)); info(menu, "Landing", std::format("{:.1f} km/h at {:.1f}, {:.1f}, {:.1f}", jump.landing_speed * 3.6f, jump.landing[0], jump.landing[1], jump.landing[2])); info(menu, "Rotation", std::format("spin {:.0f} degrees (peak {:.0f} per second), flip {:.0f} degrees", jump.spin, jump.spin_rate, jump.flip)); } // Which game the skating plays like: the game's own, or Skate 3 on one of its three difficulties. // One is always lit; a Skate 3 one goes out again as soon as one of its values is changed by hand. void feel_buttons(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::View &view) { struct Choice { const char *label, *preset, *command, *tip; }; static constexpr Choice choices[]{ {"skate.", "", "feel stock", "The game's own tuning."}, {"Skate 3 Easy", "Skate 3 Easy", "feel easy", "Skate 3 on Easy: full-height pops every time, strong pushes, generous grind lock-on, easy spins."}, {"Skate 3", "Skate 3", "feel normal", "Skate 3 on Normal: its pop, grind pops, pushing, pumping, steering, manuals, spins, flips and bails."}, {"Skate 3 Hardcore", "Skate 3 Hardcore", "feel hardcore", "Skate 3 on Hardcore: lower pops, weaker pushes, tight grind lock-on, slow auto spins."}, }; const auto active = [&](const char *preset) { return std::ranges::any_of(view.presets, [&](const trainer::PresetRow &row) { return row.builtin && row.active && row.name == preset; }); }; const bool any = active("Skate 3 Easy") || active("Skate 3") || active("Skate 3 Hardcore"); const float width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 3) / 4; for (std::size_t i = 0; i < std::size(choices); ++i) { if (i) ImGui::SameLine(); const bool on = i == 0 ? !any : active(choices[i].preset); if (on) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue); if (ImGui::Button(choices[i].label, ImVec2(width, 0))) trainer_command(menu, callbacks, choices[i].command); if (on) ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) wrapped_tooltip("%s", choices[i].tip); } } // Everything that makes a trick line, in one place: the same values and sliders as the Tune tab, // gathered, with one button for Skate 3's and one for the game's own. void trickline_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) { const bool can_edit = view.editable && callbacks.queue_console_command != nullptr; ImGui::BeginDisabled(!can_edit); begin_card(menu, "trickline", "TRICKLINING EXTRAS", "What Skate 3's tricklining had beyond its tuning"); bool extras = view.revert_boost > 0; if (toggle_row(menu, "Skate 3's tricklining extras", "The board bending boost below at x 1, and heavier revert and powerslide friction (x 3 and x 1.5: a first guess, tune them below).", extras)) trainer_command(menu, callbacks, extras ? "trickline extras on" : "trickline extras off"); note("Pick the game to play like above, then tune the tricklining part of it here. Blue = changed; Reset puts one value back. Type in a box to go past a slider's end."); end_card(); // Rows of the value table by id, looked up once per snapshot. static constexpr const char *groups[][2]{ {"REVERTS AND POWERSLIDES", "onboard_powerslide.frictionscalar_revert onboard_powerslide.frictionscalar_autorevert " "onboard_powerslide.powerslide_forwardforcescalar_revert onboard_powerslide.powerslide_forwardforcescalar_autorevert " "onboard_powerslide.frictionscalar_slide onboard_powerslide.powerslide_forwardforcescalar_slide"}, {"PUMPING", "physicsmode.pumpeffectfactor physicsmode.pumpmaxacceleration physicsmode.pumpmaxdeceleration physicsmode.unintentionalpumpscalar " "physicspumping.pumpangle physicspumping.pumpminfactor physicspumping.pumpoutoftransitionscalar"}, {"POPS", "physicsmode.jumpmaxheight physicsmode.jumpminheight physicsmode.jumpminheightmanual physicsmode.grindjumpcommonmax physicsmode.grindjumpcommonmin " "physicsmode.grindjumpboardslidemax physicsmode.grindjumpboardslidemin physicsmode.grindjumptipslidemax physicsmode.grindjumptipslidemin " "physicsonboardjumptuning.jumpadjustvelocityboostlimitground physicsonboardjumptuning.jumpadjustvelocityboostlimitgrind"}, {"SPINS AND FLIPS", "physicsairstates.maxspinspeed physicsmode.maxautobodyspinspeed physicsmode.easybodyspins physicsreckoning.flipscalar " "physicsreckoning.flipmaxspeed heldflip.straightflipcatchtime heldflip.shuvmincatchtime heldflip.varialmincatchtime " "heldflip.bigflipmincatchtime"}, {"MANUALS AND GRINDS", "physicsmanual.manualscalar_p physicsmanual.manualscalar_i physicsmanual.manualscalar_d physicsmanual.maxtiltangle " "physicsmode.grindlockdist physicsgrind.commonfrictionscalar physicsgrindsair.maxdistboardslide physicsgrindsair.maxdisttipslide " "physicsgrind.grind_tipslide_minangletoprimitive physicsgrind.slowgrindexitspeed physicsgrind.copingexitstarttime " "physicsgrind.curbexitstarttime physicsgrind.fairlysteepexitstarttime physicsfriction.noinputtime " "physicssteering.generalscalar physicssteering.damping"}, }; // What each group is for in a line, said once at the top of its card. static constexpr const char *group_notes[]{ "", "Pumping is speed without pushing: crouch into a transition and stand up out of it. Strength is how much a pump gives, the limit how " "fast it may build. Skate 3 pumped harder (18 against 13) but capped it lower.", "A line lives on pops you can count on. Max is a held pop, min a quick flick; bring them together and every pop is the same height. " "Each kind of grind has its own pair, and the stick boost is the speed you can add by pushing the stick as you pop.", "How fast you and the board turn, and how long a flip may take to come round (its catch time). Slower spins and longer catch times " "read as style; faster ones fit more into a small gap.", "Manual balance (the three balance numbers are how hard the game corrects you), how far a rail reaches out to grab you, how quickly a " "grind loses speed, and when the game ends a grind for you. The tuning skate. grew out of ended slow grinds by itself; in skate. those exit timers are off (-1).", }; if (p.trick_revision != view.revision) { p.trick_revision = view.revision; for (std::size_t g = 0; g < std::size(groups); ++g) { p.trick_rows[g].clear(); const std::string keys = groups[g][1]; for (std::size_t start = 0; start < keys.size();) { auto end = keys.find(' ', start); if (end == std::string::npos) end = keys.size(); const auto key = keys.substr(start, end - start); for (std::size_t i = 0; i < view.rows.size() && !key.empty(); ++i) if (lower(view.rows[i].id) == key) { p.trick_rows[g].push_back(i); break; } start = end + 1; } } } const auto rows = [&](std::size_t g) { for (const auto i : p.trick_rows[g]) if (i < view.rows.size()) value_row(menu, callbacks, p, view, view.rows[i], false, true); }; begin_card(menu, "trick-revert", "BOARD BENDING, REVERTS AND POWERSLIDES", "Speed out of a bent landing"); { // The revert speed boost: its strength, then what counts and what it is worth. const auto number_row = [&](const char *label, const char *command, float value, float low, float high, const char *format, const char *tip) { ImGui::PushID(command); field(menu, label); const auto key = std::string("revert:") + command; float shown = p.active == key ? static_cast(p.active_value) : value; ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("{} {:.3f}", command, shown)); if (ImGui::IsItemActive()) { p.active = key; p.active_value = shown; } else if (p.active == key) { p.active.clear(); } if (ImGui::IsItemHovered()) wrapped_tooltip("%s", tip); ImGui::SameLine(); float typed = value; ImGui::SetNextItemWidth(px(64)); if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue)) trainer_command(menu, callbacks, std::format("{} {}", command, typed)); if (ImGui::IsItemHovered()) wrapped_tooltip("%s", "Type any number and press Enter: the slider's ends are not a limit."); ImGui::PopID(); }; const auto &r = view.revert; note("Board bending is how tricklining keeps its speed. Land a spin with the board twisted off your body " "and the game snaps the board straight. In Skate 3 that snap threw you forward, so " "liners bend the board on every landing instead of pushing. skate. gives nothing for it; this boost puts the speed back."); number_row("Board bending boost", "option revert_boost", view.revert_boost, 0.0f, 5.0f, view.revert_boost <= 0 ? "off" : "x %.2f", "How hard a bend throws you forward. 0 is off, the game's own; x 1 is about +2 m/s (7 km/h) a landing.\nFrom AutoRevertBoost by Sivaes, jaq and OVM."); ImGui::Spacing(); if (ImGui::TreeNodeEx("What counts as a bend, and what it is worth", ImGuiTreeNodeFlags_SpanAvailWidth)) { number_row("Spin needed", "revert spin", r.min_spin, 0.0f, 180.0f, "%.0f deg", "How far you must have turned in the air. Lower it and smaller turns count as bends."); number_row("Board off your body", "revert twist", r.min_twist, 0.0f, 90.0f, "%.0f deg", "The rule that makes a bend: the board must land twisted at least this far off your body. Clean and short 180s land 0 to 3 degrees off, " "auto reverts and board bends 70 to 90."); number_row("or off the travel", "revert slip", r.min_slip, 0.0f, 90.0f, r.min_slip >= 90.0f ? "off" : "%.0f deg", "Off at 90, as shipped. Lower it and a landing also counts when you and the board together land this far out of line with where you are " "going. Careful: the game straightens ordinary 180s that way too, so a low number gives speed on plain 180s and flip trick 180s. A " "landing that counts by this rule alone gets the auto revert boost at most."); number_row("Boost: a small bend", "revert bend", r.bend_boost, 0.0f, 10.0f, "+%.1f m/s", "Speed added when the board turned 40 degrees past the body over the flight (times the boost strength)."); number_row("Boost: a full bend", "revert full", r.full_boost, 0.0f, 10.0f, "+%.1f m/s", "The same at 360 degrees; in between it rises in a straight line up to 140 degrees."); number_row("Boost: an auto revert", "revert auto", r.auto_boost, 0.0f, 10.0f, "+%.1f m/s", "Speed added when the board turned more than 140 degrees past the body."); number_row("No boost above", "revert max_speed", r.max_speed, 5.0f, 100.0f, "%.0f m/s", "Reverts stop adding speed here (1 m/s = 3.6 km/h)."); number_row("Time between boosts", "revert cooldown", r.cooldown, 0.0f, 3.0f, "%.2f s", "How soon the next revert can boost again."); number_row("Shortest flight", "revert min_air", r.min_air, 0.0f, 2.0f, "%.2f s", "A hop shorter than this never boosts."); if (ImGui::Button("Reset these rules")) trainer_command(menu, callbacks, "revert reset"); tip("Puts the rules above back to the trainer's defaults."); ImGui::TreePop(); } ImGui::Spacing(); } note("Friction: how much speed a revert or a powerslide scrubs off (higher loses more). Skate 3's were heavier, which is why bending " "mattered there. Forward force: a push the game adds while you revert or slide."); rows(0); end_card(); trick_heights(menu, callbacks, p, view, std::string{}); for (std::size_t g = 1; g < std::size(groups); ++g) { if (p.trick_rows[g].empty()) continue; begin_card(menu, groups[g][0], groups[g][0]); note(group_notes[g]); rows(g); end_card(); } ImGui::EndDisabled(); } // The player's own follow camera, in place of the game's: one framing on the board, one on foot. void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) { begin_card(menu, "rigs", "THE GAME'S CAMERAS", "Move the game's own cameras: they keep their smoothing and stay out of walls"); if (ImGui::Button("SunJay's Low Cam")) trainer_command(menu, callbacks, "camera rig sunjay"); if (ImGui::IsItemHovered()) wrapped_tooltip("Both on-board cameras low and close, the numbers of SunJay's Low Cam mod."); ImGui::SameLine(); if (ImGui::Button("Low, a little higher")) trainer_command(menu, callbacks, "camera rig low+"); if (ImGui::IsItemHovered()) wrapped_tooltip("The low camera lower and closer, but with more of the skater in view."); ImGui::SameLine(); if (ImGui::Button("The game's own")) trainer_command(menu, callbacks, "camera rig stock"); tip("Puts all three of the game's cameras back as they shipped."); static constexpr const char *rig_ids[]{"low", "high", "foot"}; static constexpr const char *rig_titles[]{"Low camera (on board)", "High camera (on board)", "On foot"}; for (std::size_t i = 0; i < trainer::camera_rigs; ++i) { ImGui::SeparatorText(rig_titles[i]); const auto &rig = view.rigs[i]; const auto slider = [&](const char *label, const char *field, float value, float low, float high, const char *format, const char *tip) { ImGui::PushID(static_cast(i)); ImGui::PushID(field); menu::field(menu, label); const auto key = std::string("rig:") + rig_ids[i] + field; float shown = p.active == key ? static_cast(p.active_value) : value; ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera rig {} {} {:.3f}", rig_ids[i], field, shown)); if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) wrapped_tooltip("%s", tip); ImGui::SameLine(); float typed = value; ImGui::SetNextItemWidth(px(64)); if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue)) trainer_command(menu, callbacks, std::format("camera {} {} {} {}", "rig", rig_ids[i], field, typed)); if (ImGui::IsItemHovered()) wrapped_tooltip("Type any number and press Enter: the slider's ends are not a limit."); if (ImGui::IsItemActive()) { p.active = key; p.active_value = shown; } else if (p.active == key) { p.active.clear(); } ImGui::PopID(); ImGui::PopID(); }; slider("Distance", "distance", rig.distance, 0.3f, 3.0f, "x %.2f", "How far behind you the camera sits. 1 is the game's own."); slider("Height", "height", rig.height, -1.0f, 2.0f, "%+.2f m", "Moves the camera and the point it looks at up or down together."); slider("Camera only", "raise", rig.raise, -1.0f, 2.0f, "%+.2f m", "Moves the camera alone, so it looks more down at you (or up from below)."); if (i == 2) slider("Shoulder", "side", rig.side, 0.0f, 2.0f, "%.2f", "Over the shoulder: 1 is the game's own (centred), above 1 moves right, below 1 moves left, by that many metres."); else slider("Side", "side", rig.side, -2.0f, 2.0f, "x %.2f", "The camera's sideways offset. 1 is the game's own, 0 is dead centre, negative is the other shoulder."); } note(("Found: " + view.rigs_found + ". A change shows at once, or the next time that camera starts.").c_str()); end_card(); begin_card(menu, "camera", "A CAMERA OF YOUR OWN (EXPERIMENTAL)", "Replaces the game's camera altogether"); bool on = view.camera_on; if (toggle_row(menu, "Use my camera", "Replaces the game's camera with the one set below. It stays behind the way you travel and does not avoid walls.", on)) trainer_command(menu, callbacks, std::format("camera {}", on ? "on" : "off")); static constexpr const char *presets[]{"Low", "Low plus", "Skate 3", "High", "Shoulder", "Shoulder left"}; static constexpr const char *notes[]{ "Close to the ground behind the board, looking slightly up: the Skate 1 feel.", "The low camera raised a little, so more of the skater shows.", "Close chase camera with a wide lens.", "Further back and above, for lines and big gaps.", "On foot: over the right shoulder.", "On foot: over the left shoulder."}; for (std::size_t i = 0; i < std::size(presets); ++i) { if (i) ImGui::SameLine(); if (ImGui::Button(presets[i])) trainer_command(menu, callbacks, std::string("camera preset ") + presets[i]); if (ImGui::IsItemHovered()) wrapped_tooltip("%s", notes[i]); } if (view.camera_game.empty()) note("Skate a few seconds with the game's own camera and its framing is measured here."); else { info(menu, "The game frames you from", view.camera_game); if (ImGui::Button("Start from the game's framing")) trainer_command(menu, callbacks, "camera game"); tip("Copies the framing measured from the game's camera into your camera's sliders below."); } end_card(); const auto set_card = [&](const char *id, const char *title, const char *which, const trainer::CameraSet &set) { begin_card(menu, id, title); const auto slider = [&](const char *label, const char *field, float value, float low, float high, const char *format, const char *tip) { ImGui::PushID(field); ImGui::PushID(which); menu::field(menu, label); const auto key = std::string("camera:") + which + field; float shown = p.active == key ? static_cast(p.active_value) : value; ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x)); if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera set {} {} {:.3f}", which, field, shown)); if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) wrapped_tooltip("%s", tip); ImGui::SameLine(); float typed = value; ImGui::SetNextItemWidth(px(64)); if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue)) trainer_command(menu, callbacks, std::format("camera {} {} {} {}", "set", which, field, typed)); if (ImGui::IsItemHovered()) wrapped_tooltip("Type any number and press Enter: the slider's ends are not a limit."); if (ImGui::IsItemActive()) { p.active = key; p.active_value = shown; } else if (p.active == key) { p.active.clear(); } ImGui::PopID(); ImGui::PopID(); }; slider("Distance", "distance", set.distance, 0.5f, 10.0f, "%.2f m", "How far behind the skater the camera sits."); slider("Height", "height", set.height, -0.5f, 5.0f, "%.2f m", "How far above the skater's feet. Low numbers give the classic low camera."); slider("Tilt", "pitch", set.pitch, -60.0f, 30.0f, "%.0f deg", "Negative looks down at the skater, positive looks up."); slider("Shoulder", "side", set.side, -1.5f, 1.5f, "%.2f m", "Sideways offset: positive is over the right shoulder."); slider("Field of view", "fov", set.fov, 0.0f, 120.0f, set.fov < 20.0f ? "the game's" : "%.0f deg", "How wide the lens is. All the way left keeps the game's own."); slider("Follow lag", "lag", set.lag, 0.0f, 0.5f, "%.2f s", "How far the camera trails behind your movement. 0 is rigid."); end_card(); }; set_card("camera-board", "ON THE BOARD", "board", view.camera_board); set_card("camera-foot", "ON FOOT", "foot", view.camera_foot); } void map_tab(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::View &view) { const auto telemetry = trainer::telemetry(); begin_card(menu, "hud", "HUD"); bool hud = view.hud, jump = view.hud_jump, logging = view.logging; if (toggle_row(menu, "Speed and air HUD", "Speed, air time and height in the corner while you skate.", hud)) trainer_command(menu, callbacks, std::format("option hud {}", hud ? 1 : 0)); if (toggle_row(menu, "Jump read-out", "After each landing: takeoff speed and angle, height, distance, landing speed.", jump)) trainer_command(menu, callbacks, std::format("option hud_jump {}", jump ? 1 : 0)); if (toggle_row(menu, "Record telemetry", "Write position and speed every frame to a CSV in %LOCALAPPDATA%\\ReSkate\\trainer\\telemetry.", logging)) trainer_command(menu, callbacks, std::format("option log {}", logging ? 1 : 0)); end_card(); begin_card(menu, "now", "RIGHT NOW"); if (!telemetry.skater) { note("No skater yet."); } else { info(menu, "Speed", std::format("{:.1f} km/h ({:.2f} m/s), top {:.1f} km/h", telemetry.speed * 3.6f, telemetry.speed, telemetry.top_speed * 3.6f)); info(menu, "Position", std::format("{:.2f}, {:.2f}, {:.2f}", telemetry.position[0], telemetry.position[1], telemetry.position[2])); info(menu, "Heading", std::format("{:.0f} degrees", telemetry.heading)); } end_card(); if (telemetry.last.serial) { begin_card(menu, "last-jump", "LAST JUMP", std::format("Jump {}", telemetry.last.serial).c_str()); jump_lines(menu, telemetry.last); end_card(); } if (telemetry.best.serial && telemetry.best.serial != telemetry.last.serial) { begin_card(menu, "best-jump", "LONGEST JUMP ON THIS MAP"); jump_lines(menu, telemetry.best); end_card(); } begin_card(menu, "map", "THIS MAP", view.map.empty() ? "No level loaded" : view.map.c_str()); if (!view.map_note.empty()) note(view.map_note.c_str()); if (!view.map_preset.empty()) { info(menu, "Author's preset", view.map_preset); ImGui::BeginDisabled(!view.editable); if (ImGui::Button("Apply the author's preset")) trainer_command(menu, callbacks, "preset apply map"); tip("Sets the values this map's maker recommends for it."); ImGui::EndDisabled(); } for (std::size_t i = 0; i < view.spots.size(); ++i) { ImGui::PushID(static_cast(i)); if (ImGui::Button("Go")) trainer_command(menu, callbacks, std::format("spot {}", i + 1)); tip("Takes you to this spot."); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(view.spots[i].name.c_str()); ImGui::PopID(); } if (view.map_note.empty() && view.map_preset.empty() && view.spots.empty()) note("Map makers can ship a trainer.json in their mod folder with spots and a recommended preset."); end_card(); } } // namespace bool trainer_take_open() { const auto view = trainer::view(); auto &p = page(); if (view->open_serial == p.open_serial) return false; p.open_serial = view->open_serial; // `trainer open` still takes "presets": they live on the Tune tab now. p.tab = view->open_tab == 7 ? 2 : view->open_tab == 3 ? 3 : view->open_tab == 2 ? 1 : 0; // by name, see `trainer open` if (view->open_tab == 8) p.mode = 3; // the trick line section of the Tune tab if (view->open_tab >= 4 && view->open_tab < 7) p.mode = std::clamp(view->open_tab - 4, 0, 2); p.show_page = true; return true; } bool trainer_page_wanted() { auto &p = page(); const bool wanted = p.show_page; p.show_page = false; return wanted; } void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) { auto &p = page(); const auto view = trainer::view(); ImGui::BeginDisabled(!callbacks.queue_console_command); if (ImGui::Button("RESET EVERYTHING")) { p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.revert_editing = false; trainer_command(menu, callbacks, "reset everything"); } tip("Puts back every value, lock, preset, dial, trick setting and camera: the game exactly as it shipped."); ImGui::EndDisabled(); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); if (view->ready && view->stock) ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "STOCK: the game is exactly as it shipped. Nothing of the trainer's is on."); else if (view->ready) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "MODIFIED: this button puts back every value, lock, preset and trick slider."); else ImGui::TextDisabled("The game as it shipped: every value, lock, preset and trick slider."); // `preset export` leaves its line here; the clipboard belongs to this thread. if (!p.share_seen) { p.share_seen = true; p.share_serial = view->share_serial; } else if (p.share_serial != view->share_serial) { p.share_serial = view->share_serial; ImGui::SetClipboardText(view->share_text.c_str()); } category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "CAMERA", "MAP & HUD"}, "trainer-tabs"); ImGui::PushID(p.tab); ImGui::BeginChild("trainer-tab", ImVec2(0, page_body_height(menu))); switch (p.tab) { case 0: tune_tab(menu, model, callbacks, p, *view); break; case 1: practice_tab(menu, model, callbacks, p, *view); break; case 2: camera_tab(menu, callbacks, p, *view); break; default: map_tab(menu, callbacks, *view); break; } ImGui::EndChild(); ImGui::PopID(); } } // namespace dingosdk::overlay::menu namespace dingosdk::overlay { bool trainer_open_requested() { return menu::trainer_take_open(); } bool trainer_pad_menu_pressed(bool menu_visible) { static ControllerComboLatch latch; ControllerInput input; DingoSDKOverlayReadControllerInput(&input, true); constexpr std::uint32_t combo = 0x100 | 0x200 | 0x80; // LB + RB + R3 if (!latch.update(combo, input, !trainer::view()->pad_menu)) return false; if (!menu_visible) menu::page().show_page = true; return true; } bool trainer_hud_pending() { const auto view = trainer::view(); return (view->hud || view->hud_jump) && trainer::telemetry().skater; } void draw_trainer_hud() { const auto view = trainer::view(); if (!view->hud && !view->hud_jump) return; const auto telemetry = trainer::telemetry(); if (!telemetry.skater) return; static std::uint64_t shown_jump{}; static double jump_until{}; if (telemetry.last.serial != shown_jump) { shown_jump = telemetry.last.serial; jump_until = shown_jump ? ImGui::GetTime() + 7.0 : 0.0; } std::vector lines; if (view->hud) { lines.push_back(std::format("{:.0f} km/h", telemetry.speed * 3.6f)); if (telemetry.airborne) lines.push_back(std::format("air {:.2f} s {:.2f} m", telemetry.air_time, telemetry.height)); } if (view->hud_jump && telemetry.last.serial && ImGui::GetTime() < jump_until) { const auto &jump = telemetry.last; lines.push_back(std::format("jump {}: {:.1f} km/h at {:.0f} deg", jump.serial, jump.takeoff_speed * 3.6f, jump.takeoff_angle)); lines.push_back(std::format("{:.2f} s {:.2f} m high {:.2f} m far", jump.air_time, jump.height, jump.distance)); lines.push_back(std::format("landed at {:.1f} km/h, {:.2f} m lower", jump.landing_speed * 3.6f, jump.drop)); if (jump.spin >= 45.0f || jump.flip >= 90.0f) lines.push_back(std::format("spin {:.0f} deg flip {:.0f} deg", jump.spin, jump.flip)); } if (lines.empty()) return; auto *draw = ImGui::GetForegroundDrawList(); const auto &display = ImGui::GetIO().DisplaySize; const float scale = std::clamp(display.y / 1080.0f, 0.75f, 2.5f); auto *font = ImGui::GetFont(); const float big = 34.0f * scale, small = 18.0f * scale, pad = 10.0f * scale; float width = 0, height = pad * 2; for (std::size_t i = 0; i < lines.size(); ++i) { const float size = i == 0 && view->hud ? big : small; width = std::max(width, font->CalcTextSizeA(size, FLT_MAX, 0, lines[i].c_str()).x); height += size + 4.0f * scale; } const ImVec2 origin(display.x - width - pad * 2 - 24.0f * scale, 24.0f * scale); draw->AddRectFilled(origin, ImVec2(origin.x + width + pad * 2, origin.y + height), IM_COL32(12, 14, 18, 170), 6.0f * scale); float y = origin.y + pad; for (std::size_t i = 0; i < lines.size(); ++i) { const float size = i == 0 && view->hud ? big : small; draw->AddText(font, size, ImVec2(origin.x + pad, y), IM_COL32(245, 245, 240, 255), lines[i].c_str()); y += size + 4.0f * scale; } } } // namespace dingosdk::overlay