Files
ReSkate/Extension/UI/Overlay/plugins_page.cpp
T
AyeZeeBB d884c1c161 Plugins: the rest of the core set (overlay, menu panels, sounds, mods, session and messages)
- overlay: reskate.on("draw", function(d) ...) each tick, with text, rect, line, circle, poly
  and world_text, world_line, world_circle (projected through the camera as the frame draws),
  size and world_to_screen; 4000 shapes a frame. reskate.ui.toast through ReSkate's notices, three
  in 5 s. Images are left for later: they need texture slots in the overlay's renderer.
- menu: reskate.menu.panel(title, build) on a new PLUGINS tab of the ADDONS page, with heading,
  label, separator, button, toggle, slider and dropdown, built ten times a second while it shows.
- audio.ui: the game's menu sounds by name, ten a second. mods.read: installed(name) and list(),
  from the discovery thread's scan.
- session.read: active, is_dedicated, players, me, host, and player_joined / player_left.
  net.session: send(to, message) to the same plugin on other games (by its manifest.json name) in
  the session's throwdown packets, tagged 0xD1 (plugin_wire.h); 4 KB each, 16 KB a second.
- The overlay and the host share plugin_ui.h (shapes, panels, actions) and plugin_camera.h.
- Fixed: the overlay asked ImGui for the window's size before its context existed, which crashed
  on the first frames; it now reads the size from the context.
- reskate-test has overlay (ptest hud), menu, audio, mods and session (ptest ping) modules;
  reskate-hostile has draw-flood, toast-spam, sound-spam, message-flood and panel-loop.

Checked in game: ptest all 62 passed, 0 failed; the test HUD, the PLUGINS tab and its panel; every
hostile attack stopped or refused, trick-loop and panel-loop included. Not yet: ptest ping with a
second player.
2026-10-19 13:20:43 -05:00

103 lines
4.0 KiB
C++

#include "skate_menu_internal.h"
#include "Extension/Plugins/plugin_ui.h"
#include <map>
#include <string>
#include <vector>
// The ADDONS page's PLUGINS tab: each plugin's panels (reskate.menu.panel), drawn with the menu's
// own widgets. Plugins build them on the client thread ten times a second while this tab shows
// (plugin_ui.h), and what the player does goes back to them as actions.
namespace dingosdk::overlay::menu {
namespace {
// What the player last set on a widget, shown until the plugin's next build agrees, or a second
// passes: the plugin's answer comes a few frames later.
struct Edit {
bool on{};
float value{};
int chosen{};
double at{};
};
std::map<std::string, Edit>& edits() {
static auto* value = new std::map<std::string, Edit>;
return *value;
}
}
void plugins_addon(SkateMenu& menu) {
plugin_ui::note_panels_shown();
const auto panels = plugin_ui::latest_panels();
const double now = ImGui::GetTime();
std::erase_if(edits(), [&](const auto& item) { return now - item.second.at > 1.0; });
if (!panels || panels->empty()) {
begin_card(menu, "plugins-none", "PLUGINS");
note("No plugin has a panel here. A plugin adds one with reskate.menu.panel (it needs the \"menu\" permission). "
"plugin list in the console shows every plugin.");
end_card();
return;
}
int index = 0;
for (const auto& panel : *panels) {
ImGui::PushID(index++);
begin_card(menu, "plugin-panel", panel.title.c_str(), panel.plugin.c_str());
int item = 0;
for (const auto& w : panel.widgets) {
ImGui::PushID(item++);
const auto key = panel.plugin + '\n' + panel.title + '\n' + w.id;
auto& all = edits();
const auto edit = all.find(key);
const auto send = [&](plugin_ui::Action action, Edit remembered) {
action.plugin = panel.plugin;
action.panel = panel.title;
action.widget = w.id;
plugin_ui::act(std::move(action));
remembered.at = now;
all[key] = remembered;
};
using Kind = plugin_ui::Widget::Kind;
switch (w.kind) {
case Kind::heading: section(menu, w.text.c_str()); break;
case Kind::label: ImGui::TextWrapped("%s", w.text.c_str()); break;
case Kind::separator: ImGui::Separator(); break;
case Kind::button:
if (ImGui::Button(w.text.c_str(), ImVec2(-FLT_MIN, px(34)))) send({}, {});
break;
case Kind::toggle: {
bool on = edit != all.end() ? edit->second.on : w.on;
if (edit != all.end() && edit->second.on == w.on) all.erase(edit);
if (toggle_row(menu, w.text.c_str(), "", on)) {
plugin_ui::Action action;
action.on = on;
send(action, {on});
}
break;
}
case Kind::slider: {
float value = edit != all.end() ? edit->second.value : w.value;
field(menu, w.text.c_str());
if (slider_float("##slider", &value, w.minimum, w.maximum, "%.2f", ImGuiSliderFlags_AlwaysClamp)) {
plugin_ui::Action action;
action.value = value;
send(action, {false, value});
}
break;
}
case Kind::dropdown: {
int selected = (edit != all.end() ? edit->second.chosen : w.chosen) - 1;
if (edit != all.end() && edit->second.chosen == w.chosen) all.erase(edit);
if (option_row(menu, w.text.c_str(), "", selected, w.choices)) {
plugin_ui::Action action;
action.chosen = selected + 1;
send(action, {false, 0, selected + 1});
}
break;
}
}
ImGui::PopID();
}
end_card();
ImGui::PopID();
}
}
}