Files
ReSkate/Extension/UI/Overlay/plugins_page.cpp
T
AyeZeeBB 5c0993afea Plugins: the wider set (phase 3), behind one Game interface the tests fake
- Skater: skater.state() and the skater_state and bail events (events.skater); noclip and boosts
  through the console's own queue, under the host's rules (skater.control); a save point of the
  plugin's own (trainer); the game's HUD hidden while any plugin asks (hud).
- World: park editor objects listed, placed, moved and removed through the editor's own edits and
  the session's object rules (world.objects); time of day, world controls and park rotation as
  the World menu sets them, refused where the host chooses (world.environment); fast travel and
  levels, refused for guests (travel); AI skaters, outside sessions only (world.ai).
- Session: other players' skaters as last heard (session.skaters, a feed the session publishes
  each frame, plugin_session_feed.h); a plugin's tag above a player's nametag (session.nametags);
  voice volume (voice.control); chat lines as events (chat.read) and sending one, one every 3 s
  (chat.send); throwdown state and events (throwdowns).
- Music from outside players (music), progress (progression.read) and game settings
  (settings.read), read only; named actions on keys or pad combos, pressed while the game has the
  input (input); full-screen screens with the input, closed by Esc (ui.screen).
- Libraries and events between plugins: expose, require_plugin, emit, on_plugin; plain data is
  copied across VMs and a plugin in a call cannot be called back into.
- net.http: HTTPS to the hosts reskate-plugin.json's new "domains" lists; no redirects, cookies
  or Windows sign-in; private and local addresses refused; 10 a minute, 4 waiting, 1 MB back.
- Left for engine work (listed in the design doc): respawn and freeze, manuals, the camera, trick
  animations, replays, starting throwdowns, the game's own soundtrack, overlay images, rebinding
  plugin keys in the Binds page, local-only chat lines.
- reskate-test has a wider module (ptest noclip, boost, hidehud, object, night, day, ai, screen,
  bind, music, chat, web by hand); reskate-hostile has chat-spam, edit-spam, travel-spam,
  web-elsewhere, bind-spam and screen-loop.

All host tests pass; not yet tried in game.
2026-10-19 13:49:29 -05:00

105 lines
4.2 KiB
C++

#include "skate_menu_internal.h"
#include "Extension/Plugins/plugin_ui.h"
#include <algorithm>
#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 || std::none_of(panels->begin(), panels->end(), [](const auto& p) { return !p.screen; })) {
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) {
if (panel.screen) continue; // a full-screen screen (plugin_overlay.cpp), not a panel
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();
}
}
}