mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 21:05:58 +02:00
reskate.throwdowns.mode{...} adds a mode to the game's own Throwdowns page: a card on
the picker, the real Spot Battle setup page relabelled with the mode's name, logo,
description, players, timer and rows (sliders, choices, buttons), and the native start
flag. The plugin hears throwdown_mode events (opened, setting, button, placed with the
flag's position and heading, start, leave, cancelled) and runs the mode itself;
release_flag takes the flag down. The flag's queue never starts a Spot Battle: its
waiting card's Start goes to the plugin, MaxPlayers is held at 10 and it is never
relayed as a drop. After SmokeyDawg58's one_up_menu and one_up_placement
(reskate-custom-mods), generalised over a mode registry.
The page is his 4-column logo grid whether or not a plugin has a mode. Its widgets and
grid records, and the game assets it uses that only some levels load, ship in
ReSkate.dll as the throwdown_menu built-in pack, taken from his asset mod's FBMOD
(dingosdk_ebx_pick trims the asset list in place). A mode's logo is a texture path in
its own mod, so no mod edits the shared list. The FPV drone is no longer a built-in pack.
Native menu data: text initialization, change-only writes, retained array publication,
owned-model rollback. Tests: registry and setup helpers, the plugin API, a test plugin
with two modes.
702 lines
30 KiB
C++
702 lines
30 KiB
C++
#include "plugin_api.h"
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <format>
|
|
#include <optional>
|
|
|
|
// The reskate API, v1 base set plus console and storage (docs: ReSkate Plugin System Design).
|
|
// Every function checks what it is given before acting on it, and no value a plugin passes
|
|
// reaches anything outside its own VM unchecked. Errors are raised the way Lua reports them:
|
|
// luaL_error, or a C++ exception, which Luau turns into a Lua error with its message.
|
|
namespace dingosdk::plugins {
|
|
namespace {
|
|
// Events and the permission each needs (none for the base set).
|
|
struct Event {
|
|
std::string_view name, permission;
|
|
};
|
|
constexpr std::array<Event, 25> events{{
|
|
{"load", {}}, {"unload", {}}, {"tick", {}}, {"level_loaded", {}}, {"level_unloading", {}},
|
|
{"trick_started", "events.tricks"}, {"trick_landed", "events.tricks"}, {"line_broken", "events.tricks"},
|
|
{"draw", "overlay"}, {"player_joined", "session.read"}, {"player_left", "session.read"}, {"message", "net.session"},
|
|
{"skater_state", "events.skater"}, {"bail", "events.skater"}, {"chat", "chat.read"}, {"track_changed", "music"},
|
|
{"throwdown", "throwdowns"}, {"action", "input"}, {"screen_closed", "ui.screen"},
|
|
{"board_landed", "events.skater"}, {"body_hit", "events.skater"}, {"bail_end", "events.skater"},
|
|
{"physics_step", "events.physics"}, {"grind_exit", "events.skater"}, {"throwdown_mode", "throwdowns"},
|
|
}};
|
|
constexpr std::size_t maximum_key = 128;
|
|
constexpr std::size_t maximum_command = 48;
|
|
constexpr std::size_t maximum_listing = 1000;
|
|
|
|
// A plugin that caught its own memory or time error in a pcall gets no further: nothing it asks
|
|
// for is done, and the next safe point stops it.
|
|
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;
|
|
}
|
|
|
|
// The table being filled is on top of the stack.
|
|
void set_function(lua_State* L, const char* name, lua_CFunction function) {
|
|
lua_pushcfunction(L, function, name);
|
|
lua_setfield(L, -2, name);
|
|
}
|
|
|
|
std::string text_of(lua_State* L, int index) {
|
|
std::size_t length = 0;
|
|
const char* text = luaL_tolstring(L, index, &length);
|
|
std::string result(text, length);
|
|
lua_pop(L, 1);
|
|
return result;
|
|
}
|
|
|
|
// What turning Lua into JSON may cost outside the VM's own heap.
|
|
struct Budget {
|
|
std::size_t values = 1u << 20;
|
|
std::size_t bytes = 16u << 20;
|
|
void spend(std::size_t count, std::size_t size) {
|
|
if (count > values || size > bytes) throw std::runtime_error("the value is too large to turn into JSON");
|
|
values -= count;
|
|
bytes -= size;
|
|
}
|
|
};
|
|
|
|
Json to_json(lua_State* L, int index, int depth, Budget& budget) {
|
|
if (depth > 32) throw std::runtime_error("tables nested deeper than 32 cannot be turned into JSON");
|
|
index = lua_absindex(L, index);
|
|
budget.spend(1, 0);
|
|
switch (lua_type(L, index)) {
|
|
case LUA_TNIL: return nullptr;
|
|
case LUA_TBOOLEAN: return lua_toboolean(L, index) != 0;
|
|
case LUA_TNUMBER: {
|
|
const double value = lua_tonumber(L, index);
|
|
if (!std::isfinite(value)) throw std::runtime_error("JSON has no NaN or infinity");
|
|
if (value == std::floor(value) && std::fabs(value) <= 9007199254740992.0) return static_cast<std::int64_t>(value);
|
|
return value;
|
|
}
|
|
case LUA_TSTRING: {
|
|
std::size_t length = 0;
|
|
const char* text = lua_tolstring(L, index, &length);
|
|
budget.spend(0, length);
|
|
return std::string(text, length);
|
|
}
|
|
case LUA_TTABLE: {
|
|
luaL_checkstack(L, 4, "tables nested too deep");
|
|
const int length = lua_objlen(L, index);
|
|
bool list = length > 0;
|
|
int count = 0;
|
|
lua_pushnil(L);
|
|
while (lua_next(L, index)) {
|
|
++count;
|
|
if (lua_type(L, -2) != LUA_TNUMBER) list = false;
|
|
else {
|
|
const double key = lua_tonumber(L, -2);
|
|
if (key != std::floor(key) || key < 1 || key > length) list = false;
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
if (list && count == length) {
|
|
auto result = Json::array();
|
|
for (int i = 1; i <= length; ++i) {
|
|
lua_rawgeti(L, index, i);
|
|
result.push_back(to_json(L, -1, depth + 1, budget));
|
|
lua_pop(L, 1);
|
|
}
|
|
return result;
|
|
}
|
|
auto result = Json::object();
|
|
lua_pushnil(L);
|
|
while (lua_next(L, index)) {
|
|
if (lua_type(L, -2) != LUA_TSTRING)
|
|
throw std::runtime_error("only a list (keys 1 to n) or a table with string keys can be turned into JSON");
|
|
std::size_t size = 0;
|
|
const char* key = lua_tolstring(L, -2, &size);
|
|
budget.spend(0, size);
|
|
result[std::string_view(key, size)] = to_json(L, -1, depth + 1, budget);
|
|
lua_pop(L, 1);
|
|
}
|
|
return result;
|
|
}
|
|
default:
|
|
throw std::runtime_error(std::string("a ") + lua_typename(L, lua_type(L, index)) + " cannot be turned into JSON");
|
|
}
|
|
}
|
|
|
|
void push_json_value(lua_State* L, const Json& value, int depth) {
|
|
luaL_checkstack(L, 4, "JSON nested too deep");
|
|
if (depth > 64) throw std::runtime_error("JSON nested too deep");
|
|
if (value.is_null()) lua_pushnil(L);
|
|
else if (value.is_boolean()) lua_pushboolean(L, value.get<bool>());
|
|
else if (value.is_number()) lua_pushnumber(L, value.get<double>());
|
|
else if (value.is_string()) lua_pushlstring(L, value.string().data(), value.string().size());
|
|
else if (value.is_array()) {
|
|
lua_createtable(L, static_cast<int>(value.size()), 0);
|
|
int index = 0;
|
|
for (const auto& item : value) {
|
|
push_json_value(L, item, depth + 1);
|
|
lua_rawseti(L, -2, ++index);
|
|
}
|
|
} else {
|
|
lua_createtable(L, 0, static_cast<int>(value.size()));
|
|
for (const auto& [key, item] : value.items()) {
|
|
lua_pushlstring(L, key.data(), key.size());
|
|
push_json_value(L, item, depth + 1);
|
|
lua_rawset(L, -3);
|
|
}
|
|
}
|
|
}
|
|
|
|
void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) {
|
|
if (!plugin.has(permission))
|
|
luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission);
|
|
}
|
|
|
|
std::string key_of(lua_State* L, int index) {
|
|
std::size_t length = 0;
|
|
const char* key = luaL_checklstring(L, index, &length);
|
|
if (length == 0 || length > maximum_key) luaL_error(L, "a storage key must be 1 to %d characters", static_cast<int>(maximum_key));
|
|
return std::string(key, length);
|
|
}
|
|
|
|
// A file or folder of the plugin's own package, never outside it: the path is checked as text,
|
|
// then once more after links and junctions are resolved.
|
|
std::filesystem::path package_path(lua_State* L, Plugin& plugin, std::string_view path, bool folder) {
|
|
std::string relative = folder && path.empty() ? std::string{} : safe_relative_path(path);
|
|
if (relative.empty() && !(folder && path.empty()))
|
|
luaL_error(L, "\"%s\" is not a path inside the plugin's package (use / between folders, no ..)", std::string(path).c_str());
|
|
const auto root = plugin.directory();
|
|
const auto full = relative.empty() ? root : root / std::filesystem::path(relative);
|
|
std::error_code code;
|
|
const auto real_root = std::filesystem::weakly_canonical(root, code);
|
|
const auto real = std::filesystem::weakly_canonical(full, code);
|
|
const auto inside = real.lexically_relative(real_root);
|
|
if (code || (!relative.empty() && (inside.empty() || *inside.begin() == ".."))) luaL_error(L, "that path leads outside the package");
|
|
return full;
|
|
}
|
|
|
|
// A position a plugin passes: a vector, finite and within 100 km of the map's centre.
|
|
Vec3 position_of(lua_State* L, int index, const char* what) {
|
|
const float* v = luaL_checkvector(L, index);
|
|
for (int i = 0; i < 3; ++i)
|
|
if (!std::isfinite(v[i]) || std::fabs(v[i]) > 1e5f) luaL_error(L, "%s must be a finite position within 100 km", what);
|
|
return {v[0], v[1], v[2]};
|
|
}
|
|
void push_position(lua_State* L, const Vec3& v) { lua_pushvector(L, v[0], v[1], v[2]); }
|
|
}
|
|
|
|
std::filesystem::path package_file(lua_State* L, Plugin& plugin, std::string_view path) { return package_path(L, plugin, path, false); }
|
|
|
|
struct ApiFunctions {
|
|
static int log(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto message = text_of(L, 1);
|
|
const std::string level = luaL_optstring(L, 2, "info");
|
|
if (level != "info" && level != "warn" && level != "error") luaL_argerror(L, 2, "the level must be \"info\", \"warn\" or \"error\"");
|
|
plugin.log(level == "error" ? LogLevel::error : level == "warn" ? LogLevel::warn : LogLevel::info, message);
|
|
return 0;
|
|
}
|
|
static int print(lua_State* L) {
|
|
std::string line;
|
|
const int count = lua_gettop(L);
|
|
for (int i = 1; i <= count; ++i) {
|
|
if (i > 1) line += '\t';
|
|
line += text_of(L, i);
|
|
}
|
|
plugin_of(L).log(LogLevel::info, line);
|
|
return 0;
|
|
}
|
|
static int on(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const std::string event = luaL_checkstring(L, 1);
|
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
|
const auto known = std::find_if(events.begin(), events.end(), [&](const Event& e) { return e.name == event; });
|
|
if (known == events.end()) luaL_error(L, "there is no event \"%s\" in this ReSkate", event.c_str());
|
|
if (!known->permission.empty() && !plugin.has(known->permission))
|
|
luaL_error(L, "the %s event needs the \"%s\" permission in reskate-plugin.json", event.c_str(), std::string(known->permission).c_str());
|
|
auto& list = plugin.handlers_[event];
|
|
if (list.size() >= plugin.limits_.handlers) luaL_error(L, "more than %d handlers for one event", static_cast<int>(plugin.limits_.handlers));
|
|
lua_pushvalue(L, 2);
|
|
list.push_back(lua_ref(L, -1));
|
|
lua_pop(L, 1);
|
|
return 0;
|
|
}
|
|
static int now(lua_State* L) {
|
|
lua_pushnumber(L, plugin_of(L).services_.now_ms());
|
|
return 1;
|
|
}
|
|
static int timer(lua_State* L, bool repeat) {
|
|
auto& plugin = plugin_of(L);
|
|
const double ms = luaL_checknumber(L, 1);
|
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
|
if (!std::isfinite(ms) || ms < (repeat ? 1 : 0)) luaL_argerror(L, 1, repeat ? "must be 1 ms or more" : "must be 0 ms or more");
|
|
if (plugin.timers_.size() >= plugin.limits_.timers) luaL_error(L, "more than %d timers", static_cast<int>(plugin.limits_.timers));
|
|
lua_pushvalue(L, 2);
|
|
const int function = lua_ref(L, -1);
|
|
lua_pop(L, 1);
|
|
const int id = plugin.next_timer_++;
|
|
plugin.timers_[id] = {plugin.services_.now_ms() + ms, repeat ? ms : 0, function};
|
|
lua_pushinteger(L, id);
|
|
return 1;
|
|
}
|
|
static int after(lua_State* L) { return timer(L, false); }
|
|
static int every(lua_State* L) { return timer(L, true); }
|
|
static int cancel(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto found = plugin.timers_.find(luaL_checkinteger(L, 1));
|
|
if (found != plugin.timers_.end()) {
|
|
lua_unref(L, found->second.function);
|
|
plugin.timers_.erase(found);
|
|
}
|
|
return 0;
|
|
}
|
|
static int level(lua_State* L) {
|
|
const auto name = plugin_of(L).services_.level();
|
|
if (name.empty()) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 1);
|
|
lua_pushlstring(L, name.data(), name.size());
|
|
lua_setfield(L, -2, "name");
|
|
return 1;
|
|
}
|
|
static int read(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto file = package_path(L, plugin, luaL_checkstring(L, 1), false);
|
|
std::string error;
|
|
const auto text = read_package_file(file, plugin.limits_.file_bytes, error);
|
|
if (error == "not found") {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
if (!error.empty()) luaL_error(L, "%s %s", luaL_checkstring(L, 1), error.c_str());
|
|
lua_pushlstring(L, text.data(), text.size());
|
|
return 1;
|
|
}
|
|
static int read_json(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto file = package_path(L, plugin, luaL_checkstring(L, 1), false);
|
|
std::string error;
|
|
const auto text = read_package_file(file, plugin.limits_.file_bytes, error);
|
|
if (error == "not found") {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
if (!error.empty()) luaL_error(L, "%s %s", luaL_checkstring(L, 1), error.c_str());
|
|
push_json(L, Json::parse(text, {plugin.limits_.file_bytes, 64, 1u << 20}));
|
|
return 1;
|
|
}
|
|
static int list(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
const auto folder = package_path(L, plugin, luaL_optstring(L, 1, ""), true);
|
|
std::vector<std::string> names;
|
|
std::error_code code;
|
|
for (std::filesystem::directory_iterator it(folder, code), end; !code && it != end; it.increment(code)) {
|
|
const auto name = it->path().filename().wstring();
|
|
if (!std::all_of(name.begin(), name.end(), [](wchar_t c) { return c >= 0x20 && c < 0x7f; })) continue;
|
|
std::string ascii;
|
|
for (const wchar_t c : name) ascii += static_cast<char>(c);
|
|
if (it->is_directory(code)) ascii += '/';
|
|
names.push_back(std::move(ascii));
|
|
if (names.size() > maximum_listing) luaL_error(L, "the folder holds more than %d entries", static_cast<int>(maximum_listing));
|
|
}
|
|
std::sort(names.begin(), names.end());
|
|
lua_createtable(L, static_cast<int>(names.size()), 0);
|
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
|
lua_pushlstring(L, names[i].data(), names[i].size());
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int encode(lua_State* L) {
|
|
luaL_checkany(L, 1);
|
|
const auto text = lua_to_json(L, 1).dump(lua_toboolean(L, 2) ? 2 : -1);
|
|
lua_pushlstring(L, text.data(), text.size());
|
|
return 1;
|
|
}
|
|
static int decode(lua_State* L) {
|
|
std::size_t length = 0;
|
|
const char* text = luaL_checklstring(L, 1, &length);
|
|
push_json(L, Json::parse(std::string_view(text, length), {16u << 20, 64, 1u << 20}));
|
|
return 1;
|
|
}
|
|
static int command(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "console", "reskate.console.command");
|
|
std::size_t length = 0;
|
|
const char* text = luaL_checklstring(L, 1, &length);
|
|
const std::string help = luaL_checkstring(L, 2);
|
|
luaL_checktype(L, 3, LUA_TFUNCTION);
|
|
const std::string name(text, length);
|
|
// Lowercase words of letters, digits, _ and -, one space between them, at most three.
|
|
bool valid = !name.empty() && name.size() <= maximum_command && name.front() != ' ' && name.back() != ' ' &&
|
|
name.find(" ") == std::string::npos && std::count(name.begin(), name.end(), ' ') < 3;
|
|
for (const char c : name) valid = valid && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == ' ');
|
|
if (!valid) luaL_error(L, "\"%s\" is not a command name: 1 to 3 lowercase words of a-z, 0-9, _ and -", name.c_str());
|
|
if (plugin.commands_.size() >= plugin.limits_.commands) luaL_error(L, "more than %d commands", static_cast<int>(plugin.limits_.commands));
|
|
if (plugin.commands_.contains(name)) luaL_error(L, "the command \"%s\" is registered already", name.c_str());
|
|
if (const auto refusal = plugin.services_.claim_command(plugin.id(), name); !refusal.empty())
|
|
luaL_error(L, "%s", refusal.c_str());
|
|
lua_pushvalue(L, 3);
|
|
plugin.commands_[name] = {help.substr(0, 200), lua_ref(L, -1)};
|
|
lua_pop(L, 1);
|
|
return 0;
|
|
}
|
|
static int console_print(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "console", "reskate.console.print");
|
|
plugin.print(text_of(L, 1));
|
|
return 0;
|
|
}
|
|
static int storage_get(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
const auto key = key_of(L, 1);
|
|
const auto found = plugin.storage_.find(key);
|
|
if (found == plugin.storage_.end()) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
push_json(L, *found);
|
|
return 1;
|
|
}
|
|
// Sets a key and checks the whole store against the quota; the value it had comes back on failure.
|
|
static void store(lua_State* L, Plugin& plugin, const std::string& key, Json value) {
|
|
std::optional<Json> previous;
|
|
if (const auto found = plugin.storage_.find(key); found != plugin.storage_.end()) previous = *found;
|
|
if (value.is_null()) plugin.storage_.erase(key);
|
|
else plugin.storage_[key] = std::move(value);
|
|
if (const auto size = plugin.storage_.dump().size(); size > plugin.limits_.storage_bytes) {
|
|
if (previous) plugin.storage_[key] = std::move(*previous);
|
|
else plugin.storage_.erase(key);
|
|
luaL_error(L, "storage is full: that would make it %d KB, and a plugin has %d KB", static_cast<int>(size >> 10),
|
|
static_cast<int>(plugin.limits_.storage_bytes >> 10));
|
|
}
|
|
plugin.storage_dirty_ = true;
|
|
}
|
|
static int storage_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
const auto key = key_of(L, 1);
|
|
store(L, plugin, key, lua_to_json(L, 2));
|
|
return 0;
|
|
}
|
|
static int storage_add(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
const auto key = key_of(L, 1);
|
|
const double by = luaL_optnumber(L, 2, 1);
|
|
if (!std::isfinite(by)) luaL_argerror(L, 2, "must be a finite number");
|
|
double value = 0;
|
|
if (const auto found = plugin.storage_.find(key); found != plugin.storage_.end()) {
|
|
if (!found->is_number()) luaL_error(L, "storage key \"%s\" does not hold a number", key.c_str());
|
|
value = found->get<double>();
|
|
}
|
|
value += by;
|
|
store(L, plugin, key, value == std::floor(value) && std::fabs(value) <= 9007199254740992.0
|
|
? Json(static_cast<std::int64_t>(value)) : Json(value));
|
|
lua_pushnumber(L, value);
|
|
return 1;
|
|
}
|
|
static int storage_keys(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "storage", "reskate.storage");
|
|
lua_createtable(L, static_cast<int>(plugin.storage_.size()), 0);
|
|
int index = 0;
|
|
for (const auto& item : plugin.storage_.items()) {
|
|
lua_pushlstring(L, item.first.data(), item.first.size());
|
|
lua_rawseti(L, -2, ++index);
|
|
}
|
|
return 1;
|
|
}
|
|
// skater.read: the local skater as the game has it this tick, or nil without one.
|
|
static std::optional<SkaterState> skater(lua_State* L, const char* what) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.read", what);
|
|
return plugin.services_.skater();
|
|
}
|
|
static int skater_get(lua_State* L) {
|
|
const auto s = skater(L, "reskate.skater.get");
|
|
if (!s) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 6);
|
|
push_position(L, s->position);
|
|
lua_setfield(L, -2, "position");
|
|
lua_pushnumber(L, s->heading);
|
|
lua_setfield(L, -2, "heading");
|
|
lua_pushnumber(L, s->speed);
|
|
lua_setfield(L, -2, "speed");
|
|
lua_pushnumber(L, s->vertical);
|
|
lua_setfield(L, -2, "vertical_speed");
|
|
lua_pushboolean(L, s->airborne);
|
|
lua_setfield(L, -2, "airborne");
|
|
lua_pushnumber(L, s->air_time);
|
|
lua_setfield(L, -2, "air_time");
|
|
return 1;
|
|
}
|
|
static int skater_position(lua_State* L) {
|
|
const auto s = skater(L, "reskate.skater.position");
|
|
if (s) push_position(L, s->position);
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
static int skater_heading(lua_State* L) {
|
|
const auto s = skater(L, "reskate.skater.heading");
|
|
if (s) lua_pushnumber(L, s->heading);
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
static int skater_speed(lua_State* L) {
|
|
const auto s = skater(L, "reskate.skater.speed");
|
|
if (s) lua_pushnumber(L, s->speed);
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
static int skater_on_ground(lua_State* L) {
|
|
const auto s = skater(L, "reskate.skater.on_ground");
|
|
if (s) lua_pushboolean(L, !s->airborne);
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
// skater.teleport: true when the teleport was sent (it happens within a few seconds); false
|
|
// and why when the host has turned teleporting off, it cannot be done now, or it is asked too often.
|
|
static int skater_teleport(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.teleport", "reskate.skater.teleport");
|
|
const auto at = position_of(L, 1, "the teleport's position");
|
|
float heading = 0;
|
|
if (lua_isnoneornil(L, 2)) {
|
|
if (const auto s = plugin.services_.skater()) heading = s->heading;
|
|
} else {
|
|
const double given = luaL_checknumber(L, 2);
|
|
if (!std::isfinite(given)) luaL_argerror(L, 2, "the heading must be a finite number of radians");
|
|
heading = static_cast<float>(std::remainder(given, 2 * 3.14159265358979323846));
|
|
}
|
|
const double now = plugin.services_.now_ms();
|
|
if (now - plugin.teleport_second_ >= 1000) {
|
|
plugin.teleport_second_ = now;
|
|
plugin.teleports_ = 0;
|
|
}
|
|
const char* refusal = nullptr;
|
|
if (++plugin.teleports_ > plugin.limits_.teleports_per_second) refusal = "asked more than twice in a second";
|
|
else switch (plugin.services_.teleport(at, heading)) {
|
|
case Teleport::sent: break;
|
|
case Teleport::forbidden: refusal = "the host has turned teleporting off"; break;
|
|
case Teleport::unavailable: refusal = "teleporting is unavailable now (no skater, or a load)"; break;
|
|
}
|
|
lua_pushboolean(L, refusal == nullptr);
|
|
if (!refusal) return 1;
|
|
lua_pushstring(L, refusal);
|
|
return 2;
|
|
}
|
|
// world.raycast: where the line from `from` to `to` first meets the world's collision, or nil
|
|
// when it is clear. Collision is loaded only near the skater: far off, a ray meets nothing.
|
|
static int raycast(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.raycast", "reskate.world.raycast");
|
|
const auto from = position_of(L, 1, "the ray's start");
|
|
const auto to = position_of(L, 2, "the ray's end");
|
|
const float dx = to[0] - from[0], dy = to[1] - from[1], dz = to[2] - from[2];
|
|
if (std::sqrt(dx * dx + dy * dy + dz * dz) > plugin.limits_.raycast_metres)
|
|
luaL_error(L, "a ray is at most %d m long", static_cast<int>(plugin.limits_.raycast_metres));
|
|
if (++plugin.raycasts_ > plugin.limits_.raycasts_per_tick)
|
|
luaL_error(L, "more than %d raycasts in one tick", plugin.limits_.raycasts_per_tick);
|
|
if (const auto hit = plugin.services_.raycast(from, to)) push_position(L, *hit);
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
// events.tricks: the score HUD now (the line's score, its multiplier, a trick going on), or nil.
|
|
static int score(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "events.tricks", "reskate.score.current");
|
|
const auto now = plugin.services_.score();
|
|
if (!now) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushnumber(L, now->line);
|
|
lua_setfield(L, -2, "line");
|
|
lua_pushnumber(L, now->multiplier);
|
|
lua_setfield(L, -2, "multiplier");
|
|
lua_pushboolean(L, now->in_trick);
|
|
lua_setfield(L, -2, "in_trick");
|
|
lua_pushlstring(L, now->trick.data(), now->trick.size());
|
|
lua_setfield(L, -2, "trick");
|
|
lua_pushlstring(L, now->detail.data(), now->detail.size());
|
|
lua_setfield(L, -2, "detail");
|
|
lua_pushboolean(L, now->manual);
|
|
lua_setfield(L, -2, "manual");
|
|
return 1;
|
|
}
|
|
// Modules come from the plugin's own package, beside its entry: require("./rules") or
|
|
// require("rules") is <entry folder>/rules.luau or rules.lua. Each runs once.
|
|
static int require(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
std::string name = luaL_checkstring(L, 1);
|
|
if (name.starts_with("./")) name.erase(0, 2);
|
|
const auto relative = safe_relative_path(name);
|
|
if (relative.empty()) luaL_error(L, "require(\"%s\"): a module is a path inside the plugin's package, with no ..", name.c_str());
|
|
const auto in_root = [&](const std::string& path) { return plugin.code_root_.empty() ? path : plugin.code_root_ + "/" + path; };
|
|
std::vector<std::string> candidates;
|
|
if (relative.ends_with(".lua") || relative.ends_with(".luau")) candidates.push_back(in_root(relative));
|
|
else {
|
|
candidates.push_back(in_root(relative + ".luau"));
|
|
candidates.push_back(in_root(relative + ".lua"));
|
|
}
|
|
std::string key;
|
|
std::filesystem::path file;
|
|
for (const auto& candidate : candidates) {
|
|
const auto path = package_path(L, plugin, candidate, false);
|
|
std::error_code code;
|
|
if (std::filesystem::is_regular_file(path, code)) {
|
|
key = candidate;
|
|
file = path;
|
|
break;
|
|
}
|
|
}
|
|
if (key.empty()) luaL_error(L, "require(\"%s\"): there is no %s", name.c_str(), candidates.front().c_str());
|
|
lua_getref(L, plugin.modules_);
|
|
lua_getfield(L, -1, key.c_str());
|
|
if (!lua_isnil(L, -1)) return 1;
|
|
lua_pop(L, 1);
|
|
if (plugin.loading_modules_.contains(key)) luaL_error(L, "require(\"%s\"): it requires itself, through another module", name.c_str());
|
|
plugin.loading_modules_.insert(key);
|
|
struct Done {
|
|
Plugin& plugin;
|
|
std::string key;
|
|
~Done() { plugin.loading_modules_.erase(key); }
|
|
} done{plugin, key};
|
|
if (!plugin.load_chunk(L, key, file)) lua_error(L);
|
|
lua_call(L, 0, 1);
|
|
if (lua_isnil(L, -1)) {
|
|
lua_pop(L, 1);
|
|
lua_pushboolean(L, 1);
|
|
}
|
|
lua_pushvalue(L, -1);
|
|
lua_setfield(L, -3, key.c_str());
|
|
return 1;
|
|
}
|
|
};
|
|
|
|
Json lua_to_json(lua_State* L, int index) {
|
|
Budget budget;
|
|
return to_json(L, index, 0, budget);
|
|
}
|
|
|
|
void push_json(lua_State* L, const Json& value) { push_json_value(L, value, 0); }
|
|
|
|
void Api::install(Plugin& plugin, lua_State* L) {
|
|
const auto readonly = [&] { lua_setreadonly(L, -1, true); };
|
|
lua_createtable(L, 0, 16);
|
|
lua_pushinteger(L, api_version);
|
|
lua_setfield(L, -2, "api_version");
|
|
lua_pushlstring(L, plugin.id().data(), plugin.id().size());
|
|
lua_setfield(L, -2, "id");
|
|
// "client" for the copy on a player's game, "server" for the session authority's copy.
|
|
lua_pushstring(L, plugin.services_.authority() ? "server" : "client");
|
|
lua_setfield(L, -2, "side");
|
|
set_function(L, "log", ApiFunctions::log);
|
|
set_function(L, "on", ApiFunctions::on);
|
|
|
|
lua_createtable(L, 0, 4);
|
|
set_function(L, "now", ApiFunctions::now);
|
|
set_function(L, "after", ApiFunctions::after);
|
|
set_function(L, "every", ApiFunctions::every);
|
|
set_function(L, "cancel", ApiFunctions::cancel);
|
|
readonly();
|
|
lua_setfield(L, -2, "time");
|
|
|
|
lua_createtable(L, 0, 1);
|
|
set_function(L, "current", ApiFunctions::level);
|
|
readonly();
|
|
lua_setfield(L, -2, "level");
|
|
|
|
lua_createtable(L, 0, 3);
|
|
set_function(L, "read", ApiFunctions::read);
|
|
set_function(L, "read_json", ApiFunctions::read_json);
|
|
set_function(L, "list", ApiFunctions::list);
|
|
readonly();
|
|
lua_setfield(L, -2, "package");
|
|
|
|
lua_createtable(L, 0, 2);
|
|
set_function(L, "encode", ApiFunctions::encode);
|
|
set_function(L, "decode", ApiFunctions::decode);
|
|
readonly();
|
|
lua_setfield(L, -2, "json");
|
|
|
|
lua_createtable(L, 0, 2);
|
|
set_function(L, "command", ApiFunctions::command);
|
|
lua_pushcfunction(L, ApiFunctions::console_print, "print");
|
|
lua_setfield(L, -2, "print");
|
|
readonly();
|
|
lua_setfield(L, -2, "console");
|
|
|
|
lua_createtable(L, 0, 4);
|
|
lua_pushcfunction(L, ApiFunctions::storage_get, "get");
|
|
lua_setfield(L, -2, "get");
|
|
lua_pushcfunction(L, ApiFunctions::storage_set, "set");
|
|
lua_setfield(L, -2, "set");
|
|
lua_pushcfunction(L, ApiFunctions::storage_add, "add");
|
|
lua_setfield(L, -2, "add");
|
|
lua_pushcfunction(L, ApiFunctions::storage_keys, "keys");
|
|
lua_setfield(L, -2, "keys");
|
|
readonly();
|
|
lua_setfield(L, -2, "storage");
|
|
|
|
lua_createtable(L, 0, 6);
|
|
set_function(L, "get", ApiFunctions::skater_get);
|
|
lua_pushcfunction(L, ApiFunctions::skater_position, "position");
|
|
lua_setfield(L, -2, "position");
|
|
lua_pushcfunction(L, ApiFunctions::skater_heading, "heading");
|
|
lua_setfield(L, -2, "heading");
|
|
lua_pushcfunction(L, ApiFunctions::skater_speed, "speed");
|
|
lua_setfield(L, -2, "speed");
|
|
lua_pushcfunction(L, ApiFunctions::skater_on_ground, "on_ground");
|
|
lua_setfield(L, -2, "on_ground");
|
|
lua_pushcfunction(L, ApiFunctions::skater_teleport, "teleport");
|
|
lua_setfield(L, -2, "teleport");
|
|
readonly();
|
|
lua_setfield(L, -2, "skater");
|
|
|
|
lua_createtable(L, 0, 1);
|
|
set_function(L, "current", ApiFunctions::score);
|
|
readonly();
|
|
lua_setfield(L, -2, "score");
|
|
|
|
lua_createtable(L, 0, 1);
|
|
set_function(L, "raycast", ApiFunctions::raycast);
|
|
readonly();
|
|
lua_setfield(L, -2, "world");
|
|
|
|
install_ui_api(plugin, L);
|
|
install_more_api(plugin, L);
|
|
install_world_api(plugin, L);
|
|
readonly();
|
|
lua_setglobal(L, "reskate");
|
|
lua_pushcfunction(L, ApiFunctions::print, "print");
|
|
lua_setglobal(L, "print");
|
|
lua_pushcfunction(L, ApiFunctions::require, "require");
|
|
lua_setglobal(L, "require");
|
|
}
|
|
|
|
void push_player(lua_State* L, const SessionPlayer& player) {
|
|
lua_createtable(L, 0, 4);
|
|
const auto id = std::to_string(player.id);
|
|
lua_pushlstring(L, id.data(), id.size());
|
|
lua_setfield(L, -2, "id");
|
|
lua_pushlstring(L, player.name.data(), player.name.size());
|
|
lua_setfield(L, -2, "name");
|
|
lua_pushboolean(L, player.me);
|
|
lua_setfield(L, -2, "me");
|
|
lua_pushboolean(L, player.host);
|
|
lua_setfield(L, -2, "host");
|
|
lua_pushboolean(L, player.server);
|
|
lua_setfield(L, -2, "server");
|
|
}
|
|
}
|