mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 19:56:00 +02:00
A preset's dial refused 0 in its box and in `trainer dial` (it works on a log scale). A factor of 0 now sets every value the dial moves to 0; a value that moves the other way stays stock instead of going to infinity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011m6eM3ps6bttTbZqo9Gz6Y
2257 lines
145 KiB
C++
2257 lines
145 KiB
C++
#include "trainer_page.h"
|
|
#include "trainer.h"
|
|
#include "trainer_classes.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 <span>
|
|
#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{};
|
|
// The Tune tab is one page of sections. A function that draws several cards is told which of them a section
|
|
// takes (`pick`; empty: all), and card_end() has to know whether the last one was left out.
|
|
std::vector<std::string_view> pick;
|
|
bool card_skipped{};
|
|
int scroll_to{-1}; // a section `trainer open` asked for: opened and scrolled to, once
|
|
bool folds_seeded{}; // the saved folds were handed to the menu
|
|
bool manage{}; // the preset row's YOUR PRESETS card is open
|
|
int preset_pick{-1}; // which of the player's presets the row's box shows
|
|
// Rows a card shows by name (left out of their section's FINE TUNING), and each section's named rows by rank.
|
|
std::vector<char> placed;
|
|
std::uint64_t placed_revision{~0ull};
|
|
std::array<std::vector<std::size_t>, trainer::topic_count> topic_rows;
|
|
int group{1}; // EVERY VALUE: 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 manual_edit{1}, hippy_edit{1}, nocomply_edit{1}, boneless_edit{1}, offboard_edit{1}, flip_edit{1}, pump_edit{1};
|
|
std::array<float, trainer::flip_tricks.size()> trick_edit{}; // the speed of each flip trick
|
|
std::array<bool, trainer::flip_tricks.size()> trick_editing{};
|
|
float grind_side_edit{1}, grind_push_edit{};
|
|
float grind_range_edit{};
|
|
bool grind_range_editing{};
|
|
bool grind_push_editing{};
|
|
bool grind_side_editing{}, grind_watching{};
|
|
float revert_edit{};
|
|
bool revert_editing{};
|
|
// Rows of the value table that cards show by name, for one snapshot (place_rows).
|
|
std::array<std::vector<std::size_t>, 4> trick_rows;
|
|
std::array<std::vector<std::size_t>, 6> transition_rows;
|
|
bool manual_editing{}, hippy_editing{}, nocomply_editing{}, boneless_editing{}, offboard_editing{}, flip_editing{}, pump_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{}, show_cheats{};
|
|
float gravity_edit{1};
|
|
bool gravity_editing{};
|
|
// QUICK SETTINGS: what is changed (kind 0 a section's line, 1 a dial, 2 one of the trainer's settings, 3 a value).
|
|
struct QuickItem {
|
|
std::uint8_t kind{}, topic{};
|
|
std::size_t index{};
|
|
};
|
|
std::vector<QuickItem> quick_all;
|
|
std::uint64_t quick_revision{~0ull};
|
|
bool in_quick{}; // drawing that list: a row there is not the one "Go to" points at
|
|
// "Go to": the value to point at (tinted for a few seconds, scrolled to once), and the section whose cards open.
|
|
std::string flash;
|
|
double flash_until{};
|
|
bool flash_scroll{};
|
|
float flash_y{-1};
|
|
int open_cards{-1}, current_topic{-1};
|
|
};
|
|
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words, int part);
|
|
void trickline_section(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, Page &p, const trainer::View &view);
|
|
void transition_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view);
|
|
Page &page() {
|
|
static auto *value = new Page;
|
|
return *value;
|
|
}
|
|
// A card of this page: it folds, and a section can leave it out (Page::pick). card_end() follows either way.
|
|
bool card(SkateMenu &menu, Page &p, const char *id, const char *title, const char *subtitle = nullptr) {
|
|
p.card_skipped = !p.pick.empty() && std::ranges::find(p.pick, std::string_view(id)) == p.pick.end();
|
|
if (!p.card_skipped && p.open_cards > 0 && p.open_cards == p.current_topic) set_fold(id, true); // "Go to" shows what it points at
|
|
return !p.card_skipped && fold_card(menu, id, title, subtitle);
|
|
}
|
|
void card_end(Page &p) {
|
|
if (!p.card_skipped) end_card();
|
|
p.card_skipped = false;
|
|
}
|
|
// A slider shows what the player is dragging until the trainer's own number catches up; after a reset there is nothing
|
|
// to catch up with.
|
|
void clear_edits(Page &p) {
|
|
p.manual_editing = p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.pump_editing = p.revert_editing = false;
|
|
p.grind_side_editing = p.grind_push_editing = p.grind_range_editing = p.gravity_editing = false;
|
|
p.trick_editing = {};
|
|
p.active.clear();
|
|
}
|
|
// 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);
|
|
}
|
|
|
|
// The rows EVERY VALUE and a search show.
|
|
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);
|
|
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.group > 1 && static_cast<std::size_t>(p.group) <= view.groups.size() + 1 ? &view.groups[static_cast<std::size_t>(p.group) - 2] : nullptr;
|
|
for (std::size_t i = 0; i < view.rows.size(); ++i) {
|
|
const auto &row = view.rows[i];
|
|
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);
|
|
}
|
|
}
|
|
// Rows of the value table that cards show by name.
|
|
constexpr const char *trick_groups[]{
|
|
"onboard_powerslide.frictionscalar_revert onboard_powerslide.frictionscalar_autorevert "
|
|
"onboard_powerslide.powerslide_forwardforcescalar_revert onboard_powerslide.powerslide_forwardforcescalar_autorevert",
|
|
"physicsmode.jumpmaxheight physicsmode.jumpminheight physicsmode.jumpminheightmanual physicsonboardjumptuning.jumpadjustvelocityboostlimitground",
|
|
"physicsairstates.maxspinspeed physicsmode.maxautobodyspinspeed physicsmode.easybodyspins physicsreckoning.flipscalar "
|
|
"physicsreckoning.flipmaxspeed heldflip.straightflipcatchtime heldflip.shuvmincatchtime heldflip.varialmincatchtime heldflip.bigflipmincatchtime",
|
|
"physicsmanual.manualscalar_p physicsmanual.manualscalar_i physicsmanual.manualscalar_d physicsmanual.maxtiltangle physicsfriction.noinputtime "
|
|
"physicssteering.generalscalar physicssteering.damping",
|
|
};
|
|
constexpr std::string_view coping_rows[]{"physicsgrind.copingexitstarttime", "physicsgrind.fairlysteepexitstarttime", "physicsgrind.curbexitstarttime",
|
|
"physicsgrind.slowgrindexitspeed", "physicsgrind.nonpopexitangledeg", "physicsgrind.grindreckoningsmoothnessonexit",
|
|
"physicsgrind.grindreckoningsmoothnessdropin"};
|
|
constexpr std::string_view reentry_rows[]{"physicstrajectory.scoregrind", "physicsgrindsair.maxdist50_50", "physicsgrindsair.maxdist5_0",
|
|
"physicsgrindsair.maxdisttipslide", "physicsgrindsair.maxdistboardslide", "physicstrajectory.grindlandingmaxangle"};
|
|
constexpr std::string_view plant_rows[]{"physicshandplant.exitanglerangestart", "physicshandplant.exitanglerangeend", "physicshandplant.minnormalvel",
|
|
"physicshandplant.maxnormalvel", "physicshandplant.maxparallelvel", "physicshandplant.maxverticalvel",
|
|
"physicshandplant.minlipclearance", "physicshandplant.maxlipclearance", "physicshandplant.maxangleofapproach",
|
|
"physicshandplant.waitforanimtime", "physicshandplant.bstofsanglethreshold", "physicshandplant.fstobsanglethreshold",
|
|
"physicshandplant.linesegmentoffset", "physicshandplant.linesegmentpad", "physicshandplant.segmentcastheightpad"};
|
|
constexpr std::string_view lip_rows[]{"physicsliptrick.liptrickcomapexheight", "physicsliptrick.timetoapexvsspeed x", "physicsliptrick.skateboardrotationslerpvstime x",
|
|
"physicsliptrick.maxrotationpersecond", "physicsliptrick.lipquerymaxlipdistance",
|
|
"physicsliptrick.lipquerymaxdirectionchangedegrees", "physicsliptrick.launchgroundslopethreshold",
|
|
"physicsliptrick.lipquerylipslopethreshold", "physicsliptrick.lipquerygroundslopethreshold", "physicsrollin.maxrollintime",
|
|
"physicsrollin.skateboardrollinduration", "physicsrollin.minlipbashdownwardspeed", "physicsrollin.undershootamount"};
|
|
constexpr std::string_view vert_rows[]{"physicsreckoning.vertjumpalignmaxgroundnormaly", "physicsreckoning.vertjumpalignmaxangle",
|
|
"physicsreckoning.vertjumpalignminjumpdiry", "physicsreckoning.vertjumpalignfactor",
|
|
"physicstrajectory.naturalairoffvertsscalar", "physicsairstates.naturalairmaxdist", "physicstrajectory.scoretransition",
|
|
"physicstrajectory.mintimeafterapexfortransition", "physicstrajectory.scoremiddlebonus",
|
|
"physicstrajectory.scorelandingdirection", "physicstrajectory.maxtrajectoryadjust", "physicstrajectory.coneanglex",
|
|
"physicstrajectory.coneanglez", "physicstrajectory.mintimefortrajectorytobevalid"};
|
|
constexpr std::string_view pump_rows[]{"physicsmode.pumpeffectfactor", "physicsmode.pumpmaxacceleration", "physicsmode.pumpmaxdeceleration",
|
|
"physicsmode.unintentionalpumpscalar", "physicspumping.pumpangle", "physicspumping.pumpminfactor",
|
|
"physicspumping.pumpoutoftransitionscalar", "physicspumping.pumpoutoftransitioncheckdistancescalar",
|
|
"physicsantipumping.maxdownwardangledeg", "physicsantipumping.maxdownwardangledeg_nobanks"};
|
|
constexpr std::span<const std::string_view> transition_lists[]{coping_rows, reentry_rows, plant_rows, lip_rows, vert_rows, pump_rows};
|
|
// Once per snapshot: where each of those rows is in the table, and each section's other named rows in their order.
|
|
void place_rows(Page &p, const trainer::View &view) {
|
|
if (p.placed_revision == view.revision) return;
|
|
p.placed_revision = view.revision;
|
|
p.placed.assign(view.rows.size(), 0);
|
|
std::vector<std::string> ids(view.rows.size());
|
|
for (std::size_t i = 0; i < view.rows.size(); ++i) ids[i] = lower(view.rows[i].id);
|
|
const auto find = [&](std::string_view key, std::vector<std::size_t> &into) {
|
|
for (std::size_t i = 0; i < ids.size(); ++i)
|
|
if (ids[i] == key) {
|
|
into.push_back(i);
|
|
p.placed[i] = 1;
|
|
return;
|
|
}
|
|
};
|
|
for (std::size_t g = 0; g < std::size(trick_groups); ++g) {
|
|
p.trick_rows[g].clear();
|
|
const std::string_view keys = trick_groups[g];
|
|
for (std::size_t start = 0; start < keys.size();) {
|
|
auto end = keys.find(' ', start);
|
|
if (end == std::string_view::npos) end = keys.size();
|
|
if (end > start) find(keys.substr(start, end - start), p.trick_rows[g]);
|
|
start = end + 1;
|
|
}
|
|
}
|
|
for (std::size_t g = 0; g < std::size(transition_lists); ++g) {
|
|
p.transition_rows[g].clear();
|
|
for (const auto key : transition_lists[g]) find(key, p.transition_rows[g]);
|
|
}
|
|
for (auto &rows : p.topic_rows) rows.clear();
|
|
for (std::size_t i = 0; i < view.rows.size(); ++i)
|
|
if (const auto &row = view.rows[i]; row.topic && row.topic < trainer::topic_count && !row.friendly.empty() && !p.placed[i]) p.topic_rows[row.topic].push_back(i);
|
|
for (auto &rows : p.topic_rows) std::ranges::sort(rows, {}, [&](std::size_t i) { return view.rows[i].rank; });
|
|
}
|
|
// A row belongs on a section when it does something: the game was found reading it, or it has been changed or locked
|
|
// (so it can be put back). The rest are on EVERY VALUE only.
|
|
bool row_works(const trainer::Row &row) { return row.used || row.touched || row.frozen; }
|
|
|
|
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.help, row.help.empty() ? "" : " ", 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);
|
|
// The bar spans a sensible range around the game's own value (sliding covers it); the box beside it, or
|
|
// stepping on past the end, takes any number.
|
|
const double size = std::max({std::abs(row.stock), whole ? 5.0 : 0.01});
|
|
const double low = scale ? 0.0 : row.stock >= 0 ? (row.stock == 0 ? -size * 10 : 0.0) : row.stock * 4,
|
|
high = scale ? 4.0 : row.stock <= 0 ? (row.stock == 0 ? size * 10 : 0.0) : row.stock * 4;
|
|
ImGui::BeginDisabled(!can);
|
|
if (slider_double("##value", &value, low, high, whole ? "%.0f" : scale ? "x %.2f" : "%.4g"))
|
|
trainer_command(menu, callbacks, std::format("set {} {}", row.id, number(value)));
|
|
ImGui::EndDisabled();
|
|
}
|
|
ImGui::PopID();
|
|
return;
|
|
}
|
|
ImGui::PushID(row.id.c_str());
|
|
if (!p.in_quick && !p.flash.empty() && p.flash == row.id) {
|
|
// "Go to" pointed here.
|
|
const auto at = ImGui::GetCursorScreenPos();
|
|
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(at.x - px(4), at.y - px(1)), ImVec2(at.x + ImGui::GetContentRegionAvail().x, at.y + ImGui::GetFrameHeight() + px(1)),
|
|
IM_COL32(60, 130, 230, 80));
|
|
p.flash_y = at.y;
|
|
}
|
|
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\n%s\nStock: %s%s",
|
|
row.help.empty() ? std::format("{} / {}: one of the game's own tuning values, under the game's own name. No plain explanation "
|
|
"is written for it yet; the name says what it belongs to.", row.group, row.label).c_str() : row.help.c_str(),
|
|
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) {
|
|
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())
|
|
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("Easy Body 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. 0 turns what it moves to 0.");
|
|
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();
|
|
}
|
|
|
|
// The player's own presets, above the tabs: pick one and load it, save what is set now under a name. "Manage" opens
|
|
// the list: share one as a line of text, import one that was shared, delete, tie one to this map.
|
|
void preset_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
|
|
std::vector<const trainer::PresetRow *> own;
|
|
for (const auto &preset : view.presets)
|
|
if (!preset.builtin) own.push_back(&preset);
|
|
const bool can_edit = view.editable && callbacks.queue_console_command != nullptr;
|
|
if (p.preset_pick < 0 || p.preset_pick >= static_cast<int>(own.size())) {
|
|
p.preset_pick = 0;
|
|
for (std::size_t i = 0; i < own.size(); ++i)
|
|
if (own[i]->active) p.preset_pick = static_cast<int>(i);
|
|
}
|
|
const trainer::PresetRow *picked = own.empty() ? nullptr : own[static_cast<std::size_t>(p.preset_pick)];
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("Preset");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(px(190));
|
|
if (ImGui::BeginCombo("##preset", picked ? picked->name.c_str() : "none saved yet")) {
|
|
for (std::size_t i = 0; i < own.size(); ++i) {
|
|
const auto label = own[i]->active ? own[i]->name + " (on)" : own[i]->name;
|
|
ImGui::PushID(static_cast<int>(i));
|
|
if (ImGui::Selectable(label.c_str(), static_cast<int>(i) == p.preset_pick)) p.preset_pick = static_cast<int>(i);
|
|
ImGui::PopID();
|
|
}
|
|
if (own.empty()) ImGui::TextDisabled("Set the game up the way you like it, give it a name, press Save.");
|
|
ImGui::EndCombo();
|
|
}
|
|
tip("Your saved setups. Pick one, then Load: it sets every value and setting saved in it on top of what is set now.");
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(!picked || !can_edit);
|
|
if (picked && picked->active) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue);
|
|
if (ImGui::Button("Turn off")) {
|
|
clear_edits(p);
|
|
trainer_command(menu, callbacks, "preset remove " + picked->name);
|
|
}
|
|
ImGui::PopStyleColor();
|
|
tip("Puts everything this preset set back to the game's own.");
|
|
} else {
|
|
if (ImGui::Button("Load") && picked) {
|
|
clear_edits(p);
|
|
trainer_command(menu, callbacks, "preset apply " + picked->name);
|
|
}
|
|
tip("Sets every value and setting saved in this preset. Press RESET EVERYTHING first to get exactly the preset and nothing else.");
|
|
}
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine(0, px(18));
|
|
ImGui::SetNextItemWidth(px(140));
|
|
ImGui::InputTextWithHint("##preset-name", "new 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.stock);
|
|
if (ImGui::Button("Save")) trainer_command(menu, callbacks, std::string("preset save ") + p.preset_name.data());
|
|
ImGui::EndDisabled();
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
|
wrapped_tooltip("%s", view.stock ? "Nothing is changed, so there is nothing to save yet."
|
|
: "Saves everything that is changed right now under this name: tuning values, trick, grind, transition and bending "
|
|
"settings. Not saved: locks, No Bail, Noclip, Dark Pop, the game speed and your game's own settings.");
|
|
ImGui::SameLine();
|
|
if (p.manage) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue);
|
|
const bool pressed = ImGui::Button("Manage");
|
|
if (p.manage) ImGui::PopStyleColor();
|
|
if (pressed) p.manage = !p.manage;
|
|
tip("Share a preset as a line of text, import one, delete one, or have one load by itself on this map.");
|
|
if (!p.manage) return;
|
|
begin_card(menu, "preset-manage", "YOUR PRESETS", "Share, import, delete, load by itself on this map");
|
|
ImGui::BeginDisabled(!can_edit);
|
|
const float button = ImGui::CalcTextSize("Turn off").x + ImGui::GetStyle().FramePadding.x * 2;
|
|
for (const auto *preset : own) {
|
|
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("Load", ImVec2(button, 0))) {
|
|
trainer_command(menu, callbacks, "preset apply " + preset->name);
|
|
}
|
|
tip("Load sets every value and setting saved in this preset. Turn off puts those 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(205));
|
|
if (ImGui::SmallButton("This map")) trainer_command(menu, callbacks, "profile set " + preset->name);
|
|
tip("Loads 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();
|
|
}
|
|
if (own.empty()) note("No preset saved yet: set the game up, type a name above and press Save.");
|
|
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 load it.");
|
|
if (!view.map.empty()) {
|
|
info(menu, "This map loads", view.profile_preset.empty() ? "nothing by itself" : view.profile_preset);
|
|
if (!view.profile_preset.empty() && ImGui::Button("Stop loading it on this map")) trainer_command(menu, callbacks, "profile clear");
|
|
}
|
|
end_card();
|
|
}
|
|
|
|
// ---- the first section: every kind of preset, and everything that is changed in one list ---------
|
|
// One of the trainer's own settings (a trick height, a grind switch, a bending rule ...) as a row that changes it.
|
|
void setup_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const trainer::SetupRow &row) {
|
|
ImGui::PushID(row.key.c_str());
|
|
ImGui::Dummy(ImVec2(ImGui::GetFrameHeight(), ImGui::GetFrameHeight())); // where a value's lock box is
|
|
ImGui::SameLine();
|
|
const float column = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.52f;
|
|
ImGui::PushStyleColor(ImGuiCol_Text, skate_theme::blue);
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted(row.title.c_str());
|
|
ImGui::PopStyleColor();
|
|
if (ImGui::IsItemHovered())
|
|
wrapped_tooltip("One of the trainer's own settings (%s). The game's own: %s. Go to shows it where it lives, with its explanation.", row.key.c_str(),
|
|
row.kind == trainer::setup_mode ? "left to the game" : row.kind == trainer::setup_switch ? "off" : number(row.stock).c_str());
|
|
ImGui::SameLine(column);
|
|
const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
|
|
ImGui::BeginDisabled(!view.editable || !callbacks.queue_console_command);
|
|
const auto id = "setup:" + row.key;
|
|
double value = p.active == id ? p.active_value : row.value;
|
|
const float width = std::max(px(90), ImGui::GetContentRegionAvail().x - reset - ImGui::GetStyle().ItemSpacing.x);
|
|
if (row.kind == trainer::setup_switch) {
|
|
bool on = value != 0;
|
|
if (ImGui::Checkbox("##value", &on)) trainer_command(menu, callbacks, std::format("setup {} {}", row.key, on ? 1 : 0));
|
|
} else if (row.kind == trainer::setup_mode) {
|
|
static constexpr const char *modes[]{"The game's own", "Held off", "Held on"};
|
|
int mode = value < 0 ? 0 : value != 0 ? 2 : 1;
|
|
ImGui::SetNextItemWidth(width);
|
|
if (ImGui::Combo("##value", &mode, modes, 3)) trainer_command(menu, callbacks, std::format("setup {} {}", row.key, mode - 1));
|
|
} else {
|
|
ImGui::SetNextItemWidth(width);
|
|
const auto speed = static_cast<float>(std::max({std::abs(row.stock), std::abs(value), 0.05}) * 0.006);
|
|
if (ImGui::DragScalar("##value", ImGuiDataType_Double, &value, speed, &row.low, &row.high, "%.4g", ImGuiSliderFlags_AlwaysClamp))
|
|
trainer_command(menu, callbacks, std::format("setup {} {}", row.key, number(value)));
|
|
if (ImGui::IsItemActive()) {
|
|
p.active = id;
|
|
p.active_value = value;
|
|
} else if (p.active == id) {
|
|
p.active.clear();
|
|
}
|
|
if (!ImGui::IsItemActive()) tip("Drag left or right to change it, or double-click to type a number.");
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Reset", ImVec2(reset, 0))) {
|
|
clear_edits(p);
|
|
trainer_command(menu, callbacks, std::format("setup {} {}", row.key, number(row.stock)));
|
|
}
|
|
tip("Puts this one setting back to the game's own.");
|
|
ImGui::EndDisabled();
|
|
ImGui::PopID();
|
|
}
|
|
// Which feel the grind and the transition settings add up to: 0 skate.'s own, 1 Skate 3's, 2 a mix of the player's.
|
|
int grind_feel(const trainer::View &view) {
|
|
bool like_skate = true, like_skate3 = true;
|
|
for (std::size_t i = 0; i < trainer::grind_switches.size(); ++i) {
|
|
if (view.grind_mode[i] >= 0) like_skate = false;
|
|
if (view.grind_mode[i] != trainer::skate3_grind_mode(i)) like_skate3 = false;
|
|
}
|
|
for (std::size_t i = 0; i < trainer::grind_settings.size(); ++i) {
|
|
if (view.grind_setting[i] >= 0) like_skate = false;
|
|
if (view.grind_setting[i] != trainer::grind_settings[i].skate3) like_skate3 = false;
|
|
}
|
|
if (view.grind_push != 0.0f || view.grind_range != 0.0f) like_skate = false;
|
|
if (std::abs(view.grind_push - 1.8f) > 0.005f || view.grind_range == 0.0f) like_skate3 = false;
|
|
return like_skate ? 0 : like_skate3 ? 1 : 2;
|
|
}
|
|
int transition_feel(const trainer::View &view) {
|
|
const bool preset_on = std::ranges::any_of(view.presets, [](const trainer::PresetRow &row) { return row.builtin && row.active && row.name == "Transition: Skate 3"; });
|
|
bool like_skate = !preset_on, like_skate3 = preset_on;
|
|
for (std::size_t i = 0; i < trainer::transition_settings.size(); ++i) {
|
|
if (view.tr_setting[i] >= 0) like_skate = false;
|
|
if (view.tr_setting[i] != trainer::transition_settings[i].skate3) like_skate3 = false;
|
|
}
|
|
if (view.tr_lean != 0.0f || view.tr_carry != 0.0f || view.tr_unstick != 0.0f || view.tr_lockout != 0.0f || view.tr_spine || view.tr_plant != 0.0f) like_skate = false;
|
|
if (view.tr_lean == 0.0f || view.tr_carry == 0.0f || view.tr_unstick == 0.0f || view.tr_lockout == 0.0f || view.tr_plant == 0.0f) like_skate3 = false;
|
|
return like_skate ? 0 : like_skate3 ? 1 : 2;
|
|
}
|
|
// Every kind of preset in one card: the whole-game feels, the grind and transition feels, the tricklining extras,
|
|
// the built-in styles and the player's own. Whatever one sets shows up in QUICK SETTINGS below it.
|
|
void presets_card(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, bool can_edit) {
|
|
if (card(menu, p, "presets", "PRESETS", "A whole setup in one press. Blue = on. What it changes is listed in QUICK SETTINGS below.")) {
|
|
ImGui::BeginDisabled(!can_edit);
|
|
// Buttons in a row that wraps under where the row started.
|
|
float column{};
|
|
bool first = true;
|
|
const auto begin = [&](const char *label, const char *about) {
|
|
field(menu, label, about);
|
|
column = ImGui::GetCursorPosX();
|
|
first = true;
|
|
};
|
|
const auto button = [&](const char *label, bool on, const char *about) {
|
|
const float width = ImGui::CalcTextSize(label).x + ImGui::GetStyle().FramePadding.x * 2;
|
|
if (!first) {
|
|
if (ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemSpacing.x + width < ImGui::GetWindowPos().x + ImGui::GetContentRegionMax().x) ImGui::SameLine();
|
|
else ImGui::SetCursorPosX(column);
|
|
}
|
|
first = false;
|
|
if (on) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue);
|
|
const bool pressed = ImGui::Button(label);
|
|
if (on) ImGui::PopStyleColor();
|
|
if (about && *about && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) wrapped_tooltip("%s", about);
|
|
if (pressed) clear_edits(p);
|
|
return pressed;
|
|
};
|
|
const auto active = [&](std::string_view name) {
|
|
return std::ranges::any_of(view.presets, [&](const trainer::PresetRow &row) { return row.builtin && row.active && row.name == name; });
|
|
};
|
|
begin("Whole game", "Skate 3's own numbers for everything the two games share, on one of its difficulties.");
|
|
const bool any = active("Skate 3 Easy") || active("Skate 3") || active("Skate 3 Hardcore");
|
|
if (button("skate.", !any, "The game's own tuning: takes a Skate 3 choice off again.")) trainer_command(menu, callbacks, "feel stock");
|
|
if (button("Skate 3 Easy", active("Skate 3 Easy"), "Skate 3 on Easy: full-height pops every time, strong pushes, generous grind lock-on, easy spins.")) trainer_command(menu, callbacks, "feel easy");
|
|
if (button("Skate 3", active("Skate 3"), "Skate 3 on Normal: its pop, grind pops, pushing, pumping, steering, manuals, spins, flips and bails.")) trainer_command(menu, callbacks, "feel normal");
|
|
if (button("Skate 3 Hardcore", active("Skate 3 Hardcore"), "Skate 3 on Hardcore: lower pops, weaker pushes, tight grind lock-on, slow auto spins.")) trainer_command(menu, callbacks, "feel hardcore");
|
|
const int grinds = grind_feel(view), transition = transition_feel(view);
|
|
begin("Grinds", "How a grind is ridden and left. GRINDS has every row behind it.");
|
|
if (button("skate.##grind", grinds == 0, "Every grind setting and switch is left to the game.")) trainer_command(menu, callbacks, "grind feel skate");
|
|
if (button("Skate 3##grind", grinds == 1, "The left stick alone turns the board on a one-truck grind, the right stick tips and pops, a pop goes sideways the way the left stick is held, "
|
|
"and what skate. added to leaving a grind is off.")) trainer_command(menu, callbacks, "grind feel skate3");
|
|
if (grinds == 2) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("your own mix");
|
|
}
|
|
begin("Transition", "Bowls, quarter pipes, vert and spines. TRANSITION has every row behind it.");
|
|
if (button("skate.##transition", transition == 0, "Everything on transition is left to the game.")) trainer_command(menu, callbacks, "transition feel skate");
|
|
if (button("Skate 3##transition", transition == 1, "Lean into the ramp on a coping grind, roll off the coping at your angle, no hang-ups coming back in, handplants that come back in "
|
|
"high on the wall, with Skate 3's coping timers.")) trainer_command(menu, callbacks, "transition feel skate3");
|
|
if (transition == 2) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("your own mix");
|
|
}
|
|
begin("Tricklining", "What Skate 3's tricklining had beyond its tuning. TRICKLINING has every row behind it.");
|
|
if (button("Skate 3's extras", view.revert_boost > 0, "The board bending boost at x 1, heavier revert and powerslide friction and higher footplants."))
|
|
trainer_command(menu, callbacks, view.revert_boost > 0 ? "trickline extras off" : "trickline extras on");
|
|
begin("Styles", "The built-in presets. Press one to put it on, press it again to take it off. Each also has a dial in its section.");
|
|
for (const auto &preset : view.presets) {
|
|
if (!preset.builtin || preset.name == "Stock" || preset.name.starts_with("Skate 3") || preset.name.starts_with("Transition:")) continue;
|
|
if (button(preset.name.c_str(), preset.active, preset.note.c_str()))
|
|
trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name);
|
|
}
|
|
begin("Yours", "The presets you saved or imported. Save, share and delete them in the row above the tabs.");
|
|
bool own = false;
|
|
for (const auto &preset : view.presets) {
|
|
if (preset.builtin) continue;
|
|
own = true;
|
|
ImGui::PushID(preset.name.c_str());
|
|
if (button(preset.name.c_str(), preset.active, preset.active ? "On. Press to put what it set back to the game's own." : "Press to load it on top of what is set now."))
|
|
trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name);
|
|
ImGui::PopID();
|
|
}
|
|
if (!own) ImGui::TextDisabled("none yet: set the game up, then name it and Save in the row above the tabs");
|
|
ImGui::EndDisabled();
|
|
}
|
|
card_end(p);
|
|
}
|
|
// The cheats and the addons that change how the skater plays (Skater > Cheats, Addons > Grind Exits, Board Break): listed in
|
|
// QUICK SETTINGS, named on the MODIFIED line and turned back by RESET EVERYTHING like the trainer's own values. Road Rash and
|
|
// Hall of Meat only show bails, so they never count.
|
|
struct CheatLine {
|
|
std::string name, value; // value: what it is set to, empty for a plain switch
|
|
int addon{-1}; // where it lives: an Addons page, else Skater > Cheats
|
|
DebugRequest off{}; // how Reset puts it back: a debug request, or (with `console`) a command
|
|
std::string console;
|
|
};
|
|
std::vector<CheatLine> cheat_lines(const Model &model) {
|
|
std::vector<CheatLine> lines;
|
|
const auto &debug = model.debug;
|
|
if (debug.noclip) lines.push_back({"Noclip", {}, -1, {DebugAction::set_noclip, false}});
|
|
if (debug.no_bail) lines.push_back({"No Bail", {}, -1, {DebugAction::set_no_bail, false}});
|
|
if (debug.dark_pop) lines.push_back({"Dark Pop", {}, -1, {DebugAction::set_dark_pop_enabled, false}});
|
|
// A boost's stock speed is 0: off (InteractiveDebug).
|
|
const auto boost = [&](const char *name, float speed, DebugAction action) {
|
|
if (speed > 1e-3f) lines.push_back({name, std::format("+{:.1f}", speed), -1, {action, false, 0.0f}});
|
|
};
|
|
boost("Forward boost", debug.forward_velocity_speed, DebugAction::set_forward_velocity_speed);
|
|
boost("Up boost", debug.up_velocity_speed, DebugAction::set_up_velocity_speed);
|
|
boost("Off-board up boost", debug.offboard_up_velocity_speed, DebugAction::set_offboard_up_velocity_speed);
|
|
if (debug.revert_boost)
|
|
lines.push_back({"Revert Boost", std::format("x{:.1f}", debug.revert_boost_speed), -1, {DebugAction::set_revert_boost_enabled, false}});
|
|
if (const auto &grind = model.grind_exit; grind.available && grind.enabled) {
|
|
std::string value = grind.toe_heel ? "Toe / Heel" : "Left / Right";
|
|
if (grind.toe_heel) value += grind.stance == 2 ? ", goofy" : grind.stance == 1 ? ", regular" : ", no stance";
|
|
value += std::format(", pop-out {}%", grind.strength);
|
|
lines.push_back({"Grind exit triggers", value, addon_grind_exits, {}, "grindexit exits off"});
|
|
}
|
|
if (const auto &bb = model.board_break; bb.available && bb.enabled)
|
|
lines.push_back({"Board Break", std::format("break {}%, grip tape {}%, wear {}%, worn {:.0f}%", bb.sensitivity, bb.joined, bb.wear_rate, bb.worn * 100.0f),
|
|
addon_board_break, {}, "boardbreak 0"});
|
|
return lines;
|
|
}
|
|
void cheat_off(SkateMenu &menu, const CallbacksV3 &callbacks, const CheatLine &line) {
|
|
if (!line.console.empty()) {
|
|
if (callbacks.queue_console_command) send_console(menu, callbacks, line.console);
|
|
} else if (callbacks.queue_debug) debug_request(menu, callbacks, line.off);
|
|
}
|
|
// The cheats and addons rows at the top of QUICK SETTINGS: what each is set to, where it lives, and Reset.
|
|
void cheat_rows(SkateMenu &menu, const CallbacksV3 &callbacks, const std::vector<CheatLine> &lines) {
|
|
if (lines.empty()) return;
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::PushFont(menu.bold);
|
|
ImGui::TextUnformatted("CHEATS AND ADDONS");
|
|
ImGui::PopFont();
|
|
for (std::size_t i = 0; i < lines.size(); ++i) {
|
|
const auto &line = lines[i];
|
|
ImGui::PushID(line.name.c_str());
|
|
if (ImGui::Button("Go to")) {
|
|
if (line.addon >= 0) open_page(menu, page_addons, line.addon);
|
|
else {
|
|
open_page(menu, page_skater);
|
|
menu.skater_tab = 2;
|
|
}
|
|
}
|
|
tip(line.addon >= 0 ? "Opens the addon's page." : "Opens Skater > Cheats.");
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(line.console.empty() ? !callbacks.queue_debug : !callbacks.queue_console_command);
|
|
if (ImGui::Button("Reset")) cheat_off(menu, callbacks, line);
|
|
ImGui::EndDisabled();
|
|
tip(line.off.action == DebugAction::set_forward_velocity_speed || line.off.action == DebugAction::set_up_velocity_speed ||
|
|
line.off.action == DebugAction::set_offboard_up_velocity_speed
|
|
? "Back to 0: off, as the game shipped."
|
|
: "Turns it off.");
|
|
ImGui::SameLine();
|
|
ImGui::AlignTextToFramePadding();
|
|
if (line.value.empty()) ImGui::Text("%s: on", line.name.c_str());
|
|
else ImGui::Text("%s: %s", line.name.c_str(), line.value.c_str());
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::Separator();
|
|
}
|
|
std::string quick_id(std::string_view id) {
|
|
std::string result = "qh:" + std::string(id);
|
|
for (auto &c : result)
|
|
if (c == ' ') c = '_';
|
|
return result;
|
|
}
|
|
// Everything that is changed right now, in one list: change it here, hide a row that is done with, or go to where it lives.
|
|
void quick_card(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, bool can_edit) {
|
|
constexpr std::uint8_t order[]{trainer::topic_game, trainer::topic_pop, trainer::topic_speed, trainer::topic_bails, trainer::topic_grinds, trainer::topic_trickline,
|
|
trainer::topic_transition, trainer::topic_pumping, trainer::topic_foot, trainer::topic_none};
|
|
// Once per snapshot: what is changed, section by section.
|
|
if (p.quick_revision != view.revision) {
|
|
p.quick_revision = view.revision;
|
|
p.quick_all.clear();
|
|
std::vector<std::string> ids;
|
|
for (const auto topic : order) {
|
|
for (std::size_t i = 0; i < view.presets.size(); ++i)
|
|
if (const auto &preset = view.presets[i]; preset.builtin && preset.dial && preset.topic == topic && std::abs(preset.factor - 1.0) > 1e-4) {
|
|
p.quick_all.push_back({1, topic, i});
|
|
ids.push_back(quick_id("dial:" + preset.name));
|
|
}
|
|
for (std::size_t i = 0; i < view.setups.size(); ++i)
|
|
if (view.setups[i].topic == topic) {
|
|
p.quick_all.push_back({2, topic, i});
|
|
ids.push_back(quick_id(view.setups[i].key));
|
|
}
|
|
for (std::size_t i = 0; i < view.rows.size(); ++i)
|
|
if (const auto &row = view.rows[i]; row.touched && (row.topic < trainer::topic_count ? row.topic : 0) == topic) {
|
|
p.quick_all.push_back({3, topic, i});
|
|
ids.push_back(quick_id(row.id));
|
|
}
|
|
}
|
|
// A hidden row that has gone back to the game's own is forgotten: changed again, it shows again.
|
|
for (const auto &[id, hidden] : view.folds)
|
|
if (hidden && id.starts_with("qh:") && std::ranges::find(ids, id) == ids.end()) {
|
|
set_fold(id, false);
|
|
trainer_command(menu, callbacks, std::format("fold {} 0", id));
|
|
}
|
|
}
|
|
const auto id_of = [&](const Page::QuickItem &item) {
|
|
return item.kind == 1 ? quick_id("dial:" + view.presets[item.index].name) : item.kind == 2 ? quick_id(view.setups[item.index].key) : quick_id(view.rows[item.index].id);
|
|
};
|
|
const auto hide = [&](const std::string &id, bool hidden) {
|
|
set_fold(id, hidden);
|
|
trainer_command(menu, callbacks, std::format("fold {} {}", id, hidden ? 1 : 0));
|
|
};
|
|
// What is shown: the rows not hidden, each section under a line of its own.
|
|
std::vector<Page::QuickItem> shown;
|
|
std::size_t hidden{};
|
|
int last = -1;
|
|
for (const auto &item : p.quick_all) {
|
|
if (item.index >= (item.kind == 1 ? view.presets.size() : item.kind == 2 ? view.setups.size() : view.rows.size())) continue;
|
|
if (fold_open(id_of(item), false)) {
|
|
++hidden;
|
|
continue;
|
|
}
|
|
if (last != item.topic) shown.push_back({0, item.topic, 0});
|
|
last = item.topic;
|
|
shown.push_back(item);
|
|
}
|
|
// Counted as the section's bar counts: a dial's row stands for values that have rows of their own.
|
|
const auto changed = static_cast<std::size_t>(std::ranges::count_if(p.quick_all, [](const Page::QuickItem &item) { return item.kind != 1; }));
|
|
const auto cheats = cheat_lines(model);
|
|
const auto subtitle = p.quick_all.empty() && cheats.empty()
|
|
? std::string("Nothing is changed")
|
|
: std::format("{} changed{}", changed + cheats.size(), hidden ? std::format(", {} hidden", hidden) : std::string{});
|
|
if (card(menu, p, "quick", "QUICK SETTINGS", subtitle.c_str())) {
|
|
cheat_rows(menu, callbacks, cheats);
|
|
if (p.quick_all.empty() && !cheats.empty()) {
|
|
// Only cheats and addons are on: their rows above are the whole list.
|
|
} else if (p.quick_all.empty()) {
|
|
note("Everything you change shows up here, whatever section it is in: pick a preset above, move a dial or set a value below. Change it again here, hide the "
|
|
"rows you are done with, or press Go to for where a row lives and what is around it.");
|
|
} else {
|
|
const auto go = [&](const Page::QuickItem &item) {
|
|
p.scroll_to = item.topic;
|
|
p.open_cards = item.topic;
|
|
p.flash.clear();
|
|
if (item.kind == 3) {
|
|
const auto &row = view.rows[item.index];
|
|
p.flash = row.id;
|
|
p.flash_until = ImGui::GetTime() + 4.0;
|
|
p.flash_scroll = true;
|
|
if (!item.topic) {
|
|
// EVERY VALUE: its group, so the values beside it are the related ones.
|
|
p.only_changed = false;
|
|
const auto found = std::ranges::find(view.groups, row.group);
|
|
p.group = found == view.groups.end() ? 1 : static_cast<int>(found - view.groups.begin()) + 2;
|
|
}
|
|
}
|
|
};
|
|
if (hidden) {
|
|
if (ImGui::SmallButton(std::format("Show the {} hidden", hidden).c_str()))
|
|
for (const auto &item : p.quick_all)
|
|
if (const auto id = id_of(item); fold_open(id, false)) hide(id, false);
|
|
tip("Brings back every row you hid.");
|
|
ImGui::SameLine();
|
|
}
|
|
if (!shown.empty()) {
|
|
if (ImGui::SmallButton("Hide all")) {
|
|
for (const auto &item : p.quick_all)
|
|
if (const auto id = id_of(item); !fold_open(id, false)) hide(id, true);
|
|
}
|
|
tip("Hides every row here. Nothing is changed by hiding: the rows stay set, and each comes back by itself the next time it changes from the game's own.");
|
|
ImGui::SameLine();
|
|
}
|
|
ImGui::TextDisabled("Hide: off this list, still set. Go to: where it lives. Reset: back to the game's own.");
|
|
const float hide_width = ImGui::CalcTextSize("Hide").x + ImGui::GetStyle().FramePadding.x * 2, go_width = ImGui::CalcTextSize("Go to").x + ImGui::GetStyle().FramePadding.x * 2;
|
|
p.in_quick = true;
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(static_cast<int>(shown.size()), ImGui::GetFrameHeightWithSpacing());
|
|
while (clipper.Step())
|
|
for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; ++n) {
|
|
const auto &item = shown[static_cast<std::size_t>(n)];
|
|
ImGui::PushID(n);
|
|
if (item.kind == 0) {
|
|
const auto &topic = trainer::topics[item.topic];
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::PushFont(menu.bold);
|
|
if (item.topic) ImGui::TextUnformatted(topic.title.data(), topic.title.data() + topic.title.size());
|
|
else ImGui::TextUnformatted("OTHER VALUES");
|
|
ImGui::PopFont();
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Open section")) {
|
|
p.scroll_to = item.topic;
|
|
p.flash.clear();
|
|
}
|
|
tip("Opens this section and scrolls to it.");
|
|
} else {
|
|
if (ImGui::Button("Hide", ImVec2(hide_width, 0))) hide(id_of(item), true);
|
|
tip("Takes this row off the list. It stays set. It comes back by itself the next time it changes from the game's own, or with \"Show hidden\".");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Go to", ImVec2(go_width, 0))) go(item);
|
|
tip("Opens the section this lives in and scrolls to it, with the related settings around it.");
|
|
ImGui::SameLine();
|
|
if (item.kind == 1) {
|
|
ImGui::BeginDisabled(!can_edit);
|
|
dial_row(menu, callbacks, p, view.presets[item.index]);
|
|
ImGui::EndDisabled();
|
|
} else if (item.kind == 2) {
|
|
setup_row(menu, callbacks, p, view, view.setups[item.index]);
|
|
} else {
|
|
value_row(menu, callbacks, p, view, view.rows[item.index], !item.topic, true);
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
p.in_quick = false;
|
|
if (shown.empty()) note("Every changed row is hidden.");
|
|
}
|
|
}
|
|
card_end(p);
|
|
}
|
|
|
|
// ---- the Tune tab: one page of sections ----------------------------------------------------------
|
|
// The bar that opens and closes one section; `summary` says what is changed inside.
|
|
bool topic_header(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, std::uint8_t topic, const std::string &summary, bool open_by_default) {
|
|
const auto &one = trainer::topics[topic];
|
|
const std::string id = "section-" + std::string(one.key);
|
|
bool open = fold_open(id, open_by_default);
|
|
if (p.scroll_to == topic) {
|
|
if (!open) set_fold(id, open = true);
|
|
ImGui::SetScrollHereY(0.0f);
|
|
p.scroll_to = -1;
|
|
}
|
|
const auto at = ImGui::GetCursorScreenPos();
|
|
const float height = ImGui::GetFrameHeight() + px(6), width = ImGui::GetContentRegionAvail().x;
|
|
ImGui::PushID(id.c_str());
|
|
const bool pressed = ImGui::InvisibleButton("##section", ImVec2(width, height));
|
|
const bool hovered = ImGui::IsItemHovered();
|
|
ImGui::PopID();
|
|
if (pressed) {
|
|
set_fold(id, open = !open);
|
|
trainer_command(menu, callbacks, std::format("fold {} {}", id, open ? 1 : 0));
|
|
}
|
|
auto *draw = ImGui::GetWindowDrawList();
|
|
draw->AddRectFilled(at, ImVec2(at.x + width, at.y + height), hovered ? skate_theme::tile_light : skate_theme::tile_grey);
|
|
draw->AddRectFilled(at, ImVec2(at.x + px(4), at.y + height), skate_theme::blue);
|
|
fold_arrow(ImVec2(at.x + px(10), at.y), height, open, IM_COL32(255, 255, 255, 255));
|
|
const float text_y = at.y + (height - ImGui::GetFontSize()) * 0.5f;
|
|
ImGui::SetCursorScreenPos(ImVec2(at.x + px(16) + ImGui::GetFontSize() * 0.9f, text_y));
|
|
ImGui::PushFont(menu.bold);
|
|
ImGui::TextUnformatted(one.title.data(), one.title.data() + one.title.size());
|
|
ImGui::PopFont();
|
|
const float summary_width = summary.empty() ? 0.0f : ImGui::CalcTextSize(summary.c_str()).x + px(20);
|
|
ImGui::SameLine(0, px(12));
|
|
if (ImGui::GetCursorScreenPos().x + ImGui::CalcTextSize(one.about.data(), one.about.data() + one.about.size()).x < at.x + width - summary_width)
|
|
ImGui::TextDisabled("%.*s", static_cast<int>(one.about.size()), one.about.data());
|
|
else ImGui::NewLine();
|
|
if (!summary.empty()) {
|
|
ImGui::SetCursorScreenPos(ImVec2(at.x + width - summary_width + px(10), text_y));
|
|
ImGui::PushStyleColor(ImGuiCol_Text, skate_theme::blue);
|
|
ImGui::TextUnformatted(summary.c_str());
|
|
ImGui::PopStyleColor();
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(at.x, at.y + height));
|
|
ImGui::Dummy(ImVec2(0, px(open ? 8.0f : 4.0f)));
|
|
return open;
|
|
}
|
|
// The built-in presets of one section: a dial each, then the ones that are plain switches.
|
|
void dials_card(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, std::uint8_t topic, bool can_edit) {
|
|
const auto shown = [&](const trainer::PresetRow &preset) {
|
|
return preset.builtin && preset.topic == topic && preset.name != "Stock" && !preset.name.starts_with("Skate 3") && !preset.name.starts_with("Transition:");
|
|
};
|
|
if (std::ranges::none_of(view.presets, shown)) return;
|
|
const std::string id = "dials-" + std::string(trainer::topics[topic].key);
|
|
if (card(menu, p, id.c_str(), "DIALS AND SWITCHES", "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 (shown(preset) && preset.dial) dial_row(menu, callbacks, p, preset);
|
|
bool first = true;
|
|
for (const auto &preset : view.presets) {
|
|
if (!shown(preset) || preset.dial) 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();
|
|
}
|
|
card_end(p);
|
|
}
|
|
// A section's named values that no other card of it shows, one at a time.
|
|
void fine_card(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, std::uint8_t topic) {
|
|
const auto &rows = p.topic_rows[topic];
|
|
if (std::ranges::none_of(rows, [&](std::size_t i) { return row_works(view.rows[i]); })) return;
|
|
const std::string id = "fine-" + std::string(trainer::topics[topic].key);
|
|
if (card(menu, p, id.c_str(), "FINE TUNING", "One value at a time. Blue = changed. The box on the left locks a value.")) {
|
|
for (const auto i : rows)
|
|
if (row_works(view.rows[i])) value_row(menu, callbacks, p, view, view.rows[i], false, true);
|
|
}
|
|
card_end(p);
|
|
}
|
|
// The whole table: every tuning value the trainer knows, by the game's own groups.
|
|
void every_value(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
|
|
// 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::SameLine();
|
|
ImGui::BeginDisabled(!view.editable || !callbacks.queue_console_command || !view.touched);
|
|
if (ImGui::Button("Reset values")) {
|
|
clear_edits(p);
|
|
trainer_command(menu, callbacks, "reset all");
|
|
}
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) wrapped_tooltip("Every tuning value back to the game's own. Locked values stay, and so do the trick, grind and transition settings.");
|
|
ImGui::EndDisabled();
|
|
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.");
|
|
filter_rows(p, view);
|
|
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.group <= 1 || p.only_changed;
|
|
ImGuiListClipper clipper;
|
|
clipper.Begin(static_cast<int>(p.shown.size()));
|
|
if (p.flash_scroll && !p.flash.empty())
|
|
for (std::size_t i = 0; i < p.shown.size(); ++i)
|
|
if (view.rows[p.shown[i]].id == p.flash) clipper.IncludeItemByIndex(static_cast<int>(i));
|
|
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, false);
|
|
if (p.shown.empty()) note("Nothing matches.");
|
|
}
|
|
// What one section holds. Cards that live in the two long functions are picked out of them by id.
|
|
void topic_body(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, std::uint8_t topic, bool can_edit) {
|
|
const auto from_trickline = [&](std::initializer_list<std::string_view> ids) {
|
|
p.pick.assign(ids);
|
|
trickline_section(menu, model, callbacks, p, view);
|
|
p.pick.clear();
|
|
};
|
|
const auto from_transition = [&](std::initializer_list<std::string_view> ids) {
|
|
p.pick.assign(ids);
|
|
transition_section(menu, callbacks, p, view);
|
|
p.pick.clear();
|
|
};
|
|
switch (topic) {
|
|
case trainer::topic_game:
|
|
presets_card(menu, callbacks, p, view, can_edit);
|
|
quick_card(menu, model, callbacks, p, view, can_edit);
|
|
return; // its rows are the other sections'
|
|
case trainer::topic_pop:
|
|
dials_card(menu, callbacks, p, view, topic, can_edit);
|
|
ImGui::BeginDisabled(!can_edit);
|
|
trick_heights(menu, callbacks, p, view, std::string{}, 1);
|
|
ImGui::EndDisabled();
|
|
from_trickline({"trick-pops", "trick-spins"});
|
|
break;
|
|
case trainer::topic_grinds:
|
|
from_trickline({"grind-feel", "grind-stick", "grind-exits"});
|
|
dials_card(menu, callbacks, p, view, topic, can_edit);
|
|
break;
|
|
case trainer::topic_trickline:
|
|
from_trickline({"trickline", "trick-revert"});
|
|
// Dark Pop is ReSkate's own switch (not a tuning value), so it is live even where the tuning is locked.
|
|
dark_pop_controls(menu, model, callbacks);
|
|
from_trickline({"trick-manuals"});
|
|
dials_card(menu, callbacks, p, view, topic, can_edit);
|
|
break;
|
|
case trainer::topic_transition:
|
|
from_transition({"transition-feel", "transition-coping", "transition-reentry", "transition-plant", "transition-lip", "transition-vert"});
|
|
break;
|
|
case trainer::topic_pumping:
|
|
from_transition({"transition-pump"});
|
|
break;
|
|
case trainer::topic_foot:
|
|
dials_card(menu, callbacks, p, view, topic, can_edit);
|
|
ImGui::BeginDisabled(!can_edit);
|
|
trick_heights(menu, callbacks, p, view, std::string{}, 2);
|
|
ImGui::EndDisabled();
|
|
break;
|
|
default:
|
|
dials_card(menu, callbacks, p, view, topic, can_edit);
|
|
break;
|
|
}
|
|
fine_card(menu, callbacks, p, view, topic);
|
|
}
|
|
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;
|
|
}
|
|
const bool can_edit = view.editable && callbacks.queue_console_command != nullptr;
|
|
constexpr std::uint8_t order[]{trainer::topic_game, trainer::topic_pop, trainer::topic_speed, trainer::topic_bails, trainer::topic_grinds, trainer::topic_trickline,
|
|
trainer::topic_transition, trainer::topic_pumping, trainer::topic_foot, trainer::topic_none};
|
|
// One search for the whole tab: dials, switches, trick sliders and every value.
|
|
{
|
|
const float open_all = ImGui::CalcTextSize("Open all").x + ImGui::GetStyle().FramePadding.x * 2, fold_all = ImGui::CalcTextSize("Fold all").x + ImGui::GetStyle().FramePadding.x * 2;
|
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - open_all - fold_all - ImGui::GetStyle().ItemSpacing.x * 2);
|
|
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 in every section at once. Clear the box to go back to the sections.");
|
|
const auto all = [&](bool open) {
|
|
for (const auto topic : order) {
|
|
const std::string id = "section-" + std::string(trainer::topics[topic].key);
|
|
set_fold(id, open);
|
|
trainer_command(menu, callbacks, std::format("fold {} {}", id, open ? 1 : 0));
|
|
}
|
|
};
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Open all", ImVec2(open_all, 0))) all(true);
|
|
tip("Opens every section.");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Fold all", ImVec2(fold_all, 0))) all(false);
|
|
tip("Closes every section. A click on a section's bar opens it; a click on a card's title folds that card.");
|
|
}
|
|
const auto words = lower(p.search.data());
|
|
place_rows(p, view);
|
|
if (!words.empty()) {
|
|
// Searching looks through everything at once, whatever section it is in.
|
|
const auto listed = [&](const trainer::PresetRow &preset) {
|
|
return preset.builtin && preset.name != "Stock" && !preset.name.starts_with("Skate 3") && !preset.name.starts_with("Transition:") &&
|
|
contains_words(lower(preset.title + " " + preset.name + " " + preset.note), words);
|
|
};
|
|
if (std::ranges::any_of(view.presets, listed)) {
|
|
begin_card(menu, "dials-found", "DIALS AND SWITCHES", "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 (listed(preset) && preset.dial) dial_row(menu, callbacks, p, preset);
|
|
bool first = true;
|
|
for (const auto &preset : view.presets) {
|
|
if (!listed(preset) || preset.dial) continue;
|
|
if (!first) ImGui::SameLine();
|
|
first = false;
|
|
preset_switch(menu, callbacks, preset);
|
|
}
|
|
ImGui::EndDisabled();
|
|
end_card();
|
|
}
|
|
ImGui::BeginDisabled(!can_edit);
|
|
trick_heights(menu, callbacks, p, view, words, 0);
|
|
ImGui::EndDisabled();
|
|
trainer::note_class_list_shown();
|
|
filter_rows(p, view);
|
|
ImGui::TextDisabled("%zu values match", p.shown.size());
|
|
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)]], true, false);
|
|
if (p.shown.empty()) note("No value matches. Grind, transition and bending settings are in their sections: clear the box to see them.");
|
|
return;
|
|
}
|
|
if (!p.flash.empty() && ImGui::GetTime() > p.flash_until) p.flash.clear();
|
|
p.flash_y = -1;
|
|
for (const auto topic : order) {
|
|
const auto &one = trainer::topics[topic];
|
|
const std::size_t all = view.touched + view.setups_changed;
|
|
const std::size_t count = topic == trainer::topic_game ? all : topic ? view.changed[topic] : view.touched;
|
|
const auto summary = !count ? std::string{} : topic == trainer::topic_game ? std::format("{} changed in all", count) : topic ? std::format("{} changed", count)
|
|
: std::format("{} values changed in all", count);
|
|
p.current_topic = topic;
|
|
// A section with something changed in it starts open, so nothing that is on can hide. The first one is the way in.
|
|
if (!topic_header(menu, callbacks, p, topic, summary, topic == trainer::topic_game || (topic != trainer::topic_none && count > 0))) continue;
|
|
ImGui::Indent(px(10));
|
|
ImGui::PushID(static_cast<int>(topic));
|
|
if (!topic) {
|
|
every_value(menu, callbacks, p, view);
|
|
} else {
|
|
topic_body(menu, model, callbacks, p, view, topic, can_edit);
|
|
if (topic != trainer::topic_game) {
|
|
ImGui::BeginDisabled(!can_edit || !view.changed[topic]);
|
|
if (ImGui::SmallButton(std::format("Reset {}", std::string(one.title)).c_str())) {
|
|
clear_edits(p);
|
|
trainer_command(menu, callbacks, std::format("reset section {}", one.key));
|
|
}
|
|
ImGui::EndDisabled();
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
|
wrapped_tooltip("%s", "Puts everything this section shows back to the game's own: its values, dials and settings. Locked values stay. "
|
|
"Nothing in the other sections is touched.");
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
ImGui::Unindent(px(10));
|
|
ImGui::Dummy(ImVec2(0, px(6)));
|
|
}
|
|
p.current_topic = p.open_cards = -1;
|
|
// "Go to" a value: once it has been drawn, the page scrolls to it (it is inside a card, which does not scroll).
|
|
if (p.flash_scroll && p.flash_y >= 0) {
|
|
ImGui::SetScrollY(std::max(0.0f, ImGui::GetScrollY() + p.flash_y - ImGui::GetWindowPos().y - ImGui::GetWindowHeight() * 0.3f));
|
|
p.flash_scroll = false;
|
|
}
|
|
}
|
|
|
|
// The trick sliders. `part` 0: every one that matches the search words; 1: the TRICKS card of POP AND TRICKS; 2: the
|
|
// jump on foot (ON FOOT). Pump power is on PUMPING and the board bending boost on TRICKLINING.
|
|
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words, int part) {
|
|
static constexpr const char *advanced_label = "Advanced trick speed (a speed for each flip trick: kickflip, heelflip, 360 flip, varial, shuvit, hardflip, laser flip, impossible)";
|
|
static constexpr const char *labels[]{"Gravity", "Flip trick speed", advanced_label, "Pump power (pumping transitions)", "Manual pop height", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height",
|
|
"Board bending boost (revert speed)"};
|
|
const auto wanted = [&](const char *label) {
|
|
const std::string_view name(label);
|
|
if (part == 2) return name == "Off-board jump height";
|
|
if (part == 1) return name != "Off-board jump height" && !name.starts_with("Pump power") && !name.starts_with("Board bending");
|
|
return contains_words(lower(std::string(label) + " tricks"), words);
|
|
};
|
|
if (std::ranges::none_of(labels, wanted)) return;
|
|
if (card(menu, p, part == 2 ? "trick-foot" : part == 1 ? "trick-heights" : "trick-found", part == 2 ? "JUMP" : "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, bool always = false) {
|
|
if (!always && !wanted(label)) return false;
|
|
field(menu, label, help);
|
|
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 true;
|
|
}
|
|
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;
|
|
};
|
|
if (slider("Gravity", "gravity", view.gravity, p.gravity_edit, p.gravity_editing,
|
|
"Gravity in the air, on the board and on foot. 1 is the game's own. At 0.5 the same pop goes twice as high and hangs twice as long; at 2 you "
|
|
"drop twice as fast. It acts in the air only: rolling down a hill, grinds and a bailed skater are the game's own. The game aims an air off a "
|
|
"ramp back at the ramp with its own gravity, so with another gravity a vert air can come down somewhere else. It takes hold at once.",
|
|
3.0f) && view.gravity != 1.0f && !view.gravity_live)
|
|
ImGui::TextDisabled("On foot this game build keeps its own gravity: the setting acts on the board only.");
|
|
slider("Manual pop height", "manual_height", view.manual_height, p.manual_edit, p.manual_editing,
|
|
"How high you pop out of a manual or a nose manual. 1 is the game's own (lower than a riding ollie). Ollie height raises both.");
|
|
slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing,
|
|
"How fast the board turns in every flip trick. It does not change how high you pop. Below 1 flips are slower, above 1 faster. A slow "
|
|
"flip is slow from the start and is not hurried round before you land: with too little air you land on a board that is still turning. "
|
|
"It takes about 15 seconds to take hold after every level load: the trainer has to find the game's flip animations again first.",
|
|
3.0f);
|
|
if (!view.flip_live && (view.flip_speed != 1.0f || view.flip_advanced || view.flip_flick) && wanted("Flip trick speed"))
|
|
ImGui::TextDisabled("Finding the flip animations: flip speeds take hold about 15 seconds after a level loads.");
|
|
if (wanted("Flip trick speed") || wanted("Slow flips pop higher") || wanted("Flick tricks at full speed")) {
|
|
bool pop = view.flip_pop;
|
|
if (ImGui::Checkbox("Slow flips pop higher, so they land clean", &pop)) trainer_command(menu, callbacks, std::format("option flip_pop {}", pop ? 1 : 0));
|
|
if (ImGui::IsItemHovered())
|
|
wrapped_tooltip("%s", "A slowed flip needs more time than a normal pop gives: without this the game holds the landing back and the "
|
|
"skater sinks before riding away. With it every pop into a slowed flip trick (riding, out of a manual, out of a "
|
|
"grind or slide) goes at least as high as the trick needs: about 0.9 m at x 0.5 and 2.9 m at x 0.25, where the "
|
|
"game's own pop is 0.4 m. Ollies, nollies and boned ollies are slowed and popped the same way while this is on "
|
|
"(without it the flip trick speed leaves them alone).");
|
|
bool flick = view.flip_flick;
|
|
if (ImGui::Checkbox("Flick tricks at full speed", &flick)) trainer_command(menu, callbacks, std::format("option flip_flick {}", flick ? 1 : 0));
|
|
if (ImGui::IsItemHovered())
|
|
wrapped_tooltip("%s", "The game turns the board slower the slower the stick was flicked, so tricks with a long stick motion (front foot "
|
|
"and nollie impossibles, 360 pop shuvits) turn slower than a kickflip at the same flip trick speed and land "
|
|
"half way round. With this every flip trick turns as if the flick was a full one: with 'Slow flips pop higher' "
|
|
"they all get the same pop and all come round in it. It works at every flip trick speed. With it on a slow "
|
|
"flick no longer gives a slow flip.");
|
|
}
|
|
if (wanted(advanced_label)) {
|
|
bool on = view.flip_advanced;
|
|
if (ImGui::Checkbox("Advanced trick speed", &on)) trainer_command(menu, callbacks, std::format("option flip_advanced {}", on ? 1 : 0));
|
|
tip("Gives every flip trick a speed of its own, on top of the flip trick speed above: a kickflip at 1.0 and a 360 flip at 0.6, say. "
|
|
"A trick turns at the speed above times its own. Nollie tricks use their regular trick's speed (the nollie impossibles have their own). The ollie's speed only acts "
|
|
"with 'Slow flips pop higher' on. Pop height is not changed. "
|
|
"Unticked, the speeds below are kept but not used. Like the speed above, they take hold about 15 seconds after every level load.");
|
|
if (view.flip_advanced_blocked) ImGui::TextDisabled("Off in this multiplayer game: the host's rules apply here.");
|
|
if (on && ImGui::TreeNodeEx("Speed of each flip trick", ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
for (std::size_t i = 0; i < trainer::flip_tricks.size(); ++i) {
|
|
const auto &trick = trainer::flip_tricks[i];
|
|
const std::string label(trick.label), option(trick.key);
|
|
slider(label.c_str(), option.c_str(), view.flip_trick[i], p.trick_edit[i], p.trick_editing[i],
|
|
"How fast the board turns in this trick, times the flip trick speed above. 1 leaves it at that speed.", 3.0f, false, true);
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
}
|
|
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 (slider("Pump power (pumping transitions)", "pump_power", view.pump_power, p.pump_edit, p.pump_editing,
|
|
"How much speed pumping a transition gives. 1 is the game's own; 2 gives about one more of the game's pumps on top of each "
|
|
"of yours, 10 about nine more, and under 1 takes speed away. The game still decides when you are pumping: the extra comes "
|
|
"for as long as it counts you as pumping. It is found in the game about 15 seconds after a level loads.")) {
|
|
if (view.pump_blocked) ImGui::TextDisabled("Off in this multiplayer game: the host's rules apply here.");
|
|
else if (view.pump_power != 1.0f && !view.pump_live) ImGui::TextDisabled("Finding the game's pumping in memory: it acts about 15 seconds after a level loads.");
|
|
}
|
|
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. TRICKLINING 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 (part == 1) {
|
|
if (ImGui::Button("Reset tricks")) {
|
|
clear_edits(p);
|
|
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.");
|
|
}
|
|
card_end(p);
|
|
}
|
|
|
|
void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
|
|
const auto telemetry = trainer::telemetry();
|
|
if (card(menu, p, "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", "How fast the whole game runs: 1x is normal, 0.5x half speed. Type a number for anything past the slider.");
|
|
auto value = static_cast<float>(current);
|
|
if (::dingosdk::overlay::menu::slider_float("##game-speed", &value, 0.05f, 2.0f, current == 0.0 ? "paused" : "%.2fx") && value >= 0)
|
|
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<double, 5> 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<std::size_t>(pressed - 1)]);
|
|
note("The jump read-out measures real time, so it reads low while the game is slowed.");
|
|
}
|
|
}
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "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");
|
|
{
|
|
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", "How long after a bail before you are put back at the marker, in seconds.");
|
|
float delay = view.return_delay;
|
|
::dingosdk::overlay::menu::slider_float("##return-delay", &delay, 0.0f, 10.0f, "%.1f s");
|
|
if (!ImGui::IsItemActive()) tip("How long after a bail before you are put back at the marker. Type a number for longer than the slider.");
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option return_delay {:.2f}", std::max(0.0f, 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));
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "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");
|
|
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();
|
|
}
|
|
card_end(p);
|
|
}
|
|
|
|
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.
|
|
// 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 Model &, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
|
|
const bool can_edit = view.editable && callbacks.queue_console_command != nullptr;
|
|
ImGui::BeginDisabled(!can_edit);
|
|
if (card(menu, p, "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, heavier revert and powerslide friction (x 3 and x 1.5) and higher footplants (no comply and "
|
|
"boneless height x 1.5). First guesses: tune each of them below.", extras))
|
|
trainer_command(menu, callbacks, extras ? "trickline extras on" : "trickline extras off");
|
|
note("Turning it off puts back only what still holds these numbers: a value you have since moved stays yours. Blue = changed; Reset puts one value "
|
|
"back. Type in a box to go past a slider's end.");
|
|
}
|
|
card_end(p);
|
|
if (card(menu, p, "grind-feel", "GRINDS FEEL LIKE", "The game's own, or the way Skate 3 rode a grind")) {
|
|
{
|
|
{
|
|
// The two feels: every switch the game's own, or skate.'s own ways off a grind held off.
|
|
bool like_skate = true, like_skate3 = true;
|
|
for (std::size_t i = 0; i < trainer::grind_switches.size(); ++i) {
|
|
if (view.grind_mode[i] >= 0) like_skate = false;
|
|
if (view.grind_mode[i] != trainer::skate3_grind_mode(i)) like_skate3 = false;
|
|
}
|
|
for (std::size_t i = 0; i < trainer::grind_settings.size(); ++i) {
|
|
if (view.grind_setting[i] >= 0) like_skate = false;
|
|
if (view.grind_setting[i] != trainer::grind_settings[i].skate3) like_skate3 = false;
|
|
}
|
|
if (view.grind_push != 0.0f) like_skate = false;
|
|
if (std::abs(view.grind_push - 1.8f) > 0.005f) like_skate3 = false;
|
|
if (view.grind_range != 0.0f) like_skate = false;
|
|
if (view.grind_range == 0.0f) like_skate3 = false; // any range of your own keeps it Skate 3
|
|
field(menu, "Grinds feel like");
|
|
if (ImGui::RadioButton("skate.", like_skate)) trainer_command(menu, callbacks, "grind feel skate");
|
|
tip("Every grind setting and switch is left to the game.");
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("Skate 3", like_skate3)) trainer_command(menu, callbacks, "grind feel skate3");
|
|
tip("Turns off what skate. added to leaving a grind: leaning off without a pop, the sideways ollie, "
|
|
"exits onto another grind, double grinds, stronger and camera-relative exits, linear drive. The left stick alone turns the board on a "
|
|
"one-truck grind, the right stick tips it (5-0, nose grind) and pops, and a pop goes sideways the way the left stick is held. Change any "
|
|
"row on the two cards below afterwards to make your own mix.");
|
|
note("Skate 3 feel holds five of your game's own grind settings for as long as it is on. Your own settings are not changed: the game's "
|
|
"Settings menu keeps showing yours, and they are back the moment you pick skate. here. The left stick's turn needs an Xbox-style "
|
|
"(XInput) controller.");
|
|
}
|
|
}
|
|
}
|
|
card_end(p);
|
|
if (card(menu, p, "grind-stick", "STICK AND POPS ON A GRIND", "How far the left stick turns the board, and which way a pop goes")) {
|
|
{
|
|
field(menu, "Pop: side force");
|
|
float side = p.grind_side_editing ? p.grind_side_edit : view.grind_side;
|
|
const float side_box = px(64);
|
|
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - side_box - ImGui::GetStyle().ItemSpacing.x));
|
|
if (ImGui::SliderFloat("##grind-side", &side, 0.0f, 10.0f, "x %.2f")) {
|
|
p.grind_side_edit = side;
|
|
p.grind_side_editing = true;
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("grind side {:.2f}", p.grind_side_edit));
|
|
if (!ImGui::IsItemActive() && p.grind_side_editing && std::abs(view.grind_side - p.grind_side_edit) < 0.006f) p.grind_side_editing = false;
|
|
if (!ImGui::IsItemActive())
|
|
tip("Multiplies the part of a grind or slide pop that goes sideways off the edge, measured against the way you were travelling on it. "
|
|
"1 is the game's own, 0 pops straight along the edge. Type any number in the box: the slider's ends are not a limit.");
|
|
ImGui::SameLine();
|
|
float side_typed = view.grind_side;
|
|
ImGui::SetNextItemWidth(side_box);
|
|
if (ImGui::InputFloat("##grind-side-typed", &side_typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue) && side_typed >= 0) {
|
|
p.grind_side_editing = false;
|
|
trainer_command(menu, callbacks, std::format("grind side {}", side_typed));
|
|
}
|
|
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", "Type any multiplier and press Enter: the slider's end is not a limit.");
|
|
{
|
|
field(menu, "Pop: push with stick");
|
|
float push = p.grind_push_editing ? p.grind_push_edit : view.grind_push;
|
|
const float push_box = px(64);
|
|
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - push_box - ImGui::GetStyle().ItemSpacing.x));
|
|
if (ImGui::SliderFloat("##grind-push", &push, -5.0f, 5.0f, "%.2f m/s")) {
|
|
p.grind_push_edit = push;
|
|
p.grind_push_editing = true;
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("grind push {:.2f}", p.grind_push_edit));
|
|
if (!ImGui::IsItemActive() && p.grind_push_editing && std::abs(view.grind_push - p.grind_push_edit) < 0.006f) p.grind_push_editing = false;
|
|
if (!ImGui::IsItemActive())
|
|
tip("Skate 3's rule: a pop off a grind goes sideways the way the left stick is held, by this many m/s at full stick. Skate 3's own number "
|
|
"is 1.8; 0 is off. A negative number pushes the other way. Type any number in the box.");
|
|
ImGui::SameLine();
|
|
float push_typed = view.grind_push;
|
|
ImGui::SetNextItemWidth(push_box);
|
|
if (ImGui::InputFloat("##grind-push-typed", &push_typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue)) {
|
|
p.grind_push_editing = false;
|
|
trainer_command(menu, callbacks, std::format("grind push {}", push_typed));
|
|
}
|
|
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", "Type any speed and press Enter: the slider's ends are not a limit.");
|
|
}
|
|
{
|
|
// How far the stick turns the board on a grind: slider, and a box for any number.
|
|
struct Speed { const char *label, *key, *help; float view; float *edit; bool *editing; float low{0}, high{10}; const char *zero{"game's own"}; };
|
|
const Speed speeds[]{
|
|
{"Left stick turn range", "range",
|
|
"Skate 3's turn on a grind: the left stick alone turns the board, with Skate 3's own dead zone, curve and easing, up to this many degrees "
|
|
"at the end of the stick. The right stick's sideways push no longer turns it; its up and down still tips the board (5-0, nose grind) and "
|
|
"pops. 0 is off: skate.'s own turn, which follows where the right stick points. A negative number turns the other way.", view.grind_range,
|
|
&p.grind_range_edit, &p.grind_range_editing, -180.0f, 180.0f, "off"},
|
|
};
|
|
for (const auto &one : speeds) {
|
|
field(menu, one.label, one.help);
|
|
ImGui::PushID(one.key);
|
|
float value = *one.editing ? *one.edit : one.view;
|
|
const float box = px(64);
|
|
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - box - ImGui::GetStyle().ItemSpacing.x));
|
|
if (ImGui::SliderFloat("##speed", &value, one.low, one.high, value == 0.0f ? one.zero : "%.2f")) {
|
|
*one.edit = value;
|
|
*one.editing = true;
|
|
}
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("grind {} {:.2f}", one.key, *one.edit));
|
|
if (!ImGui::IsItemActive() && *one.editing && std::abs(one.view - *one.edit) < 0.006f) *one.editing = false;
|
|
if (!ImGui::IsItemActive()) tip(one.help);
|
|
ImGui::SameLine();
|
|
float typed = one.view;
|
|
ImGui::SetNextItemWidth(box);
|
|
if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue)) {
|
|
*one.editing = false;
|
|
trainer_command(menu, callbacks, std::format("grind {} {}", one.key, std::max(one.low, typed)));
|
|
}
|
|
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", "Type any speed and press Enter: the slider's end is not a limit.");
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
card_end(p);
|
|
if (card(menu, p, "grind-exits", "GRIND EXITS AND TRANSFERS", "Your game's own grind settings and switches, each left to the game or held off or on")) {
|
|
{
|
|
if (!p.grind_watching) {
|
|
trainer_command(menu, callbacks, "grind watch 1");
|
|
p.grind_watching = true;
|
|
}
|
|
note("GAME SETTINGS: the first six rows are settings of your own game (its Settings menu has them). Ticking one here holds it on or off for as long "
|
|
"as the trainer says so; your own setting is not changed and comes back with \"Game's own\", RESET EVERYTHING or leaving the game.");
|
|
// The game's own settings first: they change which grind code runs.
|
|
for (std::size_t i = 0; i < trainer::grind_settings.size(); ++i) {
|
|
const auto &one = trainer::grind_settings[i];
|
|
const std::string label(one.label), help = std::string(one.help) + " (The game's setting " + std::string(one.setting) + ", " +
|
|
(one.stock ? "on" : "off") + " by default.) Tick or untick to hold it there while the trainer runs; your own setting is not changed, and Game's own gives it back.";
|
|
field(menu, label.c_str());
|
|
ImGui::PushID(static_cast<int>(100 + i));
|
|
const bool forced = view.grind_setting[i] >= 0;
|
|
bool on = forced ? view.grind_setting[i] != 0 : view.grind_own[i] != 0;
|
|
if (ImGui::Checkbox("##on", &on)) trainer_command(menu, callbacks, std::format("grind {} {}", one.key, on ? "on" : "off"));
|
|
tip(help.c_str());
|
|
ImGui::SameLine();
|
|
if (forced) {
|
|
ImGui::TextUnformatted(view.grind_setting[i] ? "held on" : "held off");
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(yours: %s)", view.grind_own[i] ? "on" : "off");
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Game's own")) trainer_command(menu, callbacks, std::format("grind {} game", one.key));
|
|
} else {
|
|
ImGui::TextDisabled("%s", view.grind_own[i] ? "your game setting: on" : "your game setting: off");
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
for (std::size_t i = 0; i < trainer::grind_switches.size(); ++i) {
|
|
const auto &one = trainer::grind_switches[i];
|
|
const std::string label(one.label), help = std::string(one.help) + " (" + std::string(one.state) + ".) Game's own leaves it to the game; Off and On hold it there.";
|
|
field(menu, label.c_str());
|
|
ImGui::PushID(static_cast<int>(i));
|
|
if (one.read_only) {
|
|
ImGui::TextDisabled("%s", view.grind_live[i] < 0 ? "not in use yet" : view.grind_live[i] ? "happening now" : "not now");
|
|
tip(help.c_str());
|
|
ImGui::PopID();
|
|
continue;
|
|
}
|
|
// A tick box: what is in force now (held, or the game's own). A click holds it the other way.
|
|
const bool held = view.grind_mode[i] >= 0;
|
|
bool on = held ? view.grind_mode[i] != 0 : view.grind_live[i] > 0;
|
|
if (ImGui::Checkbox("##on", &on)) trainer_command(menu, callbacks, std::format("grind {} {}", one.key, on ? "on" : "off"));
|
|
tip(help.c_str());
|
|
ImGui::SameLine();
|
|
if (held) {
|
|
ImGui::TextUnformatted(view.grind_mode[i] ? "held on" : "held off");
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Game's own")) trainer_command(menu, callbacks, std::format("grind {} game", one.key));
|
|
} else {
|
|
ImGui::TextDisabled("%s", view.grind_live[i] < 0 ? "game's own (not in use yet)" : view.grind_live[i] ? "game's own: on" : "game's own: off");
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
note("Tick or untick a switch to hold it there; Game's own gives it back to the game. These are the game's names for its own switches: what each one "
|
|
"changes in play has not been measured yet. A switch has no value until the game first uses it (most appear "
|
|
"once you are on the board, the rest on the first grind), and they take hold about 15 seconds after a level loads.");
|
|
}
|
|
}
|
|
card_end(p);
|
|
|
|
// What each group of rows is for in a line, said once at the top of its card.
|
|
static constexpr const char *group_cards[][3]{
|
|
{"", "", ""},
|
|
{"trick-pops", "POPS",
|
|
"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. The stick "
|
|
"boost is the speed you can add by pushing the stick as you pop. Pops out of grinds are on GRINDS."},
|
|
{"trick-spins", "SPINS AND FLIPS",
|
|
"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."},
|
|
{"trick-manuals", "MANUALS AND STEERING",
|
|
"Manual balance (the three balance numbers are how hard the game corrects you), how far the board may tip, and how the board steers and settles."},
|
|
};
|
|
const auto rows = [&](std::size_t g) {
|
|
for (const auto i : p.trick_rows[g])
|
|
if (i < view.rows.size() && row_works(view.rows[i])) value_row(menu, callbacks, p, view, view.rows[i], false, true);
|
|
};
|
|
|
|
if (card(menu, p, "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, tip);
|
|
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);
|
|
}
|
|
card_end(p);
|
|
for (std::size_t g = 1; g < std::size(group_cards); ++g) {
|
|
if (std::ranges::none_of(p.trick_rows[g], [&](std::size_t i) { return i < view.rows.size() && row_works(view.rows[i]); })) continue;
|
|
if (card(menu, p, group_cards[g][0], group_cards[g][1])) {
|
|
note(group_cards[g][2]);
|
|
rows(g);
|
|
}
|
|
card_end(p);
|
|
}
|
|
ImGui::EndDisabled();
|
|
}
|
|
|
|
|
|
// Transition skating in one place: bowls, quarter pipes, vert and spines. The feel (the game's own or Skate 3's), the
|
|
// trainer's own help back into the ramp, the game's own settings and every tuning value that concerns a lip.
|
|
void transition_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);
|
|
// A slider with a box for any number beside it; `command` gets the number appended.
|
|
const auto number_row = [&](const char *label, const char *command, float value, float low, float high, const char *format, const char *help) {
|
|
ImGui::PushID(command);
|
|
field(menu, label, help);
|
|
const auto key = std::string("transition:") + 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", help);
|
|
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();
|
|
};
|
|
// The game's own settings shown on one card: a tick box each, forced or left to the game.
|
|
const auto settings = [&](std::uint8_t card) {
|
|
for (std::size_t i = 0; i < trainer::transition_settings.size(); ++i) {
|
|
const auto &one = trainer::transition_settings[i];
|
|
if (one.card != card) continue;
|
|
const std::string label(one.label), help = std::string(one.help) + " (The game's setting " + std::string(one.setting) + ", " +
|
|
(one.stock ? "on" : "off") + " by default.) A setting of your own game: ticking it here holds it there while the trainer runs. Your own "
|
|
"setting is not changed, and Game's own gives it back.";
|
|
field(menu, label.c_str());
|
|
ImGui::PushID(static_cast<int>(200 + i));
|
|
const bool forced = view.tr_setting[i] >= 0;
|
|
bool on = forced ? view.tr_setting[i] != 0 : view.tr_own[i] != 0;
|
|
if (ImGui::Checkbox("##on", &on)) trainer_command(menu, callbacks, std::format("transition {} {}", one.key, on ? "on" : "off"));
|
|
tip(help.c_str());
|
|
ImGui::SameLine();
|
|
if (forced) {
|
|
ImGui::TextUnformatted(view.tr_setting[i] ? "held on" : "held off");
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(yours: %s)", view.tr_own[i] ? "on" : "off");
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Game's own")) trainer_command(menu, callbacks, std::format("transition {} game", one.key));
|
|
} else {
|
|
ImGui::TextDisabled("%s", view.tr_own[i] ? "your game setting: on" : "your game setting: off");
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
};
|
|
// One of the grind card's switches, shown here too because it decides how a coping is ridden.
|
|
const auto grind_switch = [&](std::string_view key, const char *more) {
|
|
for (std::size_t i = 0; i < trainer::grind_switches.size(); ++i) {
|
|
const auto &one = trainer::grind_switches[i];
|
|
if (one.key != key) continue;
|
|
const std::string label(one.label), help = std::string(one.help) + " " + more + " (" + std::string(one.state) + "; the same switch as on GRINDS.)";
|
|
field(menu, label.c_str());
|
|
ImGui::PushID(static_cast<int>(300 + i));
|
|
const bool held = view.grind_mode[i] >= 0;
|
|
bool on = held ? view.grind_mode[i] != 0 : view.grind_live[i] > 0;
|
|
if (ImGui::Checkbox("##on", &on)) trainer_command(menu, callbacks, std::format("grind {} {}", one.key, on ? "on" : "off"));
|
|
tip(help.c_str());
|
|
ImGui::SameLine();
|
|
if (held) {
|
|
ImGui::TextUnformatted(view.grind_mode[i] ? "held on" : "held off");
|
|
ImGui::SameLine();
|
|
if (ImGui::SmallButton("Game's own")) trainer_command(menu, callbacks, std::format("grind {} game", one.key));
|
|
} else {
|
|
ImGui::TextDisabled("%s", view.grind_live[i] < 0 ? "game's own (not in use yet)" : view.grind_live[i] ? "game's own: on" : "game's own: off");
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
};
|
|
// Only rows that do something: a value the game was never found reading is left out (it is still on EVERY VALUE),
|
|
// unless it has been changed, so it can be put back.
|
|
const auto rows = [&](std::size_t g, std::size_t first = 0, std::size_t count = ~std::size_t{0}) {
|
|
for (std::size_t n = first; n < p.transition_rows[g].size() && n - first < count; ++n)
|
|
if (const auto i = p.transition_rows[g][n]; i < view.rows.size() && (view.rows[i].used || view.rows[i].touched || view.rows[i].frozen))
|
|
value_row(menu, callbacks, p, view, view.rows[i], false, true);
|
|
};
|
|
const auto setting_forced_on = [&](std::string_view key) {
|
|
for (std::size_t i = 0; i < trainer::transition_settings.size(); ++i)
|
|
if (trainer::transition_settings[i].key == key) return view.tr_setting[i] == 1;
|
|
return false;
|
|
};
|
|
|
|
if (card(menu, p, "transition-feel", "TRANSITION FEELS LIKE", "Bowls, quarter pipes, vert and spines: the game's own, or the way Skate 3 rode them")) {
|
|
{
|
|
const bool preset_on = std::ranges::any_of(view.presets, [](const trainer::PresetRow &row) { return row.builtin && row.active && row.name == "Transition: Skate 3"; });
|
|
bool like_skate = !preset_on, like_skate3 = preset_on;
|
|
for (std::size_t i = 0; i < trainer::transition_settings.size(); ++i) {
|
|
if (view.tr_setting[i] >= 0) like_skate = false;
|
|
if (view.tr_setting[i] != trainer::transition_settings[i].skate3) like_skate3 = false;
|
|
}
|
|
if (view.tr_lean != 0.0f || view.tr_carry != 0.0f || view.tr_unstick != 0.0f || view.tr_lockout != 0.0f || view.tr_spine || view.tr_plant != 0.0f) like_skate = false;
|
|
// Any number of your own on a help that is on keeps it Skate 3.
|
|
if (view.tr_lean == 0.0f || view.tr_carry == 0.0f || view.tr_unstick == 0.0f || view.tr_lockout == 0.0f || view.tr_plant == 0.0f) like_skate3 = false;
|
|
field(menu, "Transition feels like");
|
|
if (ImGui::RadioButton("skate.", like_skate)) trainer_command(menu, callbacks, "transition feel skate");
|
|
tip("Everything on this list is left to the game.");
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("Skate 3", like_skate3)) trainer_command(menu, callbacks, "transition feel skate3");
|
|
tip("Turns on every help below at Skate 3's numbers: lean into the ramp on a coping grind, roll off the coping at the angle you came in, no hang-ups coming "
|
|
"back in, handplants that come back in high on the wall. Sets Skate 3's coping exit timers and handplant exit. Airs stay skate.'s own, which is "
|
|
"already Skate 3's rule (stick forward flies out onto the deck, anything else comes back in), and so does pumping (use Pump power and Pump assist). "
|
|
"Change any row afterwards to make your own mix.");
|
|
note("New in this version, and built from Skate 3's own rules before any of it was measured in skate.: tell us what each help does on your ramps. "
|
|
"With the jump read-out on (MAP & HUD) every coping grind, handplant and hang-up writes a line starting \"Trainer transition:\" to the log. "
|
|
"Grinds have their own choice of feel on GRINDS. The lean needs an Xbox-style (XInput) controller.");
|
|
}
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "transition-coping", "COPING GRINDS AND STALLS", "On the lip of a bowl, a pipe or a quarter")) {
|
|
number_row("Lean into the ramp", "transition lean", view.tr_lean, -1.0f, 1.0f, view.tr_lean == 0.0f ? "off" : "%.2f",
|
|
"skate. stands the skater straight up on top of the coping. With this the skater leans into the ramp, the more the faster the grind runs along the "
|
|
"coping (nothing below 1 m/s, all of it from 5 m/s). It works by showing the game your left stick pushed toward the ramp for as long as you leave that "
|
|
"stick alone; move the stick and it is yours. 1 is as far as the game's lean goes without taking you off the grind; Skate 3 feel uses 0.6. "
|
|
"A negative number leans the other way, should it go out over the deck.");
|
|
number_row("Roll off at your angle", "transition carry", view.tr_carry, 0.0f, 1.0f, view.tr_carry == 0.0f ? "off" : "x %.2f",
|
|
"skate. turns the board straight down the ramp when a coping grind ends without a pop. With this the speed you had along the coping is put back "
|
|
"for the first third of a second, so a fast grind rolls off at a shallow angle, as it did in Skate 3. 1 keeps all of it, 0 is off.");
|
|
number_row("Roll-off drop speed", "transition drop", view.tr_drop, 0.0f, 5.0f, view.tr_drop == 0.0f ? "no limit" : "%.2f m/s",
|
|
"While the speed along the coping is being kept (the row above), the speed across the coping into the ramp is held to this. Skate 3 nudged the "
|
|
"board in at 1 m/s at most. 0 is no limit. Does nothing with the row above off.");
|
|
settings(trainer::transition_card_coping);
|
|
grind_switch("coping", "On, as the game has it, riding straight up into a coping does not lock onto it the old way.");
|
|
grind_switch("linear", "Off, the game's older hold on the edge runs: the one the coping exit timers below belong to.");
|
|
note("The exit timers: in Skate 3 a coping grind ended by itself. After the time set here the skater leaned further and further into the ramp and rolled "
|
|
"in. skate. has them off (-1). Skate 3: 0.1 s riding straight up into the coping, 0.8 s coming at it from below, 2 s on a curb.");
|
|
rows(0);
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "transition-reentry", "RE-ENTRY AND HANG-UPS", "Coming back in off the coping, and over a spine")) {
|
|
number_row("No hang-ups", "transition unstick", view.tr_unstick, 0.0f, 5.0f, view.tr_unstick == 0.0f ? "off" : "x %.2f",
|
|
"Skate 3's rule for a hang-up: slower than 1.1 m/s for longer than a sixth of a second after leaving the coping (or after landing an air off a "
|
|
"transition without riding away). The skater is then pushed into the ramp and down until they are moving again. The number is how hard; 1 is "
|
|
"the first setting, 0 is off.");
|
|
number_row("Coping re-catch delay", "transition lockout", view.tr_lockout, 0.0f, 3.0f, view.tr_lockout == 0.0f ? "off" : "%.2f s",
|
|
"After a coping grind ends without a pop the skater does not reach for an edge for this long (the reach rows below are held at 0, then put back). "
|
|
"Skate 3 waited 70 frames, 1.17 s. 0 is off.");
|
|
{
|
|
field(menu, "Airs clear the coping");
|
|
bool spine = view.tr_spine;
|
|
if (ImGui::Checkbox("##spine", &spine)) trainer_command(menu, callbacks, spine ? "transition spine on" : "transition spine off");
|
|
tip("An air that leaves a transition going up and over the lip (little of its speed runs along the lip) does not reach for an edge until it has "
|
|
"landed: the far coping of a spine, or the coping it left, cannot catch the board. A carve along the lip into a coping grind is left alone. "
|
|
"With this on, an air straight up cannot be landed back on the coping as a grind either.");
|
|
}
|
|
note("How far the skater reaches for an edge from the air, and what an edge is worth against the ground as a place to land:");
|
|
rows(1);
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "transition-plant", "HANDPLANTS", "Where a plant lets you go")) {
|
|
number_row("Skate 3 plant exit", "transition plant", view.tr_plant, 0.0f, 1.0f, view.tr_plant == 0.0f ? "off" : "x %.2f",
|
|
"When a handplant ends the skater leaves the lip at 0.9 m/s or more into the ramp and starts falling from rest, as off the top of an arc, in place of "
|
|
"the downward speed the plant leaves them with. 1 is all the way, 0 is off. If plants still come down at the bottom of the ramp with this on, "
|
|
"the plant itself carries the skater down before it lets go: the log line says how far (\"lost inside the plant\").");
|
|
settings(trainer::transition_card_plant);
|
|
note("The game's own numbers for a plant. It looks for a wall to come down on in a fan of directions from along the coping (0) to straight in (90), and "
|
|
"leaves the lip with a speed between the two limits. Skate 3: fan to 69 degrees, 0.9 m/s out, up to 2 m/s along the coping.");
|
|
rows(2);
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "transition-lip", "LIP TRICKS AND DROP-INS", "Stalls on the lip, rolling in from the deck")) {
|
|
settings(trainer::transition_card_lip);
|
|
rows(3);
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "transition-vert", "VERT AIRS AND LANDINGS", "The pop off a wall, and where the game brings you down")) {
|
|
settings(trainer::transition_card_vert);
|
|
note("A pop off a steep wall is straightened to go up the wall, tipped in or out by the stick; in the air the game picks a landing from a fan of "
|
|
"directions by score. These are the numbers for both.");
|
|
rows(4);
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "transition-pump", "PUMP POWER AND GAME SETTINGS", "How much a pump gives, and how much of it you have to do")) {
|
|
number_row("Pump power", "option pump_power", view.pump_power, 0.0f, 10.0f, "x %.2f",
|
|
"How much speed pumping a transition gives. 1 is the game's own; 2 gives about one more of the game's pumps on top of each of yours, 10 about "
|
|
"nine more, and under 1 takes speed away. The game still decides when you are pumping: the extra comes for as long as it counts you as "
|
|
"pumping. It is found in the game about 15 seconds after a level loads.");
|
|
if (view.pump_blocked) ImGui::TextDisabled("Off in this multiplayer game: the host's rules apply here.");
|
|
else if (view.pump_power != 1.0f && !view.pump_live) ImGui::TextDisabled("Finding the game's pumping in memory: it acts about 15 seconds after a level loads.");
|
|
note("GAME SETTINGS: the next three rows are your game's own settings, shown here as the game holds them. The trainer never changes them: "
|
|
"change them in the game's own Settings menu. No preset carries them, no reset touches them and they never count as MODIFIED.");
|
|
// The game's own options: the same values its Settings menu holds, changed from either place.
|
|
ImGui::EndDisabled(); // these are the player's own game settings: theirs to change in any session
|
|
for (std::size_t i = 0; i < trainer::transition_options.size(); ++i) {
|
|
const auto &one = trainer::transition_options[i];
|
|
const std::string label(one.label), help = std::string(one.help) + " GAME SETTING (" + std::string(one.setting) +
|
|
"): this is your game's own setting, shown as the game holds it. The trainer does not change it: use "
|
|
"the game's Settings menu. It is not part of presets, resets or the STOCK badge.";
|
|
ImGui::PushID(static_cast<int>(400 + i));
|
|
field(menu, label.c_str());
|
|
// Shown only: the trainer never writes the player's own game settings.
|
|
if (one.boolean) ImGui::TextUnformatted(view.tr_option[i] != 0 ? "On" : "Off");
|
|
else ImGui::Text("%d", view.tr_option[i]);
|
|
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", help.c_str());
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "GAME SETTING");
|
|
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", help.c_str());
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::BeginDisabled(!can_edit);
|
|
settings(trainer::transition_card_pump);
|
|
// The first seven rows are the older pumping code's numbers: skate.'s newer pumping does not read them, so they
|
|
// are only shown while Legacy pumping is forced on (or to put back one that was changed).
|
|
if (setting_forced_on("legacypump")) {
|
|
note("Legacy pumping is on: these are its numbers. Skate 3: strength 18, limit 10, 0.7 of a pump given without pumping.");
|
|
rows(5, 0, 7);
|
|
} else {
|
|
note("Pump power above works with the game's own pumping. Tick Legacy pumping for Skate 3's pumping and its strength and limit rows: "
|
|
"skate.'s newer pumping does not read those numbers, so they are not shown until then.");
|
|
for (std::size_t n = 0; n < 7 && n < p.transition_rows[5].size(); ++n)
|
|
if (const auto i = p.transition_rows[5][n]; i < view.rows.size() && (view.rows[i].touched || view.rows[i].frozen))
|
|
value_row(menu, callbacks, p, view, view.rows[i], false, true);
|
|
}
|
|
rows(5, 7);
|
|
}
|
|
card_end(p);
|
|
ImGui::EndDisabled();
|
|
}
|
|
|
|
void map_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
|
|
const auto telemetry = trainer::telemetry();
|
|
if (card(menu, p, "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));
|
|
}
|
|
card_end(p);
|
|
|
|
if (card(menu, p, "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));
|
|
}
|
|
}
|
|
card_end(p);
|
|
|
|
if (telemetry.last.serial) {
|
|
if (card(menu, p, "last-jump", "LAST JUMP", std::format("Jump {}", telemetry.last.serial).c_str())) {
|
|
jump_lines(menu, telemetry.last);
|
|
}
|
|
card_end(p);
|
|
}
|
|
if (telemetry.best.serial && telemetry.best.serial != telemetry.last.serial) {
|
|
if (card(menu, p, "best-jump", "LONGEST JUMP ON THIS MAP")) {
|
|
jump_lines(menu, telemetry.best);
|
|
}
|
|
card_end(p);
|
|
}
|
|
|
|
if (card(menu, p, "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<int>(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.");
|
|
}
|
|
card_end(p);
|
|
|
|
note("Your presets are above the tabs now, beside RESET EVERYTHING. Play like Skate 3 is on TUNE, under WHOLE GAME.");
|
|
}
|
|
} // 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;
|
|
// By name, see `trainer open`: 0 tune, 1 presets, 2 practice, 3 map; the rest are sections of TUNE
|
|
// (4 realistic and 5 fun were lists of earlier versions: the top of TUNE). 7 movement: noclip, No Bail
|
|
// and the boosts are on Skater > Cheats.
|
|
const int tab = view->open_tab;
|
|
if (tab == 7) {
|
|
p.show_cheats = true;
|
|
return true;
|
|
}
|
|
p.tab = tab == 2 ? 1 : tab == 3 ? 2 : 0;
|
|
if (tab == 1) p.manage = true;
|
|
constexpr int sections[]{-1, -1, -1, -1, -1, -1, trainer::topic_none, -1, trainer::topic_trickline, trainer::topic_transition, trainer::topic_pop, trainer::topic_speed,
|
|
trainer::topic_bails, trainer::topic_grinds, trainer::topic_pumping, trainer::topic_foot, trainer::topic_trickline, trainer::topic_pop,
|
|
trainer::topic_game};
|
|
if (tab >= 0 && tab < static_cast<int>(std::size(sections)) && sections[tab] >= 0) {
|
|
p.scroll_to = sections[tab];
|
|
p.search[0] = 0;
|
|
}
|
|
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;
|
|
}
|
|
bool trainer_cheats_wanted() {
|
|
auto &p = page();
|
|
const bool wanted = p.show_cheats;
|
|
p.show_cheats = false;
|
|
return wanted;
|
|
}
|
|
// The game speed the PRACTICE tab sets (1: the game's own; also when it cannot be read).
|
|
double game_speed(const Model &model) {
|
|
for (const auto &row : model.engine_settings)
|
|
if (row.name == "SimulationTime.TimeScale" && row.available) {
|
|
try {
|
|
const auto first = row.value.find_first_of("-.0123456789");
|
|
if (first != std::string::npos) return std::stod(row.value.substr(first));
|
|
} catch (...) {}
|
|
}
|
|
return 1.0;
|
|
}
|
|
void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) {
|
|
auto &p = page();
|
|
const auto view = trainer::view();
|
|
// Which sections and cards are open is the player's, kept in the trainer's file.
|
|
if (!p.folds_seeded && view->revision) {
|
|
p.folds_seeded = true;
|
|
for (const auto &[id, open] : view->folds) set_fold(id, open);
|
|
}
|
|
{
|
|
std::string id;
|
|
bool open{};
|
|
while (take_fold_change(id, open)) trainer_command(menu, callbacks, std::format("fold {} {}", id, open ? 1 : 0));
|
|
}
|
|
// Everything that changes how the game plays, said in words. The first parts are the trainer's own; the cheats,
|
|
// grind exits, Board Break and the game speed are ReSkate's and count all the same.
|
|
const double speed = game_speed(model);
|
|
std::string what;
|
|
const auto add = [&](const std::string &part) { what += (what.empty() ? "" : ", ") + part; };
|
|
if (view->ready) {
|
|
if (view->touched) add(std::format("{} value{}", view->touched, view->touched == 1 ? "" : "s"));
|
|
static constexpr std::pair<std::uint8_t, const char *> kinds[]{{trainer::topic_pop, "trick settings"}, {trainer::topic_grinds, "grind settings"},
|
|
{trainer::topic_trickline, "bending settings"}, {trainer::topic_transition, "transition settings"},
|
|
{trainer::topic_pumping, "pumping settings"}, {trainer::topic_foot, "on-foot jump"}};
|
|
for (const auto &[topic, name] : kinds)
|
|
if (view->settings[topic]) add(name);
|
|
}
|
|
const auto cheats = cheat_lines(model);
|
|
for (const auto &line : cheats) add(line.name);
|
|
if (std::abs(speed - 1.0) > 1e-3) add(std::format("game speed x{:.2f}", speed));
|
|
// A guest under the host's physics would only wipe a setup they cannot see at work here.
|
|
ImGui::BeginDisabled(!callbacks.queue_console_command || view->session_enforced);
|
|
if (ImGui::Button("RESET EVERYTHING")) {
|
|
clear_edits(p);
|
|
trainer_command(menu, callbacks, "reset everything");
|
|
// The cheats, grind exits, Board Break and the game speed change how the game plays too: off with the rest.
|
|
for (const auto &line : cheats) cheat_off(menu, callbacks, line);
|
|
if (callbacks.queue_debug && model.debug.probe) debug_request(menu, callbacks, {DebugAction::set_probe_enabled, false});
|
|
if (std::abs(speed - 1.0) > 1e-3) send_console(menu, callbacks, "reset SimulationTime.TimeScale");
|
|
}
|
|
tip("Puts back every value, lock, preset, dial, trick, grind, transition and bending setting, turns No Bail, Noclip, Dark Pop, Revert Boost, "
|
|
"grind exit triggers and Board Break off, puts the boosts back to 0 (off), sets the "
|
|
"game speed to normal and stops loading a preset on this map: the game exactly as it shipped. Your saved presets are kept, and so are your "
|
|
"game's own settings.");
|
|
ImGui::EndDisabled();
|
|
if (view->session_enforced && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
|
wrapped_tooltip("%s", "Off in this multiplayer game: the host controls physics here. Your own setup is kept for when you leave.");
|
|
ImGui::SameLine();
|
|
ImGui::AlignTextToFramePadding();
|
|
const auto locks = view->locks ? std::format(" ({} locked value{})", view->locks, view->locks == 1 ? "" : "s") : std::string{};
|
|
if (view->session_enforced) ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "HOST'S PHYSICS: your own setup is kept for when you leave.");
|
|
else if (!view->ready && what.empty()) ImGui::TextDisabled("The game as it shipped: every value, lock, preset and setting.");
|
|
else if (what.empty()) ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.45f, 1.0f), "STOCK: the game is exactly as it shipped.%s", locks.c_str());
|
|
else {
|
|
ImGui::PushTextWrapPos(0.0f);
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "MODIFIED: %s.%s", what.c_str(), locks.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
if (ImGui::IsItemHovered())
|
|
wrapped_tooltip("%s", "STOCK means nothing here is changing how the game plays. Your game's own settings (the rows marked GAME SETTING) are yours and never "
|
|
"count; a locked value that is still the game's own does not count either. Open TUNE: every section that holds a change says so on its bar.");
|
|
// The search for the game's tuning values reads all of the game's memory on a thread of its own: values set meanwhile
|
|
// reach the game when it ends, and on a machine short of memory the game can stutter while it runs.
|
|
if (const auto ms = trainer::class_search_ms()) {
|
|
ImGui::TextColored(ImVec4(0.55f, 0.75f, 1.0f, 1.0f), "Finding the game's tuning values... %llu s", static_cast<unsigned long long>(ms / 1000));
|
|
if (ImGui::IsItemHovered())
|
|
wrapped_tooltip("%s", "The trainer is reading the game's memory for the numbers it changes (after a level loads, a respawn or a new "
|
|
"setting). Changes you make meanwhile reach the game when it is done. It usually takes seconds; when the PC is "
|
|
"short of free memory it can take minutes and the game may stutter until it ends.");
|
|
}
|
|
if (view->ready) preset_row(menu, callbacks, p, *view);
|
|
// `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());
|
|
}
|
|
// Above the tabs, so every tab says why its controls do nothing in this session.
|
|
if (view->session_enforced) {
|
|
if (card(menu, p, "session-note", "MULTIPLAYER: THE HOST CONTROLS PHYSICS")) {
|
|
warn("You are in a multiplayer game where the host controls physics. This menu will not work in this multiplayer game.");
|
|
note("You skate with the host's setup. HUD and markers still work. Your own setup comes back when you leave.");
|
|
}
|
|
card_end(p);
|
|
} else if (view->boosts_blocked) {
|
|
if (card(menu, p, "session-note", "MULTIPLAYER: BOOSTS ARE OFF")) {
|
|
warn("The host has turned boosts off in this multiplayer game: trick sliders, flip speed, auto push and the board bending boost will not work here.");
|
|
note("The physics values still apply. The rest comes back when the host allows boosts or you leave.");
|
|
}
|
|
card_end(p);
|
|
}
|
|
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);
|
|
p.pick.clear();
|
|
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, p, *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<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
|