#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 // 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}; bool hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{}; std::uint64_t open_serial{}; // the last `trainer open` acted on bool show_page{}; }; void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view); Page &page() { static auto *value = new Page; return *value; } 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; 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) 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() || p.mode < 2 ? (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) { if (console_style()) { // Full screen: a switch, or the value nudged a step at a time (a fiftieth of its size). ImGui::PushID(row.id.c_str()); const auto label = friendly && !row.friendly.empty() ? row.friendly : with_group ? row.group + " / " + row.label : row.label; const auto hint = std::format("{}. The game's own is {}.{}", row.id, number(row.stock), row.used ? "" : " The game was not found reading it, so changing it may do nothing."); const bool can = view.editable && callbacks.queue_console_command; if (row.kind == trainer::Kind::flag) { bool on = row.value != 0; if (toggle_row(menu, label.c_str(), hint.c_str(), on, can)) // (a label that changes would lose the focus) trainer_command(menu, callbacks, std::format("set {} {}", row.id, on ? 1 : 0)); } else { field(menu, (row.touched ? label + " *" : label).c_str(), hint.c_str()); const bool scale = row.kind == trainer::Kind::curve || row.kind == trainer::Kind::graph; const bool whole = row.kind == trainer::Kind::integer; double value = row.value; set_console_step(whole ? 1.0 : scale ? 0.05 : std::max({std::abs(row.stock), std::abs(value), 0.01}) * 0.02); ImGui::BeginDisabled(!can); if (slider_double("##value", &value, scale ? 0.0 : -1.0e6, 1.0e6, whole ? "%.0f" : scale ? "x %.2f" : "%.4g", ImGuiSliderFlags_AlwaysClamp)) trainer_command(menu, callbacks, std::format("set {} {}", row.id, number(value))); ImGui::EndDisabled(); } ImGui::PopID(); return; } 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()) ImGui::SetTooltip("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()) ImGui::SetTooltip("%s\nStock: %s%s", 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 ? 0.0 : -1.0e6, high = 1.0e6; 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 (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); } 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()) ImGui::SetTooltip("%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) { if (console_style()) { ImGui::PushID(preset.name.c_str()); const auto hint = std::format("{} 1 is the game's own; \"{}\" is x {}.", preset.note, preset.name, number(preset.amount)); field(menu, std::abs(preset.factor - 1.0) > 1e-4 ? (preset.title + " *").c_str() : preset.title.c_str(), hint.c_str()); double value = preset.factor; const double low = preset.amount < 1 ? 0.05 : 0.2, high = preset.amount < 1 ? 3.0 : 10.0; if (slider_double("##dial", &value, low, high, "x %.2f", ImGuiSliderFlags_Logarithmic) && value > 0) trainer_command(menu, callbacks, std::format("dial {} {}", number(value), preset.name)); ImGui::PopID(); (void)p; return; } 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()) ImGui::SetTooltip("%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(); } 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()) ImGui::SetTooltip("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()) ImGui::SetTooltip("%s", preset.note.c_str()); if (moved) { ImGui::SameLine(); if (ImGui::Button("Reset", ImVec2(reset, 0))) trainer_command(menu, callbacks, "dial 1 " + preset.name); } 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 and the whole table. if (console_style()) { static const std::vector modes{"Realistic: tone it down", "Fun: turn it up", "Everything"}; option_row(menu, "Showing", "Left / right: a short list to tone the game down, one to turn it up, or every value.", p.mode, modes); } else { static constexpr const char *modes[]{"REALISTIC", "FUN", "EVERYTHING"}; const float width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3; for (int i = 0; i < 3; ++i) { if (i) ImGui::SameLine(); const bool on = p.mode == i; if (on) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue); if (ImGui::Button(modes[i], ImVec2(width, 0))) p.mode = i; 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; 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 && (p.mode == 2 || (preset.modes & list))) dial_row(menu, callbacks, p, preset); // The presets that are plain switches, and the ones that set several dials at once. bool first = true; if (console_style()) for (const auto &preset : view.presets) { // Full screen, every preset as a switch: on while all it sets holds. if (!preset.builtin || preset.name == "Stock" || !(p.mode == 2 || (preset.modes & list))) continue; bool on = preset.active; ImGui::PushID(preset.name.c_str()); if (toggle_row(menu, preset.name.c_str(), preset.note.c_str(), on, can_edit)) trainer_command(menu, callbacks, std::string(on ? "preset apply " : "preset remove ") + preset.name); ImGui::PopID(); } else for (const auto &preset : view.presets) { if (!preset.builtin || preset.dial || preset.name == "Stock" || !(p.mode == 2 || (preset.modes & list))) 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 > Cheats. 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}); } if (console_style()) { const auto changed = std::format("{} values changed. Every one back to the game's own; locked values stay.", view.touched); const int pressed = button_row(menu, "tune-reset", {{"RESET VALUES", can_edit && view.touched > 0, false, changed.c_str()}, {"RESET EVERYTHING", callbacks.queue_console_command != nullptr, false, "The game as it shipped: every value, lock, preset and trick slider."}}); if (pressed == 0) trainer_command(menu, callbacks, "reset all"); if (pressed == 1) { p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false; trainer_command(menu, callbacks, "reset everything"); } } else { ImGui::BeginDisabled(!can_edit || !view.touched); if (ImGui::Button("Reset values")) trainer_command(menu, callbacks, "reset all"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("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); ImGui::EndDisabled(); if (p.mode == 2) { begin_card(menu, "preset-save", "YOUR PRESETS", "Save what is changed right now"); if (console_style()) { for (const auto &preset : view.presets) { if (preset.builtin) continue; const int pressed = button_row(menu, preset.name.c_str(), { {preset.name.c_str(), can_edit, preset.active, preset.active ? "On: A turns it off." : "A turns it on."}, {"USE ON THIS MAP", can_edit, view.profile_preset == preset.name, "Apply it every time this map loads."}, {"DELETE", can_edit, false, "Delete this preset."}}); if (pressed == 0) trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name); if (pressed == 1) trainer_command(menu, callbacks, "profile set " + preset.name); if (pressed == 2) trainer_command(menu, callbacks, "preset delete " + preset.name); } if (!view.map.empty()) { info(menu, "This map applies", view.profile_preset.empty() ? "nothing" : view.profile_preset); if (!view.profile_preset.empty() && button_row(menu, "profile-clear", {{"STOP APPLYING IT ON THIS MAP"}}) == 0) trainer_command(menu, callbacks, "profile clear"); } note("Name and save a preset in the window menu (View): it needs a keyboard."); end_card(); } else { 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); } 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(120)); if (ImGui::SmallButton("This map")) trainer_command(menu, callbacks, "profile set " + preset.name); ImGui::SameLine(); if (ImGui::SmallButton("Delete")) trainer_command(menu, callbacks, "preset delete " + preset.name); 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()); 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()); ImGui::EndDisabled(); 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 && console_style()) { trainer::note_class_list_shown(); p.search[0] = 0; // (searching wants a keyboard: the window menu has it) std::vector groups{"Every group"}; for (const auto &group : view.groups) groups.push_back(group); int group = std::max(p.group, 1) - 1; if (option_row(menu, "Group", "Left / right for one group of values.", group, groups)) p.group = group + 1; toggle_row(menu, "Only what I changed", "Only the values you changed or locked.", p.only_changed); toggle_row(menu, "Graph points", "Every point of the tuning graphs, not just one multiplier per graph.", p.graph_points); toggle_row(menu, "Values with no use found", "Also the values the game was never found reading. Changing those will probably do nothing.", p.show_unused); } else 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); ImGui::InputTextWithHint("##search", "Search every value...", p.search.data(), p.search.size()); ImGui::SameLine(); 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(); } ImGui::Checkbox("Only what I changed", &p.only_changed); ImGui::SameLine(); ImGui::Checkbox("Graph points", &p.graph_points); if (ImGui::IsItemHovered()) ImGui::SetTooltip("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()) ImGui::SetTooltip("Also list the tuning values that the game was never found or seen reading.\nChanging those will probably do nothing."); } else { note(console_style() ? "The same values the dials move, one at a time. A * marks one you changed." : "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 && !console_style()) { 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 = p.mode == 2 && (p.search[0] != 0 || 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) { 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, float high = 50.0f, bool limited = false) { field(menu, label); ImGui::PushID(option); if (console_style()) { float shown = editing ? edit : value; if (slider_float("##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; ImGui::PopID(); return; } 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; 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()) ImGui::SetTooltip(limited ? "Type a multiplier and press Enter." : "Type any multiplier and press Enter: the slider's end is not a limit."); ImGui::PopID(); }; slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing, 3.0f, true); if (ImGui::IsItemHovered()) ImGui::SetTooltip("Slows board flips. Above 1 the game's own limit on how fast a board turns takes over."); slider("No comply height", "nocomply_height", view.nocomply_height, p.nocomply_edit, p.nocomply_editing); slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing); slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing); slider("Off-board jump height", "offboard_height", view.offboard_height, p.offboard_edit, p.offboard_editing); if (button_row(menu, "tricks-reset", {{"RESET TRICKS"}}) == 0) { p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false; trainer_command(menu, callbacks, "reset tricks"); } 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 (::dingosdk::overlay::menu::slider_float("##game-speed", &value, 0.05f, 2.0f, current == 0.0 ? "paused" : "%.2fx", ImGuiSliderFlags_AlwaysClamp)) set(value); const int pressed = button_row(menu, "speeds", {{current == 0.0 ? "RESUME" : "PAUSE", true, current == 0.0}, {"0.1x", true, current == 0.1}, {"0.25x", true, current == 0.25}, {"0.5x", true, current == 0.5}, {"0.75x", true, current == 0.75}, {"1x", true, current == 1.0}}); constexpr std::array quick{0.1, 0.25, 0.5, 0.75, 1.0}; if (pressed == 0) set(current == 0.0 ? 1.0 : 0.0); else if (pressed > 0) set(quick[static_cast(pressed - 1)]); 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"); { const bool map = !view.map.empty(); const int pressed = button_row(menu, "marker-actions", {{"SAVE HERE", map && telemetry.skater, false, "Put the selected marker where you are."}, {"GO", map && marker.set, false, "Go to the selected marker."}, {"CLEAR", map && marker.set, false, "Empty the selected marker."}}); if (pressed == 0) trainer_command(menu, callbacks, "marker save"); if (pressed == 1) trainer_command(menu, callbacks, "marker go"); if (pressed == 2) trainer_command(menu, callbacks, "marker clear"); } 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; ::dingosdk::overlay::menu::slider_float("##return-delay", &delay, 0.0f, 10.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp); 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)); end_card(); begin_card(menu, "teleport", "TELEPORT"); // The waypoint line is why the map's registry is read at all: only while this is drawn. trainer::note_teleport_card_shown(); info(menu, "Map waypoint", view.waypoint_set ? std::format("{:.1f}, {:.1f}, {:.1f}", view.waypoint[0], view.waypoint[1], view.waypoint[2]) : "none - place one on the pause map"); if (button_row(menu, "waypoint", {{"GO TO MAP WAYPOINT", view.waypoint_set && telemetry.skater, false, "The waypoint you placed on the pause map."}}) == 0) trainer_command(menu, callbacks, "waypoint"); if (telemetry.skater) info(menu, "You are at", std::format("{:.2f}, {:.2f}, {:.2f}", telemetry.position[0], telemetry.position[1], telemetry.position[2])); if (console_style()) { // Typing a position wants a keyboard: full screen keeps one, set from where you are. info(menu, "Saved spot", std::format("{:.2f}, {:.2f}, {:.2f}", p.teleport[0], p.teleport[1], p.teleport[2])); const int pressed = button_row(menu, "teleport-actions", {{"SET IT TO HERE", telemetry.skater, false, "Keep where you are now."}, {"GO THERE", telemetry.skater, false, "Teleport to the saved spot."}}); if (pressed == 0) p.teleport = telemetry.position; if (pressed == 1) trainer_command(menu, callbacks, std::format("tp {:.3f} {:.3f} {:.3f}", p.teleport[0], p.teleport[1], p.teleport[2])); end_card(); return; } field(menu, "X, Y, Z"); ImGui::InputFloat3("##teleport", p.teleport.data(), "%.2f"); ImGui::BeginDisabled(!telemetry.skater); if (ImGui::Button("Here")) p.teleport = telemetry.position; 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])); 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()); 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()); 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)); } 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); if (button_row(menu, "author-preset", {{"APPLY THE AUTHOR'S PRESET", view.editable}}) == 0) trainer_command(menu, callbacks, "preset apply map"); } for (std::size_t i = 0; i < view.spots.size(); ++i) { ImGui::PushID(static_cast(i)); if (console_style()) { if (button_row(menu, "spot", {{("GO TO " + view.spots[i].name).c_str()}}) == 0) trainer_command(menu, callbacks, std::format("spot {}", i + 1)); } else { if (ImGui::Button("Go")) trainer_command(menu, callbacks, std::format("spot {}", i + 1)); 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 >= 4 ? 0 : std::clamp(view->open_tab - 1, 0, 2); if (view->open_tab >= 4) 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(); if (!console_style()) { 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 = false; trainer_command(menu, callbacks, "reset everything"); } ImGui::EndDisabled(); ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::TextDisabled("The game as it shipped: every value, lock, preset and trick slider."); } category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "MAP & HUD"}, "trainer-tabs"); // (the cheats are on Skater > Cheats) ImGui::PushID(p.tab); ImGui::BeginChild("trainer-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened); switch (p.tab) { case 0: tune_tab(menu, model, callbacks, p, *view); break; case 1: practice_tab(menu, model, 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_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