Files
ReSkate/Extension/UI/Overlay/plugin_overlay.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

239 lines
12 KiB
C++

#include "plugin_overlay.h"
#include "Extension/Plugins/plugin_camera.h"
#include "Extension/Plugins/plugin_ui.h"
#include "Extension/UI/Overlay/game_images.h"
#include "Extension/UI/Overlay/overlay_internal.h"
#include "Extension/UI/Overlay/xray_overlay.h"
#include <algorithm>
#include <imgui_internal.h>
#include <memory>
namespace dingosdk::overlay {
namespace {
// Render thread: the layers taken by plugins_pending, drawn by draw_plugins.
std::shared_ptr<const std::vector<plugin_ui::Layer>>& frame() {
static auto* value = new std::shared_ptr<const std::vector<plugin_ui::Layer>>;
return *value;
}
ImVec2 at(const plugin_ui::Vec3& p) { return ImVec2(p[0], p[1]); }
ImU32 shadow_of(ImU32 colour) {
const auto alpha = static_cast<ImU32>(((colour >> IM_COL32_A_SHIFT) & 0xff) * 0.75f);
return IM_COL32(0, 0, 0, alpha);
}
void text(ImDrawList* draw, ImFont* font, float k, ImVec2 where, const plugin_ui::Shape& shape) {
const float size = shape.style.size * k;
const auto width = font->CalcTextSizeA(size, FLT_MAX, 0.0f, shape.text.c_str()).x;
if (shape.style.align == 1) where.x -= width * 0.5f;
else if (shape.style.align == 2) where.x -= width;
if (shape.style.shadow) {
const float offset = std::max(1.0f, size * 0.06f);
draw->AddText(font, size, ImVec2(where.x + offset, where.y + offset), shadow_of(shape.style.colour), shape.text.c_str());
}
draw->AddText(font, size, where, shape.style.colour, shape.text.c_str());
}
void circle(ImDrawList* draw, ImVec2 centre, float radius, const plugin_ui::Style& style, float k) {
if (radius <= 0) return;
if (style.fill) draw->AddCircleFilled(centre, radius, style.colour);
else draw->AddCircle(centre, radius, style.colour, 0, style.thickness * k);
}
}
// The screen Esc closed, until its plugin's next build no longer has it open.
static std::string& closed_screen() {
static auto* value = new std::string;
return *value;
}
static const plugin_ui::Panel* open_screen() {
const auto panels = plugin_ui::latest_panels();
static std::shared_ptr<const std::vector<plugin_ui::Panel>> held;
held = panels;
if (!held) return nullptr;
for (const auto& panel : *held)
if (panel.screen) {
if (closed_screen() == panel.plugin + '\n' + panel.title) return nullptr;
return &panel;
}
closed_screen().clear();
return nullptr;
}
// Called before the overlay's ImGui context exists or is made current, too: its size is read from
// the context itself (what its last frame had), never through ImGui's current one.
bool plugins_pending() {
detail::state().plugin_screen_visible.store(open_screen() != nullptr);
if (const auto* context = detail::state().context) {
const auto display = context->IO.DisplaySize;
if (display.x > 0 && display.y > 0) plugin_ui::set_display(display.x, display.y);
}
frame() = plugin_ui::latest_layers();
return frame() && !frame()->empty();
}
void draw_plugin_screen() {
const auto* screen = open_screen();
if (!screen) return;
const auto display = ImGui::GetIO().DisplaySize;
const float k = std::max(1.0f, display.y / 1080.0f);
auto& s = detail::state();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(display);
ImGui::SetNextWindowBgAlpha(0.94f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(64 * k, 48 * k));
const auto flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings;
if (ImGui::Begin("##plugin-screen", nullptr, flags)) {
ImGui::SetWindowFontScale(1.25f * k);
if (s.menu.heading) ImGui::PushFont(s.menu.heading);
ImGui::TextUnformatted(screen->title.c_str());
if (s.menu.heading) ImGui::PopFont();
ImGui::TextDisabled("%s - Esc closes it", screen->plugin.c_str());
ImGui::Separator();
ImGui::Spacing();
int item = 0;
for (const auto& w : screen->widgets) {
ImGui::PushID(item++);
plugin_ui::Action action{screen->plugin, screen->title, w.id};
bool acted = false;
using Kind = plugin_ui::Widget::Kind;
switch (w.kind) {
case Kind::heading:
ImGui::Spacing();
if (s.menu.bold) ImGui::PushFont(s.menu.bold);
ImGui::TextUnformatted(w.text.c_str());
if (s.menu.bold) ImGui::PopFont();
break;
case Kind::label: ImGui::TextWrapped("%s", w.text.c_str()); break;
case Kind::separator: ImGui::Separator(); break;
case Kind::bar:
ImGui::ProgressBar((w.value - w.minimum) / (w.maximum - w.minimum), ImVec2(480 * k, 0), w.text.c_str());
break;
case Kind::button: acted = ImGui::Button(w.text.c_str(), ImVec2(320 * k, 0)); break;
case Kind::toggle: {
bool on = w.on;
if (ImGui::Checkbox(w.text.c_str(), &on)) {
action.on = on;
acted = true;
}
break;
}
case Kind::slider: {
float value = w.value;
ImGui::SetNextItemWidth(480 * k);
if (ImGui::SliderFloat(w.text.c_str(), &value, w.minimum, w.maximum, "%.2f", ImGuiSliderFlags_AlwaysClamp)) {
action.value = value;
acted = true;
}
break;
}
case Kind::dropdown: {
int selected = std::clamp(w.chosen - 1, 0, static_cast<int>(w.choices.size()) - 1);
ImGui::SetNextItemWidth(480 * k);
if (ImGui::BeginCombo(w.text.c_str(), w.choices.empty() ? "" : w.choices[static_cast<std::size_t>(selected)].c_str())) {
for (int i = 0; i < static_cast<int>(w.choices.size()); ++i)
if (ImGui::Selectable(w.choices[static_cast<std::size_t>(i)].c_str(), i == selected)) {
action.chosen = i + 1;
acted = true;
}
ImGui::EndCombo();
}
break;
}
}
if (acted) plugin_ui::act(std::move(action));
ImGui::PopID();
}
// Esc closes it at once here; its plugin hears screen_closed.
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) && !ImGui::GetIO().WantTextInput) {
plugin_ui::act({screen->plugin, screen->title, plugin_ui::close_widget});
closed_screen() = screen->plugin + '\n' + screen->title;
s.plugin_screen_visible.store(false);
}
}
ImGui::End();
ImGui::PopStyleVar();
}
void draw_plugins() {
const auto layers = frame();
if (!layers || layers->empty()) return;
const auto display = ImGui::GetIO().DisplaySize;
if (display.x <= 0 || display.y <= 0) return;
const float k = display.y / 1080.0f;
auto& s = detail::state();
auto* fallback = ImGui::GetFont();
const std::array<ImFont*, 4> fonts{s.menu.bold ? s.menu.bold : fallback, s.menu.body ? s.menu.body : fallback,
s.menu.heading ? s.menu.heading : fallback, s.menu.title ? s.menu.title : fallback};
auto* draw = ImGui::GetBackgroundDrawList();
const auto camera = plugin_camera::projector(display.x, display.y);
using Kind = plugin_ui::Shape::Kind;
for (const auto& layer : *layers)
for (const auto& shape : layer.shapes) {
const auto& style = shape.style;
const auto& p = shape.points;
auto* font = fonts[static_cast<std::size_t>(std::clamp(style.font, 0, 3))];
switch (shape.kind) {
case Kind::text: text(draw, font, k, at(p[0]), shape); break;
case Kind::rect:
if (style.fill) draw->AddRectFilled(at(p[0]), at(p[1]), style.colour, style.rounding * k);
else draw->AddRect(at(p[0]), at(p[1]), style.colour, style.rounding * k, 0, style.thickness * k);
break;
case Kind::line: draw->AddLine(at(p[0]), at(p[1]), style.colour, style.thickness * k); break;
case Kind::circle: circle(draw, at(p[0]), shape.radius, style, k); break;
case Kind::poly: {
std::vector<ImVec2> points;
for (const auto& point : p) points.push_back(at(point));
if (style.fill) draw->AddConvexPolyFilled(points.data(), static_cast<int>(points.size()), style.colour);
else draw->AddPolyline(points.data(), static_cast<int>(points.size()), style.colour, ImDrawFlags_Closed, style.thickness * k);
break;
}
case Kind::world_text:
if (const auto on = camera ? camera->project(p[0]) : std::nullopt) text(draw, font, k, ImVec2((*on)[0], (*on)[1]), shape);
break;
case Kind::world_line: {
const auto a = camera ? camera->project(p[0]) : std::nullopt;
const auto b = camera ? camera->project(p[1]) : std::nullopt;
if (a && b) draw->AddLine(ImVec2((*a)[0], (*a)[1]), ImVec2((*b)[0], (*b)[1]), style.colour, style.thickness * k);
break;
}
case Kind::world_circle:
if (const auto on = camera ? camera->project(p[0]) : std::nullopt)
circle(draw, ImVec2((*on)[0], (*on)[1]), std::min(shape.radius * camera->focal / (*on)[2], display.y), style, k);
break;
case Kind::image: {
ImVec2 size;
if (const auto id = detail::plugin_image(shape.text, size))
draw->AddImageRounded(id, at(p[0]), at(p[1]), ImVec2(style.uv[0], style.uv[1]), ImVec2(style.uv[2], style.uv[3]), style.colour,
style.rounding * k);
break;
}
case Kind::world_image: {
ImVec2 size;
const auto id = detail::plugin_image(shape.text, size);
const auto on = camera ? camera->project(p[0]) : std::nullopt;
if (!id || !on || size.y <= 0) break;
const float span_x = std::abs(style.uv[2] - style.uv[0]), span_y = std::abs(style.uv[3] - style.uv[1]);
const float aspect = span_y > 0 ? size.x * span_x / (size.y * span_y) : 1.0f;
const float height = std::min(shape.radius * camera->focal / (*on)[2], display.y), width = height * aspect;
draw->AddImage(id, ImVec2((*on)[0] - width * .5f, (*on)[1] - height * .5f), ImVec2((*on)[0] + width * .5f, (*on)[1] + height * .5f),
ImVec2(style.uv[0], style.uv[1]), ImVec2(style.uv[2], style.uv[3]), style.colour);
break;
}
case Kind::game_image:
(void)draw_game_image(draw, shape.text, at(p[0]), at(p[1]), style.colour, static_cast<GameImageFit>(std::clamp(shape.fit, 0, 2)),
shape.edge * k);
break;
case Kind::gradient:
if (shape.colours.size() == 4)
draw->AddRectFilledMultiColor(at(p[0]), at(p[1]), shape.colours[0], shape.colours[1], shape.colours[2], shape.colours[3]);
break;
case Kind::skeleton: {
if (shape.colours.size() != skater_body::count) break;
std::array<ImU32, skater_body::count> paint{};
std::copy(shape.colours.begin(), shape.colours.end(), paint.begin());
draw_xray(draw, paint, static_cast<float>(style.colour >> IM_COL32_A_SHIFT & 0xff) / 255.0f);
break;
}
}
}
}
}