Files
ReSkate/Extension/UI/Overlay/skate_menu_addons.cpp
T
AyeZeeBB db52508bdb Plugins: Hall of Meat and grind exits are plugin mods now; their C++ is gone
Hall of Meat (contrib/plugins/hall-of-meat) and grind exit triggers
(contrib/plugins/grind-exits) are Lua plugins: the bail model, its Meat card,
each map's best, the trigger rules, settings, console commands and menu panels
are theirs, with their own checks that dingosdk_plugin_tests runs.

What has to run in the game stays in core as plugin services:
- every physics step of the skater (skater_physics, the physics_step event,
  permission events.physics);
- the skater's x-ray (xray, xray_render, xray_skeleton; reskate.skater.xray and
  the draw handler's d:skeleton);
- skate.'s bottom left HUD corner (hud_corner; reskate.hud.corner);
- the game's slow motion, never in a session (game_speed; reskate.time.slow and
  speed, permission game.speed);
- the side of the next pop out of a grind and pad controls kept from the game
  (grind_exit; reskate.skater.grind_exit, the grind_exit event, reskate.input.hide);
- skate.'s own pictures by name (game_images; d:game_image), d:gradient and a
  font for drawn text.

Removed: Extension/HallOfMeat, grind_exit_control and its commands, the HALL OF
MEAT and GRIND EXITS add-on pages, home tile, hub and native menu rows, their
menu models, and the hallofmeat and grindexit built-in commands.
2026-10-19 16:51:28 -05:00

261 lines
15 KiB
C++

#include "skate_menu_internal.h"
#include "skate_style.h"
#include "Extension/UI/skate_theme.h"
#include "Extension/UI/NativeMenu/native_tools_view.h"
#include <algorithm>
#include <array>
#include <imgui_internal.h>
#include <string>
// The ADDONS page (what ReSkate adds to skate.: music, and the plugins' panels)
// 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) {
// Two a row full screen.
int placed = 1;
const auto next = [&] {
if (console_style() && placed++ % 2) 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);
next();
addon_tile(menu, "addon-plugins", "PLUGINS", "What the plugins in your Mods folder add: their panels and settings.", nullptr, 0,
addon_plugins);
}
}
void addons_page(SkateMenu& menu, const Model& model, const CallbacksV3& callbacks) {
menu.addon = std::clamp(menu.addon, 0, static_cast<int>(addon_count) - 1);
if (menu.addon == addon_all && !menu.fullscreen_drawn) menu.addon = addon_first; // (a window has no overview)
// 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) {
static constexpr std::array<const char*, addon_count> names{"ALL ADDONS", "MUSIC", "PLUGINS"};
std::vector<const char*> labels(names.begin(), names.end());
category_tabs(menu, menu.addon, labels, "addon-tabs");
menu.addon = std::clamp(menu.addon, 0, static_cast<int>(addon_count) - 1);
}
ImGui::PushID(menu.addon);
switch (menu.addon) {
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_plugins) plugins_addon(menu);
else all_addons(menu);
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);
}
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", "Music, and your plugins' panels", 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();
}
}