mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 18:45:57 +02:00
1094 lines
68 KiB
C++
1094 lines
68 KiB
C++
#include "trainer_page.h"
|
|
#include "trainer.h"
|
|
#include "Extension/UI/Overlay/skate_menu_internal.h"
|
|
#include "Extension/UI/skate_theme.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdarg>
|
|
#include <format>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// 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<char, 96> 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<std::size_t> 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<char, 49> preset_name{};
|
|
std::array<float, 3> 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<std::vector<std::size_t>, 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<char>(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<std::size_t>(p.group) <= view.groups.size() + 1
|
|
? &view.groups[static_cast<std::size_t>(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<float>(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<const char *, int> 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<std::size_t>(p.group) <= view.groups.size() + 1
|
|
? view.groups[static_cast<std::size_t>(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<int>(i) + 2)) p.group = static_cast<int>(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<int>(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<std::size_t>(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<char, 256> 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<float>(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::size_t>(std::clamp(view.slot, 0, static_cast<int>(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<float>(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<int>(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<float>(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<float>(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<int>(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<std::string> 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
|