Files
ReSkate/Extension/Plugins/plugin_manifest.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

142 lines
7.4 KiB
C++

#include "plugin_manifest.h"
#include "Engine/Core/Json/json.h"
#include <algorithm>
#include <array>
#include <fstream>
#include <stdexcept>
namespace dingosdk::plugins {
namespace {
constexpr std::array<std::string_view, 40> permissions{
"console", "storage", "overlay", "menu", "ui.screen", "input", "hud", "events.tricks", "events.skater",
"skater.read", "skater.teleport", "skater.control", "skater.animation", "trainer", "camera",
"world.raycast", "world.objects", "world.environment", "world.ai", "travel", "session.read",
"session.skaters", "session.nametags", "net.session", "net.http", "voice.control", "chat.read",
"chat.send", "chat.local", "server", "throwdowns", "replay", "music", "progression.read", "settings.read", "audio.ui", "audio", "outfit", "board", "mods.read"};
}
bool known_permission(std::string_view name) noexcept {
for (const auto known : permissions) if (known == name) return true;
return false;
}
std::string safe_relative_path(std::string_view path) {
if (path.empty() || path.size() > 260 || path.front() == '/') return {};
for (const unsigned char c : path)
if (c < 0x20 || c > 0x7e || c == '\\' || c == ':') return {};
std::size_t start = 0;
while (start <= path.size()) {
const auto end = std::min(path.find('/', start), path.size());
const auto segment = path.substr(start, end - start);
if (segment.empty() || segment == "." || segment == "..") return {};
// Windows drops trailing dots and spaces, so "a." would open "a".
if (segment.back() == '.' || segment.back() == ' ') return {};
start = end + 1;
}
return std::string(path);
}
const char* side_name(Side side) noexcept {
return side == Side::server ? "server" : side == Side::both ? "both" : "client";
}
Manifest parse_manifest(std::string_view text) {
if (text.size() > maximum_manifest_bytes) throw std::runtime_error("reskate-plugin.json is larger than 64 KB");
Json json;
try {
json = Json::parse(text, {maximum_manifest_bytes, 8, 4096});
} catch (const std::exception& error) {
throw std::runtime_error(std::string("reskate-plugin.json is not valid JSON: ") + error.what());
}
if (!json.is_object()) throw std::runtime_error("reskate-plugin.json must hold a JSON object");
Manifest result;
if (!json.contains("api") || !json.at("api").is_number_integer())
throw std::runtime_error("reskate-plugin.json needs \"api\": the API version it was written for (1)");
const auto api = json.at("api").get<std::int64_t>();
if (api < 1) throw std::runtime_error("\"api\" must be 1 or more");
if (api > api_version)
throw std::runtime_error("written for plugin API " + std::to_string(api) + ", and this ReSkate has API " +
std::to_string(api_version) + ": update ReSkate");
result.api = static_cast<int>(api);
if (!json.contains("entry") || !json.at("entry").is_string())
throw std::runtime_error("reskate-plugin.json needs \"entry\": the Lua file to run, such as \"plugin/main.lua\"");
result.entry = safe_relative_path(json.at("entry").string());
if (result.entry.empty())
throw std::runtime_error("\"entry\" must be a path inside the mod folder, with / between folders and no ..");
if (!result.entry.ends_with(".lua") && !result.entry.ends_with(".luau"))
throw std::runtime_error("\"entry\" must be a .lua or .luau file");
if (json.contains("side")) {
const auto& side = json.at("side");
const auto name = side.is_string() ? side.string() : std::string{};
if (name == "client") result.side = Side::client;
else if (name == "server") result.side = Side::server;
else if (name == "both") result.side = Side::both;
else throw std::runtime_error("\"side\" must be \"client\", \"server\" or \"both\"");
}
if (json.contains("permissions")) {
const auto& list = json.at("permissions");
if (!list.is_array()) throw std::runtime_error("\"permissions\" must be a list of names");
for (const auto& item : list) {
if (!item.is_string()) throw std::runtime_error("\"permissions\" must be a list of names");
const auto& name = item.string();
if (!known_permission(name))
throw std::runtime_error("unknown permission \"" + name + "\": check the spelling, or update ReSkate");
result.permissions.insert(name);
}
}
if (json.contains("domains")) {
const auto& list = json.at("domains");
if (!list.is_array() || list.size() > 16) throw std::runtime_error("\"domains\" must be a list of at most 16 host names");
for (const auto& item : list) {
if (!item.is_string()) throw std::runtime_error("\"domains\" must be a list of host names");
std::string name = item.string();
for (auto& c : name) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + 32);
const bool valid = !name.empty() && name.size() <= 253 && name.front() != '.' && name.back() != '.' &&
name.find("..") == std::string::npos && name.find('.') != std::string::npos &&
std::all_of(name.begin(), name.end(), [](char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '-'; });
if (!valid) throw std::runtime_error("\"" + item.string() + "\" is not a host name for \"domains\" (such as api.example.com: no scheme, port, path or *)");
result.domains.push_back(name);
}
if (!result.domains.empty() && !result.permissions.contains("net.http"))
throw std::runtime_error("\"domains\" is for web requests: add the \"net.http\" permission too");
}
return result;
}
namespace {
constexpr std::array<std::string_view, 22> runnable{".exe", ".dll", ".sys", ".scr", ".com", ".cpl", ".msi", ".msp",
".bat", ".cmd", ".ps1", ".psm1", ".vbs", ".vbe", ".js", ".jse", ".wsf", ".wsh", ".hta", ".lnk", ".pif", ".reg"};
constexpr std::size_t most_files = 20000;
// path::string() throws on names outside the ANSI code page; a name here is only shown or compared.
std::string ascii_lowercase(const std::filesystem::path& path) {
std::string text;
for (const auto c : path.generic_wstring())
text += c < 0x80 ? static_cast<char>(c >= L'A' && c <= L'Z' ? c + 32 : c) : '?';
return text;
}
}
std::string check_package_files(const std::filesystem::path& directory) {
std::error_code code;
std::size_t files = 0;
for (std::filesystem::recursive_directory_iterator it(directory, std::filesystem::directory_options::skip_permission_denied, code), end;
!code && it != end; it.increment(code)) {
std::error_code entry_code;
if (!it->is_regular_file(entry_code)) continue;
if (++files > most_files) return "it holds more than 20000 files";
const auto shown = ascii_lowercase(std::filesystem::relative(it->path(), directory, entry_code));
const auto extension = ascii_lowercase(it->path().extension());
bool program = std::find(runnable.begin(), runnable.end(), extension) != runnable.end();
if (!program) {
std::ifstream in(it->path(), std::ios::binary);
char magic[2]{};
program = in.read(magic, 2) && magic[0] == 'M' && magic[1] == 'Z';
}
if (program) return "it holds " + shown + ", a program or a script Windows runs. A plugin never ships one";
}
return {};
}
}