diff --git a/Extension/Trainer/CHANGELOG.md b/Extension/Trainer/CHANGELOG.md index ae1f06c7..327d4993 100644 --- a/Extension/Trainer/CHANGELOG.md +++ b/Extension/Trainer/CHANGELOG.md @@ -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 diff --git a/Extension/Trainer/Test/asset_list.cpp b/Extension/Trainer/Test/asset_list.cpp index 882e4d5e..e31abd59 100644 --- a/Extension/Trainer/Test/asset_list.cpp +++ b/Extension/Trainer/Test/asset_list.cpp @@ -4,6 +4,8 @@ #include "Engine/Resource/ebx_document.h" #include "Engine/Vfs/game_bundles.h" #include +#include +#include #include #include #include @@ -14,6 +16,7 @@ #include #include +#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 /.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 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(byte) >> 4]; + text += digits[static_cast(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(bytes.data()), static_cast(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) { diff --git a/Extension/Trainer/trainer.cpp b/Extension/Trainer/trainer.cpp index f8a2a954..dd5c9f61 100644 --- a/Extension/Trainer/trainer.cpp +++ b/Extension/Trainer/trainer.cpp @@ -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(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(value), flip_low, flip_high) : name == "revert_boost" ? std::clamp(static_cast(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{} && !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(); } @@ -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 &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 &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 &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 &a) { if (!value) return "error: return_delay needs seconds."; s.return_delay = std::clamp(static_cast(*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."; diff --git a/Extension/Trainer/trainer.h b/Extension/Trainer/trainer.h index 49eed1f5..60b7a9a5 100644 --- a/Extension/Trainer/trainer.h +++ b/Extension/Trainer/trainer.h @@ -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//trainer.json). diff --git a/Extension/Trainer/trainer_classes.cpp b/Extension/Trainer/trainer_classes.cpp index 268d3307..96cacd45 100644 --- a/Extension/Trainer/trainer_classes.cpp +++ b/Extension/Trainer/trainer_classes.cpp @@ -81,6 +81,8 @@ struct Found { std::mutex mutex; std::vector flip; float flip_wanted{1}, flip_written{1}; + std::vector 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, mirror_count> mirror_fields{}; @@ -278,6 +280,36 @@ std::vector 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(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 &gates) noexcept { + __try { + const auto *words = reinterpret_cast(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(address), &info, sizeof(info)) != sizeof(info) || info.State != MEM_COMMIT || info.Protect != PAGE_READWRITE) + return false; + const auto low = reinterpret_cast(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 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(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 diff --git a/Extension/Trainer/trainer_classes.h b/Extension/Trainer/trainer_classes.h index bd55765a..3cfbd76a 100644 --- a/Extension/Trainer/trainer_classes.h +++ b/Extension/Trainer/trainer_classes.h @@ -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 { diff --git a/Extension/Trainer/trainer_page.cpp b/Extension/Trainer/trainer_page.cpp index 50bd0a3a..6921079a 100644 --- a/Extension/Trainer/trainer_page.cpp +++ b/Extension/Trainer/trainer_page.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -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(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(i) + 2)) p.group = static_cast(i) + 2; ImGui::EndCombo(); } + tip("Shows one group of tuning values at a time."); ImGui::Checkbox("Only what I changed", &p.only_changed); + tip("Lists only the values that differ from the game's own."); ImGui::SameLine(); ImGui::Checkbox("Graph points", &p.graph_points); - if (ImGui::IsItemHovered()) 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(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(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(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(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();