mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 22:16:00 +02:00
General APIs from pull request 269 (the player's own item tabs and outfit layers), whose tabs,
panels and prompts are now a plugin's (contrib/plugins/item-tabs, kept on this machine):
- reskate.menu.grid_show(keys): a plugin's list in the item grid that has the focus, drawn from
every grid of the screen; reskate.menu.grid_mark(keys): the game's check on tiles. menu.grid()
also says whether a list shows, whether it is empty, and where the grid's own prompts end.
- reskate.outfit.on_top(items) and on_top_state(): items worn on top of the outfit in the slots it
leaves empty, the item given the slot's category, a slot tried and left if the game reports a
failed load (ReSkate.layer_slots.txt remembers which load where); paid store items refused.
Part of the shared outfit service, with reskate.outfit.set.
- reskate.input.game_keys(keys): presses the game does not see, by any way it reads the keyboard;
"Delete" is a key name.
- d:game_image draws the customization menu's 17 category icons ("cas_tops", ...).
dingosdk_plugin_tests skips the test of a contrib plugin that is not on this machine.
278 lines
13 KiB
C++
278 lines
13 KiB
C++
#pragma once
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
// What plugins show, handed from the client thread (where plugins run) to the render thread (the
|
|
// overlay draws it): each plugin's shapes for this frame, and its own page in the menu's PLUGINS
|
|
// section (reskate.menu.page, and its panels as cards on it) with what the player did there on the
|
|
// way back. Header only, so the overlay and
|
|
// the plugin host share it without depending on each other.
|
|
namespace dingosdk::plugin_ui {
|
|
using Vec3 = std::array<float, 3>;
|
|
|
|
struct Style {
|
|
std::uint32_t colour = 0xffffffff; // ImGui's IM_COL32 order: 0xAABBGGRR
|
|
float size = 18; // text height, in pixels at 1080 lines
|
|
float thickness = 2;
|
|
bool fill{};
|
|
float rounding{};
|
|
int align{}; // text: 0 left, 1 centre, 2 right of its point
|
|
bool shadow = true;
|
|
std::array<float, 4> uv{0, 0, 1, 1}; // pictures: the part of the picture drawn (u0, v0, u1, v1)
|
|
int font{}; // text: 0 bold, 1 body, 2 heading, 3 title, 4 title_large (the menus' fonts)
|
|
float wrap{}; // text: wrapped to this width (pixels at 1080 lines); 0 one line
|
|
};
|
|
// The skater's bodies by the ragdoll's order (Engine/Game/Skater/skater_body.h's names): d:skeleton's paint.
|
|
inline constexpr std::array<std::string_view, 24> body_names{
|
|
"Board", "Head", "Neck", "Left hand", "Left forearm", "Left upper arm", "Left collarbone",
|
|
"Right hand", "Right forearm", "Right upper arm", "Right collarbone", "Upper chest", "Chest",
|
|
"Lower back", "Spine", "Left toes", "Left foot", "Left shin", "Left thigh",
|
|
"Right toes", "Right foot", "Right shin", "Right thigh", "Pelvis"};
|
|
// The game's own pictures a plugin draws by name (game_image; Extension/UI/Overlay/game_images.h).
|
|
inline constexpr std::array<std::string_view, 31> game_images{
|
|
"stopwatch", "wipeout", "wipeout_broken", "spread_eagle", "airtime", "gap_height", "flaming_wheel", "roll",
|
|
"thrasher", "brush_bar", "rough_tile", "scratches", "streak", "loading_board",
|
|
// The customization menu's category icons (its tabs'), white on clear.
|
|
"cas_tops", "cas_bottoms", "cas_shoes", "cas_outfit", "cas_fullbody", "cas_headwear", "cas_headaccessories", "cas_eyewear", "cas_handwear", "cas_socks", "cas_deck", "cas_trucks", "cas_wheels", "cas_gripcolor", "cas_grippattern", "cas_sticker", "cas_decorator_vfxcosmetic"};
|
|
// The loading board is a flipbook: this many frames, at 30 a second (style.frame).
|
|
inline constexpr int loading_board_frames = 27;
|
|
// Screen shapes are in pixels from the top left; world shapes in world metres, projected through
|
|
// the camera as the frame is drawn, and left out behind it.
|
|
struct Shape {
|
|
// image: `text` is the picture's file, points[0] and [1] its corners; world_image: centred on
|
|
// points[0], `radius` metres tall. game_image: `text` names one of game_images, `fit` 0 contain,
|
|
// 1 shape, 2 panel (its corners `edge` pixels). gradient: points[0] and [1] the corners, `colours`
|
|
// the top left, top right, bottom right and bottom left. skeleton: the local skater's x-ray, a
|
|
// colour for each body in `colours` (body_names' order; 0 leaves it out), the style's alpha
|
|
// fading the whole.
|
|
// rough_rect: skate.'s hand-cut tile from points[0] to [1], `seed` its edge, `radius` its scale.
|
|
// halftone: a screen-print of `columns` x `rows` dots from points[0]. scribble: the dry-brush
|
|
// scribble over points[0] to [1]. clip: what follows is clipped to points[0] to [1], until unclip.
|
|
enum class Kind { text, rect, line, circle, poly, world_text, world_line, world_circle, image, world_image, game_image, gradient, skeleton,
|
|
rough_rect, halftone, scribble, clip, unclip } kind{Kind::text};
|
|
std::vector<Vec3> points;
|
|
float radius{};
|
|
std::string text;
|
|
Style style;
|
|
std::vector<std::uint32_t> colours;
|
|
int fit{};
|
|
float edge{};
|
|
int frame{}; // game_image: a flipbook's frame (the loading board's, 0 to 26)
|
|
std::uint32_t seed{};
|
|
int columns{}, rows{};
|
|
};
|
|
struct Layer {
|
|
std::string plugin;
|
|
std::vector<Shape> shapes;
|
|
};
|
|
|
|
struct Widget {
|
|
// bar: a read-only gauge, `value` between `minimum` and `maximum` (an axis, a level). card: a
|
|
// titled tile the widgets after it go in (`help` its subtitle). note / warn: muted help text, a
|
|
// warning. info: `text` and its value in `help`. tabs: the page's tabs, `choices` and `chosen`.
|
|
// toggle and dropdown: `help` says what it does. slider: `format` shows its value ("%.0f %%").
|
|
// button: `on` makes it the card's main action.
|
|
enum class Kind { heading, label, separator, button, toggle, slider, dropdown, bar, card, note, warn, info, tabs } kind{Kind::label};
|
|
std::string id, text;
|
|
bool on{};
|
|
float value{}, minimum{}, maximum{1};
|
|
std::vector<std::string> choices;
|
|
int chosen{};
|
|
std::string help, format;
|
|
};
|
|
struct Panel {
|
|
std::string plugin, title;
|
|
std::vector<Widget> widgets;
|
|
bool screen{}; // a full-screen screen (reskate.ui.screen)
|
|
int dock{}; // a screen docked to a side, the game showing beside it: 1 left, 2 right; 0 full screen;
|
|
// 3 no panel at all (the plugin draws its own with the draw handler)
|
|
float width{}; // a docked screen's share of the window's width
|
|
bool page{}; // the plugin's page (reskate.menu.page); else a panel, a card on that page
|
|
};
|
|
// A plugin with a page in the menu's PLUGINS section: its id, and the page's name there (its page's
|
|
// title, else the plugin's id when it only has panels).
|
|
struct PageEntry {
|
|
std::string plugin, title;
|
|
bool operator==(const PageEntry&) const = default;
|
|
};
|
|
// The widget a full-screen screen's Esc sends.
|
|
inline constexpr char close_widget[] = "__close";
|
|
// A plugin's bind (reskate.input.bind) as the Binds page lists it: the plugin's own combo, and the
|
|
// one in use (the player's, when they recorded or cleared one; 0 is none).
|
|
struct Bind {
|
|
std::string plugin, action;
|
|
std::uint32_t own{}, combo{};
|
|
};
|
|
// The Binds page's change to one: a combo (0 clears it), or nullopt for the plugin's own again.
|
|
struct Rebind {
|
|
std::string plugin, action;
|
|
std::optional<std::uint32_t> combo;
|
|
};
|
|
// What the player did to a widget: pressed a button, or set a toggle, slider or dropdown.
|
|
struct Action {
|
|
std::string plugin, panel, widget;
|
|
bool on{};
|
|
float value{};
|
|
int chosen{};
|
|
};
|
|
|
|
namespace detail {
|
|
inline std::mutex mutex;
|
|
inline std::shared_ptr<const std::vector<Layer>> layers = std::make_shared<std::vector<Layer>>();
|
|
inline std::shared_ptr<const std::vector<Panel>> panels = std::make_shared<std::vector<Panel>>();
|
|
inline std::shared_ptr<const std::vector<PageEntry>> pages = std::make_shared<std::vector<PageEntry>>();
|
|
inline std::string shown_page; // under mutex: the plugin whose page the menu shows
|
|
inline std::vector<Action> actions;
|
|
inline std::shared_ptr<const std::vector<Bind>> binds = std::make_shared<std::vector<Bind>>();
|
|
inline std::vector<Rebind> rebinds;
|
|
inline std::atomic<std::int64_t> panels_seen_ms{};
|
|
inline std::atomic<float> display_width{1920}, display_height{1080};
|
|
// The menus' four fonts as the overlay has them (set_font): each printable character's advance at the
|
|
// font's own size, for d:measure.
|
|
struct FontMetrics {
|
|
float size{};
|
|
std::array<float, 96> advance{}; // characters 32 to 127
|
|
};
|
|
inline std::array<FontMetrics, 5> fonts{};
|
|
inline std::int64_t now_ms() {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
}
|
|
|
|
// Client thread: this tick's shapes and panels. Render thread: the latest of each.
|
|
inline void publish_layers(std::vector<Layer> value) {
|
|
auto shared = std::make_shared<const std::vector<Layer>>(std::move(value));
|
|
std::lock_guard lock(detail::mutex);
|
|
detail::layers = std::move(shared);
|
|
}
|
|
inline std::shared_ptr<const std::vector<Layer>> latest_layers() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return detail::layers;
|
|
}
|
|
inline void publish_panels(std::vector<Panel> value) {
|
|
auto shared = std::make_shared<const std::vector<Panel>>(std::move(value));
|
|
std::lock_guard lock(detail::mutex);
|
|
detail::panels = std::move(shared);
|
|
}
|
|
inline std::shared_ptr<const std::vector<Panel>> latest_panels() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return detail::panels;
|
|
}
|
|
// Client thread, whenever they change: the plugins with a page. Render thread: the latest.
|
|
inline void publish_pages(std::vector<PageEntry> value) {
|
|
auto shared = std::make_shared<const std::vector<PageEntry>>(std::move(value));
|
|
std::lock_guard lock(detail::mutex);
|
|
detail::pages = std::move(shared);
|
|
}
|
|
inline std::shared_ptr<const std::vector<PageEntry>> latest_pages() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return detail::pages;
|
|
}
|
|
// Render thread: a widget changed. At most 64 wait, so a stuck client thread holds no more.
|
|
inline void act(Action action) {
|
|
std::lock_guard lock(detail::mutex);
|
|
if (detail::actions.size() < 64) detail::actions.push_back(std::move(action));
|
|
}
|
|
inline std::vector<Action> take_actions() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return std::exchange(detail::actions, {});
|
|
}
|
|
// Client thread: every plugin's binds, whenever they change.
|
|
inline void publish_binds(std::vector<Bind> value) {
|
|
auto shared = std::make_shared<const std::vector<Bind>>(std::move(value));
|
|
std::lock_guard lock(detail::mutex);
|
|
detail::binds = std::move(shared);
|
|
}
|
|
inline std::shared_ptr<const std::vector<Bind>> latest_binds() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return detail::binds;
|
|
}
|
|
// Render thread: the player recorded, cleared or reset a plugin's bind.
|
|
inline void rebind(Rebind change) {
|
|
std::lock_guard lock(detail::mutex);
|
|
if (detail::rebinds.size() < 64) detail::rebinds.push_back(std::move(change));
|
|
}
|
|
inline std::vector<Rebind> take_rebinds() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return std::exchange(detail::rebinds, {});
|
|
}
|
|
// Render thread, each frame a plugin's page shows: pages are built only while one is open, and then
|
|
// only that plugin's.
|
|
inline void note_page_shown(std::string plugin) {
|
|
detail::panels_seen_ms.store(detail::now_ms(), std::memory_order_relaxed);
|
|
std::lock_guard lock(detail::mutex);
|
|
if (detail::shown_page != plugin) detail::shown_page = std::move(plugin);
|
|
}
|
|
inline bool panels_shown() { return detail::now_ms() - detail::panels_seen_ms.load(std::memory_order_relaxed) < 500; }
|
|
inline std::string shown_page() {
|
|
std::lock_guard lock(detail::mutex);
|
|
return detail::shown_page;
|
|
}
|
|
// Render thread, each frame: the game window's size, which plugins draw in.
|
|
inline void set_display(float width, float height) {
|
|
detail::display_width.store(width, std::memory_order_relaxed);
|
|
detail::display_height.store(height, std::memory_order_relaxed);
|
|
}
|
|
inline std::array<float, 2> display() {
|
|
return {detail::display_width.load(std::memory_order_relaxed), detail::display_height.load(std::memory_order_relaxed)};
|
|
}
|
|
// Render thread, when the overlay's fonts are (re)built.
|
|
inline void set_font(int font, const detail::FontMetrics& metrics) {
|
|
if (font < 0 || font > 4) return;
|
|
std::lock_guard lock(detail::mutex);
|
|
detail::fonts[static_cast<std::size_t>(font)] = metrics;
|
|
}
|
|
// How big `text` is drawn in `font` at `size` pixels (the window's, not 1080's), wrapped to `wrap`
|
|
// pixels when that is more than 0, as the overlay draws it. Before the fonts are known, a guess.
|
|
inline std::array<float, 2> measure(int font, float size, std::string_view text, float wrap) {
|
|
detail::FontMetrics m;
|
|
{
|
|
std::lock_guard lock(detail::mutex);
|
|
m = detail::fonts[static_cast<std::size_t>(std::clamp(font, 0, 4))];
|
|
}
|
|
const float scale = m.size > 0 ? size / m.size : 1.0f;
|
|
const auto advance = [&](char c) {
|
|
const auto u = static_cast<unsigned char>(c);
|
|
if (m.size <= 0) return size * 0.55f;
|
|
return (u >= 32 && u < 128 ? m.advance[u - 32] : m.advance['?' - 32]) * scale;
|
|
};
|
|
float widest = 0, line = 0;
|
|
int lines = 1;
|
|
std::size_t at = 0;
|
|
while (at < text.size()) {
|
|
if (text[at] == '\n') {
|
|
widest = std::max(widest, line);
|
|
line = 0;
|
|
++lines;
|
|
++at;
|
|
continue;
|
|
}
|
|
// A word and the spaces before it: it goes on a new line when it would pass the wrap.
|
|
std::size_t end = at;
|
|
float word = 0;
|
|
while (end < text.size() && text[end] == ' ') word += advance(text[end++]);
|
|
while (end < text.size() && text[end] != ' ' && text[end] != '\n') word += advance(text[end++]);
|
|
if (wrap > 0 && line > 0 && line + word > wrap) {
|
|
widest = std::max(widest, line);
|
|
++lines;
|
|
float trimmed = word;
|
|
for (std::size_t i = at; i < end && text[i] == ' '; ++i) trimmed -= advance(' ');
|
|
line = trimmed;
|
|
} else line += word;
|
|
at = end;
|
|
}
|
|
widest = std::max(widest, line);
|
|
return {widest, static_cast<float>(lines) * size};
|
|
}
|
|
}
|