Trainer: let slow flips stay slow (the finish-before-landing rule can be switched off), help on every control, wrapped tooltips

This commit is contained in:
andrewnakas committed 2026-10-06 14:46:35 -06:00
1 parent c3c52168e5
commit 4d6969cbba
7 files changed
+252 -40

No files matched your search

+7
View File
@@ -5,6 +5,13 @@
Built on ReSkate 1.1.3. From what players asked for in the ReSkate Discord. Everything marked "untested" is in so that it
can be tried: tell us what it does.
- **Let slow flips stay slow.** The game speeds a flip trick up so the board is round 1/6 s before
the landing it predicts, however slow the flip speed is set: that is why a slowed flip still
came round on a small pop. The new switch under TRICKS turns that rule off (`trainer option
flip_gate 0`): the board turns as slowly as set and lands however far round it got. It is saved
and shared with your presets, and off under a host's enforced physics.
- **Help on everything.** Every button, slider and switch of the trainer says what it does when
you point at it, and the tips wrap instead of running off the screen.
- **Camera tab.** Move the game's own cameras: distance, height and side for the low camera, the
high camera and the on-foot camera, with a "SunJay's Low Cam" button (the numbers of SunJay's
mod, which showed where the game keeps them). The cameras keep their smoothing and stay out of
+45 -2
View File
@@ -4,6 +4,8 @@
#include "Engine/Resource/ebx_document.h"
#include "Engine/Vfs/game_bundles.h"
#include <algorithm>
#include <fstream>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <iostream>
@@ -14,6 +16,7 @@
#include <variant>
#include <vector>
#pragma warning(disable : 4996) // std::getenv in a command line tool
namespace ebx = dingosdk::frostbite::ebx;
namespace {
std::string lower(std::string_view text) {
@@ -115,8 +118,15 @@ int main(int argc, char **argv) {
try {
const std::filesystem::path game_root(argv[1]);
const dingosdk::vfs::GameData data(game_root);
const auto found = data.read_bundle(data.read_toc("Win32/levels/game/bam_levelroot/bam_levelroot.toc"),
"win32/levels/game/bam_levelroot/bam_levelroot");
// ASSET_TOC / ASSET_BUNDLE: another superbundle and bundle than the level root; ASSET_BUNDLE=? lists the names.
const auto *toc_name = std::getenv("ASSET_TOC");
const auto *bundle_name = std::getenv("ASSET_BUNDLE");
const auto toc = data.read_toc(toc_name ? toc_name : "Win32/levels/game/bam_levelroot/bam_levelroot.toc");
if (bundle_name && std::string_view(bundle_name) == "?") {
for (const auto &entry : toc.bundles) std::cout << entry.name << std::endl;
return 0;
}
const auto found = data.read_bundle(toc, bundle_name ? bundle_name : "win32/levels/game/bam_levelroot/bam_levelroot");
if (!found) throw std::runtime_error("the level root bundle was not found");
const auto &bundle = *found;
const std::string mode = argv[2];
@@ -176,6 +186,39 @@ int main(int argc, char **argv) {
}
return 1;
}
if (mode == "--export" && argc >= 5) {
// The raw bytes of every asset whose name contains a word, as <folder>/<number>.ebx,
// with one line each: number, name, file id as hex, the ids it imports.
const std::filesystem::path folder(argv[3]);
std::filesystem::create_directories(folder);
std::vector<std::string> wanted;
for (int i = 4; i < argc; ++i) wanted.push_back(lower(argv[i]));
const auto hex = [](const dingosdk::frostbite::Guid &guid) {
static const char digits[] = "0123456789abcdef";
std::string text;
for (const auto byte : guid.bytes) {
text += digits[static_cast<unsigned>(byte) >> 4];
text += digits[static_cast<unsigned>(byte) & 15];
}
return text;
};
for (std::size_t i = 0; i < bundle.manifest.ebx.size(); ++i) {
const auto name = lower(bundle.manifest.ebx[i].name);
if (std::ranges::none_of(wanted, [&](const std::string &w) { return name.find(w) != std::string::npos; })) continue;
try {
const auto *payload = bundle.payload(dingosdk::frostbite::AssetKind::ebx, i);
if (!payload) continue;
const auto bytes = data.read(*payload);
const auto document = ebx::read_document(bytes);
std::ofstream(folder / (std::to_string(i) + ".ebx"), std::ios::binary)
.write(reinterpret_cast<const char *>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
std::cout << i << '\t' << name << '\t' << hex(document.fileGuid) << '\t' << document.rootType << '\t';
for (const auto &import : document.imports) std::cout << hex(import.fileGuid) << ' ';
std::cout << '\n';
} catch (const std::exception &) {}
}
return 0;
}
if (mode == "--dump" && argc >= 4) {
const auto wanted = lower(argv[3]);
for (std::size_t i = 0; i < bundle.manifest.ebx.size(); ++i) {
+23 -3
View File
@@ -126,6 +126,7 @@ struct State {
float offboard_height{1};
// Board flip tricks: a multiplier on the game's flip speed curves (trainer_classes.h).
float flip_speed{1};
bool flip_gate{true}; // false: the game's finish-before-landing rule for flip tricks is held off
bool offboard_grounded{}; // off the board and not moving up or down at the last tick
const char *boost_name{""};
// The no comply and the boneless are launched by the trick scripts too (trainer_jump.cpp).
@@ -322,6 +323,7 @@ void load_store() {
s.nocomply_height = std::clamp(o.value("nocomply_height", 1.0f), height_low, height_high);
s.offboard_height = std::clamp(o.value("offboard_height", 1.0f), height_low, height_high);
s.flip_speed = std::clamp(o.value("flip_speed", 1.0f), flip_low, flip_high);
s.flip_gate = o.value("flip_gate", true);
s.boneless_height = std::clamp(o.value("boneless_height", 1.0f), height_low, height_high);
s.revert_boost = std::clamp(o.value("revert_boost", 0.0f), 0.0f, revert_boost_high);
s.slot = std::clamp(o.value("slot", 0), 0, static_cast<int>(marker_slots) - 1);
@@ -395,6 +397,7 @@ void save_store() {
options["nocomply_height"] = s.nocomply_height;
options["offboard_height"] = s.offboard_height;
options["flip_speed"] = s.flip_speed;
options["flip_gate"] = s.flip_gate;
options["boneless_height"] = s.boneless_height;
options["revert_boost"] = s.revert_boost;
options["slot"] = s.slot;
@@ -927,6 +930,7 @@ std::string apply_dial(std::string_view name, double factor) {
constexpr std::string_view trick_prefix = "trick.";
constexpr std::string_view trick_names[]{"flip_speed", "hippy_height", "nocomply_height", "boneless_height", "offboard_height", "revert_boost"};
// What a trick slider is when the game is left alone: the multipliers 1, the revert boost off.
constexpr std::string_view flip_gate_name = "flip_gate";
constexpr float trick_stock(std::string_view name) { return name == "revert_boost" ? 0.0f : 1.0f; }
float *trick_option(std::string_view name) {
auto &s = state();
@@ -938,6 +942,8 @@ float *trick_option(std::string_view name) {
bool set_trick_key(std::string_view key, double value) {
if (!key.starts_with(trick_prefix)) return false;
const auto name = key.substr(trick_prefix.size());
// The slow-flip switch rides along as a number: 0 holds the game's finish-before-landing rule off.
if (name == flip_gate_name) state().flip_gate = value != 0;
if (auto *option = trick_option(name))
*option = name == "flip_speed" ? std::clamp(static_cast<float>(value), flip_low, flip_high)
: name == "revert_boost" ? std::clamp(static_cast<float>(value), 0.0f, revert_boost_high)
@@ -1529,6 +1535,8 @@ void build_view() {
next->nocomply_height = s.nocomply_height;
next->offboard_height = s.offboard_height;
next->flip_speed = s.flip_speed;
next->flip_gate = s.flip_gate;
next->flip_gate_found = flip_gates() != 0;
next->boneless_height = s.boneless_height;
next->revert_boost = s.revert_boost;
next->revert = s.revert;
@@ -1551,7 +1559,7 @@ void build_view() {
next->camera_game = std::format("{:.1f} m behind, {:.1f} m up, tilt {:.0f} degrees, FOV {:.0f}", profile.distance, profile.height, profile.pitch, profile.fov);
next->open_serial = s.open_serial;
next->stock = s.revert == RevertTuning{} && !s.camera_on && s.rigs == std::array<RigSetting, camera_rigs>{} && !next->touched && s.active.empty() && std::ranges::none_of(s.entries, &Entry::frozen) &&
std::ranges::all_of(trick_names, [](std::string_view name) { return *trick_option(name) == trick_stock(name); });
std::ranges::all_of(trick_names, [](std::string_view name) { return *trick_option(name) == trick_stock(name); }) && s.flip_gate;
next->share_text = s.share_text;
next->share_serial = s.share_serial;
next->open_tab = s.open_tab;
@@ -1608,6 +1616,7 @@ std::string export_preset(const std::string &name) {
if (e.touched) values[e.key] = e.value;
for (const auto trick : trick_names)
if (const auto *option = trick_option(trick); *option != trick_stock(trick)) values[std::string(trick_prefix) + std::string(trick)] = *option;
if (!s.flip_gate) values[std::string(trick_prefix) + std::string(flip_gate_name)] = 0;
title = "My setup";
} else {
for (const auto &[user_name, saved] : s.user)
@@ -1665,7 +1674,8 @@ std::string import_preset(const std::string &file_name) {
for (const auto &[key, value] : json.at("values").items()) {
if (!value.is_number() || values.size() >= 512) continue;
const auto id = lower(key);
const bool trick = id.starts_with(trick_prefix) && trick_option(std::string_view(id).substr(trick_prefix.size()));
const bool trick = id.starts_with(trick_prefix) && (trick_option(std::string_view(id).substr(trick_prefix.size())) ||
std::string_view(id).substr(trick_prefix.size()) == flip_gate_name);
if (!trick && !find_entry(id)) ++unknown; // kept: another build of the game may have it
values[id] = value.get<double>();
}
@@ -1745,6 +1755,9 @@ void tick(std::uintptr_t base, std::uintptr_t client, bool playing, const std::s
}
}
want_flip_speed(s.boosts.flip);
// The player's own choice, and only where their own trick settings count: not under a
// host's enforced physics or in a session that has turned boosts off.
want_flip_gate(s.flip_gate || s.enforced || (multiplayer_session_active() && !session_boosts_allowed()));
if (s.boosts.flip != s.flip_applied) {
// The catch times follow the flip speed (want_class).
s.flip_applied = s.boosts.flip;
@@ -1978,6 +1991,7 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
}
if (v == "reset" && lower(arg(0)) == "tricks") {
s.hippy_height = s.nocomply_height = s.boneless_height = s.offboard_height = s.flip_speed = 1.0f;
s.flip_gate = true;
s.revert_boost = 0.0f;
changed();
return "Trick heights and flip trick speed are the game's own again.";
@@ -2006,6 +2020,7 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
s.camera_on = false;
s.rigs = {};
s.hippy_height = s.nocomply_height = s.boneless_height = s.offboard_height = s.flip_speed = 1.0f;
s.flip_gate = true;
s.revert_boost = 0.0f;
s.revert = {};
changed();
@@ -2073,6 +2088,7 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
if (e.touched) values[e.key] = e.value;
for (const auto trick : trick_names)
if (const auto *option = trick_option(trick); *option != trick_stock(trick)) values[std::string(trick_prefix) + std::string(trick)] = *option;
if (!s.flip_gate) values[std::string(trick_prefix) + std::string(flip_gate_name)] = 0;
if (values.empty()) return "error: nothing is changed, so there is nothing to save.";
const auto count = values.size();
s.user[name] = std::move(values);
@@ -2157,12 +2173,16 @@ std::string run(std::string_view verb, const std::vector<std::string> &a) {
if (!value) return "error: return_delay needs seconds.";
s.return_delay = std::clamp(static_cast<float>(*value), 0.0f, 10.0f);
} else if (!flag(arg(1), on)) {
return "error: usage: trainer option hud|hud_jump|auto_return|pad|pad_menu|log 0|1";
return "error: usage: trainer option hud|hud_jump|auto_return|pad|pad_menu|flip_gate|log 0|1";
} else if (name == "hud") s.hud = on;
else if (name == "hud_jump") s.hud_jump = on;
else if (name == "auto_return") s.auto_return = on;
else if (name == "pad") s.pad_shortcuts = on;
else if (name == "pad_menu") s.pad_menu = on;
else if (name == "flip_gate") {
if (!on && !editable(&why)) return "error: " + why;
s.flip_gate = on;
}
else if (name == "log") {
set_logging(on);
if (on && !s.logging) return "error: the telemetry file could not be created.";
+2
View File
@@ -90,6 +90,8 @@ struct View {
RevertTuning revert; // what counts as a revert and what it is worth
float offboard_height{1}; // a jump on foot
float flip_speed{1}; // board flip tricks: x of the game's own speed
bool flip_gate{true}; // the game brings a flip round before the landing; false: slow flips stay slow
bool flip_gate_found{}; // the rule's number was found in memory (the switch can act)
// HUD
bool hud{}, hud_jump{}, logging{};
// The loaded map and what its author ships for the trainer (Mods/<mod>/trainer.json).
+70 -4
View File
@@ -81,6 +81,8 @@ struct Found {
std::mutex mutex;
std::vector<FlipCurve> flip;
float flip_wanted{1}, flip_written{1};
std::vector<std::uintptr_t> gates; // where the flick warp limit's number lives
bool gate_kept{true}; // false: the trainer holds it off
Copies copies;
MirrorCopies mirror_copies;
std::array<std::array<int, 15>, mirror_count> mirror_fields{};
@@ -278,6 +280,36 @@ std::vector<FlipCurve> find_flip_curves(std::uintptr_t curve_type) {
}
return result;
}
// The flip gate. A flip trick's clip stretches its flip by 1 / the flip speed, but never past
// Float.Anim.MaxTrickFlickWarpTime, which an expression of the animation graph sets to the time
// left before landing less one number: 1/6 s as shipped. That number sits in the expression's
// kernel between words that do not change; far below zero, the limit never binds and a slow flip
// stays slow all the way to the ground.
constexpr float gate_stock = 1.0f / 6.0f, gate_off = -1000.0f;
bool is_gate(std::uintptr_t address, std::uintptr_t low, std::uintptr_t high) noexcept {
if (address < low + 12 || address + 12 > high) return false;
const auto *words = reinterpret_cast<const std::uint32_t *>(address);
return words[2] == 0x2419u && words[-3] == 0x00010300u && !words[-2] && !words[-1] && !words[1] &&
(words[0] == bits(gate_stock) || words[0] == bits(gate_off));
}
void collect_gates(std::uintptr_t start, std::size_t size, std::vector<std::uintptr_t> &gates) noexcept {
__try {
const auto *words = reinterpret_cast<const std::uint32_t *>(start);
for (std::size_t i = 2; i < size / 4; ++i)
if (words[i] == 0x2419u && is_gate(start + (i - 2) * 4, start, start + size) && gates.size() < 16) gates.push_back(start + (i - 2) * 4);
} __except (EXCEPTION_EXECUTE_HANDLER) {}
}
bool gate_still_there(std::uintptr_t address) noexcept {
__try {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<void *>(address), &info, sizeof(info)) != sizeof(info) || info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE)
return false;
const auto low = reinterpret_cast<std::uintptr_t>(info.BaseAddress);
return is_gate(address, low, low + info.RegionSize);
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
DWORD WINAPI search(void *) noexcept {
auto &f = found();
try {
@@ -390,9 +422,17 @@ DWORD WINAPI search(void *) noexcept {
flip_found = f.flip.size();
f.dirty = true;
}
std::size_t gates_found{};
{
std::vector<std::uintptr_t> gates;
each_region([&](std::uintptr_t start, std::size_t size) { collect_gates(start, size, gates); });
std::lock_guard lock(f.mutex);
f.gates = std::move(gates);
gates_found = f.gates.size();
}
logging::write(logging::Level::info, logging::Channel::skater,
std::format("Trainer: found {} of {} game tuning classes ({} copies) and {} flip trick speed curves in {} MB, {} ms.", classes,
class_count, copies, flip_found, bytes >> 20, GetTickCount64() - started));
std::format("Trainer: found {} of {} game tuning classes ({} copies), {} flip trick speed curves and {} flip landing limit in {} MB, {} ms.",
classes, class_count, copies, flip_found, gates_found, bytes >> 20, GetTickCount64() - started));
} catch (...) {}
f.searches.fetch_add(1, std::memory_order_release);
f.searching.store(false, std::memory_order_release);
@@ -404,6 +444,18 @@ DWORD WINAPI search(void *) noexcept {
// checked: one that went back to the game's number is written again, one that holds neither is
// someone else's memory now and is forgotten.
bool same(float a, float b) noexcept { return std::abs(a - b) <= 1e-5f * std::max(1.0f, std::abs(b)); }
// Brings every known copy of the flip gate's number to what is wanted; forgets the ones that are
// someone else's memory now.
std::size_t write_gates(Found &f) noexcept {
std::erase_if(f.gates, [](std::uintptr_t address) { return !gate_still_there(address); });
std::size_t count{};
const float want = f.gate_kept ? gate_stock : gate_off;
for (const auto address : f.gates) {
float now{};
if (peek(address, now) && now != want && put(address, want)) ++count;
}
return count;
}
void write_flip_curve(const FlipCurve &curve, float factor) noexcept {
for (std::size_t i = 0; i < curve.outputs.size(); ++i) (void)put(curve.points + i * curve_point + curve_y, curve.outputs[i] * factor);
// The bounds move with the outputs, or the curve clamps the scaled values back.
@@ -421,6 +473,8 @@ bool refresh_classes(std::uint64_t now) noexcept {
if (!lock.owns_lock() || !f.ready || f.searching.load(std::memory_order_acquire)) return false;
bool lost = false;
std::size_t repaired{};
repaired += write_gates(f);
lost = lost || (!f.gate_kept && f.gates.empty());
if (f.flip_written != 1) {
std::erase_if(f.flip, [&](const FlipCurve &curve) {
float y{};
@@ -513,7 +567,7 @@ void want_class_value(std::size_t field, float value) noexcept {
bool classes_wanted() noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
if (f.flip_wanted != 1) return true;
if (f.flip_wanted != 1 || !f.gate_kept) return true;
for (std::size_t i = 0; i < class_field_count; ++i)
if (f.wanted[i] != class_fields[i].stock) return true;
return false;
@@ -577,6 +631,18 @@ void want_flip_speed(float factor) noexcept {
f.dirty = true;
}
}
void want_flip_gate(bool kept) noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
if (f.gate_kept == kept) return;
f.gate_kept = kept;
(void)write_gates(f);
}
std::size_t flip_gates() noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
return f.gates.size();
}
std::size_t flip_curves() noexcept {
auto &f = found();
std::lock_guard lock(f.mutex);
@@ -610,7 +676,7 @@ std::string classes_summary() {
result += std::format("{}{}:{}+{}", c ? ", " : "", class_specs[c].key, f.copies[c].size() - static_cast<std::size_t>(slots), slots);
}
for (std::size_t m = 0; m < mirror_count; ++m) result += std::format(", native copy {}:{}", m, f.mirror_copies[m].size());
result += std::format(", flip trick speed curves:{}", f.flip.size());
result += std::format(", flip trick speed curves:{}, flip landing limit:{}{}", f.flip.size(), f.gates.size(), f.gate_kept ? "" : " (held off)");
return result;
}
} // namespace dingosdk::trainer
+6
View File
@@ -53,6 +53,12 @@ std::string classes_summary();
void want_flip_speed(float factor) noexcept;
// How many flip trick speed curves were found (0: the multiplier does nothing yet).
std::size_t flip_curves() noexcept;
// The game hurries a flip trick so that it is round 1/6 s before the landing it predicts, however
// slow the flip speed is set. `kept` false holds that rule off: the board turns as slowly as set
// and lands however far round it got. The search finds the rule's number; flip_gates says whether
// it did (0: nothing to switch yet).
void want_flip_gate(bool kept) noexcept;
std::size_t flip_gates() noexcept;
// Where field `field` (an index into class_fields) lives: each copy's address and kind ('c' the
// class's own layout, 's' the eight-byte slots, 'n' native code's copy). Returns how many.
struct FieldCopy {
+99 -31
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdarg>
#include <format>
#include <string>
#include <vector>
@@ -49,6 +50,21 @@ Page &page() {
static auto *value = new Page;
return *value;
}
// A tooltip that wraps at a readable width instead of running off the screen as one line.
void wrapped_tooltip(const char *format, ...) {
if (!ImGui::BeginTooltip()) return;
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 26.0f);
va_list arguments;
va_start(arguments, format);
ImGui::TextV(format, arguments);
va_end(arguments);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
// What the control just drawn does, shown while the pointer (or the controller's focus) is on it.
void tip(const char *text) {
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) wrapped_tooltip("%s", text);
}
std::string lower(std::string_view text) {
std::string result(text);
std::ranges::transform(result, result.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
@@ -108,7 +124,7 @@ void value_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tra
ImGui::PushID(row.id.c_str());
bool frozen = row.frozen;
if (ImGui::Checkbox("##freeze", &frozen)) trainer_command(menu, callbacks, std::format("freeze {} {}", row.id, frozen ? 1 : 0));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Lock: presets and Reset values leave this value alone.\nReset everything (top of the page) clears locks too.\nYou do not need it for a change to apply.");
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;
@@ -118,7 +134,7 @@ void value_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tra
ImGui::TextUnformatted(row.used ? label.c_str() : (label + " (no use found)").c_str());
if (tinted) ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s%s%s\nStock: %s%s", row.help.c_str(), row.help.empty() ? "" : "\n", row.id.c_str(), number(row.stock).c_str(),
wrapped_tooltip("%s%s%s\nStock: %s%s", row.help.c_str(), row.help.empty() ? "" : "\n", row.id.c_str(), number(row.stock).c_str(),
row.used ? "" : "\nThe game was not found (or seen) reading this value, so changing it may do nothing.");
ImGui::SameLine(column);
const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
@@ -147,10 +163,14 @@ void value_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tra
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();
@@ -162,7 +182,7 @@ void preset_switch(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer:
const bool pressed = ImGui::Button(preset.name.c_str());
if (preset.active) ImGui::PopStyleColor();
if (pressed) trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name);
if (ImGui::IsItemHovered() && !preset.note.empty()) ImGui::SetTooltip("%s", preset.note.c_str());
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) {
@@ -174,7 +194,7 @@ void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trai
ImGui::TextUnformatted(preset.title.c_str());
if (moved) ImGui::PopStyleColor();
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s\n1 is the game's own; \"%s\" is x %s.", preset.note.c_str(), preset.name.c_str(), number(preset.amount).c_str());
wrapped_tooltip("%s\n1 is the game's own; \"%s\" is x %s.", preset.note.c_str(), preset.name.c_str(), number(preset.amount).c_str());
ImGui::SameLine(column);
const float button = ImGui::CalcTextSize("Fast Parkour Flips").x + ImGui::GetStyle().FramePadding.x * 2;
const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
@@ -192,22 +212,24 @@ void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trai
} 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()) ImGui::SetTooltip("Type any multiplier and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Type any multiplier and press Enter: the slider's ends are not a limit.");
ImGui::SameLine();
if (preset.active) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue);
const bool pressed = ImGui::Button(preset.name.c_str(), ImVec2(button, 0));
if (preset.active) ImGui::PopStyleColor();
if (pressed) trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", preset.note.c_str());
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();
}
@@ -235,6 +257,10 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
const bool on = p.mode == mode;
if (on) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue);
if (ImGui::Button(label, ImVec2(width, 0))) p.mode = mode;
tip(mode == 0 ? "A short list for toning the game down: lower pops, slower pushes, harder bails."
: mode == 1 ? "A short list for turning the game up: bigger pops, faster pushes, moon jumps."
: mode == 3 ? "Everything that makes a trick line in one place: board bending, reverts, pumping, pops, spins, manuals and grinds."
: "Every dial, your own presets and every tuning value the trainer knows.");
if (on) ImGui::PopStyleColor();
}
}
@@ -243,6 +269,7 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
// One search for the whole tab: dials, switches, trick sliders and every value.
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
ImGui::InputTextWithHint("##search", "Search: dials, tricks and every value...", p.search.data(), p.search.size());
tip("Type a word to find dials, trick sliders and tuning values on every list at once. Clear the box to go back to the list.");
const auto words = lower(p.search.data());
const bool searching = !words.empty();
const auto listed = [&](const trainer::PresetRow &preset) {
@@ -291,7 +318,7 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
}
ImGui::BeginDisabled(!can_edit || !view.touched);
if (ImGui::Button("Reset values")) trainer_command(menu, callbacks, "reset all");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Every tuning value back to the game's own. Locked values stay.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Every tuning value back to the game's own. Locked values stay.");
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
@@ -316,6 +343,7 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
} else if (ImGui::Button("Turn on", ImVec2(button, 0))) {
trainer_command(menu, callbacks, "preset apply " + preset.name);
}
tip("Turn on sets every value saved in this preset. Turn off puts those values back to the game's own.");
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(preset.name.c_str());
@@ -323,11 +351,13 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
ImGui::TextDisabled("%s", preset.note.c_str());
ImGui::SameLine(ImGui::GetContentRegionAvail().x + ImGui::GetCursorPosX() - px(170));
if (ImGui::SmallButton("This map")) trainer_command(menu, callbacks, "profile set " + preset.name);
tip("Applies this preset by itself every time this map loads.");
ImGui::SameLine();
if (ImGui::SmallButton("Share")) trainer_command(menu, callbacks, "preset export " + preset.name);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Copies this preset to the clipboard as one line of text. Paste it in a chat;\nanyone with the trainer can import it.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Copies this preset to the clipboard as one line of text. Paste it in a chat;\nanyone with the trainer can import it.");
ImGui::SameLine();
if (ImGui::SmallButton("Delete")) trainer_command(menu, callbacks, "preset delete " + preset.name);
tip("Removes this preset from your list.");
ImGui::PopID();
}
ImGui::EndDisabled();
@@ -335,22 +365,24 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
const float save = ImGui::CalcTextSize("Save").x + ImGui::GetStyle().FramePadding.x * 2;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - save - ImGui::GetStyle().ItemSpacing.x);
ImGui::InputText("##preset-name", p.preset_name.data(), p.preset_name.size());
tip("A name for the preset you are about to save.");
ImGui::SameLine();
ImGui::BeginDisabled(!p.preset_name[0] || !view.touched);
if (ImGui::Button("Save", ImVec2(save, 0))) trainer_command(menu, callbacks, std::string("preset save ") + p.preset_name.data());
tip("Saves every value, dial and trick setting that is changed right now as a preset under this name. Share it from its row afterwards.");
ImGui::EndDisabled();
// Sharing: a preset travels as one line of text on the clipboard.
ImGui::BeginDisabled(view.stock);
if (ImGui::Button("Share what is changed now")) trainer_command(menu, callbacks, "preset export current");
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Copies your current setup to the clipboard as one line of text.");
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()) ImGui::SetTooltip("Copy a preset line someone shared (it starts with RST1:), then press this.\nIt is added to your presets; nothing changes until you turn it on.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Copy a preset line someone shared (it starts with RST1:), then press this.\nIt is added to your presets; nothing changes until you turn it on.");
if (!view.map.empty()) {
info(menu, "This map applies", view.profile_preset.empty() ? "nothing" : view.profile_preset);
if (!view.profile_preset.empty() && ImGui::Button("Stop applying it on this map")) trainer_command(menu, callbacks, "profile clear");
@@ -376,14 +408,16 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
if (ImGui::Selectable(view.groups[i].c_str(), p.group == static_cast<int>(i) + 2)) p.group = static_cast<int>(i) + 2;
ImGui::EndCombo();
}
tip("Shows one group of tuning values at a time.");
ImGui::Checkbox("Only what I changed", &p.only_changed);
tip("Lists only the values that differ from the game's own.");
ImGui::SameLine();
ImGui::Checkbox("Graph points", &p.graph_points);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Show every point of the tuning graphs, not just one multiplier per graph.");
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())
ImGui::SetTooltip("Also list the tuning values that the game was never found or seen reading.\nChanging those will probably do nothing.");
wrapped_tooltip("Also list the tuning values that the game was never found or seen reading.\nChanging those will probably do nothing.");
} else if (searching) {
trainer::note_class_list_shown();
} else {
@@ -407,13 +441,13 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
}
void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const std::string &words) {
static constexpr const char *labels[]{"Flip trick speed", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height",
static constexpr const char *labels[]{"Flip trick speed", "Let slow flips stay slow (no finish before landing)", "No comply height", "Boneless height", "Hippy jump height", "Off-board jump height",
"Board bending boost (revert speed)"};
const auto wanted = [&](const char *label) { return words.empty() || contains_words(lower(std::string(label) + " tricks"), words); };
if (std::ranges::none_of(labels, wanted)) return;
begin_card(menu, "trick-heights", "TRICKS", "1.0 is the game's own");
// `limited`: the game itself stops at the slider's end, so a typed number is held to it too.
const auto slider = [&](const char *label, const char *option, float value, float &edit, bool &editing, float high = 50.0f, bool limited = false) {
const auto slider = [&](const char *label, const char *option, float value, float &edit, bool &editing, const char *help, float high = 50.0f, bool limited = false) {
if (!wanted(label)) return false;
field(menu, label);
ImGui::PushID(option);
@@ -426,6 +460,7 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
}
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);
@@ -434,15 +469,28 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
trainer_command(menu, callbacks, std::format("option {} {}", option, typed));
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(limited ? "Type a multiplier and press Enter." : "Type any multiplier and press Enter: the slider's end is not a limit.");
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("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing, 3.0f) && ImGui::IsItemHovered()) ImGui::SetTooltip("Slows board flips (and gives them that much longer to come round). Above 1 the game's own limit on how fast a board turns takes over.");
slider("No comply height", "nocomply_height", view.nocomply_height, p.nocomply_edit, p.nocomply_editing);
slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing);
slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing);
slider("Off-board jump height", "offboard_height", view.offboard_height, p.offboard_edit, p.offboard_editing);
slider("Flip trick speed", "flip_speed", view.flip_speed, p.flip_edit, p.flip_editing,
"How fast the board turns in a flip trick. Below 1 a flip is slower and gets that much longer to come round. On its own the game still "
"hurries a slow flip so it is finished before you land: tick the switch below to stop that. Above 1 the game's own limit on how fast a "
"board turns takes over.",
3.0f);
if (wanted("Let slow flips stay slow (no finish before landing)")) {
bool slow = !view.flip_gate;
if (ImGui::Checkbox("Let slow flips stay slow", &slow)) trainer_command(menu, callbacks, std::format("option flip_gate {}", slow ? 0 : 1));
if (ImGui::IsItemHovered())
wrapped_tooltip("%s", "The game speeds a flip up so the board is round 1/6 of a second before the landing it predicts, however slow the flip speed "
"is set. That is why a slowed flip still comes round on a small pop. Ticked: that rule is off. The board turns as "
"slowly as you set it and lands however far round it got, so a flip you start too late or too low is a bail.");
if (slow && !view.flip_gate_found) ImGui::TextDisabled("Not found in the game yet: it takes effect a few seconds after a level loads.");
}
slider("No comply height", "nocomply_height", view.nocomply_height, p.nocomply_edit, p.nocomply_editing, "How high a no comply pops. 1 is the game's own.");
slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing, "How high a boneless pops. 1 is the game's own.");
slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing, "How high you jump off the board in a hippy jump. 1 is the game's own.");
slider("Off-board jump height", "offboard_height", view.offboard_height, p.offboard_edit, p.offboard_editing, "How high you jump on foot. 1 is the game's own.");
if (wanted("Board bending boost (revert speed)")) {
// Not a multiplier: 0 is the game's own (no boost).
field(menu, "Board bending boost");
@@ -457,7 +505,7 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
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())
ImGui::SetTooltip("Board bending: land with the board turned out of line and the game snaps it straight. With this on, that snap\n"
wrapped_tooltip("Board bending: land with the board turned out of line and the game snaps it straight. With this on, that snap\n"
"throws you forward, as it did in Skate 3: chain bends to keep and build speed without pushing.\n"
"x 1 gives about +1.5 to +2.6 m/s a landing. 0 is off, the game's own. The Tricklining list has the details.\n"
"From AutoRevertBoost by Sivaes, jaq and OVM.");
@@ -468,7 +516,7 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
p.revert_editing = false;
trainer_command(menu, callbacks, std::format("option revert_boost {}", typed));
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Type any strength and press Enter: the slider's end is not a limit.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Type any strength and press Enter: the slider's end is not a limit.");
ImGui::PopID();
}
if (ImGui::Button("Reset tricks")) {
@@ -476,6 +524,7 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false;
trainer_command(menu, callbacks, "reset tricks");
}
tip("Puts every slider and switch on this card back to the game's own.");
note("Not tuning values: the game sets these in its trick scripts and flip curves. Type in the box to go past a slider's end.");
end_card();
}
@@ -515,10 +564,13 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
auto value = static_cast<float>(current);
if (ImGui::SliderFloat("##game-speed", &value, 0.05f, 2.0f, current == 0.0 ? "paused" : "%.2fx", ImGuiSliderFlags_AlwaysClamp))
set(value);
if (!ImGui::IsItemActive()) tip("Slows the whole game down or speeds it up. 1x is normal speed.");
if (ImGui::Button(current == 0.0 ? "Resume" : "Pause")) set(current == 0.0 ? 1.0 : 0.0);
tip("Freezes the game where it is. Press again to carry on.");
for (const auto quick : {0.1, 0.25, 0.5, 0.75, 1.0}) {
ImGui::SameLine();
if (ImGui::Button(std::format("{}x", number(quick)).c_str())) set(quick);
tip("Sets the game speed to this straight away.");
}
note("The jump read-out measures real time, so it reads low while the game is slowed.");
}
@@ -533,12 +585,15 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
ImGui::BeginDisabled(view.map.empty());
ImGui::BeginDisabled(!telemetry.skater);
if (ImGui::Button("Save here")) trainer_command(menu, callbacks, "marker save");
tip("Saves where you are standing in the selected slot, for this map.");
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(!marker.set);
if (ImGui::Button("Go")) trainer_command(menu, callbacks, "marker go");
tip("Puts you back at the selected marker.");
ImGui::SameLine();
if (ImGui::Button("Clear")) trainer_command(menu, callbacks, "marker clear");
tip("Empties the selected slot.");
ImGui::EndDisabled();
ImGui::EndDisabled();
bool auto_return = view.auto_return;
@@ -547,6 +602,7 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
field(menu, "Return delay");
float delay = view.return_delay;
ImGui::SliderFloat("##return-delay", &delay, 0.0f, 10.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp);
if (!ImGui::IsItemActive()) tip("How long after a bail before you are put back at the marker.");
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option return_delay {:.1f}", delay));
bool pad = view.pad_shortcuts;
if (toggle_row(menu, "Controller shortcuts", "Hold LB + RB, then D-pad: up saves, down goes, left / right pick the slot.", pad))
@@ -562,17 +618,22 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
info(menu, "You are at", std::format("{:.2f}, {:.2f}, {:.2f}", telemetry.position[0], telemetry.position[1], telemetry.position[2]));
field(menu, "X, Y, Z");
ImGui::InputFloat3("##teleport", p.teleport.data(), "%.2f");
tip("A position in the game's own coordinates: X, height, Z.");
ImGui::BeginDisabled(!telemetry.skater);
if (ImGui::Button("Here")) p.teleport = telemetry.position;
tip("Fills the boxes with where you are now.");
ImGui::SameLine();
if (ImGui::Button("Go##teleport"))
trainer_command(menu, callbacks, std::format("tp {:.3f} {:.3f} {:.3f}", p.teleport[0], p.teleport[1], p.teleport[2]));
tip("Moves you to the position in the boxes. You arrive on foot.");
ImGui::SameLine();
if (ImGui::Button("Copy position"))
ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", telemetry.position[0], telemetry.position[1], telemetry.position[2]).c_str());
tip("Copies where you are to the clipboard as X, Y, Z.");
ImGui::SameLine();
if (ImGui::Button("Copy for Blender"))
ImGui::SetClipboardText(std::format("{:.3f}, {:.3f}, {:.3f}", telemetry.position[0], -telemetry.position[2], telemetry.position[1]).c_str());
tip("Copies where you are in Blender's axes (Z up), for map makers.");
ImGui::EndDisabled();
end_card();
}
@@ -609,7 +670,7 @@ void feel_buttons(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::
if (on) ImGui::PushStyleColor(ImGuiCol_Button, skate_theme::blue);
if (ImGui::Button(choices[i].label, ImVec2(width, 0))) trainer_command(menu, callbacks, choices[i].command);
if (on) ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", choices[i].tip);
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", choices[i].tip);
}
}
@@ -696,11 +757,12 @@ void trickline_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, c
} else if (p.active == key) {
p.active.clear();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip);
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;
@@ -722,6 +784,7 @@ void trickline_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, c
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();
@@ -745,12 +808,13 @@ void trickline_section(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, c
void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view) {
begin_card(menu, "rigs", "THE GAME'S CAMERAS", "Move the game's own cameras: they keep their smoothing and stay out of walls");
if (ImGui::Button("SunJay's Low Cam")) trainer_command(menu, callbacks, "camera rig sunjay");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Both on-board cameras low and close, the numbers of SunJay's Low Cam mod.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Both on-board cameras low and close, the numbers of SunJay's Low Cam mod.");
ImGui::SameLine();
if (ImGui::Button("Low, a little higher")) trainer_command(menu, callbacks, "camera rig low+");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("The low camera lower and closer, but with more of the skater in view.");
if (ImGui::IsItemHovered()) wrapped_tooltip("The low camera lower and closer, but with more of the skater in view.");
ImGui::SameLine();
if (ImGui::Button("The game's own")) trainer_command(menu, callbacks, "camera rig stock");
tip("Puts all three of the game's cameras back as they shipped.");
static constexpr const char *rig_ids[]{"low", "high", "foot"};
static constexpr const char *rig_titles[]{"Low camera (on board)", "High camera (on board)", "On foot"};
for (std::size_t i = 0; i < trainer::camera_rigs; ++i) {
@@ -764,12 +828,13 @@ void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tr
float shown = p.active == key ? static_cast<float>(p.active_value) : value;
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x));
if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera rig {} {} {:.3f}", rig_ids[i], field, shown));
if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) wrapped_tooltip("%s", tip);
ImGui::SameLine();
float typed = value;
ImGui::SetNextItemWidth(px(64));
if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue))
trainer_command(menu, callbacks, std::format("camera {} {} {} {}", "rig", rig_ids[i], field, typed));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Type any number and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Type any number and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemActive()) {
p.active = key;
@@ -777,7 +842,6 @@ void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tr
} else if (p.active == key) {
p.active.clear();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip);
ImGui::PopID();
ImGui::PopID();
};
@@ -801,12 +865,13 @@ void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tr
for (std::size_t i = 0; i < std::size(presets); ++i) {
if (i) ImGui::SameLine();
if (ImGui::Button(presets[i])) trainer_command(menu, callbacks, std::string("camera preset ") + presets[i]);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", notes[i]);
if (ImGui::IsItemHovered()) wrapped_tooltip("%s", notes[i]);
}
if (view.camera_game.empty()) note("Skate a few seconds with the game's own camera and its framing is measured here.");
else {
info(menu, "The game frames you from", view.camera_game);
if (ImGui::Button("Start from the game's framing")) trainer_command(menu, callbacks, "camera game");
tip("Copies the framing measured from the game's camera into your camera's sliders below.");
}
end_card();
const auto set_card = [&](const char *id, const char *title, const char *which, const trainer::CameraSet &set) {
@@ -819,12 +884,13 @@ void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tr
float shown = p.active == key ? static_cast<float>(p.active_value) : value;
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - px(64) - ImGui::GetStyle().ItemSpacing.x));
if (ImGui::SliderFloat("##value", &shown, low, high, format)) trainer_command(menu, callbacks, std::format("camera set {} {} {:.3f}", which, field, shown));
if (ImGui::IsItemHovered() && !ImGui::IsItemActive()) wrapped_tooltip("%s", tip);
ImGui::SameLine();
float typed = value;
ImGui::SetNextItemWidth(px(64));
if (ImGui::InputFloat("##typed", &typed, 0, 0, "%.6g", ImGuiInputTextFlags_EnterReturnsTrue))
trainer_command(menu, callbacks, std::format("camera {} {} {} {}", "set", which, field, typed));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Type any number and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemHovered()) wrapped_tooltip("Type any number and press Enter: the slider's ends are not a limit.");
if (ImGui::IsItemActive()) {
p.active = key;
@@ -832,7 +898,6 @@ void camera_tab(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const tr
} else if (p.active == key) {
p.active.clear();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip);
ImGui::PopID();
ImGui::PopID();
};
@@ -888,11 +953,13 @@ void map_tab(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::View
info(menu, "Author's preset", view.map_preset);
ImGui::BeginDisabled(!view.editable);
if (ImGui::Button("Apply the author's preset")) trainer_command(menu, callbacks, "preset apply map");
tip("Sets the values this map's maker recommends for it.");
ImGui::EndDisabled();
}
for (std::size_t i = 0; i < view.spots.size(); ++i) {
ImGui::PushID(static_cast<int>(i));
if (ImGui::Button("Go")) trainer_command(menu, callbacks, std::format("spot {}", i + 1));
tip("Takes you to this spot.");
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(view.spots[i].name.c_str());
@@ -930,6 +997,7 @@ void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = p.revert_editing = false;
trainer_command(menu, callbacks, "reset everything");
}
tip("Puts back every value, lock, preset, dial, trick setting and camera: the game exactly as it shipped.");
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::AlignTextToFramePadding();