Files
ReSkate/Extension/Plugins/plugin_api_more.cpp
T
AyeZeeBB 2d3b77701d Plugins: drone, Board Break and Road Rash are plugin mods now; their C++ is gone
The three features are Lua plugins in contrib/plugins (fpv-drone, board-break,
road-rash), built on what this adds to the plugin API: flight input, gamepad and
joystick reads with input capture, a rolled camera, a package's own sounds and a
synthesised motor (plugin_sound, plugin_wave, motor_synth), the skater's physics
events (skater_events), items worn over the outfit (outfit_overlay, skater_items),
the board's bones (board_pose), skater animation (anim_core), plugin images,
throwdowns started for chosen players, and server copies of plugins on a lobby
host and the dedicated server (server_plugins).

Removed from core: Extension/RoadRash, Extension/BoardBreak, the drone's flight,
radio, follow, OSD, beacon, outfit and sound code, its prop models (native_prop),
the DRONE, BOARD BREAK and ROAD RASH menu pages, home tiles and native menu rows,
their console commands, the drone follow debug action, the drone marker packet
(kind 36 stays unused; the roster keeps its byte) and the server's drones setting.

Kept in core, since every board needs it whichever plugins run: the breakable
deck packs (breakable_deck: which deck, its label and wheel spacing, the
boarddeck command and the BREAKABLE DECK card on the Mods page), and board_pose
keeping a deck pack's stretches out of sight, the baseplates on the deck and the
wheels at the deck's spacing on every board.
2026-10-19 16:22:15 -05:00

209 lines
11 KiB
C++

#include "plugin_api.h"
#include "lua.h"
#include "lualib.h"
#include <algorithm>
#include <cmath>
// Phase 3's parts of the reskate API (docs: ReSkate Plugin System Design, "the wider set"). As in
// plugin_api.cpp, every value is checked before it leaves the VM.
namespace dingosdk::plugins {
namespace {
Plugin& plugin_of(lua_State* L) {
auto& plugin = *static_cast<Plugin*>(lua_callbacks(L)->userdata);
if (plugin.halted()) luaL_error(L, "the plugin was stopped");
return plugin;
}
void set_function(lua_State* L, const char* name, lua_CFunction function) {
lua_pushcfunction(L, function, name);
lua_setfield(L, -2, name);
}
std::string checked_text(lua_State* L, int index, std::size_t most, const char* what) {
std::size_t length = 0;
const char* text = luaL_checklstring(L, index, &length);
if (length > most) luaL_error(L, "%s is longer than %d characters", what, static_cast<int>(most));
return std::string(text, length);
}
// A name another plugin calls or listens for: letters, digits and _ . -, 1 to 48 of them.
std::string identifier(lua_State* L, int index, const char* what) {
auto name = checked_text(L, index, 48, what);
const bool valid = !name.empty() && std::all_of(name.begin(), name.end(), [](char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-';
});
if (!valid) luaL_error(L, "%s must be 1 to 48 letters, digits, _ . or -", what);
return name;
}
}
struct MoreApi {
// ---- libraries: functions one plugin offers others --------------------------------------
// expose({name = function, ...}): what other plugins get from require_plugin. Replaces the last.
static int expose(lua_State* L) {
auto& plugin = plugin_of(L);
luaL_checktype(L, 1, LUA_TTABLE);
std::map<std::string, int, std::less<>> next;
lua_pushnil(L);
while (lua_next(L, 1)) {
if (lua_type(L, -2) != LUA_TSTRING || lua_type(L, -1) != LUA_TFUNCTION) {
for (const auto& item : next) lua_unref(L, item.second);
luaL_error(L, "expose takes a table of names and functions");
}
const std::string name = lua_tostring(L, -2);
if (next.size() >= plugin.limits_.exposed || name.size() > 48) {
for (const auto& item : next) lua_unref(L, item.second);
luaL_error(L, "at most %d functions, each name 48 characters", static_cast<int>(plugin.limits_.exposed));
}
lua_pushvalue(L, -1);
next[name] = lua_ref(L, -1);
lua_pop(L, 2);
}
for (const auto& item : plugin.exposed_) lua_unref(L, item.second);
plugin.exposed_ = std::move(next);
return 0;
}
// A proxy's call: its upvalues are the plugin and the function's name. Arguments and the result
// are copied as JSON: tables, strings, numbers and booleans cross; functions never do.
static int proxy(lua_State* L) {
auto& plugin = plugin_of(L);
const std::string target = lua_tostring(L, lua_upvalueindex(1));
const std::string function = lua_tostring(L, lua_upvalueindex(2));
auto arguments = Json::array();
const int count = lua_gettop(L);
for (int i = 1; i <= count; ++i) arguments.push_back(lua_to_json(L, i));
Json result;
const auto error = plugin.services_.call_plugin(plugin.id(), target, function, arguments, result);
if (!error.empty()) luaL_error(L, "%s.%s: %s", target.c_str(), function.c_str(), error.c_str());
push_json(L, result);
return 1;
}
// require_plugin(name): a table of the functions that plugin exposes, or nil and why.
static int require_plugin(lua_State* L) {
auto& plugin = plugin_of(L);
const auto target = identifier(L, 1, "a plugin's name");
std::vector<std::string> names;
if (const auto error = plugin.services_.exposed(target, names); !error.empty()) {
lua_pushnil(L);
lua_pushlstring(L, error.data(), error.size());
return 2;
}
lua_createtable(L, 0, static_cast<int>(names.size()));
for (const auto& name : names) {
lua_pushlstring(L, target.data(), target.size());
lua_pushlstring(L, name.data(), name.size());
lua_pushcclosurek(L, proxy, "proxy", 2, nullptr);
lua_setfield(L, -2, name.c_str());
}
lua_setreadonly(L, -1, true);
return 1;
}
// ---- events between plugins ----------------------------------------------------------------
// emit(event, data?): to every other plugin with an on_plugin handler for it, at the next tick.
static int emit(lua_State* L) {
auto& plugin = plugin_of(L);
const auto event = identifier(L, 1, "an event's name");
Json data = lua_isnoneornil(L, 2) ? Json{} : lua_to_json(L, 2);
if (data.dump().size() > plugin.limits_.event_bytes)
luaL_error(L, "an event's data is at most %d KB as JSON", static_cast<int>(plugin.limits_.event_bytes >> 10));
if (plugin.tick_number_ != plugin.emit_tick_) {
plugin.emit_tick_ = plugin.tick_number_;
plugin.emitted_ = 0;
}
if (++plugin.emitted_ > plugin.limits_.events_per_tick)
luaL_error(L, "more than %d events in one tick", static_cast<int>(plugin.limits_.events_per_tick));
lua_pushboolean(L, plugin.services_.emit(plugin.id(), event, std::move(data)));
return 1;
}
// on_plugin(source, event, fn(source, data)): source is a plugin's name, or "*" for any.
static int on_plugin(lua_State* L) {
auto& plugin = plugin_of(L);
const auto source = lua_isstring(L, 1) && std::string_view(lua_tostring(L, 1)) == "*" ? std::string("*")
: identifier(L, 1, "a plugin's name");
const auto event = identifier(L, 2, "an event's name");
luaL_checktype(L, 3, LUA_TFUNCTION);
if (plugin.listeners_.size() >= plugin.limits_.listeners)
luaL_error(L, "more than %d on_plugin handlers", static_cast<int>(plugin.limits_.listeners));
lua_pushvalue(L, 3);
plugin.listeners_.push_back({source, event, lua_ref(L, -1)});
lua_pop(L, 1);
return 0;
}
// ---- net.http ------------------------------------------------------------------------------
// request({url = "https://host/path", method = "GET" | "POST", body = "...", content_type = "..."},
// function(status, body, error)): to a host its manifest's "domains" lists, HTTPS only.
static int http_request(lua_State* L) {
auto& plugin = plugin_of(L);
if (!plugin.has("net.http")) luaL_error(L, "reskate.http.request needs the \"net.http\" permission in reskate-plugin.json");
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TFUNCTION);
lua_getfield(L, 1, "url");
const auto url = checked_text(L, -1, plugin.limits_.http_url_bytes, "a web request's url");
lua_pop(L, 1);
lua_getfield(L, 1, "method");
const std::string method = lua_isnil(L, -1) ? "GET" : luaL_checkstring(L, -1);
lua_pop(L, 1);
if (method != "GET" && method != "POST") luaL_error(L, "a web request's method is \"GET\" or \"POST\"");
lua_getfield(L, 1, "body");
const std::string body = lua_isnil(L, -1) ? std::string{} : checked_text(L, -1, plugin.limits_.http_body_bytes, "a web request's body");
lua_pop(L, 1);
lua_getfield(L, 1, "content_type");
const std::string type = lua_isnil(L, -1) ? std::string{} : checked_text(L, -1, 100, "a content type");
lua_pop(L, 1);
for (const char c : type)
if (c < 0x20 || c > 0x7e) luaL_error(L, "a content type is printable text");
// https://<host>[:443]<path>: nothing else, the host one the manifest lists.
constexpr std::string_view scheme = "https://";
if (!url.starts_with(scheme)) luaL_error(L, "a web request is HTTPS: its url starts with https://");
const auto rest = std::string_view(url).substr(scheme.size());
const auto slash = rest.find('/');
std::string host(rest.substr(0, slash));
std::string path = slash == std::string_view::npos ? std::string("/") : std::string(rest.substr(slash));
if (host.ends_with(":443")) host.resize(host.size() - 4);
for (auto& c : host) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + 32);
for (const char c : url)
if (c <= 0x20 || c > 0x7e) luaL_error(L, "a url is printable text with no spaces");
if (host.find_first_of("@:[]") != std::string::npos) luaL_error(L, "a url's host is a name from \"domains\", with no user, password or other port");
const auto& domains = plugin.manifest().domains;
if (std::find(domains.begin(), domains.end(), host) == domains.end())
luaL_error(L, "%s is not in reskate-plugin.json's \"domains\"", host.c_str());
const double now = plugin.services_.now_ms();
std::erase_if(plugin.http_sent_, [&](double at) { return now - at >= 60000; });
if (static_cast<int>(plugin.http_sent_.size()) >= plugin.limits_.http_per_minute)
luaL_error(L, "more than %d web requests in a minute", plugin.limits_.http_per_minute);
if (static_cast<int>(plugin.http_pending_.size()) >= plugin.limits_.http_in_flight)
luaL_error(L, "more than %d web requests waiting for an answer", plugin.limits_.http_in_flight);
const auto id = plugin.next_http_++;
if (!plugin.services_.http({id, plugin.id(), method, host, path, body, type})) {
lua_pushnil(L);
lua_pushstring(L, "web requests cannot be made now");
return 2;
}
plugin.http_sent_.push_back(now);
lua_pushvalue(L, 2);
plugin.http_pending_[id] = lua_ref(L, -1);
lua_pop(L, 1);
lua_pushinteger(L, static_cast<int>(id));
return 1;
}
static void install(Plugin& plugin, lua_State* L);
};
void install_more_api(Plugin& plugin, lua_State* L) { MoreApi::install(plugin, L); }
void MoreApi::install(Plugin& plugin, lua_State* L) {
(void)plugin;
// The reskate table is on top of the stack.
set_function(L, "expose", MoreApi::expose);
set_function(L, "require_plugin", MoreApi::require_plugin);
set_function(L, "emit", MoreApi::emit);
set_function(L, "on_plugin", MoreApi::on_plugin);
lua_createtable(L, 0, 1);
lua_pushcfunction(L, MoreApi::http_request, "request");
lua_setfield(L, -2, "request");
lua_setreadonly(L, -1, true);
lua_setfield(L, -2, "http");
}
}