Menu: a full screen layout for controllers beside the window, and the pages regrouped

The ReSkate menu now has two styles, switched with View / F11 or the buttons in its top bar
and saved with the profile (`ui fullscreen`):
- Window: the mouse menu as before, with a side rail of pages in groups.
- Full screen: made for a controller, in the pause menu's look. Pages across the top (LB / RB),
  each page's sections as the ONLINE page's tabs (LT / RT), button prompts at the bottom, the
  pause menu's sounds, and the pause menu's own Multiplayer pages as ONLINE.

The pages are regrouped: HOME (quick switches and a tile for each page), MAP, BUILD, SKATER
(freecam, first person, cheats), SANDBOX (once Trainer), ADDONS (drone, Board Break, Road
Rash, Hall of Meat, grind exits, music), ONLINE, PROGRESS, MODS, SETTINGS, DEVELOPER.

Full screen, every shared control is drawn for a pad: switches and option rows changed with
left / right one step a press, sliders as steppers that repeat when held, rows of buttons,
and pages that scroll with the right stick. Search boxes and typed values stay in the window.
A pad press takes the menu's focus back and hides the pointer until the mouse moves.

Also: map pictures in the map picker (official loading screens, a mod's icon) with a search
box in the window, Splash and Tutorial Island left out of it; number boxes beside the
window's sliders; the binds in groups, with an action bind to open the menu (`menu`); a menu
size for each style (`ui scalefull`); the time of day stepping from the one last asked for.
This commit is contained in:
AyeZeeBB committed 2026-10-16 23:37:33 -05:00
1 parent 20d58a4b73
commit 4c07f2493b
33 files changed
+2843 -464

No files matched your search

+2 -1
View File
@@ -89,7 +89,8 @@ struct ControllerInput {
struct ActionBind {
std::string_view key, name, label, command;
};
inline constexpr std::array<ActionBind, 8> action_binds{{
inline constexpr std::array<ActionBind, 9> action_binds{{
{"reskate_menu", "reskatemenu", "Open the ReSkate menu", "menu"},
{"first_person", "firstperson", "First person", "firstperson toggle"},
{"hide_hud", "hidehud", "Hide HUD", "hideui toggle"},
{"voice_chat", "voicechat", "Voice chat on / off", "mp voice-chat toggle"},
+49
View File
@@ -9,7 +9,9 @@
#include "Engine/Vfs/thunderstore_package.h"
#include "Extension/Multiplayer/Session/session.h"
#include "Extension/UI/Overlay/overlay.h"
#include <algorithm>
#include <atomic>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <map>
@@ -136,9 +138,56 @@ void fetch(unsigned serial, std::vector<multiplayer::NextMapWanted> maps) {
}
fetching.store(false);
}
// The menu's map grid: each level's picture, a custom map's mod icon (its path found on the client
// thread) or an official map's loading screen.
std::atomic<bool> menu_fetching{};
void fetch_menu(std::vector<std::pair<overlay::MenuMapWanted, fs::path>> maps) {
std::vector<overlay::MapVoteIcon> icons;
for (auto& [map, icon_path] : maps) {
overlay::MapVoteIcon icon;
if (!icon_path.empty()) icon.png = read_bytes(icon_path);
if (icon.png.empty()) {
icon.rgba = game_picture(map.name);
if (!icon.rgba.empty()) icon.side = picture_side;
}
if (icon.png.empty() && icon.rgba.empty()) continue;
icon.name = std::move(map.asset);
icons.push_back(std::move(icon));
}
overlay::set_menu_map_pictures(icons);
try {
logging::log(logging::Level::info, logging::Channel::assets, "Menu map grid: {} of {} map(s) with a picture.", icons.size(), maps.size());
} catch (...) {}
menu_fetching.store(false);
}
bool same_asset(std::string_view a, std::string_view b) {
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) {
return std::tolower(static_cast<unsigned char>(x)) == std::tolower(static_cast<unsigned char>(y));
});
}
void menu_tick() {
if (menu_fetching.load()) return;
auto wanted = overlay::take_menu_map_request();
if (wanted.empty()) return;
std::vector<std::pair<overlay::MenuMapWanted, fs::path>> maps;
const auto& catalog = mods::catalog();
for (auto& map : wanted) {
fs::path icon;
for (const auto& mod : catalog.mods)
if (std::ranges::any_of(mod.levels, [&](const std::string& level) { return same_asset(level, map.asset); })) {
icon = mod.directory / L"icon.png";
break;
}
maps.emplace_back(std::move(map), std::move(icon));
}
menu_fetching.store(true);
std::thread(fetch_menu, std::move(maps)).detach();
}
} // namespace
void tick() {
menu_tick();
const auto serial = multiplayer::next_maps_serial();
if (serial == wanted_serial.load() || fetching.load()) return;
wanted_serial.store(serial);
+71 -12
View File
@@ -95,9 +95,15 @@ void player_tab(SkateMenu& menu, Page& p) {
}
}
ImGui::Dummy(ImVec2(0, px(4)));
using external_player::Command;
if (console_style()) {
const int pressed = button_row(menu, "music-buttons", {{"PREVIOUS", p.station}, {p.playing ? "PAUSE" : "PLAY", p.station, true}, {"NEXT", p.station}});
if (pressed == 0) external_player::send(Command::previous);
if (pressed == 1) external_player::send(Command::toggle), p.read_at = -1;
if (pressed == 2) external_player::send(Command::next);
} else {
const float gap = px(6), width = (ImGui::GetContentRegionAvail().x - gap * 2) / 3;
ImGui::BeginDisabled(!p.station);
using external_player::Command;
if (ImGui::Button("PREVIOUS", ImVec2(width, px(36)))) external_player::send(Command::previous);
ImGui::SameLine(0, gap);
skate_theme::push_primary_button();
@@ -109,6 +115,7 @@ void player_tab(SkateMenu& menu, Page& p) {
ImGui::SameLine(0, gap);
if (ImGui::Button("NEXT", ImVec2(width, px(36)))) external_player::send(Command::next);
ImGui::EndDisabled();
}
ImGui::Dummy(ImVec2(0, px(4)));
bool notices = youtube_music::notices();
if (toggle_row(menu, "Show \"Loading song\" in the corner", "The notice in the top left while a YouTube song or a radio station is on its way.", notices)) {
@@ -121,11 +128,23 @@ void player_tab(SkateMenu& menu, Page& p) {
const auto& s = p.streams;
const bool lined_up = s.playing && s.playing_list < s.playlists.size();
begin_card(menu, "music-queue", "QUEUE", lined_up ? s.playlists[s.playing_list].name.c_str() : !p.spotify.empty() ? "Up next on Spotify" : nullptr);
if (lined_up) {
if (lined_up && console_style()) {
const auto& songs = s.playlists[s.playing_list].songs;
const bool held = s.loading || (p.pressed >= 0 && ImGui::GetTime() - p.pressed < 1.5);
for (std::size_t i = 0; i < songs.size(); ++i) {
const bool current = i == s.playing_song;
if (list_row(menu, std::to_string(i).c_str(), std::format("{:>3} {}", i + 1, songs[i].title), songs[i].artist, current, !held) && !current) {
youtube_music::play_at(s.playing_list, i);
p.pressed = ImGui::GetTime();
p.read_at = -1;
}
if (ImGui::IsItemFocused()) set_console_hint(current ? "Playing now." : "A plays this song.");
}
} else if (lined_up) {
const auto& songs = s.playlists[s.playing_list].songs;
ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputTextWithHint("##music-find", "Find a song or artist", p.find.data(), p.find.size());
ImGui::BeginChild("music-songs", ImVec2(0, std::max(px(160), ImGui::GetContentRegionAvail().y - px(330))), ImGuiChildFlags_None);
ImGui::BeginChild("music-songs", ImVec2(0, std::max(px(160), ImGui::GetContentRegionAvail().y - px(330))), ImGuiChildFlags_NavFlattened);
// (One press at a time: a song that is loading is not begun again under more presses.)
const bool held = s.loading || (p.pressed >= 0 && ImGui::GetTime() - p.pressed < 1.5);
for (std::size_t i = 0; i < songs.size(); ++i) {
@@ -147,7 +166,7 @@ void player_tab(SkateMenu& menu, Page& p) {
}
ImGui::EndChild();
} else if (!p.spotify.empty()) {
ImGui::BeginChild("music-spotify", ImVec2(0, std::max(px(160), ImGui::GetContentRegionAvail().y - px(330))), ImGuiChildFlags_None);
ImGui::BeginChild("music-spotify", ImVec2(0, std::max(px(160), ImGui::GetContentRegionAvail().y - px(330))), ImGuiChildFlags_NavFlattened);
for (std::size_t i = 0; i < p.spotify.size(); ++i) {
ImGui::PushStyleColor(ImGuiCol_Text, i ? skate_theme::white : skate_theme::blue_hover);
ImGui::TextUnformatted(std::format("{:>3} {}", i + 1, p.spotify[i].title).c_str());
@@ -168,7 +187,7 @@ void sound_tab(SkateMenu& menu, Page& p) {
bool editing{}, changed{};
begin_card(menu, "music-loudness", "LOUDNESS");
field(menu, "Music loudness", "How loud the music you play through ReSkate is in the game.");
if (ImGui::SliderFloat("##music-gain", &p.gain, 0.0f, 400.0f, "%.0f%%", ImGuiSliderFlags_AlwaysClamp)) music_inject::set_gain(p.gain / 100.0f);
if (::dingosdk::overlay::menu::slider_float("##music-gain", &p.gain, 0.0f, 400.0f, "%.0f%%", ImGuiSliderFlags_AlwaysClamp)) music_inject::set_gain(p.gain / 100.0f);
editing |= ImGui::IsItemActive();
changed |= ImGui::IsItemDeactivatedAfterEdit();
bool levelling = audio_dsp::levelling();
@@ -185,6 +204,38 @@ void sound_tab(SkateMenu& menu, Page& p) {
begin_card(menu, "music-equaliser", "EQUALISER", "Up for more of that part of the sound, down for less.");
static constexpr std::array<const char*, audio_dsp::bands> names{"BASS", "LOW MID", "MID", "HIGH MID", "TREBLE"}, where{"60 Hz", "250 Hz", "1 kHz", "4 kHz", "12 kHz"};
struct Preset {
const char* name;
std::array<float, audio_dsp::bands> bands;
};
static constexpr std::array<Preset, 5> presets{{{"FLAT", {0, 0, 0, 0, 0}}, {"BASS BOOST", {6, 3, 0, 0, 0}}, {"LOUD", {5, 2, -1, 2, 4}}, {"VOCAL", {-2, 0, 3, 3, 0}}, {"BRIGHT", {0, 0, 0, 3, 5}}}};
if (console_style()) {
for (int i = 0; i < audio_dsp::bands; ++i) {
const auto at = static_cast<std::size_t>(i);
field(menu, std::format("{} ({})", names[at], where[at]).c_str(), "Left / right a decibel at a time.");
ImGui::PushID(i);
set_console_step(1.0);
if (slider_float("##band", &p.bands[at], -audio_dsp::most_decibels, audio_dsp::most_decibels, "%+.0f dB", ImGuiSliderFlags_AlwaysClamp)) {
p.bands[at] = std::round(p.bands[at]);
audio_dsp::set_band(i, p.bands[at]);
}
editing |= ImGui::IsItemActive();
changed |= ImGui::IsItemDeactivatedAfterEdit();
ImGui::PopID();
}
const int pressed = button_row(menu, "eq-presets", {{presets[0].name, true, presets[0].bands == p.bands}, {presets[1].name, true, presets[1].bands == p.bands},
{presets[2].name, true, presets[2].bands == p.bands}, {presets[3].name, true, presets[3].bands == p.bands}, {presets[4].name, true, presets[4].bands == p.bands}});
if (pressed >= 0) {
p.bands = presets[static_cast<std::size_t>(pressed)].bands;
for (int band = 0; band < audio_dsp::bands; ++band) audio_dsp::set_band(band, p.bands[static_cast<std::size_t>(band)]);
changed = true;
}
note("The loudness, levelling and equaliser only change music played through ReSkate, not the game's own.");
end_card();
p.editing = editing;
if (changed) spotify_dashboard::save_settings();
return;
}
// Each band is a column: how much it is set to, a narrow track with a line where 0 dB is
// and a filled bar from that line to the handle, then its name and where it sits.
const float gap = px(10), column = std::min(px(96), (ImGui::GetContentRegionAvail().x - gap * (audio_dsp::bands - 1)) / audio_dsp::bands);
@@ -232,11 +283,6 @@ void sound_tab(SkateMenu& menu, Page& p) {
ImGui::SetCursorScreenPos(origin);
ImGui::Dummy(ImVec2((column + gap) * audio_dsp::bands - gap, px(24) + tall + px(46)));
ImGui::Dummy(ImVec2(0, px(6)));
struct Preset {
const char* name;
std::array<float, audio_dsp::bands> bands;
};
static constexpr std::array<Preset, 5> presets{{{"FLAT", {0, 0, 0, 0, 0}}, {"BASS BOOST", {6, 3, 0, 0, 0}}, {"LOUD", {5, 2, -1, 2, 4}}, {"VOCAL", {-2, 0, 3, 3, 0}}, {"BRIGHT", {0, 0, 0, 3, 5}}}};
for (std::size_t i = 0; i < presets.size(); ++i) {
if (i) ImGui::SameLine(0, px(6));
const bool on = presets[i].bands == p.bands;
@@ -278,6 +324,16 @@ void sources_tab(SkateMenu& menu, Page& p) {
const auto& playlist = p.streams.playlists[i];
if (playlist.kind != Kind::files) continue;
ImGui::PushID(static_cast<int>(i));
if (console_style()) {
const auto label = std::format("RESCAN {} ({} songs)", playlist.name, playlist.songs.size());
if (button_row(menu, "rescan", {{label.c_str(), !p.streams.busy}}) == 0) {
youtube_music::add_folder(playlist.url);
feedback(menu, "Reading the folder again.");
p.read_at = -1;
}
ImGui::PopID();
continue;
}
ImGui::BeginDisabled(p.streams.busy);
if (ImGui::Button("RESCAN")) {
youtube_music::add_folder(playlist.url);
@@ -300,8 +356,11 @@ void music_page(SkateMenu& menu, const Model&, const CallbacksV3&) {
read(p);
category_tabs(menu, p.tab, {"PLAYER", "SOUND", "SOURCES"}, "music-tabs");
ImGui::PushID(p.tab);
ImGui::BeginChild("music-tab", ImVec2(0, page_body_height(menu)));
if (p.tab == 0) player_tab(menu, p);
ImGui::BeginChild("music-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
if (p.tab == 0) {
player_tab(menu, p);
music_playback_card(menu);
}
else if (p.tab == 1) sound_tab(menu, p);
else sources_tab(menu, p);
ImGui::EndChild();
@@ -274,29 +274,58 @@ void set_local_values(const std::vector<std::pair<std::string, Json>>& values) n
} catch (...) { /* Saving is best effort; the runtime values already applied. */ }
}
// Shared with the overlay through the model; clamped to a legible range.
std::atomic<float>& menu_scale_cache() noexcept { static std::atomic<float> value{0}; return value; }
// Shared with the overlay through the model; clamped to a legible range. The window and the full
// screen menu each keep their own (ReSkate.MenuScale, ReSkate.MenuScaleFullscreen).
std::atomic<float>& menu_scale_cache(bool fullscreen) noexcept {
static std::atomic<float> window{0}, full{0};
return fullscreen ? full : window;
}
const char* menu_scale_key(bool fullscreen) noexcept { return fullscreen ? "ReSkate.MenuScaleFullscreen" : "ReSkate.MenuScale"; }
float local_menu_scale() noexcept {
auto& cache = menu_scale_cache();
float local_menu_scale(bool fullscreen) noexcept {
auto& cache = menu_scale_cache(fullscreen);
if (const auto value = cache.load(std::memory_order_relaxed); value > 0) return value;
float value = default_menu_scale;
try {
auto& s = local_runtime();
if (s.store)
if (const auto saved = s.store->user_value("ReSkate.MenuScale"); saved && saved->is_number())
if (const auto saved = s.store->user_value(menu_scale_key(fullscreen)); saved && saved->is_number())
value = std::clamp(static_cast<float>(saved->get<double>()), min_menu_scale, max_menu_scale);
} catch (...) { /* An unreadable preference just means the default scale. */ }
cache.store(value, std::memory_order_relaxed);
return value;
}
bool set_local_menu_scale(float value) noexcept {
if (!(value >= min_menu_scale && value <= max_menu_scale)) return false;
menu_scale_cache().store(value, std::memory_order_relaxed);
std::atomic<int>& menu_fullscreen_cache() noexcept { static std::atomic<int> value{-1}; return value; }
bool local_menu_fullscreen() noexcept {
auto& cache = menu_fullscreen_cache();
if (const auto value = cache.load(std::memory_order_relaxed); value >= 0) return value != 0;
bool value = false;
try {
auto& s = local_runtime();
if (s.store) s.store->set_user_value("ReSkate.MenuScale", static_cast<double>(value));
if (s.store)
if (const auto saved = s.store->user_value("ReSkate.MenuFullscreen"); saved && saved->is_boolean())
value = saved->get<bool>();
} catch (...) { /* An unreadable preference just means a window. */ }
cache.store(value ? 1 : 0, std::memory_order_relaxed);
return value;
}
void set_local_menu_fullscreen(bool fullscreen) noexcept {
menu_fullscreen_cache().store(fullscreen ? 1 : 0, std::memory_order_relaxed);
try {
auto& s = local_runtime();
if (s.store) s.store->set_user_value("ReSkate.MenuFullscreen", fullscreen);
} catch (...) { /* Saving is best effort; the runtime value already applied. */ }
}
bool set_local_menu_scale(float value, bool fullscreen) noexcept {
if (!(value >= min_menu_scale && value <= max_menu_scale)) return false;
menu_scale_cache(fullscreen).store(value, std::memory_order_relaxed);
try {
auto& s = local_runtime();
if (s.store) s.store->set_user_value(menu_scale_key(fullscreen), static_cast<double>(value));
} catch (...) { /* Saving is best effort; the runtime value already applied. */ }
return true;
}
+6 -2
View File
@@ -102,8 +102,12 @@ std::optional<Json> local_value(std::string_view key) noexcept;
void set_local_values(const std::vector<std::pair<std::string, Json>>& values) noexcept;
// ReSkate menu scale, clamped to a legible range. Cached so the overlay can
// read it every frame without touching the database.
float local_menu_scale() noexcept;
bool set_local_menu_scale(float value) noexcept;
// The ReSkate menu's size: the window's, or (`fullscreen`) the full screen menu's.
float local_menu_scale(bool fullscreen = false) noexcept;
bool set_local_menu_scale(float value, bool fullscreen = false) noexcept;
// Whether the ReSkate menu fills the screen (made for a controller) or is a window. Cached likewise.
bool local_menu_fullscreen() noexcept;
void set_local_menu_fullscreen(bool fullscreen) noexcept;
// The game's camera height option (UseHighCam, true is the high camera) as the game writes it,
// which its camera height button does: the listener returns the height to keep instead. Runs on
// the thread that writes the option.
+35
View File
@@ -121,5 +121,40 @@ void register_settings_commands(Commands &registry) {
out("Menu scale reset.");
};
registry.add(std::move(menu));
auto full_scale = argument("scale", Type::number);
full_scale.minimum = min_menu_scale;
full_scale.maximum = max_menu_scale;
auto menu_full = variable("ui scalefull", "Scale the full screen ReSkate menu and its text", Group::console, std::move(full_scale));
menu_full.execution = Execution::local;
menu_full.inspect = [](const Model &m) {
return State{true, std::format("{:.2f}", m.menu_scale_fullscreen), {}, "Saved with your profile.", false};
};
menu_full.run = [](const Model &, const Values &args, const Output &out) {
const auto value = static_cast<float>(std::get<double>(args[0]));
out(profile_runtime::set_local_menu_scale(value, true)
? std::format("Full screen menu scale set to {:.2f}.", value)
: std::format("error: Choose a scale from {:.2f} to {:.2f}.", min_menu_scale, max_menu_scale));
};
menu_full.reset = [](const Model &, const Output &out) {
profile_runtime::set_local_menu_scale(default_menu_scale, true);
out("Full screen menu scale reset.");
};
registry.add(std::move(menu_full));
auto fullscreen = variable("ui fullscreen", "Show the ReSkate menu full screen, made for a controller (0: a window)",
Group::console, argument("0|1", Type::boolean));
fullscreen.execution = Execution::local;
fullscreen.inspect = [](const Model &m) { return boolean_state(true, m.menu_fullscreen, {}); };
fullscreen.run = [](const Model &, const Values &args, const Output &out) {
const bool on = std::get<bool>(args[0]);
profile_runtime::set_local_menu_fullscreen(on);
out(on ? "The ReSkate menu fills the screen." : "The ReSkate menu is a window.");
};
fullscreen.reset = [](const Model &, const Output &out) {
profile_runtime::set_local_menu_fullscreen(false);
out("The ReSkate menu is a window.");
};
registry.add(std::move(fullscreen));
}
} // namespace dingosdk::console
+11 -4
View File
@@ -66,7 +66,7 @@ void register_movement_commands(Commands &registry) {
registry.add(std::move(entry));
}
auto drone_mode = variable("dronemode",
"Fly the free camera as an FPV drone. Easiest from the DRONE page of the ReSkate menu (Insert): Start flying",
"Fly the free camera as an FPV drone. Easiest from Addons > Drone in the ReSkate menu (Insert): Start flying",
Group::movement, argument("0|1", Type::boolean));
drone_mode.inspect = [](const Model &m) {
return boolean_state(m.debug.available, drone::is_enabled(), "Waiting for local player controls.",
@@ -130,13 +130,20 @@ void register_movement_commands(Commands &registry) {
: std::format("{} = {:g} (the closest allowed value)", name, value));
};
registry.add(std::move(tune));
auto menu_command = action("dronemenu", "Open the ReSkate menu on its DRONE page", Group::movement);
auto menu_command = action("dronemenu", "Open the ReSkate menu on Addons > Drone", Group::movement);
menu_command.inspect = [](const Model &) { return State{true, {}, {}, {}, false}; };
menu_command.run = [](const Model &, const Values &, const Output &out) {
drone::menu_flag().store(true);
out("Opening the DRONE page.");
out("Opening Addons > Drone.");
};
registry.add(std::move(menu_command));
auto open_menu = action("menu", "Open the ReSkate menu (bind it to a controller combo to open it with a pad)", Group::console);
open_menu.inspect = [](const Model &) { return State{true, {}, {}, {}, false}; };
open_menu.run = [](const Model &, const Values &, const Output &out) {
overlay::menu_open_flag().store(true);
out("Opening the ReSkate menu.");
};
registry.add(std::move(open_menu));
auto arm_command = action("dronearm", "Arm the drone: motors on, ready to fly", Group::movement);
arm_command.inspect = [](const Model &) { return State{true, {}, {}, {}, false}; };
arm_command.run = [](const Model &, const Values &, const Output &out) {
@@ -165,7 +172,7 @@ void register_movement_commands(Commands &registry) {
Group::movement, argument("0|1", Type::boolean));
radio_mode.inspect = [](const Model &m) {
return boolean_state(m.debug.available, drone::radio::config().enabled.load(), "Waiting for local player controls.",
"Set it up on the DRONE page (dronemenu), CONTROLLER tab");
"Set it up in Addons > Drone (dronemenu), CONTROLLER tab");
};
radio_mode.run = [](const Model &, const Values &args, const Output &out) {
const bool on = std::get<bool>(args[0]);
+1 -1
View File
@@ -189,7 +189,7 @@ inline constexpr KnobInfo knob_info[] = {
{"motorpitch", "Motor sound pitch. Higher sounds like a smaller drone", 0.4f, 2.5f, 1.0f},
{"throttlecentre", "Your throttle stick springs back to the middle (gamepads): the middle hovers, up climbs, down descends", 0.0f, 1.0f, 0.0f},
{"deadzone", "How far a stick must move before the drone reacts (0-0.3). Raise it if the drone drifts on its own", 0.0f, 0.3f, 0.03f},
{"goggles", "What your skater wears over their eyes while you fly: 0 = nothing, 1-8 = one of the eyewear choices on the DRONE page", 0.0f, 8.0f, 1.0f},
{"goggles", "What your skater wears over their eyes while you fly: 0 = nothing, 1-8 = one of the eyewear choices in Addons > Drone", 0.0f, 8.0f, 1.0f},
{"hideui", "Hide the game's own interface while you fly or the drone follows you (Start flying or Follow me hides it, stopping brings it back)", 0.0f, 1.0f, 1.0f},
{"gamepad", "Fly with a gamepad (Xbox or PlayStation) instead of an RC radio: left stick throttle and yaw, right stick pitch and roll, with gentler sticks. A arms, Menu / Options puts the drone back", 0.0f, 1.0f, 1.0f},
{"padrate", "Gamepad: how much of your rates the sticks reach at full tilt (0.2-1). Lower is calmer. In angle mode it slows turning only: full stick still leans all the way", 0.2f, 1.0f, 0.6f},
+1 -1
View File
@@ -12,7 +12,7 @@ void register_trainer_commands(Commands &registry) {
};
const Verb verbs[]{
{"status", "What the trainer is doing", nullptr, false},
{"open", "Open the menu on the trainer page", "[tune|presets|practice|map|realistic|fun|everything]", false},
{"open", "Open the menu on the Sandbox page", "[tune|presets|practice|map|realistic|fun|everything]", false},
{"set", "Set a physics tuning value live", "<value id> <number>", true},
{"freeze", "Keep a value through presets and resets", "<value id> 0|1", true},
{"reset", "Put a value (or all) back to stock", "<value id>|all", false},
+151 -36
View File
@@ -96,6 +96,31 @@ void filter_rows(Page &p, const trainer::View &view) {
void value_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::View &view, const trainer::Row &row,
bool with_group, bool friendly) {
if (console_style()) {
// Full screen: a switch, or the value nudged a step at a time (a fiftieth of its size).
ImGui::PushID(row.id.c_str());
const auto label = friendly && !row.friendly.empty() ? row.friendly : with_group ? row.group + " / " + row.label : row.label;
const auto hint = std::format("{}. The game's own is {}.{}", row.id, number(row.stock),
row.used ? "" : " The game was not found reading it, so changing it may do nothing.");
const bool can = view.editable && callbacks.queue_console_command;
if (row.kind == trainer::Kind::flag) {
bool on = row.value != 0;
if (toggle_row(menu, label.c_str(), hint.c_str(), on, can)) // (a label that changes would lose the focus)
trainer_command(menu, callbacks, std::format("set {} {}", row.id, on ? 1 : 0));
} else {
field(menu, (row.touched ? label + " *" : label).c_str(), hint.c_str());
const bool scale = row.kind == trainer::Kind::curve || row.kind == trainer::Kind::graph;
const bool whole = row.kind == trainer::Kind::integer;
double value = row.value;
set_console_step(whole ? 1.0 : scale ? 0.05 : std::max({std::abs(row.stock), std::abs(value), 0.01}) * 0.02);
ImGui::BeginDisabled(!can);
if (slider_double("##value", &value, scale ? 0.0 : -1.0e6, 1.0e6, whole ? "%.0f" : scale ? "x %.2f" : "%.4g", ImGuiSliderFlags_AlwaysClamp))
trainer_command(menu, callbacks, std::format("set {} {}", row.id, number(value)));
ImGui::EndDisabled();
}
ImGui::PopID();
return;
}
ImGui::PushID(row.id.c_str());
bool frozen = row.frozen;
if (ImGui::Checkbox("##freeze", &frozen)) trainer_command(menu, callbacks, std::format("freeze {} {}", row.id, frozen ? 1 : 0));
@@ -157,6 +182,18 @@ void preset_switch(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer:
}
// A built-in preset as a dial: one multiplier that moves everything the preset moves.
void dial_row(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const trainer::PresetRow &preset) {
if (console_style()) {
ImGui::PushID(preset.name.c_str());
const auto hint = std::format("{} 1 is the game's own; \"{}\" is x {}.", preset.note, preset.name, number(preset.amount));
field(menu, std::abs(preset.factor - 1.0) > 1e-4 ? (preset.title + " *").c_str() : preset.title.c_str(), hint.c_str());
double value = preset.factor;
const double low = preset.amount < 1 ? 0.05 : 0.2, high = preset.amount < 1 ? 3.0 : 10.0;
if (slider_double("##dial", &value, low, high, "x %.2f", ImGuiSliderFlags_Logarithmic) && value > 0)
trainer_command(menu, callbacks, std::format("dial {} {}", number(value), preset.name));
ImGui::PopID();
(void)p;
return;
}
ImGui::PushID(preset.name.c_str());
const float column = ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x * 0.40f;
const bool moved = std::abs(preset.factor - 1.0) > 1e-4;
@@ -216,7 +253,10 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
}
if (!view.editable) warn(view.blocked.c_str());
// Two short lists and the whole table.
{
if (console_style()) {
static const std::vector<std::string> modes{"Realistic: tone it down", "Fun: turn it up", "Everything"};
option_row(menu, "Showing", "Left / right: a short list to tone the game down, one to turn it up, or every value.", p.mode, modes);
} else {
static constexpr const char *modes[]{"REALISTIC", "FUN", "EVERYTHING"};
const float width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3;
for (int i = 0; i < 3; ++i) {
@@ -237,6 +277,17 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
if (preset.builtin && preset.dial && (p.mode == 2 || (preset.modes & list))) dial_row(menu, callbacks, p, preset);
// The presets that are plain switches, and the ones that set several dials at once.
bool first = true;
if (console_style())
for (const auto &preset : view.presets) {
// Full screen, every preset as a switch: on while all it sets holds.
if (!preset.builtin || preset.name == "Stock" || !(p.mode == 2 || (preset.modes & list))) continue;
bool on = preset.active;
ImGui::PushID(preset.name.c_str());
if (toggle_row(menu, preset.name.c_str(), preset.note.c_str(), on, can_edit))
trainer_command(menu, callbacks, std::string(on ? "preset apply " : "preset remove ") + preset.name);
ImGui::PopID();
}
else
for (const auto &preset : view.presets) {
if (!preset.builtin || preset.dial || preset.name == "Stock" || !(p.mode == 2 || (preset.modes & list))) continue;
if (!first && ImGui::GetItemRectMax().x + ImGui::CalcTextSize(preset.name.c_str()).x + ImGui::GetStyle().FramePadding.x * 4 <
@@ -247,13 +298,23 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
}
ImGui::EndDisabled();
if (p.mode != 0) {
// The game's own bail protection, the same switch as Skater > Movement.
// The game's own bail protection, the same switch as Skater > Cheats.
const auto &debug = model.debug;
bool no_bail = debug.no_bail;
if (toggle_row(menu, "Never bail", "No wipeouts at all. Recover from a bail before switching it on.", no_bail,
(debug.no_bail_available || debug.no_bail) && callbacks.queue_debug != nullptr))
debug_request(menu, callbacks, {DebugAction::set_no_bail, no_bail});
}
if (console_style()) {
const auto changed = std::format("{} values changed. Every one back to the game's own; locked values stay.", view.touched);
const int pressed = button_row(menu, "tune-reset", {{"RESET VALUES", can_edit && view.touched > 0, false, changed.c_str()},
{"RESET EVERYTHING", callbacks.queue_console_command != nullptr, false, "The game as it shipped: every value, lock, preset and trick slider."}});
if (pressed == 0) trainer_command(menu, callbacks, "reset all");
if (pressed == 1) {
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false;
trainer_command(menu, callbacks, "reset everything");
}
} else {
ImGui::BeginDisabled(!can_edit || !view.touched);
if (ImGui::Button("Reset values")) trainer_command(menu, callbacks, "reset all");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Every tuning value back to the game's own. Locked values stay.");
@@ -261,6 +322,7 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("%zu values changed", view.touched);
}
end_card();
ImGui::BeginDisabled(!can_edit);
@@ -269,6 +331,25 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
if (p.mode == 2) {
begin_card(menu, "preset-save", "YOUR PRESETS", "Save what is changed right now");
if (console_style()) {
for (const auto &preset : view.presets) {
if (preset.builtin) continue;
const int pressed = button_row(menu, preset.name.c_str(), {
{preset.name.c_str(), can_edit, preset.active, preset.active ? "On: A turns it off." : "A turns it on."},
{"USE ON THIS MAP", can_edit, view.profile_preset == preset.name, "Apply it every time this map loads."},
{"DELETE", can_edit, false, "Delete this preset."}});
if (pressed == 0) trainer_command(menu, callbacks, std::string(preset.active ? "preset remove " : "preset apply ") + preset.name);
if (pressed == 1) trainer_command(menu, callbacks, "profile set " + preset.name);
if (pressed == 2) trainer_command(menu, callbacks, "preset delete " + preset.name);
}
if (!view.map.empty()) {
info(menu, "This map applies", view.profile_preset.empty() ? "nothing" : view.profile_preset);
if (!view.profile_preset.empty() && button_row(menu, "profile-clear", {{"STOP APPLYING IT ON THIS MAP"}}) == 0)
trainer_command(menu, callbacks, "profile clear");
}
note("Name and save a preset in the window menu (View): it needs a keyboard.");
end_card();
} else {
ImGui::BeginDisabled(!can_edit);
const float button = ImGui::CalcTextSize("Turn off").x + ImGui::GetStyle().FramePadding.x * 2;
for (const auto &preset : view.presets) {
@@ -307,12 +388,24 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
note("\"This map\" beside one of your presets applies it every time this map loads.");
}
end_card();
}
}
// The values themselves: the short list's, or all of them.
ImGui::Spacing();
ImGui::SeparatorText(p.mode == 2 ? "EVERY VALUE" : "FINE TUNING");
if (p.mode == 2) {
if (p.mode == 2 && console_style()) {
trainer::note_class_list_shown();
p.search[0] = 0; // (searching wants a keyboard: the window menu has it)
std::vector<std::string> groups{"Every group"};
for (const auto &group : view.groups) groups.push_back(group);
int group = std::max(p.group, 1) - 1;
if (option_row(menu, "Group", "Left / right for one group of values.", group, groups)) p.group = group + 1;
toggle_row(menu, "Only what I changed", "Only the values you changed or locked.", p.only_changed);
toggle_row(menu, "Graph points", "Every point of the tuning graphs, not just one multiplier per graph.", p.graph_points);
toggle_row(menu, "Values with no use found", "Also the values the game was never found reading. Changing those will probably do nothing.",
p.show_unused);
} else if (p.mode == 2) {
// The list includes the game's tuning classes, which the trainer has to find in memory
// before an edit to one does anything: with the list open, it looks now.
trainer::note_class_list_shown();
@@ -338,10 +431,11 @@ void tune_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks,
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Also list the tuning values that the game was never found or seen reading.\nChanging those will probably do nothing.");
} else {
note("The same values the dials move, one at a time. The box on the left locks a value against presets, dials and Reset values.");
note(console_style() ? "The same values the dials move, one at a time. A * marks one you changed."
: "The same values the dials move, one at a time. The box on the left locks a value against presets, dials and Reset values.");
}
filter_rows(p, view);
if (p.mode == 2) {
if (p.mode == 2 && !console_style()) {
ImGui::SameLine();
if (p.hidden_unused) ImGui::TextDisabled("%zu shown, %zu changed, %zu hidden", p.shown.size(), view.touched, p.hidden_unused);
else ImGui::TextDisabled("%zu shown, %zu changed", p.shown.size(), view.touched);
@@ -362,6 +456,17 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
const auto slider = [&](const char *label, const char *option, float value, float &edit, bool &editing, float high = 50.0f, bool limited = false) {
field(menu, label);
ImGui::PushID(option);
if (console_style()) {
float shown = editing ? edit : value;
if (slider_float("##height", &shown, 0.1f, high, "x %.2f", (limited ? ImGuiSliderFlags_AlwaysClamp : 0) | ImGuiSliderFlags_Logarithmic)) {
edit = shown;
editing = true;
}
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option {} {:.2f}", option, edit));
if (!ImGui::IsItemActive() && editing && std::abs(value - edit) < 0.005f * std::max(1.0f, std::abs(edit))) editing = false;
ImGui::PopID();
return;
}
float shown = editing ? edit : value;
const float box = px(64);
ImGui::SetNextItemWidth(std::max(px(70), ImGui::GetContentRegionAvail().x - box - ImGui::GetStyle().ItemSpacing.x));
@@ -388,7 +493,7 @@ void trick_heights(SkateMenu &menu, const CallbacksV3 &callbacks, Page &p, const
slider("Boneless height", "boneless_height", view.boneless_height, p.boneless_edit, p.boneless_editing);
slider("Hippy jump height", "hippy_height", view.hippy_height, p.hippy_edit, p.hippy_editing);
slider("Off-board jump height", "offboard_height", view.offboard_height, p.offboard_edit, p.offboard_editing);
if (ImGui::Button("Reset tricks")) {
if (button_row(menu, "tricks-reset", {{"RESET TRICKS"}}) == 0) {
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false;
trainer_command(menu, callbacks, "reset tricks");
}
@@ -429,13 +534,13 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
};
field(menu, "Speed");
auto value = static_cast<float>(current);
if (ImGui::SliderFloat("##game-speed", &value, 0.05f, 2.0f, current == 0.0 ? "paused" : "%.2fx", ImGuiSliderFlags_AlwaysClamp))
if (::dingosdk::overlay::menu::slider_float("##game-speed", &value, 0.05f, 2.0f, current == 0.0 ? "paused" : "%.2fx", ImGuiSliderFlags_AlwaysClamp))
set(value);
if (ImGui::Button(current == 0.0 ? "Resume" : "Pause")) set(current == 0.0 ? 1.0 : 0.0);
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);
}
const int pressed = button_row(menu, "speeds", {{current == 0.0 ? "RESUME" : "PAUSE", true, current == 0.0}, {"0.1x", true, current == 0.1},
{"0.25x", true, current == 0.25}, {"0.5x", true, current == 0.5}, {"0.75x", true, current == 0.75}, {"1x", true, current == 1.0}});
constexpr std::array<double, 5> quick{0.1, 0.25, 0.5, 0.75, 1.0};
if (pressed == 0) set(current == 0.0 ? 1.0 : 0.0);
else if (pressed > 0) set(quick[static_cast<std::size_t>(pressed - 1)]);
note("The jump read-out measures real time, so it reads low while the game is slowed.");
}
}
@@ -446,23 +551,20 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
if (choice(menu, "marker-slot", slot, {"1", "2", "3", "4", "5"})) trainer_command(menu, callbacks, std::format("slot {}", slot + 1));
const auto &marker = view.markers[static_cast<std::size_t>(std::clamp(view.slot, 0, static_cast<int>(trainer::marker_slots) - 1))];
info(menu, "Selected", marker.set ? std::format("{:.1f}, {:.1f}, {:.1f}", marker.position[0], marker.position[1], marker.position[2]) : "empty");
ImGui::BeginDisabled(view.map.empty());
ImGui::BeginDisabled(!telemetry.skater);
if (ImGui::Button("Save here")) trainer_command(menu, callbacks, "marker save");
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(!marker.set);
if (ImGui::Button("Go")) trainer_command(menu, callbacks, "marker go");
ImGui::SameLine();
if (ImGui::Button("Clear")) trainer_command(menu, callbacks, "marker clear");
ImGui::EndDisabled();
ImGui::EndDisabled();
{
const bool map = !view.map.empty();
const int pressed = button_row(menu, "marker-actions", {{"SAVE HERE", map && telemetry.skater, false, "Put the selected marker where you are."},
{"GO", map && marker.set, false, "Go to the selected marker."}, {"CLEAR", map && marker.set, false, "Empty the selected marker."}});
if (pressed == 0) trainer_command(menu, callbacks, "marker save");
if (pressed == 1) trainer_command(menu, callbacks, "marker go");
if (pressed == 2) trainer_command(menu, callbacks, "marker clear");
}
bool auto_return = view.auto_return;
if (toggle_row(menu, "Return after a bail", "Go back to the selected marker when the skater wipes out.", auto_return))
trainer_command(menu, callbacks, std::format("option auto_return {}", auto_return ? 1 : 0));
field(menu, "Return delay");
float delay = view.return_delay;
ImGui::SliderFloat("##return-delay", &delay, 0.0f, 10.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp);
::dingosdk::overlay::menu::slider_float("##return-delay", &delay, 0.0f, 10.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemDeactivatedAfterEdit()) trainer_command(menu, callbacks, std::format("option return_delay {:.1f}", delay));
bool pad = view.pad_shortcuts;
if (toggle_row(menu, "Controller shortcuts", "Hold LB + RB, then D-pad: up saves, down goes, left / right pick the slot.", pad))
@@ -474,11 +576,20 @@ void practice_tab(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
trainer::note_teleport_card_shown();
info(menu, "Map waypoint", view.waypoint_set ? std::format("{:.1f}, {:.1f}, {:.1f}", view.waypoint[0], view.waypoint[1], view.waypoint[2])
: "none - place one on the pause map");
ImGui::BeginDisabled(!view.waypoint_set || !telemetry.skater);
if (ImGui::Button("Go to map waypoint")) trainer_command(menu, callbacks, "waypoint");
ImGui::EndDisabled();
if (button_row(menu, "waypoint", {{"GO TO MAP WAYPOINT", view.waypoint_set && telemetry.skater, false, "The waypoint you placed on the pause map."}}) == 0)
trainer_command(menu, callbacks, "waypoint");
if (telemetry.skater)
info(menu, "You are at", std::format("{:.2f}, {:.2f}, {:.2f}", telemetry.position[0], telemetry.position[1], telemetry.position[2]));
if (console_style()) {
// Typing a position wants a keyboard: full screen keeps one, set from where you are.
info(menu, "Saved spot", std::format("{:.2f}, {:.2f}, {:.2f}", p.teleport[0], p.teleport[1], p.teleport[2]));
const int pressed = button_row(menu, "teleport-actions", {{"SET IT TO HERE", telemetry.skater, false, "Keep where you are now."},
{"GO THERE", telemetry.skater, false, "Teleport to the saved spot."}});
if (pressed == 0) p.teleport = telemetry.position;
if (pressed == 1) trainer_command(menu, callbacks, std::format("tp {:.3f} {:.3f} {:.3f}", p.teleport[0], p.teleport[1], p.teleport[2]));
end_card();
return;
}
field(menu, "X, Y, Z");
ImGui::InputFloat3("##teleport", p.teleport.data(), "%.2f");
ImGui::BeginDisabled(!telemetry.skater);
@@ -543,16 +654,18 @@ void map_tab(SkateMenu &menu, const CallbacksV3 &callbacks, const trainer::View
if (!view.map_note.empty()) note(view.map_note.c_str());
if (!view.map_preset.empty()) {
info(menu, "Author's preset", view.map_preset);
ImGui::BeginDisabled(!view.editable);
if (ImGui::Button("Apply the author's preset")) trainer_command(menu, callbacks, "preset apply map");
ImGui::EndDisabled();
if (button_row(menu, "author-preset", {{"APPLY THE AUTHOR'S PRESET", view.editable}}) == 0) trainer_command(menu, callbacks, "preset apply map");
}
for (std::size_t i = 0; i < view.spots.size(); ++i) {
ImGui::PushID(static_cast<int>(i));
if (ImGui::Button("Go")) trainer_command(menu, callbacks, std::format("spot {}", i + 1));
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(view.spots[i].name.c_str());
if (console_style()) {
if (button_row(menu, "spot", {{("GO TO " + view.spots[i].name).c_str()}}) == 0) trainer_command(menu, callbacks, std::format("spot {}", i + 1));
} else {
if (ImGui::Button("Go")) trainer_command(menu, callbacks, std::format("spot {}", i + 1));
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(view.spots[i].name.c_str());
}
ImGui::PopID();
}
if (view.map_note.empty() && view.map_preset.empty() && view.spots.empty())
@@ -581,6 +694,7 @@ bool trainer_page_wanted() {
void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) {
auto &p = page();
const auto view = trainer::view();
if (!console_style()) {
ImGui::BeginDisabled(!callbacks.queue_console_command);
if (ImGui::Button("RESET EVERYTHING")) {
p.hippy_editing = p.nocomply_editing = p.boneless_editing = p.offboard_editing = p.flip_editing = false;
@@ -590,9 +704,10 @@ void trainer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callba
ImGui::SameLine();
ImGui::AlignTextToFramePadding();
ImGui::TextDisabled("The game as it shipped: every value, lock, preset and trick slider.");
category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "MAP & HUD"}, "trainer-tabs");
}
category_tabs(menu, p.tab, {"TUNE", "PRACTICE", "MAP & HUD"}, "trainer-tabs"); // (the cheats are on Skater > Cheats)
ImGui::PushID(p.tab);
ImGui::BeginChild("trainer-tab", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("trainer-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
switch (p.tab) {
case 0: tune_tab(menu, model, callbacks, p, *view); break;
case 1: practice_tab(menu, model, callbacks, p, *view); break;
+113
View File
@@ -2,6 +2,7 @@
#include "atmosphere_color.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <set>
namespace dingosdk::overlay::menu {
@@ -43,6 +44,77 @@ void control_row(SkateMenu& menu,const WorldControlsModel& model,const Callbacks
state.waiting=true;state.pending=std::move(v);state.deadline=ImGui::GetTime()+2.0;
}
};
if (console_style()) {
// Full screen: a row for the control (a lane of a vector or colour each), changed with
// left / right; saved once a run of steps ends, as a slider is once let go.
ImGui::PushID(c.key);
std::string hint=c.key;
if (reading.varies) hint+=". Values vary across this level's environments";
if (saved && !reading.applied) hint+=". The saved value is waiting to apply";
hint+=".";
bool immediate=false,changed=false,editing=false,deactivated=false;
if (c.type==AtmosphereType::texture) {
const auto& names=model.textures[i];
int at=-1;
for (std::size_t j=0;j<names.size();++j) if (names[j]==state.value.texture) at=static_cast<int>(j);
if (option_row(menu,c.label,names.empty() ? "No authored texture for this slot is loaded in this level." : hint.c_str(),at,names,!names.empty()) && at>=0) {
state.value.texture=names[static_cast<std::size_t>(at)];changed=immediate=true;
}
} else if (c.type==AtmosphereType::toggle) {
bool enabled=state.value.number[0]!=0;
if (toggle_row(menu,c.label,hint.c_str(),enabled)) { state.value.number[0]=enabled ? 1 : 0;changed=immediate=true; }
} else if (c.type==AtmosphereType::enumeration) {
std::vector<std::string> names;
int at=-1;
for (unsigned j=0;j<c.option_count;++j) {
const auto& option=atmosphere_options[c.option_start+j];
names.emplace_back(option.label);
if (state.value.number[0]==option.value) at=static_cast<int>(j);
}
if (option_row(menu,c.label,hint.c_str(),at,names) && at>=0) {
state.value.number[0]=atmosphere_options[c.option_start+static_cast<unsigned>(at)].value;changed=immediate=true;
}
} else {
ImGui::BeginGroup();
if (c.type==AtmosphereType::color) {
auto color=atmosphere_color_to_ui(state.value);
static constexpr const char* channels[]{" red"," green"," blue"};
for (int k=0;k<3;++k) {
field(menu,(std::string(c.label)+channels[k]).c_str(),hint.c_str());
ImGui::PushID(k);
set_console_step(0.02);
if (slider_float("##lane",&color[static_cast<std::size_t>(k)],0.0f,1.0f,"%.2f",ImGuiSliderFlags_AlwaysClamp)) {
state.value=atmosphere_color_from_ui(color,state.value);changed=true;
}
ImGui::PopID();
}
} else {
static constexpr const char* axes[]{" X"," Y"," Z"," W"};
for (unsigned lane=0;lane<c.lanes;++lane) {
field(menu,(std::string(c.label)+(c.lanes>1 ? axes[std::min(lane,3u)] : "")).c_str(),hint.c_str());
ImGui::PushID(static_cast<int>(lane));
double value=state.value.number[lane];
const double range=c.maximum-c.minimum;
set_console_step(c.type==AtmosphereType::integer ? 1.0 : range>0 && range<=1e4 ? range/100 : std::max(std::abs(value)*0.05,0.01));
if (slider_double("##lane",&value,c.minimum,c.maximum,c.type==AtmosphereType::integer ? "%.0f" : "%.4g",ImGuiSliderFlags_AlwaysClamp)) {
state.value.number[lane]=value;changed=true;
}
ImGui::PopID();
}
}
ImGui::EndGroup();
editing=ImGui::IsItemActive();
deactivated=ImGui::IsItemDeactivatedAfterEdit();
}
state.dirty |= changed;
state.editing=editing;
if (state.dirty && (immediate || deactivated || !state.editing)) {
submit(state.value);
state.dirty=false;
}
ImGui::PopID();
return;
}
ImGui::PushID(c.key);
ImGui::TableNextRow();ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding();ImGui::TextWrapped("%s",c.label);
@@ -116,6 +188,47 @@ void control_row(SkateMenu& menu,const WorldControlsModel& model,const Callbacks
}
void atmosphere_menu(SkateMenu& menu,const Model& model,const CallbacksV3& callbacks) {
const auto& controls=model.world_controls;
if (console_style()) {
// Full screen: one group at a time (fog, sky or wind), its controls as rows under their
// section names; no search box (that wants a keyboard).
static int kind=0;
static const std::vector<std::string> groups{atmosphere_groups.begin(),atmosphere_groups.end()};
const bool can=controls.environment_available && callbacks.queue_console_command;
begin_card(menu,"atmosphere","FOG, SKY AND WIND","Changes apply at once and save by themselves");
option_row(menu,"Showing","Left / right: the fog, the sky or the wind.",kind,groups);
const auto group=static_cast<unsigned>(std::clamp(kind,0,2));
std::vector<unsigned> rows;
std::vector<std::string_view> sections;
for (unsigned i=0;i<dingosdk::atmosphere_controls.size();++i) {
const auto& c=dingosdk::atmosphere_controls[i];
if (atmosphere_kind(c.property)!=group) continue;
rows.push_back(i);
if (std::find(sections.begin(),sections.end(),c.section)==sections.end()) sections.push_back(c.section);
}
const int pressed=button_row(menu,"atmosphere-reset",{
{"RESTORE THESE DEFAULTS",can,false,"This group back to the level's own settings."},
{"RESTORE ALL DEFAULTS",can,false,"Fog, sky and wind back to the level's own settings."}});
if (pressed==0 && send(menu,callbacks,std::string(atmosphere_groups[group])+".reset"))
for (const auto index:rows) { auto& e=menu.atmosphere_edit[index];e.waiting=true;e.pending.reset();e.deadline=ImGui::GetTime()+2.0; }
if (pressed==1 && send(menu,callbacks,"environment reset_environment"))
for (auto& e:menu.atmosphere_edit) { e.waiting=true;e.pending.reset();e.deadline=ImGui::GetTime()+2.0; }
if (!controls.environments[group]) note(("Waiting for this level's "+std::string(atmosphere_groups[group])+" component.").c_str());
const auto& choices=controls.choices;
if (group==0 ? choices.fog>=0 || choices.fog_distance>=0 : group==1 ? choices.sky_brightness>=0 || choices.clouds>=0
: choices.wind_strength>=0 || choices.wind_direction>=0)
warn("Console modifiers are active. Restore defaults clears these too.");
ImGui::BeginDisabled(!can);
for (const auto section_name:sections) {
if (section_name!=atmosphere_groups[group]) section(menu,std::string(section_name).c_str());
if (group==1 && (section_name=="Physical atmosphere" || section_name=="Primary light" || section_name=="Secondary light"))
note("Used when Sky Type is Physical.");
for (const auto i:rows) if (dingosdk::atmosphere_controls[i].section==section_name) control_row(menu,controls,callbacks,i);
}
ImGui::EndDisabled();
if (!controls.status.empty()) note(controls.status.c_str());
end_card();
return;
}
ImGui::Spacing();
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##atmosphere-search","Search fog, sky, and wind controls...",menu.atmosphere_search.data(),menu.atmosphere_search.size());
+22 -2
View File
@@ -3,6 +3,9 @@
#include "input_capture.h"
#include "Engine/Core/Log/logging.h"
#include <array>
#include <cfloat>
#include <climits>
#include <cmath>
#include <atomic>
#include <cstdint>
@@ -230,7 +233,23 @@ void update_menu_pointer() {
const bool watching = !active && s.hub_pointer.load() && s.input_attached && !s.stop && !s.failed &&
game_window_foreground(s.window);
io.AddFocusEvent(active || watching);
io.MouseDrawCursor = active;
// The controller in charge of the full screen menu: no pointer until the mouse moves or a button
// of it is pressed.
// (The real pointer and buttons: these reads are the game's, faked, outside OverlayInputAccess.)
static POINT last{LONG_MIN, LONG_MIN};
POINT now{};
bool have_now{}, clicked{};
{
OverlayInputAccess real;
have_now = GetCursorPos(&now) != 0;
clicked = ((GetAsyncKeyState(VK_LBUTTON) | GetAsyncKeyState(VK_RBUTTON) | GetAsyncKeyState(VK_MBUTTON)) & 0x8000) != 0;
}
const bool mouse_moved = have_now && last.x != LONG_MIN && (std::abs(now.x - last.x) > 2 || std::abs(now.y - last.y) > 2);
if (have_now) last = now;
auto& pad_in_charge = dingosdk::overlay::menu_pad_in_charge();
if (mouse_moved || clicked || !dingosdk::overlay::menu_fullscreen_shown().load()) pad_in_charge.store(false);
const bool hidden = active && pad_in_charge.load();
io.MouseDrawCursor = active && !hidden;
if (!active && !watching) {
io.ClearInputKeys(); io.ClearInputMouse();
s.raw_mouse_buttons.store(0);
@@ -241,7 +260,8 @@ void update_menu_pointer() {
POINT point{};
// The Win32 backend only polls when it thinks WM_MOUSEMOVE tracking ended.
// Skate can stop legacy messages during gameplay without WM_MOUSELEAVE.
if (GetCursorPos(&point) && ScreenToClient(s.window.load(), &point))
if (hidden) io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); // (pointing at nothing: no tile lights up under it)
else if (GetCursorPos(&point) && ScreenToClient(s.window.load(), &point))
io.AddMousePosEvent(static_cast<float>(point.x), static_cast<float>(point.y));
const bool swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0;
const std::array keys{swapped ? VK_RBUTTON : VK_LBUTTON, swapped ? VK_LBUTTON : VK_RBUTTON,
+15 -11
View File
@@ -39,7 +39,7 @@ void slider(SkateMenu& menu, const CallbacksV3& callbacks, Knob k, const char* l
field(menu, label, help.c_str());
float v = get(k);
ImGui::PushID(static_cast<int>(k));
if (ImGui::SliderFloat("##value", &v, drone::knob_info[k].lo, drone::knob_info[k].hi, format)) set(k, v);
if (::dingosdk::overlay::menu::slider_float("##value", &v, drone::knob_info[k].lo, drone::knob_info[k].hi, format)) set(k, v);
if (ImGui::IsItemDeactivatedAfterEdit()) save(callbacks, k);
ImGui::PopID();
}
@@ -48,7 +48,7 @@ void integer(SkateMenu& menu, const CallbacksV3& callbacks, Knob k, const char*
field(menu, label, help.c_str());
int v = static_cast<int>(std::lround(get(k)));
ImGui::PushID(static_cast<int>(k));
if (ImGui::SliderInt("##value", &v, static_cast<int>(drone::knob_info[k].lo), static_cast<int>(drone::knob_info[k].hi)))
if (::dingosdk::overlay::menu::slider_int("##value", &v, static_cast<int>(drone::knob_info[k].lo), static_cast<int>(drone::knob_info[k].hi)))
set(k, static_cast<float>(v));
if (ImGui::IsItemDeactivatedAfterEdit()) save(callbacks, k);
ImGui::PopID();
@@ -172,7 +172,7 @@ void fly_tab(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks)
(sent_at > 0.0 && ImGui::GetTime() - sent_at > 1.0))) { pending = -1.0f; sent_at = 0.0; }
float top = pending >= 0.0f ? pending : model.debug.camera_speed;
field(menu, "Top speed", "The same setting as freecam's flight speed.\n\nConsole: flyspeed");
if (ImGui::SliderFloat("##top-speed", &top, 0.6f, 300.0f, "%.1f", ImGuiSliderFlags_Logarithmic)) pending = top;
if (::dingosdk::overlay::menu::slider_float("##top-speed", &top, 0.6f, 300.0f, "%.1f", ImGuiSliderFlags_Logarithmic)) pending = top;
if (ImGui::IsItemDeactivatedAfterEdit()) { run(callbacks, std::format("flyspeed {}", top)); sent_at = ImGui::GetTime(); }
// The flight model's drag is set so full throttle flat out settles near four times the fly speed (m/s).
const auto speed = std::format("Flat out at about {:.0f} km/h (a little faster with boost).", top * 4.0f * 3.6f);
@@ -260,7 +260,7 @@ void handling_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
if (limited) {
field(menu, "Range");
float metres = get(Knob::kRange);
if (ImGui::SliderFloat("##range", &metres, 50.0f, 5000.0f, "%.0f m", ImGuiSliderFlags_Logarithmic)) set(Knob::kRange, metres);
if (::dingosdk::overlay::menu::slider_float("##range", &metres, 50.0f, 5000.0f, "%.0f m", ImGuiSliderFlags_Logarithmic)) set(Knob::kRange, metres);
if (ImGui::IsItemDeactivatedAfterEdit()) save(callbacks, Knob::kRange);
const auto explained = std::format(
"Measured from where you took off. The picture starts to break up at {:.0f} m and is lost at {:.0f} m. Stay without a "
@@ -552,16 +552,20 @@ void lobby_tab(SkateMenu& menu, const CallbacksV3& callbacks) {
} // namespace
void drone_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
category_tabs(menu, menu.drone_tab, {"FLY", "HANDLING", "RATES", "DISPLAY", "CONTROLLER", "LOBBY"}, "drone-tabs");
category_tabs(menu, menu.drone_tab, {"FLY", "FLIGHT", "DISPLAY", "CONTROLLER"}, "drone-tabs");
ImGui::PushID(menu.drone_tab);
ImGui::BeginChild("drone-tab", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("drone-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
switch (menu.drone_tab) {
case 0: fly_tab(menu, model, callbacks); break;
case 1: handling_tab(menu, callbacks); break;
case 2: rates_tab(menu, callbacks); break;
case 3: display_tab(menu, callbacks); break;
case 4: controller_tab(menu, callbacks); break;
default: lobby_tab(menu, callbacks); break;
case 1:
handling_tab(menu, callbacks);
rates_tab(menu, callbacks);
break;
case 2:
display_tab(menu, callbacks);
lobby_tab(menu, callbacks);
break;
default: controller_tab(menu, callbacks); break;
}
ImGui::EndChild();
ImGui::PopID();
+63 -21
View File
@@ -46,6 +46,9 @@ CallbacksV3 hub_callbacks; // under the mutex
} // namespace
void set_hub_page_feed(HubPageFeed feed) noexcept { page_feed.store(feed); }
void set_ui_sound(UiSound play) noexcept { ui_sound.store(play); }
void play_ui_sound(std::uint32_t sound) noexcept {
if (const auto play = ui_sound.load()) play(sound);
}
void set_hub_callbacks(const CallbacksV3 &callbacks) noexcept {
std::lock_guard lock(hub_callbacks_mutex);
hub_callbacks = callbacks;
@@ -157,6 +160,8 @@ struct Hub {
ULONGLONG join_asked{};
std::string failed, failed_name; // why the last join did not get in, and where to; empty: nothing to say
ULONGLONG notice_until{};
// The frame the ReSkate menu last drew these pages itself (draw_online_embedded).
int embedded_frame{-10};
};
Hub &hub() {
static Hub value;
@@ -1454,22 +1459,14 @@ bool hub_page_pending() {
return shown;
}
void draw_hub_page() {
auto &h = hub();
if (!h.shown) return;
// One frame of the pages, in `a`..`z` with their tabs at `tabs_y`: in the game's pause menu
// (draw_hub_page), or on the ReSkate menu's full screen ONLINE page (draw_online_embedded), which
// keeps B for itself.
void hub_frame(Hub &h, ImDrawList *draw, float u, ImVec2 a, ImVec2 z, float tabs_y, bool input, bool embedded) {
auto &s = state();
auto &io = ImGui::GetIO();
const auto display = io.DisplaySize;
// One unit of the game's menu in pixels, and where its page sits in this window.
const float u = std::min(display.x / layout_width, display.y / layout_height);
const float across = display.x / u, down = display.y / u, canvas = std::min(across, layout_widest);
const float page_left = ((across - canvas) * .5f + side_margin) * u, page_right = ((across + canvas) * .5f - side_margin) * u;
const float lower = (down - layout_height) * .05f; // how far a taller window moves the page down
const bool others = s.visible.load() || s.console_visible.load() || s.editor_visible.load() || s.chat_visible.load();
Ui ui{h, ImGui::GetBackgroundDrawList(), s.menu.bold ? s.menu.bold : ImGui::GetFont(), s.menu.body ? s.menu.body : ImGui::GetFont(), nullptr, u, io.MousePos,
// (A card that asks something over the page has the input while it asks.)
!others && !s.prompt_input_active.load(std::memory_order_relaxed) && game_window_foreground(s.window.load()),
false, false, false, 0, h.model, {}};
Ui ui{h, draw, s.menu.bold ? s.menu.bold : ImGui::GetFont(), s.menu.body ? s.menu.body : ImGui::GetFont(), nullptr, u, io.MousePos,
input, false, false, false, 0, h.model, {}};
ui.heading = s.menu.heading ? s.menu.heading : ui.bold;
ui.clicked = ui.input && ImGui::IsMouseClicked(ImGuiMouseButton_Left);
ui.doubled = ui.input && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
@@ -1537,16 +1534,16 @@ void draw_hub_page() {
int previous_tab{}, next_tab{};
if (ui.input && !typing_before) {
// Back closes the game's menu, which takes a moment to say so: this goes at once.
if (key_new(7) || (pressed & 0x2000)) h.dismissed = true;
if (!embedded && (key_new(7) || (pressed & 0x2000))) h.dismissed = true;
const std::array<bool, 4> held{keys[0] || (ui.pad.buttons & 1), keys[1] || (ui.pad.buttons & 2), keys[2] || (ui.pad.buttons & 4), keys[3] || (ui.pad.buttons & 8)};
const std::array<bool, 4> fresh{key_new(0) || (pressed & 1), key_new(1) || (pressed & 2), key_new(2) || (pressed & 4), key_new(3) || (pressed & 8)};
const auto tick = GetTickCount64();
int direction = -1;
for (int i = 0; i < 4; ++i)
if (fresh[static_cast<std::size_t>(i)]) direction = i, h.repeat_at = tick + 380;
if (fresh[static_cast<std::size_t>(i)]) direction = i, h.repeat_at = tick + (embedded ? 420 : 380); // (the full screen menu's pace inside it)
if (direction < 0 && tick >= h.repeat_at)
for (int i = 0; i < 4; ++i)
if (held[static_cast<std::size_t>(i)]) direction = i, h.repeat_at = tick + 70;
if (held[static_cast<std::size_t>(i)]) direction = i, h.repeat_at = tick + (embedded ? 160 : 70);
if (direction >= 0) {
// The first press only shows where the focus is.
if (h.ring || !h.focus) move_focus(h, direction == 2 ? -1 : direction == 3 ? 1 : 0, direction == 0 ? -1 : direction == 1 ? 1 : 0);
@@ -1567,9 +1564,7 @@ void draw_hub_page() {
h.items.clear();
// ---- the page's tabs
const ImVec2 a(page_left, (body_top + lower) * u);
const ImVec2 z(std::min(page_right, a.x + body_most * u), (down - (layout_height - body_bottom) - lower) * u);
const bool tools = h.page.section == 1;
const bool tools = !embedded && h.page.section == 1;
{
// (Each page keeps the tab it was left on.)
std::vector<std::pair<int, const char *>> tabs;
@@ -1595,7 +1590,7 @@ void draw_hub_page() {
h.sound = sound_tab;
set_typing(h, 0);
}
const float top = (tabs_top + lower) * u, bottom = top + tabs_height * u, size = 44 * u;
const float top = tabs_y, bottom = top + tabs_height * u, size = 44 * u;
float x = a.x;
const auto cap = [&](std::uint32_t pad_button, const char *key) {
const auto name = ui.pad.available ? controller_combo_label(pad_button, ui.pad.style) : std::string(key);
@@ -1662,4 +1657,51 @@ void draw_hub_page() {
if (const auto play = ui_sound.load(); play && (wanted != sound_navigate || tick - h.sound_at >= 45)) play(wanted), h.sound_at = tick;
}
}
void draw_hub_page() {
auto &h = hub();
if (!h.shown) return;
// (The ReSkate menu drew these pages over this one this frame: once is enough.)
if (h.embedded_frame == ImGui::GetFrameCount()) return;
auto &s = state();
const auto display = ImGui::GetIO().DisplaySize;
// One unit of the game's menu in pixels, and where its page sits in this window.
const float u = std::min(display.x / layout_width, display.y / layout_height);
const float across = display.x / u, down = display.y / u, canvas = std::min(across, layout_widest);
const float page_left = ((across - canvas) * .5f + side_margin) * u, page_right = ((across + canvas) * .5f - side_margin) * u;
const float lower = (down - layout_height) * .05f; // how far a taller window moves the page down
const bool others = s.visible.load() || s.console_visible.load() || s.editor_visible.load() || s.chat_visible.load();
const ImVec2 a(page_left, (body_top + lower) * u);
const ImVec2 z(std::min(page_right, a.x + body_most * u), (down - (layout_height - body_bottom) - lower) * u);
// (A card that asks something over the page has the input while it asks.)
hub_frame(h, ImGui::GetBackgroundDrawList(), u, a, z, (tabs_top + lower) * u,
!others && !s.prompt_input_active.load(std::memory_order_relaxed) && game_window_foreground(s.window.load()), false);
}
void draw_online_embedded(ImDrawList *draw, ImVec2 a, ImVec2 z, const Model &model, const CallbacksV3 &callbacks, bool input) {
auto &h = hub();
const int frame = ImGui::GetFrameCount();
const bool fresh = h.embedded_frame != frame - 1;
h.embedded_frame = frame;
const auto now = Clock::now();
if (fresh) {
// What is held as the page comes up is not a press on it.
h.last.clear();
h.focus = 0;
h.searched = false;
h.keys_before.fill(true);
h.pad_before = ~0U;
}
if (fresh || now >= h.next_model) {
h.next_model = now + 200ms;
h.full = model;
h.model = model.multiplayer;
h.callbacks = callbacks;
h.rows_stale = true;
}
const auto display = ImGui::GetIO().DisplaySize;
const float u = std::min(display.x / layout_width, display.y / layout_height);
hub_frame(h, draw, u, ImVec2(a.x, a.y + (tabs_height + 64) * u), z, a.y, input, true);
}
bool online_embedded_typing() { return hub().typing != 0; }
} // namespace dingosdk::overlay::detail
@@ -92,6 +92,7 @@ void set_map_download_icon(const std::string& png) noexcept {
} // namespace dingosdk::overlay
namespace dingosdk::overlay::detail {
bool decode_picture(const std::string& png, CardPixels& out, UINT side) { return decode(png, out, side); }
bool take_card_pixels(CardPixels& out) {
std::lock_guard lock(image_mutex);
if (!waiting) return false;
+144
View File
@@ -0,0 +1,144 @@
#include "overlay_internal.h"
#include "skate_menu_internal.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
// The pictures on the menu's map grid (Map > Travel). The grid asks with the levels it lists;
// Assets/map_icons.cpp answers off the game's thread (an official map's loading screen, a custom
// map's mod icon), and the pictures go into one texture, a square each, in the SRV heap's third
// slot (overlay_render.cpp, upload_menu_map_image).
namespace dingosdk::overlay {
namespace {
constexpr UINT cell = 256, columns = 8;
constexpr std::size_t most = 64; // 4 MB of pixels at most
std::string folded(std::string_view text) {
std::string result(text);
for (auto& c : result) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return result;
}
struct Pictures {
std::mutex mutex;
std::string asked; // the levels last asked for, one asset a line
std::vector<MenuMapWanted> request; // not yet taken by map_icons
// The texture: the pixels kept to upload again after a device reset, and where each level's
// picture is in them (by folded asset). `ready` are those of the pixels on the GPU now.
detail::CardPixels current, waiting_pixels;
std::map<std::string, UINT, std::less<>> waiting_cells, ready_cells;
UINT width{}, height{}, ready_width{}, ready_height{};
bool waiting{};
unsigned serial{};
ImTextureID texture{};
};
Pictures& pictures() { static auto* value = new Pictures; return *value; } // process lifetime
}
void want_menu_map_pictures(std::vector<MenuMapWanted> levels) {
std::string key;
for (const auto& level : levels) key += folded(level.asset) + '\n';
auto& p = pictures();
std::lock_guard lock(p.mutex);
if (key == p.asked) return;
p.asked = std::move(key);
if (levels.size() > most) levels.resize(most);
p.request = std::move(levels);
}
std::vector<MenuMapWanted> take_menu_map_request() noexcept {
auto& p = pictures();
std::lock_guard lock(p.mutex);
return std::exchange(p.request, {});
}
void set_menu_map_pictures(const std::vector<MapVoteIcon>& found) noexcept {
try {
std::vector<std::pair<std::string, detail::CardPixels>> squares;
for (const auto& icon : found) {
if (squares.size() >= most) break;
detail::CardPixels one;
bool ok{};
if (icon.side == cell && icon.rgba.size() == std::size_t{cell} * cell * 4) {
one.rgba = icon.rgba;
one.width = one.height = cell;
ok = true;
} else if (!icon.png.empty() && icon.png.size() <= 4 * 1024 * 1024) {
ok = detail::decode_picture(icon.png, one, cell) && one.width == cell && one.height == cell;
}
if (ok) squares.emplace_back(folded(icon.name), std::move(one));
}
auto& p = pictures();
std::lock_guard lock(p.mutex);
if (squares.empty()) return;
const auto count = static_cast<UINT>(squares.size());
detail::CardPixels atlas;
atlas.width = cell * std::min(count, columns);
atlas.height = cell * ((count + columns - 1) / columns);
atlas.rgba.assign(std::size_t{atlas.width} * atlas.height * 4, 0);
std::map<std::string, UINT, std::less<>> cells;
for (UINT i = 0; i < count; ++i) {
const UINT x = (i % columns) * cell, y = (i / columns) * cell;
for (UINT row = 0; row < cell; ++row)
std::memcpy(atlas.rgba.data() + (std::size_t{y + row} * atlas.width + x) * 4,
squares[i].second.rgba.data() + std::size_t{row} * cell * 4, std::size_t{cell} * 4);
cells[squares[i].first] = i;
}
atlas.serial = ++p.serial;
p.width = atlas.width;
p.height = atlas.height;
p.current = atlas;
p.waiting_pixels = std::move(atlas);
p.waiting_cells = std::move(cells);
p.waiting = true;
} catch (...) { /* No pictures: the grid shows each map's name on a plain tile. */ }
}
bool menu_map_picture(const std::string& asset, ImTextureID& texture, ImVec2& uv0, ImVec2& uv1) {
auto& p = pictures();
std::lock_guard lock(p.mutex);
if (!p.texture || !p.ready_width || !p.ready_height) return false;
const auto found = p.ready_cells.find(folded(asset));
if (found == p.ready_cells.end()) return false;
const float x = static_cast<float>((found->second % columns) * cell), y = static_cast<float>((found->second / columns) * cell);
texture = p.texture;
uv0 = ImVec2(x / static_cast<float>(p.ready_width), y / static_cast<float>(p.ready_height));
uv1 = ImVec2((x + cell) / static_cast<float>(p.ready_width), (y + cell) / static_cast<float>(p.ready_height));
return true;
}
namespace detail {
bool take_menu_map_pixels(CardPixels& out) {
auto& p = pictures();
std::lock_guard lock(p.mutex);
if (!p.waiting) return false;
p.waiting = false;
out = std::exchange(p.waiting_pixels, {});
return true;
}
void menu_map_pixels_uploaded(ImTextureID texture, unsigned serial) {
auto& p = pictures();
std::lock_guard lock(p.mutex);
if (serial != p.serial) return;
p.texture = texture;
p.ready_cells = p.waiting_cells;
p.ready_width = p.width;
p.ready_height = p.height;
}
void menu_map_pixels_lost() {
auto& p = pictures();
std::lock_guard lock(p.mutex);
p.texture = {};
if (!p.current.rgba.empty()) {
p.waiting_pixels = p.current;
p.waiting = true;
}
}
}
}
+25 -5
View File
@@ -111,6 +111,28 @@ void board_break_deck_card(SkateMenu& menu, const Model& model, const CallbacksV
const auto in_play = mods::board_break_in_play();
begin_card(menu, "board-break-decks", "BOARD BREAK DECK", in_play.empty() ? "none in the game" : label(in_play).c_str());
const bool busy = live_mods::busy();
if (menu::console_style()) {
// Full screen: one row, left / right to a deck and A to put it in the game.
static int staged = -1;
static std::string staged_from;
std::vector<std::string> names;
int playing = -1;
for (std::size_t i = 0; i < decks.size(); ++i) {
names.push_back(label(decks[i]) + (there(decks[i]) ? "" : " (not installed)"));
if (decks[i] == in_play) playing = static_cast<int>(i);
}
if (staged < 0 || staged >= static_cast<int>(decks.size()) || staged_from != in_play) staged = playing, staged_from = in_play;
bool load = false;
const bool waiting = staged >= 0 && decks[static_cast<std::size_t>(staged)] != in_play;
menu::option_row(menu, waiting ? "Deck - A puts it in###deck" : "Deck###deck",
waiting ? "Press A to put this deck in the game (the level loads again)." : "Left / right to pick a deck, then A to put it in the game.",
staged, names, !busy && callbacks.queue_console_command, &load);
if (load && staged >= 0) {
const auto& deck = decks[static_cast<std::size_t>(staged)];
if (!there(deck)) menu::feedback(menu, "That deck is not installed.");
else if (deck != in_play) send_console(menu, callbacks, "boardbreak deck " + deck);
}
} else {
const float gap = ImGui::GetStyle().ItemSpacing.x;
const float width = (ImGui::GetContentRegionAvail().x - gap * 3) / 4;
for (std::size_t i = 0; i < decks.size(); ++i) {
@@ -123,6 +145,7 @@ void board_break_deck_card(SkateMenu& menu, const Model& model, const CallbacksV
ImGui::EndDisabled();
ImGui::PopID();
}
}
if (busy) note("Applying mods...");
else if (!in_play.empty() && chosen != in_play && there(chosen))
note((label(chosen) + " is chosen and shows once the level has loaded again.").c_str());
@@ -428,7 +451,7 @@ void draw_custom_scripts(SkateMenu& menu) {
menu::end_card();
}
}
void draw_modding_menu(SkateMenu& menu, int tab, const Model& model, const CallbacksV3& callbacks) {
void draw_modding_menu(SkateMenu& menu, int tab, const Model&, const CallbacksV3&) {
// Loose files are switched in the launcher; custom scripts only need to know
// whether they are on.
if (!menu.loose_files_settings_loaded) {
@@ -437,9 +460,6 @@ void draw_modding_menu(SkateMenu& menu, int tab, const Model& model, const Callb
catch (const std::exception&) {}
}
if (tab == 1) draw_custom_scripts(menu);
else {
menu::board_break_deck_card(menu, model, callbacks);
draw_installed_mods(menu);
}
else draw_installed_mods(menu);
}
}
+1 -1
View File
@@ -319,7 +319,7 @@ void join_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks
// The card's real height from the last frame; the estimate only covers the first.
const float code_space = menu.multiplayer_code_height > 0 ? menu.multiplayer_code_height : direct_join_height() + px(8);
const float height = std::max(px(120), ImGui::GetContentRegionAvail().y - code_space);
ImGui::BeginChild("lobby-list", ImVec2(0, height));
ImGui::BeginChild("lobby-list", ImVec2(0, height), ImGuiChildFlags_NavFlattened);
auto *draw = ImGui::GetWindowDrawList();
const float tile_height = px(66), pad = px(14), gap = px(6);
const float join_width = px(110);
+22 -10
View File
@@ -107,7 +107,7 @@ void host_page(SkateMenu &menu, const Model &model, const CallbacksV3 &) {
ImGui::InputTextWithHint("##lobby-name", name_hint.c_str(), menu.multiplayer_lobby_name.data(),
menu.multiplayer_lobby_name.size());
field(menu, "Player limit", "Includes you.");
ImGui::SliderInt("##player-limit", &capacity, 2, multiplayer_lobby_player_limit, "%d players");
::dingosdk::overlay::menu::slider_int("##player-limit", &capacity, 2, multiplayer_lobby_player_limit, "%d players");
menu.multiplayer_capacity = capacity;
field(menu, "Update rate",
"How often nearby skaters update. Higher is smoother but uses more upload bandwidth.");
@@ -251,7 +251,7 @@ void multiplayer_display_settings(SkateMenu &menu, const Model &model) {
field(menu, "Player distance", "How far away other players are still shown as skaters. Past it they keep their nametag "
"or dot. Lower it on a busy server for more frames and less memory; all the way up shows everyone.");
float shown = menu.player_distance_pending.value_or(mp.player_distance);
ImGui::SliderFloat("##player-distance", &shown, player_distance_least, player_distance_unlimited,
::dingosdk::overlay::menu::slider_float("##player-distance", &shown, player_distance_least, player_distance_unlimited,
shown >= player_distance_unlimited ? "Everyone" : "%.0f m", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemActive()) menu.player_distance_pending = shown;
if (ImGui::IsItemDeactivatedAfterEdit()) {
@@ -283,7 +283,7 @@ void multiplayer_display_settings(SkateMenu &menu, const Model &model) {
std::array<char, 65> unused{};
field(menu, "Nametag distance", "How far away a player's name still shows. Past it they are a dot.");
float distance = menu.nametag_distance_pending.value_or(mp.nametag_distance);
ImGui::SliderFloat("##nametag-distance", &distance, 10.f, 500.f, "%.0f m", ImGuiSliderFlags_AlwaysClamp);
::dingosdk::overlay::menu::slider_float("##nametag-distance", &distance, 10.f, 500.f, "%.0f m", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemActive()) menu.nametag_distance_pending = distance;
if (ImGui::IsItemDeactivatedAfterEdit()) {
send_private(menu, "nametag-distance", std::to_string(static_cast<int>(distance)), unused, false);
@@ -338,7 +338,7 @@ void multiplayer_display_settings(SkateMenu &menu, const Model &model) {
std::array<char, 65> unused{};
field(menu, "Bubble distance", "How far away a player can be and still show a chat bubble.");
float distance = menu.chat_bubbles_distance_pending.value_or(mp.chat_bubbles_distance);
ImGui::SliderFloat("##bubble-distance", &distance, 5.f, 200.f, "%.0f m", ImGuiSliderFlags_AlwaysClamp);
::dingosdk::overlay::menu::slider_float("##bubble-distance", &distance, 5.f, 200.f, "%.0f m", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemActive()) menu.chat_bubbles_distance_pending = distance;
if (ImGui::IsItemDeactivatedAfterEdit()) {
send_private(menu, "chat-bubbles-distance", std::to_string(static_cast<int>(distance)), unused, false);
@@ -350,7 +350,7 @@ void multiplayer_display_settings(SkateMenu &menu, const Model &model) {
menu.chat_bubbles_distance_pending.reset();
field(menu, "Bubble duration", "How many seconds a chat bubble stays before it fades.");
float duration = menu.chat_bubbles_duration_pending.value_or(mp.chat_bubbles_duration);
ImGui::SliderFloat("##bubble-duration", &duration, 1.f, 30.f, "%.0f s", ImGuiSliderFlags_AlwaysClamp);
::dingosdk::overlay::menu::slider_float("##bubble-duration", &duration, 1.f, 30.f, "%.0f s", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemActive()) menu.chat_bubbles_duration_pending = duration;
if (ImGui::IsItemDeactivatedAfterEdit()) {
send_private(menu, "chat-bubbles-duration", std::to_string(static_cast<int>(duration)), unused, false);
@@ -362,7 +362,7 @@ void multiplayer_display_settings(SkateMenu &menu, const Model &model) {
menu.chat_bubbles_duration_pending.reset();
field(menu, "Bubble history", "How many recent messages stack above each skater (1-8).");
int history = menu.chat_bubbles_history_pending.value_or(mp.chat_bubbles_history);
ImGui::SliderInt("##bubble-history", &history, 1, 8, "%d lines", ImGuiSliderFlags_AlwaysClamp);
::dingosdk::overlay::menu::slider_int("##bubble-history", &history, 1, 8, "%d lines", ImGuiSliderFlags_AlwaysClamp);
if (ImGui::IsItemActive()) menu.chat_bubbles_history_pending = history;
if (ImGui::IsItemDeactivatedAfterEdit()) {
send_private(menu, "chat-bubbles-history", std::to_string(history), unused, false);
@@ -385,7 +385,7 @@ void multiplayer_network_page(SkateMenu &menu, const Model &model, const Callbac
void special_page(SkateMenu &menu, const Model &model, const CallbacksV3 &) {
const auto &mp = model.multiplayer;
std::array<char, 65> unused{};
ImGui::BeginChild("special", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("special", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
begin_card(menu, "special-tag", "YOUR TAG");
{
// Drawn the way chat and nametags draw it (role_badge.h), and faint while it is off.
@@ -517,11 +517,17 @@ void multiplayer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &ca
const bool has_session = mp.active || mp.lobby_joining;
if (has_session && !menu.multiplayer_session_seen) menu.multiplayer_tab = multiplayer_session_tab;
menu.multiplayer_session_seen = has_session;
category_tabs(menu, menu.multiplayer_tab, {"SERVERS", has_session ? "SESSION" : "HOST", "VOICE", "BANS"},
"multiplayer-categories");
// YOUR TAG only for a player the backend lists (it was the SPECIAL page).
if (mp.identity_tag.empty())
category_tabs(menu, menu.multiplayer_tab, {"SERVERS", has_session ? "SESSION" : "HOST", "VOICE", "BANS", "PLAYERS AND CHAT"},
"multiplayer-categories");
else
category_tabs(menu, menu.multiplayer_tab,
{"SERVERS", has_session ? "SESSION" : "HOST", "VOICE", "BANS", "PLAYERS AND CHAT", "YOUR TAG"},
"multiplayer-categories");
if (menu.multiplayer_tab != multiplayer_voice_tab) menu.voice_bind_capture = 0;
ImGui::PushID(menu.multiplayer_tab);
ImGui::BeginChild("multiplayer-category", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("multiplayer-category", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
switch (menu.multiplayer_tab) {
case 0:
join_page(menu, model, callbacks);
@@ -537,6 +543,12 @@ void multiplayer_page(SkateMenu &menu, const Model &model, const CallbacksV3 &ca
case multiplayer_bans_tab:
bans_page(menu, model);
break;
case multiplayer_chat_tab:
menu::multiplayer_display_settings(menu, model);
break;
default:
menu::special_page(menu, model, callbacks);
break;
}
ImGui::EndChild();
ImGui::PopID();
+9 -9
View File
@@ -168,7 +168,7 @@ void voice_controls(SkateMenu &menu, const MultiplayerModel &mp) {
field(menu, "Voice range", "How far apart players can be and still hear each other. Past this the host "
"stops sending their voice.");
float range = menu.voice_range_pending.value_or(mp.voice_range);
ImGui::SliderFloat("##voice-range", &range, min_voice_range, max_voice_range, "%.0f m",
::dingosdk::overlay::menu::slider_float("##voice-range", &range, min_voice_range, max_voice_range, "%.0f m",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
if (ImGui::IsItemActive()) menu.voice_range_pending = range;
if (ImGui::IsItemDeactivatedAfterEdit()) {
@@ -258,7 +258,7 @@ void voice_controls(SkateMenu &menu, const MultiplayerModel &mp) {
if (ImGui::Button("Choose in Steam voice settings", ImVec2(-FLT_MIN, 0)))
ShellExecuteW(nullptr, L"open", L"steam://settings/voice", nullptr, nullptr, SW_SHOWNORMAL);
field(menu, "Your microphone", "How loud other players hear you.");
changed |= ImGui::SliderFloat("##microphone", &value.microphone, 0.f, max_voice_volume, "%.2fx", ImGuiSliderFlags_AlwaysClamp);
changed |= ::dingosdk::overlay::menu::slider_float("##microphone", &value.microphone, 0.f, max_voice_volume, "%.2fx", ImGuiSliderFlags_AlwaysClamp);
if (mp.voice.transmitting) {
tag(menu, "TRANSMITTING", skate_theme::good);
ImGui::SameLine();
@@ -269,13 +269,13 @@ void voice_controls(SkateMenu &menu, const MultiplayerModel &mp) {
begin_card(menu, "hearing", "HEARING");
field(menu, "Voices volume", "How loud you hear everyone. Adjust single players below.");
changed |= ImGui::SliderFloat("##listening", &value.volume, 0.f, max_voice_volume, "%.2fx", ImGuiSliderFlags_AlwaysClamp);
changed |= ::dingosdk::overlay::menu::slider_float("##listening", &value.volume, 0.f, max_voice_volume, "%.2fx", ImGuiSliderFlags_AlwaysClamp);
changed |= toggle_row(menu, "Proximity voice", "Hear players quieter the further away they are. Off: everyone at full volume.",
value.proximity);
ImGui::BeginDisabled(!value.proximity);
field(menu, "Hearing distance", "How far away you hear nearby players. Voices get quieter with distance and "
"fade out towards this edge.");
changed |= ImGui::SliderFloat("##distance", &value.distance, min_hearing_distance, max_hearing_distance, "%.0f m",
changed |= ::dingosdk::overlay::menu::slider_float("##distance", &value.distance, min_hearing_distance, max_hearing_distance, "%.0f m",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
ImGui::EndDisabled();
end_card();
@@ -314,7 +314,7 @@ void voice_controls(SkateMenu &menu, const MultiplayerModel &mp) {
ImGui::TableNextRow(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding(); cell_text(peer.name);
ImGui::TableNextColumn(); ImGui::SetNextItemWidth(-1);
if (ImGui::SliderFloat("##volume", &gain, 0.f, max_voice_volume, "%.2fx", ImGuiSliderFlags_AlwaysClamp) &&
if (::dingosdk::overlay::menu::slider_float("##volume", &gain, 0.f, max_voice_volume, "%.2fx", ImGuiSliderFlags_AlwaysClamp) &&
send_private(menu, "voice-volume", std::to_string(peer.id) + " " + std::to_string(gain), unused, false))
menu.voice_volume_pending[peer.id] = {gain, ImGui::GetTime() + 2};
ImGui::TableNextColumn();
@@ -348,9 +348,9 @@ void ban_popup(SkateMenu &menu) {
: menu.ban_confirm_name.c_str());
ImGui::PopFont();
const bool server = menu.ban_confirm_on_server;
note(server ? "They are removed now and can never join this server again. Unban them any time in Multiplayer > Bans."
note(server ? "They are removed now and can never join this server again. Unban them any time in Online > Bans."
: "They are removed now and can never join a lobby you host, even with your join code. "
"Unban them any time in Multiplayer > Bans.");
"Unban them any time in Online > Bans.");
ImGui::Dummy(ImVec2(0, px(4)));
const float half = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * .5f;
skate_theme::push_primary_button();
@@ -702,11 +702,11 @@ void session_page(SkateMenu &menu, const Model &model) {
if (!mp.tuning_status.empty()) note(mp.tuning_status.c_str());
ImGui::Dummy(ImVec2(0, px(2)));
bool force_layers = mp.force_world_layers;
if (toggle_row(menu, "Sync world layers", "Guests follow your World > Layers choices and cannot change their own.",
if (toggle_row(menu, "Sync world layers", "Guests follow your Map > Layers and crowds choices and cannot change their own.",
force_layers, controls(mp, "layer-sync"), force_layers ? "ON" : "OFF"))
send_private(menu, "world-layer-sync", force_layers ? "on" : "off", unused, false);
if (mp.dedicated)
note("The server's layers are set with server commands: layer <key> on|off (keys are listed in World > Layers).");
note("The server's layers are set with server commands: layer <key> on|off (keys are listed in Map > Layers and crowds).");
else note(force_layers ? "Guests follow the host's world layers; their layer controls are locked."
: "Everyone picks their own world layers. Turning sync off restores each guest's own choices.");
end_card();
+36
View File
@@ -16,6 +16,7 @@
#include "Engine/Core/Console/console_entry.h"
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <string>
@@ -275,6 +276,9 @@ struct Model {
ObjectPersistenceModel object_persistence;
ParkEditorModel editor;
float menu_scale = default_menu_scale;
float menu_scale_fullscreen = default_menu_scale; // the full screen menu's own size (`ui scalefull`)
// The ReSkate menu fills the screen, made for a controller, instead of a window (`ui fullscreen`).
bool menu_fullscreen = false;
// Offline mode: no Steam, so every multiplayer page and control is hidden.
bool steam_offline = false;
ControllerBindingsModel bindings;
@@ -348,6 +352,13 @@ struct MapVoteIcon {
unsigned side{};
};
void set_map_vote_icons(const std::vector<MapVoteIcon>& icons) noexcept;
// The menu's map grid (Map > Travel): the levels it shows ask for their pictures, which
// Assets/map_icons.cpp finds off the game's thread (MapVoteIcon::name is then the level's asset).
struct MenuMapWanted {
std::string asset, name;
};
std::vector<MenuMapWanted> take_menu_map_request() noexcept;
void set_menu_map_pictures(const std::vector<MapVoteIcon>& pictures) noexcept;
// That vote's popup (map_download_card.cpp), for the runtime, which reads the controller and
// holds the game's input while it is up: whether it is, moving the pick, voting for the pick
// (the popup then closes), and closing it without a vote. Any thread.
@@ -491,6 +502,9 @@ struct CallbacksV3 {
// moving the focus, 7 pressing something, 56 changing tab). Any thread.
using UiSound = void (*)(std::uint32_t sound) noexcept;
void set_ui_sound(UiSound) noexcept;
// One of the game's menu sounds, by its number (UI/NativeMenu/ui_sound.h): 3 moving the focus,
// 7 pressing, 56 changing tab, as the pause menu's own pages make them.
void play_ui_sound(std::uint32_t sound) noexcept;
// What those pages read the game with and act through: callbacks as the overlay's own, with a
// read_model that takes a snapshot and leaves the console log alone (it is asked a few times a
// second while a page is drawn).
@@ -537,6 +551,28 @@ struct Gamepad {
unsigned buttons = 0;
};
Gamepad read_gamepad();
// The full screen menu's page draws its own controls with their own focus (ONLINE: the pause
// menu's pages): the controller then drives that, not ImGui's navigation.
inline std::atomic<bool>& menu_page_owns_pad() {
static std::atomic<bool> value{false};
return value;
}
// The controller is in charge of the full screen menu: the pointer is hidden (and points at
// nothing) until the mouse moves or clicks (overlay_render.cpp sets it, cursor.cpp clears it).
inline std::atomic<bool>& menu_pad_in_charge() {
static std::atomic<bool> value{false};
return value;
}
// The menu was last drawn full screen (its controller pace, overlay_render.cpp).
inline std::atomic<bool>& menu_fullscreen_shown() {
static std::atomic<bool> value{false};
return value;
}
// `menu` (and the controller bind for it) asks for the ReSkate menu to open; the overlay takes it.
inline std::atomic<bool>& menu_open_flag() {
static std::atomic<bool> value{false};
return value;
}
// Reads of a controller between these two calls see the real device even while ReSkate keeps
// the game from reading it (freecam or drone controller capture). Same thread, paired.
// The FPV drone reads its controller this way (Skater/drone_radio.h).
+8
View File
@@ -179,6 +179,8 @@ struct State {
// The map download card's icon (map_download_card.cpp), in the SRV heap's second slot, and
// the buffer it was uploaded from, which lives until the next one replaces it.
ComPtr<ID3D12Resource> card_texture, card_upload;
// The menu's map pictures (menu_map_pictures.cpp) in the third slot, likewise.
ComPtr<ID3D12Resource> menu_map_texture, menu_map_upload;
ComPtr<ID3D12GraphicsCommandList> commands;
ComPtr<ID3D12Fence> fence;
HANDLE fence_event = nullptr;
@@ -304,6 +306,12 @@ struct CardPixels {
bool take_card_pixels(CardPixels& pixels);
void card_image_uploaded(ImTextureID texture, unsigned serial);
void card_image_lost();
// Decodes a PNG to RGBA; with `side`, scaled to a square of that many pixels.
bool decode_picture(const std::string& png, CardPixels& out, UINT side);
// The menu's map pictures (menu_map_pictures.cpp), the same way, in the SRV heap's third slot.
bool take_menu_map_pixels(CardPixels& pixels);
void menu_map_pixels_uploaded(ImTextureID texture, unsigned serial);
void menu_map_pixels_lost();
// Game debug text (game_text_overlay.cpp): polled every presented frame.
bool game_text_pending();
void draw_game_text();
+100 -19
View File
@@ -77,6 +77,8 @@ void destroy_graphics() {
s.frames.clear();
s.card_texture.Reset(); s.card_upload.Reset();
card_image_lost();
s.menu_map_texture.Reset(); s.menu_map_upload.Reset();
menu_map_pixels_lost();
s.commands.Reset(); s.rtvs.Reset(); s.srvs.Reset(); s.fence.Reset(); s.device.Reset();
if (s.fence_event) CloseHandle(s.fence_event);
s.fence_event = nullptr;
@@ -284,7 +286,7 @@ bool setup_graphics() {
return false;
}
heap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heap.NumDescriptors = 2; // ImGui's atlas, then the map download card's icon (upload_card_image)
heap.NumDescriptors = 3; // ImGui's atlas, the map download card's icon, the menu's map pictures (upload_image)
heap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
graphics_result = s.device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&s.srvs));
if (FAILED(graphics_result)) {
@@ -524,17 +526,76 @@ void draw_menu() {
if (!visible) s.visible.store(false);
}
// The map download card's icon (map_download_card.cpp), when one is waiting: made into a
// texture in the SRV heap's second slot. Recorded on the frame's command list ahead of ImGui's
// draws, so the copy is done before anything samples it. Nothing here is fatal: without the
// icon the card shows a plain tile.
void upload_card_image(State& s) {
CardPixels pixels;
if (!take_card_pixels(pixels) || pixels.rgba.empty() || !s.device || !s.srvs) return;
// Frames still on the GPU may sample the icon this one replaces.
if (s.card_texture && s.next_fence && !completed(s.next_fence, render_fence_timeout_ms)) return;
s.card_texture.Reset();
s.card_upload.Reset();
// The ReSkate menu on a controller: while it is open the pad drives ImGui's navigation (the game
// does not see the pad then), and nothing of it reaches the overlay otherwise. X is left out: ImGui
// holds it for its window switcher.
void feed_menu_gamepad(bool menu_open) {
auto& io = ImGui::GetIO();
// Holding a direction in the full screen menu waits before it repeats, then moves at a pace
// the eye can follow (ImGui's own, made for a keyboard, races through a list).
const bool slow = menu_open && dingosdk::overlay::menu_fullscreen_shown().load();
io.KeyRepeatDelay = slow ? 0.42f : 0.275f;
io.KeyRepeatRate = slow ? 0.16f : 0.05f;
// A page with its own focus (the full screen ONLINE page) takes the D-pad and the arrow keys itself.
if (menu_open && dingosdk::overlay::menu_page_owns_pad().load()) io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard;
else io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
struct Button { unsigned bit; ImGuiKey key; };
static constexpr Button buttons[]{
{0x0001, ImGuiKey_GamepadDpadUp}, {0x0002, ImGuiKey_GamepadDpadDown}, {0x0004, ImGuiKey_GamepadDpadLeft},
{0x0008, ImGuiKey_GamepadDpadRight}, {0x0010, ImGuiKey_GamepadStart}, {0x0020, ImGuiKey_GamepadBack},
{0x0040, ImGuiKey_GamepadL3}, {0x0080, ImGuiKey_GamepadR3}, {0x0100, ImGuiKey_GamepadL1}, {0x0200, ImGuiKey_GamepadR1},
{0x1000, ImGuiKey_GamepadFaceDown}, {0x2000, ImGuiKey_GamepadFaceRight}, {0x8000, ImGuiKey_GamepadFaceUp}};
static bool fed = false;
const auto pad = menu_open ? dingosdk::overlay::read_gamepad() : dingosdk::overlay::Gamepad{};
{
// A button pressed, a trigger pulled or a stick pushed in the full screen menu hands it to
// the controller: the pointer hides until the mouse is used again.
static unsigned before{};
const bool used = pad.valid && ((pad.buttons & ~before) != 0 || pad.left_trigger > 0.5f || pad.right_trigger > 0.5f ||
std::abs(pad.left_x) > 0.5f || std::abs(pad.left_y) > 0.5f || std::abs(pad.right_x) > 0.5f ||
std::abs(pad.right_y) > 0.5f);
before = pad.valid ? pad.buttons : 0;
if (used && slow) dingosdk::overlay::menu_pad_in_charge().store(true);
if (!menu_open) dingosdk::overlay::menu_pad_in_charge().store(false);
}
if (!pad.valid && !fed) return;
for (const auto& button : buttons) io.AddKeyEvent(button.key, (pad.buttons & button.bit) != 0);
io.AddKeyAnalogEvent(ImGuiKey_GamepadL2, pad.left_trigger > 0.5f, pad.left_trigger);
io.AddKeyAnalogEvent(ImGuiKey_GamepadR2, pad.right_trigger > 0.5f, pad.right_trigger);
// The left stick moves like the D-pad, past a dead zone.
constexpr float dead = 0.35f;
const auto stick = [&](ImGuiKey key, float value) {
const float past = std::clamp((value - dead) / (1.0f - dead), 0.0f, 1.0f);
io.AddKeyAnalogEvent(key, value > dead, past);
};
stick(ImGuiKey_GamepadLStickLeft, -pad.left_x);
stick(ImGuiKey_GamepadLStickRight, pad.left_x);
stick(ImGuiKey_GamepadLStickUp, pad.left_y);
stick(ImGuiKey_GamepadLStickDown, -pad.left_y);
// The right stick scrolls the full screen menu's page.
stick(ImGuiKey_GamepadRStickUp, pad.right_y);
stick(ImGuiKey_GamepadRStickDown, -pad.right_y);
fed = pad.valid;
if (fed && !(menu_open && dingosdk::overlay::menu_page_owns_pad().load())) {
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
} else if (fed) {
io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad;
io.BackendFlags |= ImGuiBackendFlags_HasGamepad;
} else {
io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad;
io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad;
}
}
// Pixels made into a texture in the SRV heap's slot `slot`: the map download card's icon (1) or
// the menu's map pictures (2). Recorded on the frame's command list ahead of ImGui's draws, so
// the copy is done before anything samples it. Returns the texture's id for ImGui, or 0. Nothing
// here is fatal: without it a card shows a plain tile.
ImTextureID upload_image(State& s, const CardPixels& pixels, UINT slot, ComPtr<ID3D12Resource>& texture_out,
ComPtr<ID3D12Resource>& upload_out) {
texture_out.Reset();
upload_out.Reset();
D3D12_RESOURCE_DESC texture{};
texture.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
texture.Width = pixels.width;
@@ -565,10 +626,10 @@ void upload_card_image(State& s) {
IID_PPV_ARGS(&made))) ||
FAILED(s.device->CreateCommittedResource(&upload_heap, D3D12_HEAP_FLAG_NONE, &buffer, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr,
IID_PPV_ARGS(&upload))))
return;
return {};
void* mapped{};
const D3D12_RANGE none{};
if (FAILED(upload->Map(0, &none, &mapped)) || !mapped) return;
if (FAILED(upload->Map(0, &none, &mapped)) || !mapped) return {};
for (UINT row = 0; row < pixels.height; ++row)
std::memcpy(static_cast<unsigned char*>(mapped) + layout.Offset + std::size_t{row} * layout.Footprint.RowPitch,
pixels.rgba.data() + std::size_t{row} * pixels.width * 4, std::size_t{pixels.width} * 4);
@@ -590,18 +651,35 @@ void upload_card_image(State& s) {
const auto step = s.device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
auto cpu = s.srvs->GetCPUDescriptorHandleForHeapStart();
auto gpu = s.srvs->GetGPUDescriptorHandleForHeapStart();
cpu.ptr += step;
gpu.ptr += step;
cpu.ptr += std::size_t{step} * slot;
gpu.ptr += std::uint64_t{step} * slot;
D3D12_SHADER_RESOURCE_VIEW_DESC view{};
view.Format = texture.Format;
view.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
view.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
view.Texture2D.MipLevels = 1;
s.device->CreateShaderResourceView(made.Get(), &view, cpu);
s.card_texture = std::move(made);
s.card_upload = std::move(upload);
texture_out = std::move(made);
upload_out = std::move(upload);
// Drawable from the next frame on: this frame's draws were made before the copy was recorded.
card_image_uploaded(static_cast<ImTextureID>(gpu.ptr), pixels.serial);
return static_cast<ImTextureID>(gpu.ptr);
}
void upload_card_image(State& s) {
CardPixels pixels;
if (!take_card_pixels(pixels) || pixels.rgba.empty() || !s.device || !s.srvs) return;
// Frames still on the GPU may sample the icon this one replaces.
if (s.card_texture && s.next_fence && !completed(s.next_fence, render_fence_timeout_ms)) return;
if (const auto id = upload_image(s, pixels, 1, s.card_texture, s.card_upload)) card_image_uploaded(id, pixels.serial);
}
// The menu's map pictures (menu_map_pictures.cpp), when a new set is waiting.
void upload_menu_map_image(State& s) {
if (!s.device || !s.srvs) return;
if (s.menu_map_texture && s.next_fence && !completed(s.next_fence, render_fence_timeout_ms)) return;
CardPixels pixels;
if (!take_menu_map_pixels(pixels) || pixels.rgba.empty()) return;
if (const auto id = upload_image(s, pixels, 2, s.menu_map_texture, s.menu_map_upload)) menu_map_pixels_uploaded(id, pixels.serial);
}
void render(IDXGISwapChain* presented, UINT flags) {
@@ -656,6 +734,7 @@ void render(IDXGISwapChain* presented, UINT flags) {
const bool hub_frame = hub_page_pending();
if (trainer_open_requested()) s.visible.store(true);
if (dingosdk::drone::menu_flag().load()) s.visible.store(true); // `dronemenu`: the menu opens on its DRONE page
if (dingosdk::overlay::menu_open_flag().exchange(false)) s.visible.store(true); // `menu`, or its controller bind
const bool menu_frame = interactive_visible(s);
if (!menu_frame) {
if (s.ui_was_interactive) {
@@ -710,6 +789,7 @@ void render(IDXGISwapChain* presented, UINT flags) {
// (Watching the pointer for the pause menu's browser keeps the mouse state between frames,
// or every frame would look like a fresh click.)
if (!interactive) { ImGui::GetIO().ClearInputKeys(); if (!hub_frame) ImGui::GetIO().ClearInputMouse(); }
feed_menu_gamepad(interactive && s.visible.load() && !s.editor_visible.load());
ImGui::GetIO().MouseDrawCursor = owns_menu_cursor(s);
ImGui_ImplDX12_NewFrame();
{ OverlayInputAccess access; ImGui_ImplWin32_NewFrame(); }
@@ -752,6 +832,7 @@ void render(IDXGISwapChain* presented, UINT flags) {
ImGui::SetCurrentContext(previous); s.failed = true; restore_input(true); return;
}
upload_card_image(s);
upload_menu_map_image(s);
D3D12_RESOURCE_BARRIER barrier{};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = frame.buffer.Get();
+4 -4
View File
@@ -76,7 +76,7 @@ void bus_stops(SkateMenu& menu, const ProgressionModel& model, const CallbacksV3
for (const auto& row : model.bus_stops)
if (!menu.bus_stop_map || static_cast<int>(row.world) + 1 == menu.bus_stop_map) rows.push_back(&row);
note((std::to_string(rows.size()) + " bus stops, saved locally. Reload the level after edits to refresh their icons.").c_str());
ImGui::BeginChild("bus-stop-list", ImVec2(0, std::max(px(120), ImGui::GetContentRegionAvail().y)));
ImGui::BeginChild("bus-stop-list", ImVec2(0, std::max(px(120), ImGui::GetContentRegionAvail().y)), ImGuiChildFlags_NavFlattened);
ImGui::BeginDisabled(!callbacks.queue_console_command);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(rows.size()), list_row_height());
@@ -123,7 +123,7 @@ void challenge_progress(SkateMenu& menu, const ProgressionModel& model, const Ca
note((std::to_string(rows.size()) + " challenges, " + std::to_string(done) + " fully complete, " +
(!model.challenges_enabled ? "local challenges disabled" : model.challenges_hidden ? "hidden" : "saved locally") +
". Hover one to see its completed goals.").c_str());
ImGui::BeginChild("challenge-list", ImVec2(0, std::max(px(120), ImGui::GetContentRegionAvail().y)));
ImGui::BeginChild("challenge-list", ImVec2(0, std::max(px(120), ImGui::GetContentRegionAvail().y)), ImGuiChildFlags_NavFlattened);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(rows.size()), list_row_height());
while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) {
@@ -245,7 +245,7 @@ void missions(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks)
? (std::to_string(rows.size()) + " missions, saved locally. Reload the level after changing an active mission.").c_str()
: model.mission_feedback.c_str());
if (!model.missions_available) warn("The local profile is unavailable.");
ImGui::BeginChild("mission-list", ImVec2(0, std::max(px(120), ImGui::GetContentRegionAvail().y)));
ImGui::BeginChild("mission-list", ImVec2(0, std::max(px(120), ImGui::GetContentRegionAvail().y)), ImGuiChildFlags_NavFlattened);
ImGui::BeginDisabled(!model.missions_available || !callbacks.queue_console_command);
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(rows.size()), list_row_height());
@@ -278,7 +278,7 @@ void progression_page(SkateMenu& menu, const Model& model, const CallbacksV3& ca
category_tabs(menu, menu.progression_tab, {"MISSIONS", "CHALLENGES", "BUS STOPS", "PROFILE"}, "progression-categories");
const bool feedback_shown = !menu.feedback.empty() && ImGui::GetTime() < menu.feedback_until;
ImGui::PushID(menu.progression_tab);
ImGui::BeginChild("progression-category", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("progression-category", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
if (menu.progression_tab == 0) missions(menu, model, callbacks);
else if (!model.progression.available) warn("The local profile is unavailable.");
else {
File diff suppressed because it is too large. Load diff
+17 -2
View File
@@ -71,8 +71,13 @@ struct SkateMenu {
// Live while the scale slider is held; the saved value is authoritative otherwise.
float scale = default_menu_scale;
bool scale_editing{};
// Selected tab on each page; see the Page list in skate_menu.cpp.
int map_tab = 0, world_tab = 0, build_tab = 0, skater_tab = 0, settings_tab = 0, mods_tab = 0;
// Selected tab on each page; see the Page list in skate_menu_internal.h.
int map_tab = 0, build_tab = 0, skater_tab = 0, settings_tab = 0, mods_tab = 0;
int addon = 0; // the ADDONS page's addon (menu::Addon)
// Window or full screen, as just chosen, until the saved setting comes back through the model.
std::optional<bool> fullscreen_pending;
double fullscreen_until = 0;
bool fullscreen_drawn = false; // the last frame was drawn full screen
bool loose_files_settings_loaded{}, loose_files_saved{true};
bool custom_scripts_scanned{};
std::vector<custom_scripts::Script> custom_script_rows;
@@ -105,7 +110,17 @@ struct SkateMenu {
ImFont* title = nullptr; // brushed page titles
ImFont* heading = nullptr; // tile and section headers
ImFont* mono = nullptr;
// The same faces drawn larger (large_font_ratio times), swapped in while the menu is full
// screen, where everything is scaled up: text stays sharp instead of stretched.
ImFont* body_large = nullptr;
ImFont* bold_large = nullptr;
ImFont* heading_large = nullptr;
ImFont* title_large = nullptr;
// Full screen: the page's section rail (its first row of tabs, drawn down the left), indenting
// what follows it until the page is done.
float rail_indent = 0;
};
inline constexpr float large_font_ratio = 30.0f / 17.0f;
void load_skate_fonts(SkateMenu& menu);
void draw_skate_menu(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks,
bool& visible);
+293
View File
@@ -0,0 +1,293 @@
#include "skate_menu_internal.h"
#include "skate_style.h"
#include "Extension/UI/skate_theme.h"
#include "Extension/UI/NativeMenu/native_tools_view.h"
#include "Extension/Skater/drone_flight.h"
#include <algorithm>
#include <array>
#include <imgui_internal.h>
#include <string>
// The ADDONS page (what ReSkate adds to skate.: the drone, Board Break, Road Rash and Hall of Meat,
// music) and the full screen menu's HOME page.
namespace dingosdk::overlay::menu {
using namespace theme;
namespace {
// A tile for an addon on ALL ADDONS: what it is, whether it is on, and a way in.
void addon_tile(SkateMenu& menu, const char* id, const char* title, const char* about, const char* state, ImU32 state_colour,
int addon) {
if (console_style()) {
// Full screen: a tile as the pause menu's, A opens the addon.
const auto at = ImGui::GetCursorScreenPos();
const ImVec2 size((ImGui::GetContentRegionAvail().x - px(12)) * .5f, px(168));
ImGui::PushID(id);
const bool pressed = ImGui::InvisibleButton("##addon", size, ImGuiButtonFlags_EnableNav);
const bool lit = (ImGui::IsItemFocused() && ImGui::GetIO().NavVisible) || ImGui::IsItemHovered();
if (lit) set_console_hint((std::string("Open ") + title + ".").c_str());
auto* draw = ImGui::GetWindowDrawList();
const ImVec2 end(at.x + size.x, at.y + size.y);
skate_theme::rough_rect(draw, at, end, lit ? blue : IM_COL32(26, 26, 26, 235), static_cast<unsigned>(ImGui::GetItemID()) & 0xfff, px(1));
draw->PushClipRect(at, end, true);
draw->AddText(menu.heading, px(26), ImVec2(at.x + px(20), at.y + px(18)), paper, title);
if (state) {
const auto text = menu.bold->CalcTextSizeA(px(13), FLT_MAX, 0, state);
const ImVec2 tag_at(end.x - px(20) - text.x - px(14), at.y + px(22));
draw->AddRectFilled(tag_at, ImVec2(tag_at.x + text.x + px(14), tag_at.y + text.y + px(8)), lit ? paper : state_colour);
draw->AddText(menu.bold, px(13), ImVec2(tag_at.x + px(7), tag_at.y + px(4)), skate_theme::black, state);
}
draw->AddText(menu.body, px(16), ImVec2(at.x + px(20), at.y + px(66)), lit ? paper : muted, about, nullptr, size.x - px(40));
draw->PopClipRect();
ImGui::PopID();
if (pressed) open_page(menu, page_addons, addon);
return;
}
begin_card(menu, id, title);
if (state) {
ImGui::SameLine();
tag(menu, state, state_colour);
}
note(about);
ImGui::Dummy(ImVec2(0, px(2)));
if (ImGui::Button((std::string("OPEN ") + title + "##" + id).c_str(), ImVec2(-FLT_MIN, px(34)))) open_page(menu, page_addons, addon);
end_card();
}
void all_addons(SkateMenu& menu, const Model& model) {
const auto on_off = [](bool available, bool on) -> const char* { return !available ? "NOT AVAILABLE" : on ? "ON" : "OFF"; };
const auto colour = [](bool available, bool on) { return !available ? skate_theme::tile_light : on ? skate_theme::good : skate_theme::tile_light; };
addon_tile(menu, "addon-drone", "DRONE", "Fly an FPV drone from the free camera, or let it follow you and film your line.", nullptr, 0,
addon_drone);
if (console_style()) ImGui::SameLine(0, px(12));
const auto& bb = model.board_break;
addon_tile(menu, "addon-board-break", "BOARD BREAK", "Slams snap your board, boards wear out, and every crack has its sound.",
on_off(bb.available, bb.enabled), colour(bb.available, bb.enabled), addon_board_break);
const auto& rash = model.road_rash;
addon_tile(menu, "addon-road-rash", "ROAD RASH", "Bails leave marks that build up: dust, scrapes, bruises, wounds.",
on_off(rash.available, rash.enabled), colour(rash.available, rash.enabled), addon_road_rash);
if (console_style()) ImGui::SameLine(0, px(12));
const auto& meat = model.hall_of_meat;
addon_tile(menu, "addon-hall-of-meat", "HALL OF MEAT", "Your bails show the bones they hurt and score a Meat card.",
on_off(meat.available, meat.enabled), colour(meat.available, meat.enabled), addon_hall_of_meat);
const auto& grind = model.grind_exit;
addon_tile(menu, "addon-grind-exits", "GRIND EXITS", "Press LT or RT while grinding to pick the side you pop out to.",
on_off(grind.available, grind.enabled), colour(grind.available, grind.enabled), addon_grind_exits);
if (console_style()) ImGui::SameLine(0, px(12));
addon_tile(menu, "addon-music", "MUSIC", "Spotify, YouTube Music, your own files and online radio through the game's music player.",
nullptr, 0, addon_music);
}
}
void addons_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
menu.addon = std::clamp(menu.addon, 0, static_cast<int>(addon_count) - 1);
// A window lists the addons in its side rail; full screen has no rail, so they are tabs here
// (LT / RT on a controller).
if (menu.fullscreen_drawn)
category_tabs(menu, menu.addon, {"ALL ADDONS", "DRONE", "BOARD BREAK", "ROAD RASH", "HALL OF MEAT", "GRIND EXITS", "MUSIC"}, "addon-tabs");
ImGui::PushID(menu.addon);
switch (menu.addon) {
case addon_drone: drone_page(menu, model, callbacks); break;
case addon_music: music_page(menu, model, callbacks); break;
default:
ImGui::BeginChild("addon", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
if (menu.addon == addon_board_break) board_break_addon(menu, model, callbacks);
else if (menu.addon == addon_road_rash) road_rash_addon(menu, model, callbacks);
else if (menu.addon == addon_hall_of_meat) hall_of_meat_addon(menu, model, callbacks);
else if (menu.addon == addon_grind_exits) grind_exits_addon(menu, model, callbacks);
else all_addons(menu, model);
ImGui::EndChild();
break;
}
ImGui::PopID();
}
namespace {
// A HOME tile, in the pause menu's look: rough-cut, blue under the focus or the pointer. Its name
// at the top; what it is now (ON, OFF, the time of day) under it, or a line about the page it
// opens. Returns 1 when pressed (A, a click), -1 / 1 for left / right on a tile that `steps`.
int home_tile(SkateMenu& menu, const char* id, const char* name, const std::string& value, ImVec2 size, bool enabled, bool on,
bool steps, const char* hint, bool big_value = true) {
ImGui::BeginDisabled(!enabled);
const auto at = ImGui::GetCursorScreenPos();
const bool pressed = ImGui::InvisibleButton(id, size, ImGuiButtonFlags_EnableNav);
const bool hovered = ImGui::IsItemHovered(), focused = ImGui::IsItemFocused() && ImGui::GetIO().NavVisible;
const unsigned seed = static_cast<unsigned>(ImGui::GetItemID()) & 0xfff;
ImGui::EndDisabled();
int result = 0;
// A tile that steps is changed as the pause menu's settings are: A to start (it turns white),
// then left / right, and A or B when done. Until then the D-pad moves past it. A click steps it on.
static ImGuiID editing{};
const ImGuiID item = ImGui::GetItemID();
const bool mouse = pressed && hovered && ImGui::GetIO().MouseReleased[0];
if (enabled && steps) {
// (While it is held active ImGui makes no press of A, so A / B / Enter / Esc are read here.)
if (editing == item && (!ImGui::IsItemFocused() || ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false) ||
ImGui::IsKeyPressed(ImGuiKey_GamepadFaceDown, false) || ImGui::IsKeyPressed(ImGuiKey_Escape, false) ||
ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_Space, false)))
editing = 0;
else if (pressed && !mouse) editing = item;
else if (mouse) result = 1;
if (editing == item) {
result = console_step(true);
// Held active while it is changed: B ends the change, not the page.
ImGui::SetActiveID(item, ImGui::GetCurrentWindow());
}
} else if (enabled && pressed) {
result = 1;
}
const bool changing = steps && editing == item;
if (focused || hovered) set_console_hint(changing ? "Left / right to change it. A or B when done." : hint);
auto* draw = ImGui::GetWindowDrawList();
const ImVec2 end(at.x + size.x, at.y + size.y);
const bool lit = enabled && (focused || hovered);
skate_theme::rough_rect(draw, at, end, changing ? paper : lit ? blue : enabled ? IM_COL32(26, 26, 26, 235) : IM_COL32(26, 26, 26, 140), seed, px(1));
draw->PushClipRect(at, end, true);
draw->AddText(menu.bold, px(14), ImVec2(at.x + px(18), at.y + px(14)), changing ? skate_theme::black : lit ? paper : muted, name);
const float value_size = big_value ? px(24) : px(15);
const ImU32 value_ink = changing ? skate_theme::black : !enabled ? muted : lit ? paper : on ? blue : paper;
draw->AddText(big_value ? menu.heading : menu.body, value_size, ImVec2(at.x + px(18), end.y - px(16) - value_size), value_ink, value.c_str(),
nullptr, size.x - px(36));
if (steps && enabled) {
// Left / right change it.
const float s = px(6), mid = end.y - px(16) - value_size * .5f;
const ImU32 arrow = changing ? skate_theme::black : lit ? paper : muted;
draw->AddTriangleFilled(ImVec2(end.x - px(40), mid), ImVec2(end.x - px(40) + s, mid - s), ImVec2(end.x - px(40) + s, mid + s), arrow);
draw->AddTriangleFilled(ImVec2(end.x - px(16), mid), ImVec2(end.x - px(16) - s, mid - s), ImVec2(end.x - px(16) - s, mid + s), arrow);
}
draw->PopClipRect();
return result;
}
void heading(SkateMenu& menu, const char* text) {
ImGui::PushFont(menu.heading);
ImGui::TextUnformatted(text);
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, px(2)));
}
// A console line, without its reply on the page (a quick control says what it did by its tile).
void quietly(const CallbacksV3& callbacks, const std::string& command) {
if (!callbacks.queue_console_command) return;
std::array<char, 256> reply{};
callbacks.queue_console_command(callbacks.user, command.c_str(), reply.data(), reply.size());
}
}
void home_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
ImGui::BeginChild("home", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
const float gap = px(12);
const auto grid = [&](int columns) {
return (ImGui::GetContentRegionAvail().x - gap * static_cast<float>(columns - 1)) / static_cast<float>(columns);
};
const float left = ImGui::GetCursorPosX();
const auto place = [&](float width, int column) { ImGui::SameLine(left + (width + gap) * static_cast<float>(column)); };
const auto& debug = model.debug;
const auto on_off = [](bool value) { return std::string(value ? "ON" : "OFF"); };
const auto request = [&](DebugAction action, bool value) {
if (!callbacks.queue_debug) return;
std::array<char, 256> reply{};
callbacks.queue_debug(callbacks.user, {action, value}, reply.data(), reply.size());
};
// ---- the switches people reach for while skating
heading(menu, "QUICK");
{
constexpr int columns = 5;
const float width = grid(columns);
const ImVec2 size(width, px(96));
namespace tod = dingosdk::native_tools;
static constexpr std::array<const char*, 8> times{"Default", "Morning", "Noon", "Afternoon", "Evening", "Night", "Weather day", "Weather night"};
const bool tod_available = tod::time_of_day_available(model, callbacks);
const int now = time_of_day_shown(model);
const int step = home_tile(menu, "##time", "TIME OF DAY", times[static_cast<std::size_t>(std::clamp(now, 0, 7))], size, tod_available, now != 0,
true, "Press A to change the time of day. Default follows the level's own.");
// One time a press, on from the one last asked for.
if (step) set_time_of_day(menu, model, callbacks, (now + step + 8) % 8, true);
place(width, 1);
if (home_tile(menu, "##noclip", "NOCLIP", on_off(debug.noclip), size, (debug.noclip_available || debug.noclip) && callbacks.queue_debug,
debug.noclip, false, "Fly with the normal player camera, through anything. Includes No Bail.") == 1)
request(DebugAction::set_noclip, !debug.noclip);
place(width, 2);
if (home_tile(menu, "##no-bail", "NO BAIL", on_off(debug.no_bail), size, (debug.no_bail_available || debug.no_bail) && callbacks.queue_debug,
debug.no_bail, false, "Never wipe out.") == 1)
request(DebugAction::set_no_bail, !debug.no_bail);
place(width, 3);
if (home_tile(menu, "##hide-hud", "HIDE HUD", on_off(debug.game_ui_hidden), size, debug.available && debug.ui_available && callbacks.queue_debug,
debug.game_ui_hidden, false, "Keep the view clear for riding and captures.") == 1)
request(DebugAction::set_game_ui_hidden, !debug.game_ui_hidden);
place(width, 4);
if (home_tile(menu, "##first-person", "FIRST PERSON", on_off(debug.first_person), size, debug.available && callbacks.queue_debug,
debug.first_person, false, "See it through your skater's eyes.") == 1)
request(DebugAction::set_first_person, !debug.first_person);
ImGui::Dummy(ImVec2(0, gap - ImGui::GetStyle().ItemSpacing.y));
if (home_tile(menu, "##freecam", "FREECAM", on_off(debug.free_camera), size, debug.camera_available && callbacks.queue_debug,
debug.free_camera, false, "Leave your skater and fly the camera.") == 1)
request(DebugAction::set_free_camera, !debug.free_camera);
place(width, 1);
const bool flying = drone::is_enabled() && debug.free_camera;
if (home_tile(menu, "##drone", "DRONE", flying ? "FLYING" : "FLY IT", size, debug.camera_available && callbacks.queue_console_command, flying,
false, "Take off with the FPV drone, or land it. Its settings are in Addons > Drone.") == 1) {
if (flying) quietly(callbacks, "dronemode 0"), quietly(callbacks, "freecam 0");
else quietly(callbacks, "freecam 1"), quietly(callbacks, "dronemode 1");
}
place(width, 2);
const auto& bb = model.board_break;
if (home_tile(menu, "##board-break", "BOARD BREAK", on_off(bb.enabled), size, bb.available && callbacks.queue_console_command, bb.enabled,
false, "Slams snap your board. Its deck and settings are in Addons > Board Break.") == 1)
quietly(callbacks, bb.enabled ? "boardbreak 0" : "boardbreak 1");
place(width, 3);
const auto& rash = model.road_rash;
if (home_tile(menu, "##road-rash", "ROAD RASH", on_off(rash.enabled), size, rash.available && callbacks.queue_console_command, rash.enabled,
false, "Bails leave marks that build up: dust, scrapes, bruises, wounds.") == 1)
quietly(callbacks, rash.enabled ? "roadrash 0" : "roadrash 1");
place(width, 4);
const auto& meat = model.hall_of_meat;
if (home_tile(menu, "##hall-of-meat", "HALL OF MEAT", on_off(meat.enabled), size, meat.available && callbacks.queue_console_command,
meat.enabled, false, "Your bails show the bones they hurt and score a Meat card.") == 1)
quietly(callbacks, meat.enabled ? "hallofmeat 0" : "hallofmeat 1");
}
ImGui::Dummy(ImVec2(0, px(18)));
// ---- the rest of the menu
heading(menu, "GO TO");
{
constexpr int columns = 5;
const float width = grid(columns);
const ImVec2 size(width, px(96));
const auto& mp = model.multiplayer;
const std::string session = model.steam_offline ? "Offline: no multiplayer"
: mp.active ? (mp.lobby_name.empty() ? std::string("In a session") : mp.lobby_name) + " - " + std::to_string(mp.players) + " / " +
std::to_string(mp.capacity)
: "Servers, hosting, voice";
if (home_tile(menu, "##go-online", "ONLINE", session, ImVec2(width * 2 + gap, size.y), !model.steam_offline, mp.active, false,
"Join a server or a friend's lobby, host your own, voice chat.", false) == 1)
open_page(menu, page_online);
place(width, 2);
if (home_tile(menu, "##go-map", "MAP", "Travel, parks, time and weather", size, true, false, false, "Pick a map and load it.", false) == 1)
open_page(menu, page_map);
place(width, 3);
if (home_tile(menu, "##go-addons", "ADDONS", "Drone, Board Break, grind exits, music", size, true, false, false,
"What ReSkate adds to skate.", false) == 1)
open_page(menu, page_addons, addon_all);
place(width, 4);
if (home_tile(menu, "##go-music", "MUSIC", "Now playing, queue, sources", size, true, false, false, "Your music in the game's player.", false) == 1)
open_page(menu, page_addons, addon_music);
ImGui::Dummy(ImVec2(0, gap - ImGui::GetStyle().ItemSpacing.y));
if (home_tile(menu, "##go-skater", "SKATER", "Freecam, first person, cheats", size, true, false, false, "The free camera, first person, noclip and the boosts.", false) == 1)
open_page(menu, page_skater);
place(width, 1);
if (home_tile(menu, "##go-sandbox", "SANDBOX", "Tuning, practice, HUD", size, true, false, false, "Tune the game's feel and drill your lines.", false) == 1)
open_page(menu, page_sandbox);
place(width, 2);
if (home_tile(menu, "##go-build", "BUILD", "Park editor, park mods", size, true, false, false, "Make the park yours.", false) == 1)
open_page(menu, page_build);
place(width, 3);
if (home_tile(menu, "##go-mods", "MODS", "Yours and the official ones", size, true, false, false, "The mods the game has loaded.", false) == 1)
open_page(menu, page_mods);
place(width, 4);
if (home_tile(menu, "##go-settings", "SETTINGS", "Controls, screen, performance", size, true, false, false,
"Your binds, how the menu looks, the performance HUD.", false) == 1)
open_page(menu, page_settings);
}
ImGui::EndChild();
}
}
+79 -3
View File
@@ -2,6 +2,22 @@
#include "skate_menu.h"
#include <initializer_list>
#include <string>
#include <vector>
namespace dingosdk::overlay::detail {
// The game's pause menu Multiplayer pages (hub_page.cpp), drawn into the full screen menu's ONLINE
// page: in `a`..`z` of `draw`, their own focus taking the D-pad, A and LT / RT. And whether one of
// their text boxes has the keyboard.
void draw_online_embedded(ImDrawList* draw, ImVec2 a, ImVec2 z, const Model& model, const CallbacksV3& callbacks, bool input);
bool online_embedded_typing();
}
namespace dingosdk::overlay {
// The map grid's pictures (menu_map_pictures.cpp): asked for with the levels shown, then each
// level's square, once it is on the GPU.
void want_menu_map_pictures(std::vector<MenuMapWanted> levels);
bool menu_map_picture(const std::string& asset, ImTextureID& texture, ImVec2& uv0, ImVec2& uv1);
}
namespace dingosdk::overlay::menu {
// The menu chrome is laid out in pixels; px() applies the user's menu scale to
@@ -34,6 +50,39 @@ void warn(const char* text);
bool choice(SkateMenu& menu, const char* id, int& selected, std::initializer_list<const char*> options,
bool enabled = true, float width = 0.0f, std::initializer_list<const char*> tooltips = {});
void tag(SkateMenu& menu, const char* text, ImU32 colour);
// Whether the menu is drawing full screen this frame: its shared controls (toggle_row, choice, the
// sliders, cards) draw for a controller then. console_hint is what the focused one does.
bool console_style();
const std::string& console_hint();
// For a full screen control drawn by hand: left / right while it has the focus (-1, 0, 1), which
// then never moves the focus; and the line the bottom bar shows for it.
int console_step(bool focused);
void set_console_hint(const char* hint);
// The step the next full screen slider takes (left / right), in place of a fortieth of its range:
// for a value with no sensible range (nudged where it is), or one that moves in whole units.
void set_console_step(double step);
// A row of buttons. In a window, ImGui's own side by side; full screen, equal tiles across the
// row, the pause menu's look (`on` draws one as chosen). Returns the one pressed, or -1.
struct RowButton {
const char* label;
bool enabled = true;
bool on = false;
const char* hint = nullptr;
};
int button_row(SkateMenu& menu, const char* id, std::initializer_list<RowButton> buttons);
// One of a list, under a label. In a window, a drop-down after a field() label; full screen, one
// row: the label, then "< option >" that left / right change. With `pressed`, A on the row sets it
// (for a choice applied by A, such as a park layout) instead of stepping on.
bool option_row(SkateMenu& menu, const char* label, const char* hint, int& selected, const std::vector<std::string>& options,
bool enabled = true, bool* pressed = nullptr);
// Full screen: one entry of a list (a song) as a tall row, `side` at its right end; true on A.
bool list_row(SkateMenu& menu, const char* id, const std::string& text, const std::string& side, bool current, bool enabled = true);
// ImGui's sliders with a number box beside them for an exact value (none with NoInput, which
// picks from named steps). Same arguments; IsItemActive / IsItemDeactivatedAfterEdit afterwards
// cover the slider and the box together.
bool slider_float(const char* id, float* value, float low, float high, const char* format = "%.3f", ImGuiSliderFlags flags = 0);
bool slider_int(const char* id, int* value, int low, int high, const char* format = "%d", ImGuiSliderFlags flags = 0);
bool slider_double(const char* id, double* value, double low, double high, const char* format = "%.3f", ImGuiSliderFlags flags = 0);
bool primary_button(SkateMenu& menu, const char* label, bool enabled = true);
// Height left for a tab's body, keeping room for the feedback line below it.
float page_body_height(const SkateMenu& menu);
@@ -44,7 +93,7 @@ void send_console(SkateMenu& menu, const CallbacksV3& callbacks, const std::stri
void board_break_deck_card(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks);
void debug_request(SkateMenu& menu, const CallbacksV3& callbacks, DebugRequest request);
// HOST shows the host form until a session exists, then that session (SESSION).
inline constexpr int multiplayer_session_tab = 1, multiplayer_voice_tab = 2, multiplayer_bans_tab = 3;
inline constexpr int multiplayer_session_tab = 1, multiplayer_voice_tab = 2, multiplayer_bans_tab = 3, multiplayer_chat_tab = 4;
void multiplayer_display_settings(SkateMenu&, const Model&);
void special_page(SkateMenu&, const Model&, const CallbacksV3&);
void multiplayer_network_page(SkateMenu&, const Model&, const CallbacksV3&);
@@ -53,7 +102,6 @@ void progression_page(SkateMenu&, const Model&, const CallbacksV3&);
void multiplayer_page(SkateMenu&, const Model&, const CallbacksV3&);
// Sidebar pages.
void map_page(SkateMenu&, const Model&, const CallbacksV3&);
void world_page(SkateMenu&, const Model&, const CallbacksV3&);
void build_page(SkateMenu&, const Model&, const CallbacksV3&);
void skater_page(SkateMenu&, const Model&, const CallbacksV3&);
void settings_page(SkateMenu&, const Model&, const CallbacksV3&);
@@ -64,7 +112,35 @@ void developer_page(SkateMenu&, const Model&, const CallbacksV3&);
void music_page(SkateMenu&, const Model&, const CallbacksV3&);
// The FPV drone (drone_page.cpp).
void drone_page(SkateMenu&, const Model&, const CallbacksV3&);
// Extension/Trainer/trainer_page.cpp
// Extension/Trainer/trainer_page.cpp: the SANDBOX page (once the Trainer).
void trainer_page(SkateMenu&, const Model&, const CallbacksV3&);
bool trainer_page_wanted();
// Skater > CHEATS: noclip, No Bail and the boosts (skate_menu_skater.cpp).
void cheats_tab(SkateMenu&, const Model&, const CallbacksV3&);
// The addons' own pages (skate_menu_skater.cpp), and the ADDONS page itself (skate_menu_addons.cpp).
void board_break_addon(SkateMenu&, const Model&, const CallbacksV3&);
void road_rash_addon(SkateMenu&, const Model&, const CallbacksV3&);
void hall_of_meat_addon(SkateMenu&, const Model&, const CallbacksV3&);
void grind_exits_addon(SkateMenu&, const Model&, const CallbacksV3&);
void addons_page(SkateMenu&, const Model&, const CallbacksV3&);
// The time of day the menu shows: the one last asked for while the game is still changing to it
// (skate_menu_world.cpp). Setting it from a menu control goes through set_time_of_day, `quiet`
// leaving the game's reply off the page.
int time_of_day_shown(const Model&);
void set_time_of_day(SkateMenu&, const Model&, const CallbacksV3&, int next, bool quiet);
// The fullscreen menu's first page: a tile for each part of the menu (skate_menu_addons.cpp).
void home_page(SkateMenu&, const Model&, const CallbacksV3&);
// Shuffle, on the Music addon (skate_menu_settings.cpp).
void music_playback_card(SkateMenu&);
// Whether the menu fills the screen (Settings > Screen > Menu style), and changing it: saved
// with the profile (`ui fullscreen`), shown at once.
bool menu_fullscreen(const SkateMenu&, const Model&);
void set_menu_fullscreen(SkateMenu&, const CallbacksV3&, bool fullscreen);
// The pages, for the shells and the home tiles (skate_menu.cpp).
enum Page {
page_home, page_map, page_build, page_skater, page_sandbox, page_addons, page_online, page_progress, page_mods,
page_settings, page_developer, page_count
};
enum Addon { addon_all, addon_drone, addon_board_break, addon_road_rash, addon_hall_of_meat, addon_grind_exits, addon_music, addon_count };
void open_page(SkateMenu&, int page, int addon = -1);
}
+167 -68
View File
@@ -1,5 +1,7 @@
#include "Engine/Core/Platform/launcher_support.h"
#include "skate_menu_internal.h"
#include "skate_style.h"
#include "Extension/UI/skate_theme.h"
#include "Engine/Core/Profiling/profiler.h"
#include "Extension/Music/local_music_playback.h"
#include "Extension/Profile/local_profile_runtime.h"
@@ -9,7 +11,8 @@
// The SETTINGS and DEVELOPER pages.
namespace dingosdk::overlay::menu {
void ui_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
using namespace theme;
void music_playback_card(SkateMenu& menu) {
begin_card(menu, "music-playback", "MUSIC PLAYBACK");
bool shuffle = dingosdk::profile_runtime::music_shuffle_enabled();
if (toggle_row(menu, "Shuffle playlists", "Off plays songs in playlist order. On shuffles.", shuffle,
@@ -18,7 +21,10 @@ void ui_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks)
dingosdk::profile_runtime::set_local_preference("MusicShuffle", shuffle);
}
end_card();
}
// Settings > SCREEN: what shows, and how this menu looks.
void screen_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "on-screen", "ON SCREEN");
bool hidden = model.debug.game_ui_hidden;
if (toggle_row(menu, "Hide game UI", "Keep the view clear for riding and captures.", hidden,
@@ -28,29 +34,44 @@ void ui_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks)
note(back.c_str());
end_card();
begin_card(menu, "menu-scale", "MENU SIZE");
// The size of the style this is drawn in: the window and full screen keep their own.
const bool full = console_style();
const char* command = full ? "ui scalefull " : "ui scale ";
begin_card(menu, "menu-scale", "MENU SIZE", full ? "Full screen" : "Window");
// Drag locally for an immediate preview, then save once on release: the
// saved value only returns through the model on a later frame.
if (!menu.scale_editing) menu.scale = model.menu_scale;
field(menu, "ReSkate menu size");
if (!menu.scale_editing) menu.scale = full ? model.menu_scale_fullscreen : model.menu_scale;
field(menu, full ? "Full screen menu size" : "Window menu size");
const bool resettable = menu.scale != default_menu_scale;
const float reset = ImGui::CalcTextSize("Reset").x + ImGui::GetStyle().FramePadding.x * 2;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - reset - ImGui::GetStyle().ItemSpacing.x);
ImGui::SliderFloat("##menu-scale", &menu.scale, min_menu_scale, max_menu_scale, "%.2fx",
::dingosdk::overlay::menu::slider_float("##menu-scale", &menu.scale, min_menu_scale, max_menu_scale, "%.2fx",
ImGuiSliderFlags_AlwaysClamp);
menu.scale_editing = ImGui::IsItemActive();
if (ImGui::IsItemDeactivatedAfterEdit())
send_console(menu, callbacks, "ui scale " + std::to_string(menu.scale));
ImGui::SameLine();
ImGui::BeginDisabled(!resettable);
if (ImGui::Button("Reset")) {
send_console(menu, callbacks, command + std::to_string(menu.scale));
if (!full) ImGui::SameLine();
if (button_row(menu, "scale-reset", {{full ? "RESET SIZE" : "Reset", resettable}}) == 0) {
menu.scale = default_menu_scale;
send_console(menu, callbacks, "ui scale " + std::to_string(default_menu_scale));
send_console(menu, callbacks, command + std::to_string(default_menu_scale));
}
ImGui::EndDisabled();
note("Scales this menu and its text. Saved with your profile.");
note(full ? "Scales the full screen menu and its text; the window keeps its own size. Saved with your profile."
: "Scales this window and its text; the full screen menu keeps its own size. Saved with your profile.");
end_card();
begin_card(menu, "menu-style", "MENU STYLE");
int style = menu_fullscreen(menu, model) ? 1 : 0;
if (choice(menu, "menu-style", style, {"Window", "Full screen"}, callbacks.queue_console_command != nullptr, 0.0f,
{"A window over the game you can move and resize: made for a mouse.",
"Fills the screen, with bigger rows and button prompts: made for a controller."}))
set_menu_fullscreen(menu, callbacks, style == 1);
note("Also the button beside the menu's close button, F11, or View on a controller. Saved with your profile.");
end_card();
graphics_page(menu, model, callbacks);
}
// Settings > PERFORMANCE.
void performance_page(SkateMenu& menu, const Model&, const CallbacksV3& callbacks) {
begin_card(menu, "performance", "PERFORMANCE");
// The profiler is thread-safe: its state is read here directly, not through the model.
bool hud = dingosdk::profiler::hud();
@@ -63,11 +84,57 @@ void ui_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks)
dingosdk::profiler::set_window(window);
note("Console: perf, perf sample [seconds] [client|present|thread id], perf report, perf status.");
end_card();
if (!model.steam_offline) multiplayer_display_settings(menu, model);
}
namespace {
// The binds in the groups they are looked for in. An action is its number in `bind` (1..9), or
// 10 + its place in action_binds.
struct BindGroup {
const char* title;
std::initializer_list<int> actions;
};
constexpr int action_bind(std::string_view key) {
for (std::size_t i = 0; i < action_binds.size(); ++i)
if (action_binds[i].key == key) return 10 + static_cast<int>(i);
return 0;
}
const std::array<BindGroup, 4> bind_groups{{
{"GENERAL", {action_bind("reskate_menu"), action_bind("hide_hud"), action_bind("challenges"), action_bind("time_of_day")}},
{"CAMERA", {2, 1, 6, action_bind("first_person")}},
{"CHEATS AND BOOSTS", {3, action_bind("no_bail"), 4, 5, 7}},
{"ONLINE", {action_bind("voice_chat"), action_bind("nametags"), action_bind("nametag_dots"), 8, 9}},
}};
const char* bind_name(int action) {
switch (action) {
case 1: return "Freecam Controller";
case 2: return "Freecam";
case 3: return "Noclip";
case 4: return "Forward Boost";
case 5: return "Up Boost";
case 6: return "TP to Freecam";
case 7: return "Off-board Up Boost";
case 8: return "Vote yes";
case 9: return "Vote no";
default: return action >= 10 ? action_binds[static_cast<std::size_t>(action - 10)].label.data() : "";
}
}
std::uint32_t bind_combo(const ControllerBindingsModel& b, int action) {
switch (action) {
case 1: return b.freecam_controller_combo;
case 2: return b.freecam_combo;
case 3: return b.noclip_combo;
case 4: return b.forward_velocity_combo;
case 5: return b.up_velocity_combo;
case 6: return b.tp_to_freecam_combo;
case 7: return b.offboard_up_velocity_combo;
case 8: return b.vote_yes_combo;
case 9: return b.vote_no_combo;
default: return action >= 10 ? b.action_combos[static_cast<std::size_t>(action - 10)] : 0;
}
}
}
void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "controller-binds", "ACTION BINDS");
const bool available = model.bindings.available && callbacks.queue_console_command;
const auto save = [&](int action, std::uint32_t combo) {
std::array<char, 512> result{};
@@ -89,81 +156,113 @@ void binds_page(SkateMenu& menu, const Model& model, const CallbacksV3& callback
save(action, *combo);
}
}
ImGui::BeginDisabled(!available);
if (ImGui::BeginTable("controller-binds", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, px(130));
ImGui::TableSetupColumn("Controller combo / key", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##controls", ImGuiTableColumnFlags_WidthFixed, px(170));
ImGui::TableHeadersRow();
const auto row = [&](int action, const char* name, std::uint32_t combo) {
ImGui::PushID(action);
ImGui::TableNextRow(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(name);
ImGui::TableNextColumn(); ImGui::AlignTextToFramePadding();
const auto label = menu.recording_bind == action ? "Recording..." : controller_combo_label(combo, controller.style);
ImGui::TextWrapped("%s", label.c_str());
ImGui::TableNextColumn();
if (menu.recording_bind == action) {
if (ImGui::Button("Cancel", ImVec2(-1, 0))) menu.recording_bind = 0;
} else {
ImGui::BeginDisabled(menu.recording_bind != 0);
if (ImGui::Button("Record", ImVec2(px(90), 0))) {
menu.bind_capture = {};
menu.recording_bind = action;
menu.bind_capture_until = ImGui::GetTime() + 30;
menu.feedback.clear();
}
ImGui::SameLine(); ImGui::BeginDisabled(!combo);
if (ImGui::Button("Clear", ImVec2(-1, 0))) save(action, 0);
ImGui::EndDisabled();
ImGui::EndDisabled();
}
ImGui::PopID();
};
row(1, "Freecam Controller", model.bindings.freecam_controller_combo);
row(2, "Freecam", model.bindings.freecam_combo);
row(3, "Noclip", model.bindings.noclip_combo);
row(4, "Forward Boost", model.bindings.forward_velocity_combo);
row(5, "Up Boost", model.bindings.up_velocity_combo);
row(6, "TP to Freecam", model.bindings.tp_to_freecam_combo);
row(7, "Off-board Up Boost", model.bindings.offboard_up_velocity_combo);
row(8, "Vote yes", model.bindings.vote_yes_combo);
row(9, "Vote no", model.bindings.vote_no_combo);
for (std::size_t i = 0; i < action_binds.size(); ++i)
row(10 + static_cast<int>(i), action_binds[i].label.data(), model.bindings.action_combos[i]);
ImGui::EndTable();
}
ImGui::EndDisabled();
const auto record = [&](int action) {
menu.bind_capture = {};
menu.recording_bind = action;
menu.bind_capture_until = ImGui::GetTime() + (console_style() ? 10 : 30);
menu.feedback.clear();
};
const bool console = console_style();
if (menu.recording_bind) {
warn(!menu.bind_capture.ready ? "Release controller buttons and keyboard keys first." :
console ? "Hold up to five buttons together, then let go. Nothing for 10 seconds cancels." :
"Hold up to five keys together, then release them. A-Z, 0-9, Space, F1-F12, Ctrl, Shift and Alt work; controller combos work too.");
} else {
}
for (const auto& group : bind_groups) {
if (std::string_view(group.title) == "ONLINE" && model.steam_offline) continue;
begin_card(menu, group.title, group.title);
ImGui::PushID(group.title);
if (console) {
// Full screen: a row a bind, its combo at the right end. A records, Y clears.
auto* draw = ImGui::GetWindowDrawList();
for (const int action : group.actions) {
const std::uint32_t combo = bind_combo(model.bindings, action);
const bool recording = menu.recording_bind == action;
ImGui::PushID(action);
const auto at = ImGui::GetCursorScreenPos();
const float width = ImGui::GetContentRegionAvail().x, height = px(54);
ImGui::BeginDisabled(!available || (menu.recording_bind && !recording));
const bool pressed = ImGui::InvisibleButton("##bind", ImVec2(width, height), ImGuiButtonFlags_EnableNav);
ImGui::EndDisabled();
const bool focused = ImGui::IsItemFocused() && ImGui::GetIO().NavVisible, hovered = ImGui::IsItemHovered();
if (pressed && !menu.recording_bind) record(action);
if (focused && !menu.recording_bind && combo && ImGui::IsKeyPressed(ImGuiKey_GamepadFaceUp, false)) save(action, 0);
if (focused || hovered)
set_console_hint(recording ? "Hold the buttons together, then let go."
: combo ? "A records a new bind. Y clears this one." : "A records a bind.");
const ImVec2 end(at.x + width, at.y + height);
const bool lit = focused || hovered;
skate_theme::rough_rect(draw, at, end, recording ? paper : lit ? blue : IM_COL32(26, 26, 26, 235),
static_cast<unsigned>(ImGui::GetItemID()) & 0xfff, px(1));
const ImU32 text = recording ? skate_theme::black : available ? paper : muted;
const auto name = menu.bold->CalcTextSizeA(px(17), FLT_MAX, 0, bind_name(action));
draw->AddText(menu.bold, px(17), ImVec2(at.x + px(18), at.y + (height - name.y) * .5f), text, bind_name(action));
const auto label = recording ? std::string(menu.bind_capture.ready ? "Press the buttons..." : "Let go first...")
: controller_combo_label(combo, controller.style);
const auto size = menu.bold->CalcTextSizeA(px(17), FLT_MAX, 0, label.c_str());
draw->AddText(menu.bold, px(17), ImVec2(end.x - px(24) - size.x, at.y + (height - size.y) * .5f),
recording ? skate_theme::black : lit ? paper : combo ? blue : muted, label.c_str());
ImGui::PopID();
}
} else {
ImGui::BeginDisabled(!available);
if (ImGui::BeginTable("binds", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, px(150));
ImGui::TableSetupColumn("Controller combo / key", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("##controls", ImGuiTableColumnFlags_WidthFixed, px(170));
for (const int action : group.actions) {
const std::uint32_t combo = bind_combo(model.bindings, action);
ImGui::PushID(action);
ImGui::TableNextRow(); ImGui::TableNextColumn();
ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(bind_name(action));
ImGui::TableNextColumn(); ImGui::AlignTextToFramePadding();
const auto label = menu.recording_bind == action ? "Recording..." : controller_combo_label(combo, controller.style);
ImGui::TextWrapped("%s", label.c_str());
ImGui::TableNextColumn();
if (menu.recording_bind == action) {
if (ImGui::Button("Cancel", ImVec2(-1, 0))) menu.recording_bind = 0;
} else {
ImGui::BeginDisabled(menu.recording_bind != 0);
if (ImGui::Button("Record", ImVec2(px(90), 0))) record(action);
ImGui::SameLine(); ImGui::BeginDisabled(!combo);
if (ImGui::Button("Clear", ImVec2(-1, 0))) save(action, 0);
ImGui::EndDisabled();
ImGui::EndDisabled();
}
ImGui::PopID();
}
ImGui::EndTable();
}
ImGui::EndDisabled();
}
ImGui::PopID();
end_card();
}
if (!menu.recording_bind) {
note(("Record a key, keyboard chord or controller combo, such as Ctrl + F5 or " + controller_combo_label(0x300, controller.style) +
". Freecam, Freecam Controller, and Noclip toggle; boosts add velocity once per press. Off-board Up Boost also works while falling or gliding; release and press again to repeat. On-board and off-board boosts can share a combo. Give toggles different combos.").c_str());
note("Saved to your profile.");
if (!model.bindings.status.empty()) note(model.bindings.status.c_str());
if (!model.bindings.available) warn("Waiting for the local profile.");
}
if (!model.steam_offline) note("The push-to-talk button is set in Multiplayer > Voice.");
end_card();
if (!model.steam_offline) note("The push-to-talk button is set in Online > Voice.");
}
void settings_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
menu.settings_tab = std::min(menu.settings_tab, 2); // Mods moved to its own page
category_tabs(menu, menu.settings_tab, {"CONTROLS", "INTERFACE", "POST FX"}, "settings-tabs");
category_tabs(menu, menu.settings_tab, {"CONTROLS", "SCREEN", "PERFORMANCE"}, "settings-tabs");
ImGui::PushID(menu.settings_tab);
ImGui::BeginChild("settings-tab", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("settings-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
switch (menu.settings_tab) {
case 0: binds_page(menu, model, callbacks); break;
case 1: ui_page(menu, model, callbacks); break;
case 2: graphics_page(menu, model, callbacks); break;
case 1: screen_page(menu, model, callbacks); break;
case 2: performance_page(menu, model, callbacks); break;
}
ImGui::EndChild();
ImGui::PopID();
}
void developer_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
ImGui::BeginChild("developer", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("developer", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
multiplayer_network_page(menu, model, callbacks);
ImGui::EndChild();
}
+80 -46
View File
@@ -39,7 +39,7 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
const float reset = trailing_width("Default");
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - reset - ImGui::GetStyle().ItemSpacing.x);
ImGui::BeginDisabled(!callbacks.queue_debug);
if (ImGui::SliderInt("##free-camera-fov", &free_fov, 40, 120, custom ? "%d degrees" : "%d degrees (game)",
if (::dingosdk::overlay::menu::slider_int("##free-camera-fov", &free_fov, 40, 120, custom ? "%d degrees" : "%d degrees (game)",
ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_free_camera_fov, false, static_cast<float>(free_fov)});
ImGui::SameLine();
@@ -56,13 +56,16 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
int selected = 3;
for (int i = 0; i < static_cast<int>(speeds.size()); ++i)
if (std::abs(debug.camera_speed - speeds[i]) < .01f) selected = i;
if (ImGui::SliderInt("##flight-speed", &selected, 0, 6, labels[selected], ImGuiSliderFlags_NoInput))
if (::dingosdk::overlay::menu::slider_int("##flight-speed", &selected, 0, 6, labels[selected], ImGuiSliderFlags_NoInput))
debug_request(menu, callbacks, {DebugAction::set_camera_speed, false, speeds[selected]});
ImGui::EndDisabled();
if (debug.free_camera) note("Close the menu to fly. WASD / Q E move; hold the right mouse button to look.");
if (!debug.camera_available) warn(debug.camera_unavailable.c_str());
end_card();
}
void first_person_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "first-person", "FIRST PERSON");
bool first_person = debug.first_person;
if (toggle_row(menu, "First person", "See the world through the skater's eyes.", first_person,
@@ -71,7 +74,7 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
field(menu, "Field of view");
int fov = static_cast<int>(std::lround(debug.first_person_fov > 0 ? debug.first_person_fov : debug.camera_fov));
ImGui::BeginDisabled(!debug.first_person || !callbacks.queue_debug);
if (ImGui::SliderInt("##first-person-fov", &fov, 40, 120, "%d degrees", ImGuiSliderFlags_AlwaysClamp))
if (::dingosdk::overlay::menu::slider_int("##first-person-fov", &fov, 40, 120, "%d degrees", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_first_person_fov, false, static_cast<float>(fov)});
ImGui::EndDisabled();
{
@@ -83,7 +86,7 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
ImGui::BeginDisabled(!settings.stabilize || !can_edit);
const auto percent = [&](const char* label, const char* id, float value, DebugAction action) {
field(menu, label);
if (ImGui::SliderFloat(id, &value, 0, dingosdk::first_person::strength_limit, "%.0f%%", ImGuiSliderFlags_AlwaysClamp))
if (::dingosdk::overlay::menu::slider_float(id, &value, 0, dingosdk::first_person::strength_limit, "%.0f%%", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {action, false, value});
};
percent("Smoothing", "##fp-smoothing", settings.smoothing, DebugAction::set_first_person_smoothing);
@@ -116,7 +119,7 @@ void camera_controls(SkateMenu& menu, const Model& model, const CallbacksV3& cal
const auto slider = [&](const char* label, float value, float low, float high, const char* format, DebugAction action) {
field(menu, label);
ImGui::PushID(label);
if (ImGui::SliderFloat("##arm-value", &value, low, high, format, ImGuiSliderFlags_AlwaysClamp))
if (::dingosdk::overlay::menu::slider_float("##arm-value", &value, low, high, format, ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {action, false, value});
ImGui::PopID();
};
@@ -172,7 +175,7 @@ void board_break_settings_card(SkateMenu& menu, const Model& model, const Callba
int shown = edited[slot] >= 0 ? edited[slot] : value;
field(menu, label, hint);
ImGui::BeginDisabled(!console);
if (ImGui::SliderInt((std::string("##") + id).c_str(), &shown, low, high, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
if (::dingosdk::overlay::menu::slider_int((std::string("##") + id).c_str(), &shown, low, high, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
edited[slot] = std::clamp((shown + step / 2) / step * step, low, high);
edited_at[slot] = ImGui::GetTime();
}
@@ -187,15 +190,21 @@ void board_break_settings_card(SkateMenu& menu, const Model& model, const Callba
"sensitivity");
percent("bb-joined", "Grip tape holds", "How many breaks the grip tape holds together instead of a piece coming off.", bb.joined, 0, 100, 5, "joined");
percent("bb-wear-rate", "Wear speed", "How fast boards wear. 0% keeps a board as it is.", bb.wear_rate, 0, 500, 10, "wearrate");
ImGui::BeginDisabled(!console || !bb.has_deck);
if (ImGui::Button("New board", ImVec2(-FLT_MIN, 0))) send_console(menu, callbacks, "boardbreak worn 0");
ImGui::EndDisabled();
if (console_style()) {
if (button_row(menu, "bb-new", {{"NEW BOARD", console && bb.has_deck, false, "Your board as good as new: no wear."}}) == 0)
send_console(menu, callbacks, "boardbreak worn 0");
} else {
ImGui::BeginDisabled(!console || !bb.has_deck);
if (ImGui::Button("New board", ImVec2(-FLT_MIN, 0))) send_console(menu, callbacks, "boardbreak worn 0");
ImGui::EndDisabled();
}
end_card();
}
void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
// Skater > CHEATS: flying through things, never bailing, and the boosts.
void cheats_tab(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "movement", "FLIGHT & BAILS");
begin_card(menu, "cheats", "NOCLIP AND NO BAIL");
bool noclip = debug.noclip;
if (toggle_row(menu, "Noclip", "Fly with the normal player camera. Includes No Bail; uses the Freecam flight speed.", noclip,
(debug.noclip_available || debug.noclip) && callbacks.queue_debug))
@@ -210,16 +219,31 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
if (toggle_row(menu, "No Bail", bail_help, no_bail,
(debug.no_bail_available || debug.no_bail) && callbacks.queue_debug))
debug_request(menu, callbacks, {DebugAction::set_no_bail, no_bail});
bool hall_of_meat = model.hall_of_meat.enabled;
// No Bail (and noclip, which includes it) stops the bails Hall of Meat scores.
const bool bails_off = debug.no_bail || debug.no_bail_active;
const char* meat_help = hall_of_meat && bails_off
? "On, but No Bail is stopping your bails, so nothing shows. Turn No Bail (and noclip) off to use it."
: bails_off
? "Your bails show the bones they hurt and score a Meat card. Needs No Bail (and noclip) off."
: "Your bails show the bones they hurt and score a Meat card. A break slows the game in single player.";
if (toggle_row(menu, "Hall of Meat", meat_help, hall_of_meat, model.hall_of_meat.available && callbacks.queue_console_command))
send_console(menu, callbacks, hall_of_meat ? "hallofmeat 1" : "hallofmeat 0");
end_card();
begin_card(menu, "boosts", "BOOSTS", "Buttons are set in Settings > Controls");
ImGui::BeginDisabled(!callbacks.queue_debug);
field(menu, "Forward boost");
float forward_velocity = debug.forward_velocity_speed;
if (slider_float("##forward-velocity", &forward_velocity, 1.0f, 300.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_forward_velocity_speed, false, forward_velocity});
field(menu, "Up boost");
float up_velocity = debug.up_velocity_speed;
if (slider_float("##up-velocity", &up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_up_velocity_speed, false, up_velocity});
field(menu, "Off-board up boost");
float offboard_up_velocity = debug.offboard_up_velocity_speed;
if (slider_float("##offboard-up-velocity", &offboard_up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_offboard_up_velocity_speed, false, offboard_up_velocity});
ImGui::EndDisabled();
note("Controller: left stick moves, right stick looks, RT / LT rise and fall, click the left stick to boost.");
note("Keyboard: WASD / Q E, Shift to boost. Close the menu to fly.");
end_card();
}
// Addons > ROAD RASH: bails leave marks.
void road_rash_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "road-rash", "ROAD RASH");
bool road_rash = model.road_rash.enabled;
const bool rash_console = model.road_rash.available && callbacks.queue_console_command;
if (toggle_row(menu, "Road Rash", "Bails leave marks that build up: dust, scrapes, bruises, wounds. Needs the Road Rash item pack.",
@@ -229,6 +253,29 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
if (toggle_row(menu, "Road Rash blood", "Fresh blood on the worst wounds. Off, they are dry and scabbed.", rash_blood,
rash_console && model.road_rash.enabled))
send_console(menu, callbacks, rash_blood ? "roadrashblood 1" : "roadrashblood 0");
end_card();
}
// Addons > HALL OF MEAT: bails show the bones they hurt and score a Meat card.
void hall_of_meat_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& debug = model.debug;
begin_card(menu, "hall-of-meat", "HALL OF MEAT");
bool hall_of_meat = model.hall_of_meat.enabled;
// No Bail (and noclip, which includes it) stops the bails Hall of Meat scores.
const bool bails_off = debug.no_bail || debug.no_bail_active;
const char* meat_help = hall_of_meat && bails_off
? "On, but No Bail is stopping your bails, so nothing shows. Turn No Bail (and noclip) off in Skater > Cheats."
: bails_off
? "Your bails show the bones they hurt and score a Meat card. Needs No Bail (and noclip) off."
: "Your bails show the bones they hurt and score a Meat card. A break slows the game in single player.";
if (toggle_row(menu, "Hall of Meat", meat_help, hall_of_meat, model.hall_of_meat.available && callbacks.queue_console_command))
send_console(menu, callbacks, hall_of_meat ? "hallofmeat 1" : "hallofmeat 0");
end_card();
}
// Addons > BOARD BREAK: the switch, the deck in the game, and how boards break, wear and sound.
void board_break_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "board-break", "BOARD BREAK");
bool board_break = model.board_break.enabled;
if (toggle_row(menu, "Board Break", model.board_break.has_deck
? "A hard slam can snap your board: at the nose, in the middle or at the tail. Whole again once you pick it up."
@@ -238,8 +285,10 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
end_card();
board_break_deck_card(menu, model, callbacks);
board_break_settings_card(menu, model, callbacks);
}
// Grind exit control (Extension/Skater/grind_exit_control.h).
// Addons > GRIND EXITS: grind exit control (Extension/Skater/grind_exit_control.h).
void grind_exits_addon(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
const auto& grind = model.grind_exit;
const bool grind_console = grind.available && callbacks.queue_console_command;
begin_card(menu, "grind-exits", "GRIND EXITS", "Choose the side you pop out of a grind using LT/RT");
@@ -267,7 +316,7 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
int strength = edited >= 0 ? edited : grind.strength;
field(menu, "Pop-out strength", "How far to the side a trigger exit pops. 100% is the full exit; lower it for rails next to a wall.");
ImGui::BeginDisabled(!grind_console);
if (ImGui::SliderInt("##grind-strength", &strength, 10, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
if (slider_int("##grind-strength", &strength, 10, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp)) {
edited = std::clamp((strength + 2) / 5 * 5, 10, 100);
edited_at = ImGui::GetTime();
}
@@ -279,33 +328,18 @@ void movement_controls(SkateMenu& menu, const Model& model, const CallbacksV3& c
ImGui::EndDisabled();
}
end_card();
begin_card(menu, "boosts", "BOOSTS", "Buttons are set in Settings > Controls");
ImGui::BeginDisabled(!callbacks.queue_debug);
field(menu, "Forward boost");
float forward_velocity = debug.forward_velocity_speed;
if (ImGui::SliderFloat("##forward-velocity", &forward_velocity, 1.0f, 300.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_forward_velocity_speed, false, forward_velocity});
field(menu, "Up boost");
float up_velocity = debug.up_velocity_speed;
if (ImGui::SliderFloat("##up-velocity", &up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_up_velocity_speed, false, up_velocity});
field(menu, "Off-board up boost");
float offboard_up_velocity = debug.offboard_up_velocity_speed;
if (ImGui::SliderFloat("##offboard-up-velocity", &offboard_up_velocity, 1.0f, 25.0f, "+%.1f", ImGuiSliderFlags_AlwaysClamp))
debug_request(menu, callbacks, {DebugAction::set_offboard_up_velocity_speed, false, offboard_up_velocity});
ImGui::EndDisabled();
note("Controller: left stick moves, right stick looks, RT / LT rise and fall, click the left stick to boost.");
note("Keyboard: WASD / Q E, Shift to boost. Close the menu to fly.");
end_card();
}
void skater_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
category_tabs(menu, menu.skater_tab, {"CAMERA", "GAMEPLAY"}, "skater-tabs");
// (Grind exits are an addon: Addons > Grind Exits.)
category_tabs(menu, menu.skater_tab, {"FREECAM", "FIRST PERSON", "CHEATS"}, "skater-tabs");
ImGui::PushID(menu.skater_tab);
ImGui::BeginChild("skater-tab", ImVec2(0, page_body_height(menu)));
if (menu.skater_tab == 0) camera_controls(menu, model, callbacks);
else movement_controls(menu, model, callbacks);
ImGui::BeginChild("skater-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
switch (menu.skater_tab) {
case 0: camera_controls(menu, model, callbacks); break;
case 1: first_person_controls(menu, model, callbacks); break;
default: cheats_tab(menu, model, callbacks); break;
}
ImGui::EndChild();
ImGui::PopID();
}
+232 -63
View File
@@ -27,56 +27,123 @@ std::string level_name(const Level& level) {
}
void level_picker(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
begin_card(menu, "travel", "FIND YOUR SPOT");
std::vector<const Level*> destinations;
const Level* root = nullptr;
const Level* selected = nullptr;
for (const auto& level : model.levels) {
if (same(level.asset, root_asset)) root = &level;
else {
// (Not the splash level: it only hosts the startup screen, as the pause menu's map list has it.
// Nor Tutorial Island: the game's first-time tutorial, not a place to skate.)
else if (dingosdk::native_tools::listed(level) && world_level_name(level.asset) != "Tutorial Island")
destinations.push_back(&level);
if (same(level.asset, menu.destination)) selected = &level;
}
}
std::sort(destinations.begin(), destinations.end(), [](const auto* a, const auto* b) {
return level_name(*a) < level_name(*b);
});
field(menu, "Destination");
if (ImGui::BeginCombo("##destination", selected ? level_name(*selected).c_str() : "Choose a level")) {
for (const auto* level : destinations) {
ImGui::PushID(level->asset.c_str());
if (ImGui::Selectable(level_name(*level).c_str(), selected == level)) {
selected = level;
menu.destination = level->asset;
menu.feedback.clear();
}
ImGui::PopID();
}
if (destinations.empty()) ImGui::TextDisabled("Waiting for levels...");
ImGui::EndCombo();
{
std::vector<MenuMapWanted> wanted;
for (const auto* level : destinations) wanted.push_back({level->asset, level_name(*level)});
want_menu_map_pictures(std::move(wanted));
}
ImGui::Dummy(ImVec2(0, px(4)));
const bool ready = root && selected && root->can_load && selected->can_load && model.can_queue_load && callbacks.queue_load;
ImGui::BeginDisabled(!ready);
skate_theme::push_primary_button();
ImGui::PushFont(menu.heading);
if (ImGui::Button("LET'S SKATE", ImVec2(-1, px(46)))) {
const bool loading = root && root->can_load && model.can_queue_load && callbacks.queue_load;
const auto load = [&](const Level* level) {
std::array<char, 512> result{};
const auto* point = selected->automatic_start_point();
const bool queued = callbacks.queue_load(callbacks.user, root->asset.c_str(), "", selected->asset.c_str(),
const auto* point = level->automatic_start_point();
const bool queued = callbacks.queue_load(callbacks.user, root->asset.c_str(), "", level->asset.c_str(),
point ? point->c_str() : "", result.data(), result.size());
result.back() = '\0';
feedback(menu, result[0] ? result.data() : (queued ? "Loading level..." : "Load unavailable."));
feedback(menu, result[0] ? result.data() : (queued ? ("Loading " + level_name(*level) + "...").c_str() : "Load unavailable."));
};
if (!loading) {
const std::string reason = !root ? "Waiting for levels..." : !root->can_load ? root->load_block_reason : model.load_block_reason;
warn(reason.empty() ? "Level loading is unavailable." : reason.c_str());
}
ImGui::PopFont();
skate_theme::pop_primary_button();
ImGui::EndDisabled();
if (!ready) {
const std::string reason = !root ? "Waiting for levels..." : !selected ? "Choose a destination." :
!root->can_load ? root->load_block_reason : !selected->can_load ? selected->load_block_reason : model.load_block_reason;
note(reason.empty() ? "Level loading is unavailable." : reason.c_str());
// A window has a keyboard: a box to find a map by name. (Full screen shows them all.)
const bool console = console_style();
if (!console) {
static std::array<char, 64> search{};
ImGui::SetNextItemWidth(-FLT_MIN);
ImGui::InputTextWithHint("##map-search", "Search maps...", search.data(), search.size());
const auto lower = [](std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return text;
};
if (const auto words = lower(search.data()); !words.empty()) {
std::erase_if(destinations, [&](const Level* level) { return lower(level_name(*level)).find(words) == std::string::npos; });
if (destinations.empty()) note("No maps match that search.");
}
}
end_card();
// A card per map with its picture. In a window a LOAD button under each; full screen the card
// itself loads (A).
auto* draw = ImGui::GetWindowDrawList();
const auto grid = [&](const char* heading, bool custom) {
std::vector<const Level*> shown;
for (const auto* level : destinations)
if (level->custom == custom) shown.push_back(level);
if (shown.empty()) return;
section(menu, heading);
const float gap = px(12);
const float across = ImGui::GetContentRegionAvail().x;
const int columns = std::max(1, static_cast<int>((across + gap) / (px(console ? 260.0f : 210.0f) + gap)));
const float width = (across - gap * static_cast<float>(columns - 1)) / static_cast<float>(columns);
const float picture = width * 0.58f, height = picture + px(42);
for (std::size_t i = 0; i < shown.size(); ++i) {
const auto* level = shown[i];
if (i % static_cast<std::size_t>(columns)) ImGui::SameLine(0, gap);
ImGui::PushID(level->asset.c_str());
ImGui::BeginGroup();
const bool can = loading && level->can_load;
const auto at = ImGui::GetCursorScreenPos();
bool hovered{}, focused{};
if (console) {
ImGui::BeginDisabled(!can);
if (ImGui::InvisibleButton("##map", ImVec2(width, height), ImGuiButtonFlags_EnableNav) && can) load(level);
ImGui::EndDisabled();
hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled);
focused = ImGui::IsItemFocused() && ImGui::GetIO().NavVisible;
} else {
ImGui::Dummy(ImVec2(width, height));
}
const ImVec2 end(at.x + width, at.y + height);
draw->AddRectFilled(at, end, skate_theme::tile);
ImTextureID texture{};
ImVec2 uv0, uv1;
if (menu_map_picture(level->asset, texture, uv0, uv1)) {
// The middle band of the square picture, to the card's shape.
const float band = (uv1.y - uv0.y) * (1.0f - picture / width) * 0.5f;
draw->AddImage(texture, at, ImVec2(end.x, at.y + picture), ImVec2(uv0.x, uv0.y + band), ImVec2(uv1.x, uv1.y - band),
level->can_load ? IM_COL32_WHITE : IM_COL32(255, 255, 255, 90));
} else {
draw->AddRectFilled(at, ImVec2(end.x, at.y + picture), skate_theme::tile_grey);
const char* initial = custom ? "CUSTOM" : "MAP";
const auto size = menu.bold->CalcTextSizeA(px(13), FLT_MAX, 0, initial);
draw->AddText(menu.bold, px(13), ImVec2(at.x + (width - size.x) * .5f, at.y + (picture - size.y) * .5f), muted, initial);
}
draw->AddRectFilled(ImVec2(at.x, at.y + picture), end, focused ? blue : hovered ? skate_theme::tile_light : skate_theme::tile);
draw->PushClipRect(at, end, true);
draw->AddText(menu.bold, px(15), ImVec2(at.x + px(12), at.y + picture + px(12)),
focused ? skate_theme::black : level->can_load ? paper : muted, level_name(*level).c_str());
draw->PopClipRect();
if (focused) draw->AddRect(ImVec2(at.x - px(3), at.y - px(3)), ImVec2(end.x + px(3), end.y + px(3)), paper, 0, 0, px(3));
if (!console) {
ImGui::BeginDisabled(!can);
skate_theme::push_primary_button();
if (ImGui::Button("LOAD", ImVec2(width, px(32)))) load(level);
skate_theme::pop_primary_button();
ImGui::EndDisabled();
if (!level->can_load && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s", level->load_block_reason.c_str());
} else if (!level->can_load && (hovered || focused)) {
ImGui::SetTooltip("%s", level->load_block_reason.c_str());
}
ImGui::EndGroup();
ImGui::PopID();
}
};
grid("OFFICIAL MAPS", false);
grid("CUSTOM MAPS", true);
if (destinations.empty()) note("Waiting for levels...");
}
void park_editor_panel(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
@@ -269,6 +336,20 @@ void population_controls(SkateMenu& menu, const Model& model, const CallbacksV3&
return queued;
};
begin_card(menu, "population", "POPULATION");
if (console_style()) {
const bool can = controls.population_available && model.world.ready && callbacks.queue_console_command;
std::vector<std::string> modes;
for (const auto* label : population_labels) modes.emplace_back(label);
for (unsigned i = 0; i < 2; ++i) {
int chosen = (i == 0 ? c.traffic : c.pedestrians) + 1;
const auto nearby = controls.population_ready[i] ? std::to_string(controls.population[i]) + " nearby right now."
: std::string("Waiting for the population count.");
if (option_row(menu, i == 0 ? "Traffic" : "Pedestrians", nearby.c_str(), chosen, modes, can))
send(i == 0 ? "traffic" : "pedestrians", static_cast<float>(chosen - 1));
}
end_card();
return;
}
ImGui::PushID("world-controls");
ImGui::BeginDisabled(!controls.population_available || !model.world.ready || !callbacks.queue_console_command);
for (unsigned i = 0; i < 2; ++i) {
@@ -307,6 +388,45 @@ void parks(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
callbacks.queue_console_command(callbacks.user, command, result.data(), result.size());
result.back() = '\0'; feedback(menu, result.data());
};
if (console_style()) {
// Full screen: a row a slot, left / right to pick its layout and A to load it.
const bool locked = !model.parks.available || tools::parks_locked(model) || !callbacks.queue_console_command;
if (button_row(menu, "park-random", {{"LOAD RANDOM PARKS", !locked, false, "A random layout in every slot."}}) == 0) {
park_command("park random");
menu.park_edit = {};
}
auto random_on_launch = model.parks.randomize_on_launch;
if (toggle_row(menu, "Randomize on launch", "A random layout in every slot once each time the game starts. Multiplayer follows the host.",
random_on_launch, model.parks.available && callbacks.queue_console_command))
park_command(random_on_launch ? "park random-on-launch 1" : "park random-on-launch 0");
for (unsigned lot = 0; lot < park_lots.size(); ++lot) {
auto& staged = menu.park_edit[lot];
if (staged.empty() || menu.park_seen[lot] != model.parks.choices[lot] || tools::parks_locked(model))
staged = model.parks.choices[lot];
menu.park_seen[lot] = model.parks.choices[lot];
std::vector<std::string> ids{"empty"}, names{"Empty lot"};
for (unsigned family = 0; family < park_families.size(); ++family)
for (unsigned variant = 1; variant <= park_lots[lot].counts[family]; ++variant) {
ids.push_back(park_id(family, variant));
names.push_back(park_label(ids.back()));
}
int at = -1;
for (std::size_t i = 0; i < ids.size(); ++i)
if (ids[i] == staged) at = static_cast<int>(i);
const bool loaded = staged == model.parks.choices[lot];
const auto label = std::string(park_lots[lot].label) + (loaded ? "" : " - A loads it") + "###" + std::string(park_lots[lot].key);
bool load = false;
if (option_row(menu, label.c_str(), loaded ? "Left / right to pick a layout, then A to load it." : "Not loaded yet: press A to load it.",
at, names, !locked, &load) && at >= 0)
staged = ids[static_cast<std::size_t>(at)];
if (load && !staged.empty())
park_command(("park " + std::string(park_lots[lot].key) + " " + staged).c_str());
}
if (!model.parks.ready) note("Load San Vansterdam to apply park choices.");
if (!model.parks.feedback.empty()) note(model.parks.feedback.c_str());
end_card();
return;
}
ImGui::BeginDisabled(!model.parks.available || tools::parks_locked(model) || !callbacks.queue_console_command);
if (ImGui::Button("Load Random Parks")) {
park_command("park random");
@@ -379,6 +499,32 @@ void world_layers_page(SkateMenu& menu, const Model& model, const CallbacksV3& c
note("Load a map to see its world layers.");
return;
}
if (console_style()) {
// Full screen: which kind, then a switch a layer (no search box: that wants a keyboard).
static const std::vector<std::string> kinds{"All categories", "Seasonal", "Activities", "World", "Lighting", "Structure", "Debug", "Parks"};
begin_card(menu, "world-layers", "WORLD LAYERS");
option_row(menu, "Show", "Left / right for one kind of layer.", menu.world_layer_category, kinds);
const bool can = !dingosdk::native_tools::layers_locked(model) && model.world.available && model.world.ready && callbacks.queue_console_command;
unsigned shown{};
for (unsigned i = 0; i < world_layers().size(); ++i) {
const auto& layer = world_layers()[i];
if (layer.map != model.world.map || !model.world.supported[i] || world_layer_kept_off(layer.key)) continue;
if (menu.world_layer_category && layer.category != kinds[static_cast<std::size_t>(menu.world_layer_category)]) continue;
++shown;
const bool follows_game = model.world.choices[i] == "default";
bool enabled = follows_game ? model.world.enabled[i].value_or(false) : model.world.choices[i] == "on";
ImGui::PushID(layer.key.data());
if (toggle_row(menu, world_layer_title(layer.label).c_str(), layer.detail.data(), enabled, can))
send_console(menu, callbacks, "world " + std::string(layer.key) + (enabled ? " on" : " off"));
ImGui::PopID();
}
if (!shown) note("No layers of that kind on this map.");
if (button_row(menu, "layer-defaults", {{"RESTORE DEFAULTS", can, false, "Every layer on this map back to the game's control."}}) == 0)
send_console(menu, callbacks, "world defaults " + std::string(world_map_key(model.world.map)));
if (!model.world.feedback.empty()) note(model.world.feedback.c_str());
end_card();
return;
}
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * .61f);
ImGui::InputTextWithHint("##layer-search", "Search layers...", menu.world_layer_search.data(), menu.world_layer_search.size());
ImGui::SameLine(); ImGui::SetNextItemWidth(-1);
@@ -462,15 +608,27 @@ void world_layers_page(SkateMenu& menu, const Model& model, const CallbacksV3& c
if (!model.world.feedback.empty()) note(model.world.feedback.c_str());
}
void time_of_day_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks);
// One page per sidebar entry; each page's tabs switch between focused panels.
void map_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
category_tabs(menu, menu.map_tab, {"TRAVEL", "ROTATING PARKS", "WORLD LAYERS"}, "map-tabs");
category_tabs(menu, menu.map_tab, {"TRAVEL", "PARKS", "TIME AND WEATHER", "LAYERS AND CROWDS"}, "map-tabs");
ImGui::PushID(menu.map_tab);
ImGui::BeginChild("map-tab", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("map-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
const bool loaded = model.world.map != WorldMap::none;
switch (menu.map_tab) {
case 0: level_picker(menu, model, callbacks); break;
case 1: parks(menu, model, callbacks); break;
case 2: world_layers_page(menu, model, callbacks); break;
case 2:
time_of_day_page(menu, model, callbacks);
if (loaded) atmosphere_menu(menu, model, callbacks);
else note("Load a map in Map > Travel to change its fog, sky and wind.");
break;
case 3:
world_layers_page(menu, model, callbacks);
if (loaded) population_controls(menu, model, callbacks);
else note("Load a map in Map > Travel to change its traffic and pedestrians.");
break;
}
ImGui::EndChild();
ImGui::PopID();
@@ -518,19 +676,50 @@ void graphics_page(SkateMenu& menu, const Model& model, const CallbacksV3& callb
// The map's seven time-of-day world layers as one choice, like the native menu:
// forcing one turns the others off; Default hands all seven back to the level.
// A time of day is seven layer changes the game takes a few frames to make, and until they are
// all made the layers read as some other time (or none). So the time asked for is kept and shown,
// and the next step goes on from it, until the layers say it or four seconds pass.
namespace {
int tod_asked = -1;
double tod_asked_until = 0;
}
int time_of_day_shown(const Model& model) {
const int now = static_cast<int>(dingosdk::native_tools::time_of_day(model));
if (tod_asked >= 0 && (now == tod_asked || ImGui::GetTime() > tod_asked_until)) tod_asked = -1;
return tod_asked >= 0 ? tod_asked : now;
}
void set_time_of_day(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks, int next, bool quiet) {
tod_asked = std::clamp(next, 0, 7);
tod_asked_until = ImGui::GetTime() + 4.0;
for (const auto& line : dingosdk::native_tools::time_of_day_commands(model, static_cast<unsigned>(tod_asked))) {
if (!quiet) {
send_console(menu, callbacks, line);
} else if (callbacks.queue_console_command) {
std::array<char, 256> reply{};
callbacks.queue_console_command(callbacks.user, line.c_str(), reply.data(), reply.size());
}
}
}
void time_of_day_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
namespace tod = dingosdk::native_tools;
begin_card(menu, "time-of-day", "TIME OF DAY");
const bool available = tod::time_of_day_available(model, callbacks);
const int current = static_cast<int>(tod::time_of_day(model));
const auto apply = [&](unsigned next) {
for (const auto& line : tod::time_of_day_commands(model, next)) send_console(menu, callbacks, line);
};
const int current = time_of_day_shown(model);
const auto apply = [&](unsigned next) { set_time_of_day(menu, model, callbacks, static_cast<int>(next), false); };
if (console_style()) {
static const std::vector<std::string> times{"Default", "Morning", "Noon", "Afternoon", "Evening", "Night", "Weather day", "Weather night"};
int chosen = current;
if (option_row(menu, "Time of day", "Left / right to change it. Default follows the level's own time.", chosen, times, available))
apply(static_cast<unsigned>(chosen));
}
int top = current < 4 ? current : -1;
if (!console_style()) {
if (choice(menu, "tod-top", top, {"Default", "Morning", "Noon", "Afternoon"}, available)) apply(static_cast<unsigned>(top));
int bottom = current >= 4 ? current - 4 : -1;
if (choice(menu, "tod-bottom", bottom, {"Evening", "Night", "Weather day", "Weather night"}, available))
apply(static_cast<unsigned>(bottom + 4));
}
if (model.world.controlled_by_host && server_can(model.multiplayer, "tod"))
note("Your role on this server sets the time: the one you pick applies to everyone.");
else if (model.world.controlled_by_host) warn("The lobby host controls world layers, including time of day.");
@@ -540,30 +729,10 @@ void time_of_day_page(SkateMenu& menu, const Model& model, const CallbacksV3& ca
end_card();
}
void world_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
category_tabs(menu, menu.world_tab, {"TIME OF DAY", "ATMOSPHERE", "POPULATION"}, "world-tabs");
ImGui::PushID(menu.world_tab);
ImGui::BeginChild("world-tab", ImVec2(0, page_body_height(menu)));
const bool loaded = model.world.map != WorldMap::none;
switch (menu.world_tab) {
case 0: time_of_day_page(menu, model, callbacks); break;
case 1:
if (loaded) atmosphere_menu(menu, model, callbacks);
else note("Load a map in Map > Travel to change its fog, sky and wind.");
break;
case 2:
if (loaded) population_controls(menu, model, callbacks);
else note("Load a map in Map > Travel to change its traffic and pedestrians.");
break;
}
ImGui::EndChild();
ImGui::PopID();
}
void build_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
category_tabs(menu, menu.build_tab, {"PARK EDITOR", "PARK MODS", "SAVED OBJECTS"}, "build-tabs");
ImGui::PushID(menu.build_tab);
ImGui::BeginChild("build-tab", ImVec2(0, page_body_height(menu)));
ImGui::BeginChild("build-tab", ImVec2(0, page_body_height(menu)), ImGuiChildFlags_NavFlattened);
if (menu.build_tab == 0) park_editor_panel(menu, model, callbacks);
else if (menu.build_tab == 1) park_mods_panel(menu, model, callbacks);
else saved_objects(menu, model, callbacks);
+2
View File
@@ -58,6 +58,8 @@ void read_model(void*, dingosdk::overlay::Model& output) {
output.engine_settings = std::move(settings);
output.console_log.clear();
output.menu_scale = dingosdk::profile_runtime::local_menu_scale();
output.menu_scale_fullscreen = dingosdk::profile_runtime::local_menu_scale(true);
output.menu_fullscreen = dingosdk::profile_runtime::local_menu_fullscreen();
output.steam_offline = dingosdk::launcher::offline_mode();
output.multiplayer=dingosdk::multiplayer::model();
if (dingosdk::multiplayer_controls_level(output.multiplayer)) {
+2
View File
@@ -25,6 +25,8 @@ add_library(dingosdk_overlay STATIC
Extension/UI/Overlay/console_suggestions.cpp
Extension/UI/Overlay/skate_menu.cpp
Extension/UI/Overlay/skate_menu_world.cpp
Extension/UI/Overlay/skate_menu_addons.cpp
Extension/UI/Overlay/menu_map_pictures.cpp
Extension/UI/Overlay/skate_menu_skater.cpp
Extension/UI/Overlay/skate_menu_settings.cpp
Extension/UI/Overlay/park_editor.cpp