mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 00:35:59 +02:00
- reskate.room.open, leave and state, with the room permission: ReSkate walks the game's menu to the Skaters page (its navigation events and RB on an internal scripted pad, as PR 297 and PR 230 did), draws the local skater on the room's floor and hides the room's own skater and board; other players are not sent the room. Extension/World/character_room, the room placement and hiding in skater_animation, and post_native_ui_event made general. - reskate.input.text: what is typed while the plugin's screen is open; while it types, Esc is the plugin's. - Animator Studio opens in its room (the Studio room setting, as the PR's), under its loading screen, or where the skater is when the walk fails; its card's name, trick and numbers are typed into in place.
2310 lines
118 KiB
C++
2310 lines
118 KiB
C++
#include "plugin_api.h"
|
|
#include "plugin_game.h"
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <span>
|
|
|
|
// The wider set's game parts of the reskate API: the skater's state, control and save points, the
|
|
// HUD, park editor objects, the environment, travel, AI skaters, other players' skaters, nametags,
|
|
// voice, chat, throwdowns, music, progress, settings, input binds and full-screen screens. Each
|
|
// checks its permission, its arguments and its own rate, and the game (plugin_game.h) does the rest
|
|
// under the game's own rules (a session's host, the park editor's limits).
|
|
namespace dingosdk::plugins {
|
|
namespace {
|
|
// ok, or false and why: what most of these return.
|
|
int result(lua_State* L, const std::string& why) {
|
|
lua_pushboolean(L, why.empty());
|
|
if (why.empty()) return 1;
|
|
lua_pushlstring(L, why.data(), why.size());
|
|
return 2;
|
|
}
|
|
// A player by id ("7656...") or a player table ({id = ...}).
|
|
std::uint64_t player_of(lua_State* L, int index) {
|
|
std::string id;
|
|
if (lua_istable(L, index)) {
|
|
lua_getfield(L, index, "id");
|
|
id = luaL_checkstring(L, -1);
|
|
lua_pop(L, 1);
|
|
} else id = luaL_checkstring(L, index);
|
|
std::uint64_t value = 0;
|
|
if (std::from_chars(id.data(), id.data() + id.size(), value).ec != std::errc{} || !value) luaL_argerror(L, index, "not a player");
|
|
return value;
|
|
}
|
|
// A per-second budget; false past it.
|
|
bool within(double now, double& second, int& count, int most) {
|
|
if (now - second >= 1000) {
|
|
second = now;
|
|
count = 0;
|
|
}
|
|
return ++count <= most;
|
|
}
|
|
}
|
|
|
|
const char* skater_state_name(std::uint32_t state) {
|
|
if (state >= 100 && state < 200) return "riding";
|
|
if (state >= 200 && state < 300) return "air";
|
|
if (state >= 300 && state < 400) return "wipeout";
|
|
if (state >= 400 && state < 500) return "grinding";
|
|
if (state >= 500 && state < 600) return "off_board";
|
|
if (state == 602) return "planted";
|
|
return state ? "other" : "none";
|
|
}
|
|
|
|
struct WorldApi {
|
|
static Game& game(Plugin& plugin) { return plugin.services_.game(); }
|
|
|
|
// ---- events.skater -------------------------------------------------------------------------
|
|
static int state(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "events.skater", "reskate.skater.state");
|
|
const auto now = game(plugin).physics_state();
|
|
if (!now) lua_pushnil(L);
|
|
else lua_pushstring(L, skater_state_name(*now));
|
|
return 1;
|
|
}
|
|
// ---- skater.control ------------------------------------------------------------------------
|
|
static int control(lua_State* L, const char* action) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.control", "reskate.skater control");
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).control(action));
|
|
}
|
|
static int noclip(lua_State* L) { return control(L, lua_toboolean(L, 1) ? "noclip_on" : "noclip_off"); }
|
|
static int boost(lua_State* L) {
|
|
const std::string kind = luaL_checkstring(L, 1);
|
|
if (kind == "forward") return control(L, "boost_forward");
|
|
if (kind == "up") return control(L, "boost_up");
|
|
if (kind == "offboard") return control(L, "boost_offboard");
|
|
luaL_argerror(L, 1, "a boost is \"forward\", \"up\" or \"offboard\"");
|
|
return 0;
|
|
}
|
|
static int freeze(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.control", "reskate.skater.freeze");
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).freeze(plugin.id(), lua_toboolean(L, 1)));
|
|
}
|
|
// wipeout(force): the skater bails as from a slam of that force (0.5 to 3); one every 2 s.
|
|
static int wipeout(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.control", "reskate.skater.wipeout");
|
|
const double force = luaL_optnumber(L, 1, 2.5);
|
|
if (!(force >= 0.5 && force <= 3)) luaL_argerror(L, 1, "a wipeout's force is 0.5 to 3");
|
|
const double now = plugin.services_.now_ms();
|
|
if (now - plugin.wipeout_at_ < 2000) return result(L, "one wipeout every 2 s");
|
|
plugin.wipeout_at_ = now;
|
|
return result(L, game(plugin).wipeout(static_cast<float>(force)));
|
|
}
|
|
// skater.xray(on): the x-ray ready (the renderer followed) while on; d:skeleton draws it.
|
|
static int xray(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "overlay", "reskate.skater.xray");
|
|
return result(L, game(plugin).xray(plugin.id(), lua_toboolean(L, 1) != 0));
|
|
}
|
|
// skater.grind_exit(wish): the next pop out of a grind, {side = "toe"|"heel"|"left"|"right",
|
|
// stance = "regular"|"goofy", strength = 0.1 to 1}; nil the game's own exit.
|
|
static int grind_exit(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.control", "reskate.skater.grind_exit");
|
|
if (lua_isnoneornil(L, 1)) return result(L, game(plugin).grind_exit(plugin.id(), std::nullopt));
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
GrindExitWish wish;
|
|
lua_getfield(L, 1, "side");
|
|
wish.side = luaL_checkstring(L, -1);
|
|
lua_pop(L, 1);
|
|
if (wish.side != "toe" && wish.side != "heel" && wish.side != "left" && wish.side != "right")
|
|
luaL_error(L, "a grind exit's side is \"toe\", \"heel\", \"left\" or \"right\"");
|
|
lua_getfield(L, 1, "stance");
|
|
if (!lua_isnil(L, -1)) {
|
|
const std::string stance = luaL_checkstring(L, -1);
|
|
if (stance == "regular") wish.stance = 1;
|
|
else if (stance == "goofy") wish.stance = 2;
|
|
else luaL_error(L, "a stance is \"regular\" or \"goofy\"");
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 1, "strength");
|
|
if (!lua_isnil(L, -1)) {
|
|
const double strength = luaL_checknumber(L, -1);
|
|
if (!(strength >= 0.1 && strength <= 1)) luaL_error(L, "a grind exit's strength is 0.1 to 1");
|
|
wish.strength = static_cast<float>(strength);
|
|
}
|
|
lua_pop(L, 1);
|
|
return result(L, game(plugin).grind_exit(plugin.id(), std::move(wish)));
|
|
}
|
|
// board.motion(): the board's velocity in the last physics step, or nil.
|
|
static int board_motion(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.read", "reskate.board.motion");
|
|
const auto v = game(plugin).board_velocity();
|
|
if (!v) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 1);
|
|
push_position(L, *v);
|
|
lua_setfield(L, -2, "velocity");
|
|
return 1;
|
|
}
|
|
// ---- camera -------------------------------------------------------------------------------------
|
|
static float fov_at(lua_State* L, int index) {
|
|
const double fov = luaL_optnumber(L, index, 0);
|
|
if (!(fov == 0 || (fov >= 10 && fov <= 150))) luaL_argerror(L, index, "a field of view is 10 to 150 degrees (0 or none: the camera's own)");
|
|
return static_cast<float>(fov);
|
|
}
|
|
// look(position, target, fov): the view from position towards target.
|
|
static int camera_look(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "camera", "reskate.camera.look");
|
|
const auto eye = position_of(L, 1, "the camera's position");
|
|
const auto target = position_of(L, 2, "what the camera looks at");
|
|
const float fov = fov_at(L, 3);
|
|
Vec3 back{eye[0] - target[0], eye[1] - target[1], eye[2] - target[2]};
|
|
const float length = std::sqrt(back[0] * back[0] + back[1] * back[1] + back[2] * back[2]);
|
|
if (!(length > 0.01f)) luaL_error(L, "the camera's position and what it looks at are the same point");
|
|
for (auto& v : back) v /= length;
|
|
Vec3 right{back[2], 0, -back[0]};
|
|
const float flat = std::sqrt(right[0] * right[0] + right[2] * right[2]);
|
|
if (flat > 0.001f) {
|
|
right[0] /= flat;
|
|
right[2] /= flat;
|
|
} else right = {1, 0, 0}; // straight up or down
|
|
const Vec3 up{back[1] * right[2] - back[2] * right[1], back[2] * right[0] - back[0] * right[2], back[0] * right[1] - back[1] * right[0]};
|
|
const std::array<float, 16> pose{right[0], right[1], right[2], 0, up[0], up[1], up[2], 0,
|
|
back[0], back[1], back[2], 0, eye[0], eye[1], eye[2], 1};
|
|
return result(L, game(plugin).set_camera(plugin.id(), pose, fov));
|
|
}
|
|
// set(position, forward, up, fov): the view from position along forward, `up` giving its roll.
|
|
static int camera_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "camera", "reskate.camera.set");
|
|
const auto eye = position_of(L, 1, "the camera's position");
|
|
const auto forward = position_of(L, 2, "the camera's forward");
|
|
const auto up_hint = position_of(L, 3, "the camera's up");
|
|
const float fov = fov_at(L, 4);
|
|
const auto length = [](const Vec3& v) { return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); };
|
|
const auto cross = [](const Vec3& a, const Vec3& b) {
|
|
return Vec3{a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]};
|
|
};
|
|
Vec3 back{-forward[0], -forward[1], -forward[2]};
|
|
const float lb = length(back);
|
|
if (!(lb > 1e-4f)) luaL_error(L, "the camera's forward has no length");
|
|
for (auto& v : back) v /= lb;
|
|
auto right = cross(up_hint, back);
|
|
const float lr = length(right);
|
|
if (!(lr > 1e-4f)) luaL_error(L, "the camera's up is along its forward");
|
|
for (auto& v : right) v /= lr;
|
|
const auto up = cross(back, right);
|
|
const std::array<float, 16> pose{right[0], right[1], right[2], 0, up[0], up[1], up[2], 0,
|
|
back[0], back[1], back[2], 0, eye[0], eye[1], eye[2], 1};
|
|
return result(L, game(plugin).set_camera(plugin.id(), pose, fov));
|
|
}
|
|
static int camera_follow(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "camera", "reskate.camera.follow");
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).follow_player(plugin.id(), lua_isnoneornil(L, 1) ? 0 : player_of(L, 1)));
|
|
}
|
|
static int camera_release(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "camera", "reskate.camera.release");
|
|
game(plugin).release_camera(plugin.id());
|
|
return 0;
|
|
}
|
|
static int camera_held(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "camera", "reskate.camera.held");
|
|
return result(L, game(plugin).camera_status(plugin.id()));
|
|
}
|
|
static int camera_get(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "camera", "reskate.camera.get");
|
|
const auto view = game(plugin).camera_view();
|
|
if (!view) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 3);
|
|
push_position(L, view->position);
|
|
lua_setfield(L, -2, "position");
|
|
push_position(L, view->forward);
|
|
lua_setfield(L, -2, "forward");
|
|
push_position(L, view->right);
|
|
lua_setfield(L, -2, "right");
|
|
push_position(L, view->up);
|
|
lua_setfield(L, -2, "up");
|
|
lua_pushnumber(L, view->fov);
|
|
lua_setfield(L, -2, "fov");
|
|
lua_pushboolean(L, view->free_camera);
|
|
lua_setfield(L, -2, "free");
|
|
return 1;
|
|
}
|
|
// ---- trainer: one save point of the plugin's own -------------------------------------------
|
|
static int set_point(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "trainer", "reskate.trainer.set_point");
|
|
const auto s = plugin.services_.skater();
|
|
if (!s) return result(L, "there is no skater");
|
|
plugin.trainer_point_ = std::pair(s->position, s->heading);
|
|
return result(L, {});
|
|
}
|
|
static int back(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "trainer", "reskate.trainer.back");
|
|
if (!plugin.trainer_point_) return result(L, "no save point is set");
|
|
if (!within(plugin.services_.now_ms(), plugin.teleport_second_, plugin.teleports_, plugin.limits_.teleports_per_second))
|
|
return result(L, "asked more than twice in a second");
|
|
switch (plugin.services_.teleport(plugin.id(), plugin.trainer_point_->first, plugin.trainer_point_->second)) {
|
|
case Teleport::sent: return result(L, {});
|
|
case Teleport::forbidden: return result(L, "the host has turned teleporting off");
|
|
default: return result(L, "teleporting is unavailable now");
|
|
}
|
|
}
|
|
// ---- hud ---------------------------------------------------------------------------------------
|
|
static int hide_hud(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "hud", "reskate.hud.hide");
|
|
game(plugin).hide_hud(plugin.id(), lua_toboolean(L, 1) != 0);
|
|
return 0;
|
|
}
|
|
// hud.corner(hidden): skate.'s bottom left HUD hidden while true; false keeps it ready to hide.
|
|
static int hud_corner(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "hud", "reskate.hud.corner");
|
|
return result(L, game(plugin).hud_corner(plugin.id(), lua_toboolean(L, 1) != 0));
|
|
}
|
|
// time.slow(speed): the game at `speed` (0.05 to 1; 1 or nil gives it back); single player only.
|
|
static int slow(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "game.speed", "reskate.time.slow");
|
|
const double speed = luaL_optnumber(L, 1, 1.0);
|
|
if (!(speed >= 0.05 && speed <= 1)) luaL_argerror(L, 1, "a game speed is 0.05 to 1");
|
|
return result(L, game(plugin).game_speed(plugin.id(), static_cast<float>(speed)));
|
|
}
|
|
static int speed(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "game.speed", "reskate.time.speed");
|
|
lua_pushnumber(L, game(plugin).game_speed_now());
|
|
return 1;
|
|
}
|
|
// ---- world.objects -----------------------------------------------------------------------------
|
|
static int catalog(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.objects", "reskate.objects.catalog");
|
|
const auto items = game(plugin).object_catalog();
|
|
lua_createtable(L, static_cast<int>(items.size()), 0);
|
|
for (std::size_t i = 0; i < items.size(); ++i) {
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushlstring(L, items[i].key.data(), items[i].key.size());
|
|
lua_setfield(L, -2, "key");
|
|
lua_pushlstring(L, items[i].title.data(), items[i].title.size());
|
|
lua_setfield(L, -2, "title");
|
|
lua_pushlstring(L, items[i].category.data(), items[i].category.size());
|
|
lua_setfield(L, -2, "category");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int objects(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.objects", "reskate.objects.list");
|
|
const auto list = game(plugin).objects();
|
|
lua_createtable(L, static_cast<int>(list.size()), 0);
|
|
for (std::size_t i = 0; i < list.size(); ++i) {
|
|
const auto& o = list[i];
|
|
lua_createtable(L, 0, 5);
|
|
const auto id = std::to_string(o.id);
|
|
lua_pushlstring(L, id.data(), id.size());
|
|
lua_setfield(L, -2, "id");
|
|
lua_pushlstring(L, o.item.data(), o.item.size());
|
|
lua_setfield(L, -2, "item");
|
|
push_position(L, o.position);
|
|
lua_setfield(L, -2, "position");
|
|
// The heading the pose turns to, as the skater's: about world up.
|
|
const float heading = std::atan2(2 * (o.rotation[3] * o.rotation[1] + o.rotation[0] * o.rotation[2]),
|
|
1 - 2 * (o.rotation[1] * o.rotation[1] + o.rotation[0] * o.rotation[0]));
|
|
lua_pushnumber(L, heading);
|
|
lua_setfield(L, -2, "heading");
|
|
lua_pushnumber(L, o.scale);
|
|
lua_setfield(L, -2, "scale");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static GameObject pose(lua_State* L, int at, int heading_index, int scale_index) {
|
|
GameObject object;
|
|
object.position = position_of(L, at, "the object's position");
|
|
const double heading = luaL_optnumber(L, heading_index, 0);
|
|
if (!std::isfinite(heading)) luaL_argerror(L, heading_index, "a heading is a finite number of radians");
|
|
object.rotation = {0, static_cast<float>(std::sin(heading / 2)), 0, static_cast<float>(std::cos(heading / 2))};
|
|
if (scale_index) {
|
|
const double scale = luaL_optnumber(L, scale_index, 1);
|
|
if (!(scale >= 0.1 && scale <= 10)) luaL_argerror(L, scale_index, "a scale is 0.1 to 10");
|
|
object.scale = static_cast<float>(scale);
|
|
}
|
|
return object;
|
|
}
|
|
static std::uint64_t object_id(lua_State* L, int index) {
|
|
const std::string text = luaL_checkstring(L, index);
|
|
std::uint64_t id = 0;
|
|
if (std::from_chars(text.data(), text.data() + text.size(), id).ec != std::errc{} || !id) luaL_argerror(L, index, "not an object's id");
|
|
return id;
|
|
}
|
|
static int edit(lua_State* L, const char* operation, GameObject object) {
|
|
auto& plugin = plugin_of(L);
|
|
if (!within(plugin.services_.now_ms(), plugin.edit_second_, plugin.edits_, 4)) return result(L, "more than 4 edits in a second");
|
|
return result(L, game(plugin).edit_object(operation, object));
|
|
}
|
|
// ---- world.entities ----------------------------------------------------------------------------
|
|
// set(id, blueprint, transform, hidden?): the plugin's object `id` (1 to 2^31) is an entity of the
|
|
// world object blueprint at the transform, made, moved or taken away by the game a few a frame.
|
|
// A transform is {position = vector, right = vector, up = vector, forward = vector} (axes carry
|
|
// their scale), {position, yaw, pitch, roll (degrees), scale (number or vector)}, or
|
|
// {matrix = {16 numbers}} (rows right, up, forward, position).
|
|
static std::uint32_t entity_id(lua_State* L, int index) {
|
|
const double id = luaL_checknumber(L, index);
|
|
if (!(id >= 1 && id <= 2147483647.0) || id != std::floor(id)) luaL_argerror(L, index, "an object's id is a whole number from 1");
|
|
return static_cast<std::uint32_t>(id);
|
|
}
|
|
static std::array<float, 3> axis_of(lua_State* L, int table, const char* field) {
|
|
lua_getfield(L, table, field);
|
|
const float* v = luaL_checkvector(L, -1);
|
|
std::array<float, 3> result{v[0], v[1], v[2]};
|
|
lua_pop(L, 1);
|
|
return result;
|
|
}
|
|
static std::array<float, 16> transform_of(lua_State* L, int table) {
|
|
luaL_checktype(L, table, LUA_TTABLE);
|
|
std::array<float, 16> m{};
|
|
lua_getfield(L, table, "matrix");
|
|
if (!lua_isnil(L, -1)) {
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
if (lua_objlen(L, -1) != 16) luaL_error(L, "a matrix is 16 numbers");
|
|
for (int i = 0; i < 16; ++i) {
|
|
lua_rawgeti(L, -1, i + 1);
|
|
m[i] = static_cast<float>(luaL_checknumber(L, -1));
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 1);
|
|
return m;
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, table, "position");
|
|
const auto at = position_of(L, -1, "an object's position");
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, table, "right");
|
|
const bool axes = !lua_isnil(L, -1);
|
|
lua_pop(L, 1);
|
|
std::array<float, 3> right{1, 0, 0}, up{0, 1, 0}, forward{0, 0, 1};
|
|
if (axes) {
|
|
right = axis_of(L, table, "right");
|
|
up = axis_of(L, table, "up");
|
|
forward = axis_of(L, table, "forward");
|
|
} else {
|
|
const auto degrees = [&](const char* field) {
|
|
lua_getfield(L, table, field);
|
|
const double value = luaL_optnumber(L, -1, 0);
|
|
lua_pop(L, 1);
|
|
if (!std::isfinite(value)) luaL_error(L, "%s is a finite number of degrees", field);
|
|
return value * 3.14159265358979323846 / 180;
|
|
};
|
|
const double yaw = degrees("yaw"), pitch = degrees("pitch"), roll = degrees("roll");
|
|
// Yaw about up (forward = sin, 0, cos at zero pitch), then pitch about right, then roll
|
|
// about forward, as the game lays out a world object's axes.
|
|
const double cy = std::cos(yaw), sy = std::sin(yaw), cp = std::cos(pitch), sp = std::sin(pitch), cr = std::cos(roll), sr = std::sin(roll);
|
|
const std::array<double, 3> f{sy * cp, sp, cy * cp}, r0{cy, 0, -sy};
|
|
const std::array<double, 3> u0{f[1] * r0[2] - f[2] * r0[1], f[2] * r0[0] - f[0] * r0[2], f[0] * r0[1] - f[1] * r0[0]};
|
|
for (int i = 0; i < 3; ++i) {
|
|
right[i] = static_cast<float>(r0[i] * cr + u0[i] * sr);
|
|
up[i] = static_cast<float>(u0[i] * cr - r0[i] * sr);
|
|
forward[i] = static_cast<float>(f[i]);
|
|
}
|
|
lua_getfield(L, table, "scale");
|
|
std::array<float, 3> scale{1, 1, 1};
|
|
if (lua_isnumber(L, -1)) scale.fill(static_cast<float>(lua_tonumber(L, -1)));
|
|
else if (lua_isvector(L, -1)) {
|
|
const float* v = lua_tovector(L, -1);
|
|
scale = {v[0], v[1], v[2]};
|
|
} else if (!lua_isnil(L, -1)) luaL_error(L, "a scale is a number or a vector");
|
|
lua_pop(L, 1);
|
|
for (int i = 0; i < 3; ++i) right[i] *= scale[0], up[i] *= scale[1], forward[i] *= scale[2];
|
|
}
|
|
for (int i = 0; i < 3; ++i) m[i] = right[i], m[4 + i] = up[i], m[8 + i] = forward[i], m[12 + i] = at[i];
|
|
m[15] = 1;
|
|
return m;
|
|
}
|
|
static int entities_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.entities", "reskate.entities.set");
|
|
const auto id = entity_id(L, 1);
|
|
const auto asset = checked_text(L, 2, 200, "a blueprint's name");
|
|
const auto matrix = transform_of(L, 3);
|
|
// attach = {player = id?, joint = number or name}: kept on that skater's joint, the transform
|
|
// in the joint's frame.
|
|
std::optional<Game::EntityAttach> attach;
|
|
if (!lua_isnoneornil(L, 5)) {
|
|
luaL_checktype(L, 5, LUA_TTABLE);
|
|
attach.emplace();
|
|
lua_getfield(L, 5, "player");
|
|
if (!lua_isnil(L, -1)) attach->player = player_of(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 5, "joint");
|
|
attach->joint = joint_of(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
}
|
|
return result(L, game(plugin).entity_set(plugin.id(), id, asset, matrix, lua_toboolean(L, 4) != 0, attach));
|
|
}
|
|
static int entities_remove(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.entities", "reskate.entities.remove");
|
|
game(plugin).entity_remove(plugin.id(), entity_id(L, 1));
|
|
return 0;
|
|
}
|
|
static int entities_clear(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.entities", "reskate.entities.clear");
|
|
game(plugin).entity_clear(plugin.id());
|
|
return 0;
|
|
}
|
|
static int entities_status(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.entities", "reskate.entities.status");
|
|
static constexpr const char* names[] = {nullptr, "waiting", "loading", "missing", "ready", "hidden"};
|
|
const auto status = game(plugin).entity_status(plugin.id(), entity_id(L, 1));
|
|
if (status <= 0 || status > 5) lua_pushnil(L);
|
|
else lua_pushstring(L, names[status]);
|
|
return 1;
|
|
}
|
|
static int entities_ready(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.entities", "reskate.entities.ready");
|
|
lua_pushboolean(L, game(plugin).entities_ready(plugin.id()));
|
|
return 1;
|
|
}
|
|
static int place(lua_State* L) {
|
|
need(L, plugin_of(L), "world.objects", "reskate.objects.place");
|
|
auto object = pose(L, 2, 3, 4);
|
|
object.item = checked_text(L, 1, 200, "an item's key");
|
|
return edit(L, "place", object);
|
|
}
|
|
// move(id, position, heading?, scale?): what is not given stays as the object has it.
|
|
static int move(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.objects", "reskate.objects.move");
|
|
auto object = pose(L, 2, 3, 4);
|
|
object.id = object_id(L, 1);
|
|
if (lua_isnoneornil(L, 3) || lua_isnoneornil(L, 4))
|
|
for (const auto& existing : game(plugin).objects())
|
|
if (existing.id == object.id) {
|
|
if (lua_isnoneornil(L, 3)) object.rotation = existing.rotation;
|
|
if (lua_isnoneornil(L, 4)) object.scale = existing.scale;
|
|
break;
|
|
}
|
|
return edit(L, "move", object);
|
|
}
|
|
static int remove(lua_State* L) {
|
|
need(L, plugin_of(L), "world.objects", "reskate.objects.remove");
|
|
GameObject object;
|
|
object.id = object_id(L, 1);
|
|
return edit(L, "delete", object);
|
|
}
|
|
// ---- world.environment -----------------------------------------------------------------------
|
|
static int environment(lua_State* L, const char* kind, const std::string& key, const std::string& value) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.environment", "reskate.world's environment");
|
|
if (!within(plugin.services_.now_ms(), plugin.environment_second_, plugin.environments_, 4))
|
|
return result(L, "more than 4 changes in a second");
|
|
return result(L, game(plugin).set_environment(kind, key, value));
|
|
}
|
|
static int set_time(lua_State* L) { return environment(L, "tod", checked_text(L, 1, 20, "a time of day"), {}); }
|
|
static int set_control(lua_State* L) {
|
|
const auto key = checked_text(L, 1, 30, "a world control");
|
|
const double value = luaL_checknumber(L, 2);
|
|
if (!std::isfinite(value)) luaL_argerror(L, 2, "a finite number");
|
|
return environment(L, "control", key, std::to_string(value));
|
|
}
|
|
static int set_park(lua_State* L) { return environment(L, "park", checked_text(L, 1, 30, "a lot"), checked_text(L, 2, 40, "a layout")); }
|
|
// ---- travel ------------------------------------------------------------------------------------
|
|
static int locations(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "travel", "reskate.travel.locations");
|
|
const auto list = game(plugin).destinations();
|
|
lua_createtable(L, static_cast<int>(list.size()), 0);
|
|
for (std::size_t i = 0; i < list.size(); ++i) {
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushlstring(L, list[i].id.data(), list[i].id.size());
|
|
lua_setfield(L, -2, "id");
|
|
lua_pushlstring(L, list[i].name.data(), list[i].name.size());
|
|
lua_setfield(L, -2, "name");
|
|
lua_pushlstring(L, list[i].map.data(), list[i].map.size());
|
|
lua_setfield(L, -2, "map");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int levels(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "travel", "reskate.travel.levels");
|
|
const auto list = game(plugin).levels();
|
|
lua_createtable(L, static_cast<int>(list.size()), 0);
|
|
for (std::size_t i = 0; i < list.size(); ++i) {
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushlstring(L, list[i].asset.data(), list[i].asset.size());
|
|
lua_setfield(L, -2, "asset");
|
|
lua_pushlstring(L, list[i].name.data(), list[i].name.size());
|
|
lua_setfield(L, -2, "name");
|
|
lua_pushboolean(L, list[i].can_load);
|
|
lua_setfield(L, -2, "can_load");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int travel(lua_State* L, const char* kind) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "travel", "reskate.travel");
|
|
const auto target = checked_text(L, 1, 260, "a destination");
|
|
const double now = plugin.services_.now_ms();
|
|
if (now - plugin.travel_at_ < 5000) return result(L, "one trip every 5 s");
|
|
plugin.travel_at_ = now;
|
|
return result(L, game(plugin).travel(kind, target));
|
|
}
|
|
static int go(lua_State* L) { return travel(L, "location"); }
|
|
static int load(lua_State* L) { return travel(L, "level"); }
|
|
// ---- world.ai -----------------------------------------------------------------------------------
|
|
static int ai_spawn(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.ai", "reskate.ai.spawn");
|
|
const auto count = static_cast<int>(luaL_optinteger(L, 1, 1));
|
|
if (count < 1 || count > 8) luaL_argerror(L, 1, "1 to 8 at a time");
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).ai("spawn", count));
|
|
}
|
|
static int ai_clear(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.ai", "reskate.ai.clear");
|
|
return result(L, game(plugin).ai("clear", 0));
|
|
}
|
|
static int ai_count(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "world.ai", "reskate.ai.count");
|
|
lua_pushinteger(L, game(plugin).ai_count());
|
|
return 1;
|
|
}
|
|
// ---- session.skaters / session.nametags / voice.control -----------------------------------------
|
|
static int skaters(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "session.skaters", "reskate.session.skaters");
|
|
const auto list = game(plugin).peer_skaters();
|
|
lua_createtable(L, static_cast<int>(list.size()), 0);
|
|
for (std::size_t i = 0; i < list.size(); ++i) {
|
|
lua_createtable(L, 0, 4);
|
|
const auto id = std::to_string(list[i].id);
|
|
lua_pushlstring(L, id.data(), id.size());
|
|
lua_setfield(L, -2, "id");
|
|
push_position(L, list[i].position);
|
|
lua_setfield(L, -2, "position");
|
|
lua_pushnumber(L, list[i].heading);
|
|
lua_setfield(L, -2, "heading");
|
|
lua_pushnumber(L, list[i].age_ms);
|
|
lua_setfield(L, -2, "age_ms");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int tag(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "session.nametags", "reskate.session.tag");
|
|
const auto player = player_of(L, 1);
|
|
std::string text = lua_isnoneornil(L, 2) ? std::string{} : checked_text(L, 2, 16, "a nametag's tag");
|
|
for (const char c : text)
|
|
if (c < 0x20 || c > 0x7e) luaL_error(L, "a nametag's tag is printable text");
|
|
game(plugin).set_nametag(plugin.id(), player, std::move(text), colour_at(L, 3, 0xffffffff));
|
|
return 0;
|
|
}
|
|
// 0xRRGGBBAA from Lua, as the overlay draws it (IM_COL32: 0xAABBGGRR).
|
|
static std::uint32_t colour_at(lua_State* L, int index, std::uint32_t fallback) {
|
|
if (lua_isnoneornil(L, index)) return fallback;
|
|
const double value = luaL_checknumber(L, index);
|
|
if (!(value >= 0 && value <= 4294967295.0) || value != std::floor(value)) luaL_argerror(L, index, "a colour number is 0xRRGGBBAA");
|
|
const auto rgba = static_cast<std::uint32_t>(value);
|
|
return (rgba >> 24) | ((rgba >> 16) & 0xff) << 8 | ((rgba >> 8) & 0xff) << 16 | (rgba & 0xff) << 24;
|
|
}
|
|
static int voice_volume(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "voice.control", "reskate.voice.set_volume");
|
|
const auto player = player_of(L, 1);
|
|
const double volume = luaL_checknumber(L, 2);
|
|
if (!(volume >= 0 && volume <= 10)) luaL_argerror(L, 2, "a volume is 0 (muted) to 10");
|
|
if (!within(plugin.services_.now_ms(), plugin.voice_second_, plugin.voices_, 10)) return result(L, "more than 10 changes in a second");
|
|
return result(L, game(plugin).voice_volume(player, static_cast<float>(volume)));
|
|
}
|
|
// ---- chat.send ------------------------------------------------------------------------------------
|
|
static int chat_send(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "chat.send", "reskate.chat.send");
|
|
const auto text = checked_text(L, 1, 200, "a chat line");
|
|
if (text.empty() || text.front() == '/') luaL_argerror(L, 1, "a chat line, not empty and not a / command");
|
|
const double now = plugin.services_.now_ms();
|
|
if (now - plugin.chat_at_ < 3000) return result(L, "one line every 3 s");
|
|
plugin.chat_at_ = now;
|
|
return result(L, game(plugin).send_chat(text));
|
|
}
|
|
// A line only this player sees, under the plugin's name: no / commands, 5 a second.
|
|
static int chat_local(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "chat.local", "reskate.chat.notice");
|
|
const auto text = checked_text(L, 1, 200, "a chat line");
|
|
if (text.empty()) luaL_argerror(L, 1, "a chat line, not empty");
|
|
for (const unsigned char c : text)
|
|
if (c < 0x20) luaL_argerror(L, 1, "a chat line is one line of text");
|
|
const auto colour = colour_at(L, 2, 0);
|
|
if (!within(plugin.services_.now_ms(), plugin.local_chat_second_, plugin.local_chats_, 5)) return result(L, "more than 5 lines in a second");
|
|
return result(L, game(plugin).local_chat(plugin.id(), text, colour));
|
|
}
|
|
// ---- skater.animation -----------------------------------------------------------------------------
|
|
static std::size_t pose_bytes(const anim_studio::Clip& clip) { return clip.times.size() * clip.joints * 40 + clip.board.size() * 40; }
|
|
// A clip by its path in the package (an Animation Studio clip file, FAS1 to FAS3), "data:<path>" in
|
|
// the plugin's data folder (with files), or one the plugin made: "rec:<n>", "clip:<n>", "edit:<name>".
|
|
static Game::ClipRef clip_at(lua_State* L, Plugin& plugin, int index) {
|
|
const auto name = checked_text(L, index, 200, "a clip");
|
|
if (const auto found = plugin.clips_.find(name); found != plugin.clips_.end()) return found->second;
|
|
if (name.starts_with("rec:") || name.starts_with("clip:") || name.starts_with("edit:")) luaL_error(L, "there is no clip %s", name.c_str());
|
|
std::string error, bytes;
|
|
if (name.starts_with("data:")) {
|
|
need(L, plugin, "files", "a data: clip");
|
|
lua_pushlstring(L, name.data() + 5, name.size() - 5);
|
|
const auto path = data_path(L, lua_gettop(L), false);
|
|
lua_pop(L, 1);
|
|
if (const auto why = game(plugin).files_read_bytes(plugin.id(), path, bytes, 8u << 20); !why.empty()) error = why;
|
|
} else {
|
|
bytes = read_package_file(package_file(L, plugin, name), 8u << 20, error);
|
|
}
|
|
if (bytes.empty()) luaL_error(L, "the clip %s cannot be read: %s", name.c_str(), error.empty() ? "it is empty" : error.c_str());
|
|
// Its size as poses, from the header (magic, joints, frames), before anything is made of it.
|
|
if (bytes.size() >= 12) {
|
|
std::uint32_t joints{}, frames{};
|
|
std::memcpy(&joints, bytes.data() + 4, 4);
|
|
std::memcpy(&frames, bytes.data() + 8, 4);
|
|
if (joints > anim_studio::skeleton_joints) luaL_error(L, "the clip %s has more joints than the skater", name.c_str());
|
|
if (std::uint64_t{joints + 1} * frames * 40 + plugin.clip_bytes_ > plugin.limits_.clip_bytes)
|
|
luaL_error(L, "a plugin's clips hold at most %d MB of poses", static_cast<int>(plugin.limits_.clip_bytes >> 20));
|
|
}
|
|
std::shared_ptr<const anim_studio::Clip> clip;
|
|
try {
|
|
clip = std::make_shared<const anim_studio::Clip>(anim_studio::clip_from_bytes(
|
|
std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t*>(bytes.data()), bytes.size())));
|
|
} catch (const std::exception& e) {
|
|
luaL_error(L, "the clip %s is not one: %s", name.c_str(), e.what());
|
|
}
|
|
if (clip->empty()) luaL_error(L, "the clip %s has no frames", name.c_str());
|
|
plugin.clip_bytes_ += pose_bytes(*clip);
|
|
return plugin.clips_.emplace(name, std::move(clip)).first->second;
|
|
}
|
|
static float number_field(lua_State* L, int table, const char* name, float fallback, float least, float most) {
|
|
if (lua_isnoneornil(L, table)) return fallback;
|
|
luaL_checktype(L, table, LUA_TTABLE);
|
|
lua_getfield(L, table, name);
|
|
float value = fallback;
|
|
if (!lua_isnil(L, -1)) {
|
|
const double v = luaL_checknumber(L, -1);
|
|
if (!(v >= least && v <= most)) luaL_error(L, "%s is %g to %g", name, static_cast<double>(least), static_cast<double>(most));
|
|
value = static_cast<float>(v);
|
|
}
|
|
lua_pop(L, 1);
|
|
return value;
|
|
}
|
|
static bool flag_field(lua_State* L, int table, const char* name) {
|
|
if (lua_isnoneornil(L, table)) return false;
|
|
luaL_checktype(L, table, LUA_TTABLE);
|
|
lua_getfield(L, table, name);
|
|
const bool value = lua_toboolean(L, -1) != 0;
|
|
lua_pop(L, 1);
|
|
return value;
|
|
}
|
|
static Plugin& animating(lua_State* L, const char* what) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.animation", what);
|
|
return plugin;
|
|
}
|
|
// play(clip, {speed, blend, loop}): the clip on the skater's body from now, in place.
|
|
static int animation_play(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.play");
|
|
auto clip = clip_at(L, plugin, 1);
|
|
const float speed = number_field(L, 2, "speed", 1, 0.1f, 4), blend = number_field(L, 2, "blend", 0.15f, 0, 2);
|
|
const bool loop = flag_field(L, 2, "loop"), paused = flag_field(L, 2, "paused");
|
|
const float at = number_field(L, 2, "at", 0, 0, clip->duration());
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).play_clip(plugin.id(), std::move(clip), speed, blend, loop, at, paused));
|
|
}
|
|
static int animation_stop(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.stop");
|
|
game(plugin).stop_clip(plugin.id());
|
|
return 0;
|
|
}
|
|
static int animation_playing(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.playing");
|
|
lua_pushboolean(L, game(plugin).clip_playing(plugin.id()));
|
|
return 1;
|
|
}
|
|
// replace(trick, clip or nil, {pop, speed}): the game's own trick plays as the clip.
|
|
static int animation_replace(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.replace");
|
|
auto trick = checked_text(L, 1, 48, "a trick's name");
|
|
for (auto& c : trick) {
|
|
c = static_cast<char>(c >= 'A' && c <= 'Z' ? c + 32 : c);
|
|
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == ' ' || c == '-'))
|
|
luaL_argerror(L, 1, "a trick's name is the game's (\"kickflip\", \"ollie\")");
|
|
}
|
|
auto clip = lua_isnoneornil(L, 2) ? Game::ClipRef{} : clip_at(L, plugin, 2);
|
|
std::string pop;
|
|
if (!lua_isnoneornil(L, 3)) {
|
|
luaL_checktype(L, 3, LUA_TTABLE);
|
|
lua_getfield(L, 3, "pop");
|
|
if (!lua_isnil(L, -1)) pop = luaL_checkstring(L, -1);
|
|
lua_pop(L, 1);
|
|
if (pop != "" && pop != "low" && pop != "mid" && pop != "high") luaL_argerror(L, 3, "a pop is \"low\", \"mid\" or \"high\"");
|
|
}
|
|
const float speed = number_field(L, 3, "speed", 1, 0.1f, 4);
|
|
return result(L, game(plugin).replace_trick(plugin.id(), std::move(trick), std::move(pop), std::move(clip), speed));
|
|
}
|
|
// record(seconds): the skater's pose for so long (0.2 to 10 s), as a clip recorded() hands back.
|
|
static int animation_record(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.record");
|
|
const double seconds = luaL_checknumber(L, 1);
|
|
if (!(seconds >= 0.2 && seconds <= 10)) luaL_argerror(L, 1, "a recording is 0.2 to 10 seconds");
|
|
if (plugin.clip_bytes_ + 10u * 60 * 396 * 40 > plugin.limits_.clip_bytes)
|
|
return result(L, "its clips hold too much already (each recording can be 10 s of poses)");
|
|
return result(L, game(plugin).record_clip(plugin.id(), static_cast<float>(seconds)));
|
|
}
|
|
// The last recording, once it is done, as a clip's name ("rec:1"), or nil.
|
|
static int animation_recorded(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.recorded");
|
|
auto clip = game(plugin).take_recording(plugin.id());
|
|
if (!clip) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
const auto name = "rec:" + std::to_string(++plugin.recordings_);
|
|
plugin.clip_bytes_ += pose_bytes(*clip);
|
|
plugin.clips_.emplace(name, std::move(clip));
|
|
lua_pushlstring(L, name.data(), name.size());
|
|
return 1;
|
|
}
|
|
// Lets a clip go (its memory with it); one still playing plays on.
|
|
static int animation_forget(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.forget");
|
|
const auto name = checked_text(L, 1, 200, "a clip");
|
|
if (const auto found = plugin.clips_.find(name); found != plugin.clips_.end()) {
|
|
plugin.clip_bytes_ -= std::min(plugin.clip_bytes_, pose_bytes(*found->second));
|
|
plugin.clips_.erase(found);
|
|
}
|
|
return 0;
|
|
}
|
|
// seek(seconds), pause(on), time(): the plugin's playing clip moved, held or let run, and where it is.
|
|
static int animation_seek(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.seek");
|
|
const double t = luaL_checknumber(L, 1);
|
|
if (!std::isfinite(t) || t < 0) luaL_argerror(L, 1, "seconds into the clip, 0 or more");
|
|
game(plugin).seek_clip(plugin.id(), static_cast<float>(t));
|
|
return 0;
|
|
}
|
|
static int animation_pause(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.pause");
|
|
game(plugin).pause_clip(plugin.id(), lua_isnone(L, 1) || lua_toboolean(L, 1) != 0);
|
|
return 0;
|
|
}
|
|
static int animation_time(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.time");
|
|
if (const auto t = game(plugin).clip_time(plugin.id())) lua_pushnumber(L, *t);
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
// A clip's joint, or "board" (the board's mount, relative to the skater's placement).
|
|
static std::uint16_t clip_joint(lua_State* L, int index) {
|
|
if (lua_type(L, index) == LUA_TSTRING && std::string_view(lua_tostring(L, index)) == "board") return anim_studio::board_joint;
|
|
return joint_of(L, index);
|
|
}
|
|
static void push_marks(lua_State* L, float takeoff, float landing) {
|
|
if (takeoff >= 0) {
|
|
lua_pushnumber(L, takeoff);
|
|
lua_setfield(L, -2, "takeoff");
|
|
}
|
|
if (landing >= 0) {
|
|
lua_pushnumber(L, landing);
|
|
lua_setfield(L, -2, "landing");
|
|
}
|
|
}
|
|
// info(clip): {duration, frames, joints, board, takeoff, landing}.
|
|
static int animation_info(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.info");
|
|
const auto clip = clip_at(L, plugin, 1);
|
|
lua_createtable(L, 0, 6);
|
|
lua_pushnumber(L, clip->duration());
|
|
lua_setfield(L, -2, "duration");
|
|
lua_pushinteger(L, static_cast<int>(clip->times.size()));
|
|
lua_setfield(L, -2, "frames");
|
|
lua_pushinteger(L, static_cast<int>(clip->joints));
|
|
lua_setfield(L, -2, "joints");
|
|
lua_pushboolean(L, clip->board.size() == clip->times.size() && !clip->board.empty());
|
|
lua_setfield(L, -2, "board");
|
|
push_marks(L, clip->takeoff, clip->landing);
|
|
return 1;
|
|
}
|
|
// sample(clip, joint, seconds): the joint in its parent's frame at that time, {position, rotation, scale}.
|
|
static int animation_sample(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.sample");
|
|
const auto clip = clip_at(L, plugin, 1);
|
|
const auto joint = clip_joint(L, 2);
|
|
const double t = luaL_checknumber(L, 3);
|
|
if (!std::isfinite(t)) luaL_argerror(L, 3, "seconds into the clip");
|
|
anim_studio::Joint out;
|
|
if (joint == anim_studio::board_joint) {
|
|
if (!clip->sample_board(static_cast<float>(t), out)) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
} else {
|
|
if (joint >= clip->joints) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
thread_local std::vector<anim_studio::Joint> pose;
|
|
pose.resize(clip->joints);
|
|
clip->sample(static_cast<float>(t), pose);
|
|
out = pose[joint];
|
|
}
|
|
lua_createtable(L, 0, 3);
|
|
push_position(L, out.position);
|
|
lua_setfield(L, -2, "position");
|
|
push_quaternion(L, out.rotation);
|
|
lua_setfield(L, -2, "rotation");
|
|
lua_pushnumber(L, (out.scale[0] + out.scale[1] + out.scale[2]) / 3);
|
|
lua_setfield(L, -2, "scale");
|
|
return 1;
|
|
}
|
|
// joints(): the joints an animation editor offers, in order: {joint, label, moves}.
|
|
static int animation_joints(lua_State* L) {
|
|
(void)animating(L, "reskate.animation.joints");
|
|
const auto list = anim_studio::editable_joints();
|
|
lua_createtable(L, static_cast<int>(list.size()), 0);
|
|
for (std::size_t i = 0; i < list.size(); ++i) {
|
|
lua_createtable(L, 0, 3);
|
|
if (list[i].index == anim_studio::board_joint) lua_pushstring(L, "board");
|
|
else lua_pushinteger(L, list[i].index);
|
|
lua_setfield(L, -2, "joint");
|
|
lua_pushlstring(L, list[i].label.data(), list[i].label.size());
|
|
lua_setfield(L, -2, "label");
|
|
lua_pushboolean(L, list[i].moves);
|
|
lua_setfield(L, -2, "moves");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
// A clip the plugin made, kept under `name` (one it held under that name before is let go).
|
|
static void keep_clip(lua_State* L, Plugin& plugin, const std::string& name, std::shared_ptr<const anim_studio::Clip> clip) {
|
|
if (const auto found = plugin.clips_.find(name); found != plugin.clips_.end()) {
|
|
plugin.clip_bytes_ -= std::min(plugin.clip_bytes_, pose_bytes(*found->second));
|
|
plugin.clips_.erase(found);
|
|
}
|
|
if (plugin.clip_bytes_ + pose_bytes(*clip) > plugin.limits_.clip_bytes)
|
|
luaL_error(L, "a plugin's clips hold at most %d MB of poses", static_cast<int>(plugin.limits_.clip_bytes >> 20));
|
|
plugin.clip_bytes_ += pose_bytes(*clip);
|
|
plugin.clips_.emplace(name, std::move(clip));
|
|
lua_pushlstring(L, name.data(), name.size());
|
|
}
|
|
// trim(clip, from, to): the frames between those seconds as a new clip ("clip:<n>"), from 0.
|
|
static int animation_trim(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.trim");
|
|
const auto source = clip_at(L, plugin, 1);
|
|
const double from = luaL_checknumber(L, 2), to = luaL_checknumber(L, 3);
|
|
if (!(from >= 0 && to > from)) luaL_error(L, "a trim is from 0 or more to later than that");
|
|
auto clip = std::make_shared<anim_studio::Clip>();
|
|
clip->joints = source->joints;
|
|
const bool board = source->board.size() == source->times.size();
|
|
float first = -1;
|
|
for (std::size_t i = 0; i < source->times.size(); ++i) {
|
|
const float t = source->times[i];
|
|
if (t < from || t > to) continue;
|
|
if (first < 0) first = t;
|
|
clip->times.push_back(t - first);
|
|
const auto frame = source->frame(i);
|
|
clip->frames.insert(clip->frames.end(), frame.begin(), frame.end());
|
|
if (board) clip->board.push_back(source->board[i]);
|
|
}
|
|
if (clip->times.size() < 2) luaL_error(L, "a trim keeps at least two frames");
|
|
const auto mark = [&](float t) { return t >= first && t - first <= clip->times.back() ? t - first : -1.0f; };
|
|
clip->takeoff = source->takeoff >= 0 ? mark(source->takeoff) : -1;
|
|
clip->landing = source->landing >= 0 ? mark(source->landing) : -1;
|
|
keep_clip(L, plugin, "clip:" + std::to_string(++plugin.recordings_), std::move(clip));
|
|
return 1;
|
|
}
|
|
// edit(clip, tracks, {name, takeoff, landing}): the clip with keyframed edits on its joints (each
|
|
// turned in its own frame and moved in its parent's), baked into a new clip: "edit:<name>", kept in
|
|
// place of the last one of that name, or "clip:<n>".
|
|
static int animation_edit(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.edit");
|
|
const auto source = clip_at(L, plugin, 1);
|
|
luaL_checktype(L, 2, LUA_TTABLE);
|
|
anim_studio::Animation edits;
|
|
const float length = source->duration();
|
|
std::size_t keys = 0;
|
|
const int tracks = static_cast<int>(lua_objlen(L, 2));
|
|
if (tracks > 128) luaL_error(L, "at most 128 tracks");
|
|
for (int t = 1; t <= tracks; ++t) {
|
|
lua_rawgeti(L, 2, t);
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
const int track = lua_gettop(L);
|
|
lua_getfield(L, track, "joint");
|
|
const auto joint = clip_joint(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
if (joint == anim_studio::placement_joint || joint == 0) luaL_error(L, "joints 0 and 1 place the skater: they are not edited");
|
|
if (edits.track(joint)) luaL_error(L, "joint %d has two tracks", static_cast<int>(joint));
|
|
edits.tracks.push_back({joint, {}, false, false});
|
|
auto& out = edits.tracks.back();
|
|
lua_getfield(L, track, "keys");
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
const int list = lua_gettop(L);
|
|
const int count = static_cast<int>(lua_objlen(L, list));
|
|
for (int k = 1; k <= count; ++k) {
|
|
if (++keys > 4096) luaL_error(L, "at most 4096 keys");
|
|
lua_rawgeti(L, list, k);
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
const int at = lua_gettop(L);
|
|
anim_studio::Key key;
|
|
lua_getfield(L, at, "time");
|
|
const double time = luaL_checknumber(L, -1);
|
|
if (!std::isfinite(time)) luaL_error(L, "a key's time is a number");
|
|
key.time = std::clamp(static_cast<float>(time), 0.0f, length);
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "rotation");
|
|
if (!lua_isnil(L, -1)) key.rotation = quaternion_at(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "angles");
|
|
if (!lua_isnil(L, -1)) {
|
|
const auto a = position_of(L, lua_gettop(L), "a key's angles");
|
|
key.rotation = anim_studio::from_euler({a[0], a[1], a[2]});
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "offset");
|
|
if (!lua_isnil(L, -1)) {
|
|
key.offset = position_of(L, lua_gettop(L), "a key's offset");
|
|
for (const auto v : key.offset)
|
|
if (std::abs(v) > 10) luaL_error(L, "a key's offset is at most 10 m on each axis");
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "ease");
|
|
if (!lua_isnil(L, -1)) {
|
|
const std::string ease = luaL_checkstring(L, -1);
|
|
if (ease == "linear") key.ease = anim_studio::Ease::linear;
|
|
else if (ease == "step") key.ease = anim_studio::Ease::step;
|
|
else if (ease != "smooth") luaL_error(L, "a key's ease is \"smooth\", \"linear\" or \"step\"");
|
|
}
|
|
lua_pop(L, 1);
|
|
const auto place = std::upper_bound(out.keys.begin(), out.keys.end(), key.time, [](float v, const anim_studio::Key& e) { return v < e.time; });
|
|
out.keys.insert(place, key);
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pop(L, 2);
|
|
}
|
|
std::string name;
|
|
if (!lua_isnoneornil(L, 3)) {
|
|
luaL_checktype(L, 3, LUA_TTABLE);
|
|
lua_getfield(L, 3, "name");
|
|
if (!lua_isnil(L, -1)) {
|
|
name = checked_text(L, lua_gettop(L), 48, "an edit's name");
|
|
if (name.empty() || anim_studio::file_safe(name) != name) luaL_error(L, "an edit's name is letters, digits, space, - and _");
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
auto clip = std::make_shared<anim_studio::Clip>(*source);
|
|
clip->takeoff = number_field(L, 3, "takeoff", source->takeoff, -1, length);
|
|
clip->landing = number_field(L, 3, "landing", source->landing, -1, length);
|
|
const bool board = clip->board.size() == clip->times.size();
|
|
const auto* board_track = edits.track(anim_studio::board_joint);
|
|
for (std::size_t i = 0; i < clip->times.size(); ++i) {
|
|
const float t = clip->times[i];
|
|
anim_studio::apply_edits(edits, t, std::span<anim_studio::Joint>(clip->frames.data() + i * clip->joints, clip->joints));
|
|
if (board && board_track && !board_track->keys.empty() && !board_track->hidden) {
|
|
const auto e = anim_studio::evaluate(*board_track, t);
|
|
auto& b = clip->board[i];
|
|
b.rotation = anim_studio::normalised(anim_studio::multiply(b.rotation, e.rotation));
|
|
for (std::size_t a = 0; a < 3; ++a) b.position[a] += e.offset[a];
|
|
}
|
|
}
|
|
keep_clip(L, plugin, name.empty() ? "clip:" + std::to_string(++plugin.recordings_) : "edit:" + name, std::move(clip));
|
|
return 1;
|
|
}
|
|
// save(clip, path): the clip as a clip file (FAS3) in the plugin's data folder, at most 8 MB.
|
|
static int animation_save(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.save");
|
|
need(L, plugin, "files", "reskate.animation.save");
|
|
const auto clip = clip_at(L, plugin, 1);
|
|
const auto path = data_path(L, 2, false);
|
|
const auto bytes = anim_studio::clip_bytes(*clip);
|
|
if (bytes.size() > (8u << 20)) return result(L, "a clip file is at most 8 MB");
|
|
return result(L, game(plugin).files_write_bytes(plugin.id(), path, std::string_view(reinterpret_cast<const char*>(bytes.data()), bytes.size()), 8u << 20));
|
|
}
|
|
static int animation_trick(lua_State* L) {
|
|
auto& plugin = animating(L, "reskate.animation.trick");
|
|
const auto name = game(plugin).current_trick();
|
|
if (name.empty()) lua_pushnil(L);
|
|
else lua_pushlstring(L, name.data(), name.size());
|
|
return 1;
|
|
}
|
|
// ---- board: the local skateboard's 16 bones as drawn -----------------------------------------------------
|
|
static int bone_number(lua_State* L, int index) {
|
|
const double b = luaL_checknumber(L, index);
|
|
if (!(b >= 2 && b <= 15) || b != std::floor(b)) luaL_argerror(L, index, "a board's bone is 2 to 15 (0 and 1 place the board)");
|
|
return static_cast<int>(b);
|
|
}
|
|
static std::vector<int> bone_list(lua_State* L, int index) {
|
|
luaL_checktype(L, index, LUA_TTABLE);
|
|
std::vector<int> out;
|
|
const int count = lua_objlen(L, index);
|
|
if (count > 16) luaL_argerror(L, index, "a board has 16 bones");
|
|
for (int i = 1; i <= count; ++i) {
|
|
lua_rawgeti(L, index, i);
|
|
out.push_back(bone_number(L, lua_gettop(L)));
|
|
lua_pop(L, 1);
|
|
}
|
|
return out;
|
|
}
|
|
// skater.reach({hand = "right"|"left", at = vector, rotation = {x, y, z, w}?, weight = 0..1}): the
|
|
// hand joint taken to `at` in the world (turned to `rotation`), in the pose drawn and sent; for
|
|
// 0.3 s, so every tick. nil lets the arm go.
|
|
static int reach(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.animation", "reskate.skater.reach");
|
|
if (lua_isnoneornil(L, 1)) return result(L, game(plugin).skater_reach(plugin.id(), std::nullopt));
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
Game::Reach goal;
|
|
lua_getfield(L, 1, "hand");
|
|
if (!lua_isnil(L, -1)) {
|
|
const std::string hand = luaL_checkstring(L, -1);
|
|
if (hand != "right" && hand != "left") luaL_error(L, "a reach's hand is \"right\" or \"left\"");
|
|
goal.left = hand == "left";
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 1, "at");
|
|
goal.at = position_of(L, lua_gettop(L), "the reach's point");
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 1, "rotation");
|
|
if (!lua_isnil(L, -1)) goal.rotation = quaternion_at(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 1, "weight");
|
|
if (!lua_isnil(L, -1)) {
|
|
const double w = luaL_checknumber(L, -1);
|
|
if (!(w >= 0 && w <= 1)) luaL_error(L, "a reach's weight is 0 to 1");
|
|
goal.weight = static_cast<float>(w);
|
|
}
|
|
lua_pop(L, 1);
|
|
return result(L, game(plugin).skater_reach(plugin.id(), goal));
|
|
}
|
|
// A skater's joint: a number 0..394 of the animation skeleton, or one of a few names.
|
|
static std::uint16_t joint_of(lua_State* L, int index) {
|
|
if (lua_type(L, index) == LUA_TSTRING) {
|
|
static constexpr std::pair<std::string_view, std::uint16_t> named[] = {
|
|
{"right_hand", 49}, {"left_hand", 278}, {"head", 103}, {"hips", 7}, {"spine", 42}};
|
|
const std::string_view name = lua_tostring(L, index);
|
|
const auto at = std::find_if(std::begin(named), std::end(named), [&](const auto& n) { return n.first == name; });
|
|
if (at == std::end(named)) luaL_error(L, "a joint is a number 0 to 394, or right_hand, left_hand, head, hips or spine");
|
|
return at->second;
|
|
}
|
|
const double joint = luaL_checknumber(L, index);
|
|
if (!(joint >= 0 && joint <= 394) || joint != static_cast<int>(joint)) luaL_error(L, "a joint is a number 0 to 394, or right_hand, left_hand, head, hips or spine");
|
|
return static_cast<std::uint16_t>(joint);
|
|
}
|
|
static int push_joint(lua_State* L, const std::optional<Game::JointPlace>& at, std::uint16_t joint) {
|
|
if (!at) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 4);
|
|
// Its parent in the skeleton (none for the root): for turning it about another frame.
|
|
if (const auto parent = anim_studio::parent(joint); parent >= 0) {
|
|
lua_pushinteger(L, parent);
|
|
lua_setfield(L, -2, "parent");
|
|
}
|
|
push_position(L, at->position);
|
|
lua_setfield(L, -2, "position");
|
|
push_quaternion(L, at->rotation);
|
|
lua_setfield(L, -2, "rotation");
|
|
lua_pushnumber(L, at->scale);
|
|
lua_setfield(L, -2, "scale");
|
|
return 1;
|
|
}
|
|
// skater.pose({[joint] = {rotation = {x, y, z, w} | angles = vector (degrees), scale = n}, ...}, weight):
|
|
// joints turned in their own frame and scaled on top of the animation for 0.3 s; nil ends it.
|
|
static int skater_pose(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.animation", "reskate.skater.pose");
|
|
std::vector<Game::JointOffset> offsets;
|
|
if (!lua_isnoneornil(L, 1)) {
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
lua_pushnil(L);
|
|
while (lua_next(L, 1)) {
|
|
Game::JointOffset o;
|
|
o.joint = joint_of(L, -2);
|
|
if (o.joint < 2) luaL_error(L, "joints 0 and 1 place the skater in the world: they cannot be posed");
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
const int at = lua_gettop(L);
|
|
lua_getfield(L, at, "rotation");
|
|
if (!lua_isnil(L, -1)) o.rotation = quaternion_at(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "angles");
|
|
if (!lua_isnil(L, -1)) {
|
|
const auto a = position_of(L, lua_gettop(L), "a joint's angles");
|
|
o.rotation = anim_studio::from_euler({a[0], a[1], a[2]});
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "scale");
|
|
if (!lua_isnil(L, -1)) {
|
|
const double s = luaL_checknumber(L, -1);
|
|
if (!(s >= 0 && s <= 10)) luaL_error(L, "a joint's scale is 0 to 10");
|
|
o.scale = static_cast<float>(s);
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, at, "offset");
|
|
if (!lua_isnil(L, -1)) {
|
|
o.offset = position_of(L, lua_gettop(L), "a joint's offset");
|
|
for (const auto v : o.offset)
|
|
if (std::abs(v) > 2) luaL_error(L, "a joint's offset is at most 2 m on each axis");
|
|
}
|
|
lua_pop(L, 1);
|
|
if (std::any_of(offsets.begin(), offsets.end(), [&](const auto& e) { return e.joint == o.joint; }))
|
|
luaL_error(L, "joint %d is posed twice", static_cast<int>(o.joint));
|
|
if (offsets.size() >= 64) luaL_error(L, "at most 64 joints are posed at once");
|
|
offsets.push_back(o);
|
|
lua_pop(L, 1);
|
|
}
|
|
}
|
|
const double weight = luaL_optnumber(L, 2, 1);
|
|
if (!(weight >= 0 && weight <= 1)) luaL_argerror(L, 2, "a weight is 0 to 1");
|
|
return result(L, game(plugin).skater_pose(plugin.id(), offsets, static_cast<float>(weight)));
|
|
}
|
|
// skater.joint(joint): {position, rotation, scale} of one of the local skater's joints, or nil.
|
|
static int skater_joint(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.read", "reskate.skater.joint");
|
|
const auto joint = joint_of(L, 1);
|
|
return push_joint(L, game(plugin).skater_joint(0, joint), joint);
|
|
}
|
|
// session.joint(player, joint): the same of another player's skater, as this game draws it.
|
|
static int session_joint(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "session.skaters", "reskate.session.joint");
|
|
const auto player = player_of(L, 1);
|
|
const auto joint = joint_of(L, 2);
|
|
return push_joint(L, game(plugin).skater_joint(player, joint), joint);
|
|
}
|
|
// skater.hand(side): {position, rotation} of that hand joint in the world as last drawn, or nil.
|
|
static int hand(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "skater.read", "reskate.skater.hand");
|
|
const std::string side = luaL_optstring(L, 1, "right");
|
|
if (side != "right" && side != "left") luaL_argerror(L, 1, "\"right\" or \"left\"");
|
|
const auto placed = game(plugin).skater_hand(side == "left");
|
|
if (!placed) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 2);
|
|
push_position(L, placed->position);
|
|
lua_setfield(L, -2, "position");
|
|
push_quaternion(L, placed->rotation);
|
|
lua_setfield(L, -2, "rotation");
|
|
return 1;
|
|
}
|
|
static std::array<float, 4> quaternion_at(lua_State* L, int index) {
|
|
luaL_checktype(L, index, LUA_TTABLE);
|
|
std::array<float, 4> q{};
|
|
for (int i = 0; i < 4; ++i) {
|
|
lua_rawgeti(L, index, i + 1);
|
|
const double v = luaL_checknumber(L, -1);
|
|
if (!std::isfinite(v)) luaL_error(L, "a rotation is 4 finite numbers {x, y, z, w}");
|
|
q[static_cast<std::size_t>(i)] = static_cast<float>(v);
|
|
lua_pop(L, 1);
|
|
}
|
|
const float n = std::sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
|
|
if (!(n > 1e-4f)) luaL_error(L, "a rotation {x, y, z, w} has a length");
|
|
for (auto& v : q) v /= n;
|
|
return q;
|
|
}
|
|
static void push_quaternion(lua_State* L, const std::array<float, 4>& q) {
|
|
lua_createtable(L, 4, 0);
|
|
for (int i = 0; i < 4; ++i) {
|
|
lua_pushnumber(L, q[static_cast<std::size_t>(i)]);
|
|
lua_rawseti(L, -2, i + 1);
|
|
}
|
|
}
|
|
// bones(): {[0] = {position, rotation = {x, y, z, w}, scale}, ... [15]} as last drawn, or nil.
|
|
static int board_bones(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.bones");
|
|
const auto bones = game(plugin).board_bones();
|
|
if (!bones) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 15, 1);
|
|
for (int i = 0; i < 16; ++i) {
|
|
const auto& b = (*bones)[static_cast<std::size_t>(i)];
|
|
lua_createtable(L, 0, 3);
|
|
push_position(L, b.position);
|
|
lua_setfield(L, -2, "position");
|
|
push_quaternion(L, b.rotation);
|
|
lua_setfield(L, -2, "rotation");
|
|
push_position(L, b.scale);
|
|
lua_setfield(L, -2, "scale");
|
|
lua_rawseti(L, -2, i);
|
|
}
|
|
return 1;
|
|
}
|
|
// set(bone, {position, rotation, space = "local" or "world"}).
|
|
static int board_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.set");
|
|
const int bone = bone_number(L, 1);
|
|
luaL_checktype(L, 2, LUA_TTABLE);
|
|
Game::BoardBone value;
|
|
lua_getfield(L, 2, "position");
|
|
value.position = position_of(L, lua_gettop(L), "a bone's position");
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 2, "rotation");
|
|
if (!lua_isnil(L, -1)) value.rotation = quaternion_at(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 2, "space");
|
|
const std::string space = lua_isnil(L, -1) ? "local" : luaL_checkstring(L, -1);
|
|
lua_pop(L, 1);
|
|
if (space != "local" && space != "world") luaL_argerror(L, 2, "space is \"local\" or \"world\"");
|
|
return result(L, game(plugin).board_set(plugin.id(), bone, value, space == "world"));
|
|
}
|
|
static int board_hide(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.hide");
|
|
return result(L, game(plugin).board_hide(plugin.id(), bone_number(L, 1)));
|
|
}
|
|
static int board_clear(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.clear");
|
|
game(plugin).board_clear(plugin.id(), lua_isnoneornil(L, 1) ? -1 : bone_number(L, 1));
|
|
return 0;
|
|
}
|
|
// park({12, 13, 14, 15}): these bones out of sight on every board, other players' too.
|
|
static int board_park(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.park");
|
|
game(plugin).board_park(plugin.id(), bone_list(L, 1));
|
|
return 0;
|
|
}
|
|
// fit({plates = bool, axle = 0.8 to 1.6}): how every board in the game is fitted, for a deck model
|
|
// that is not the game's own: the baseplates held on the deck, the wheels' spread. fit(nil) ends it.
|
|
static int board_fit(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.fit");
|
|
if (lua_isnoneornil(L, 1)) return result(L, game(plugin).board_fit(plugin.id(), std::nullopt));
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
Game::BoardFit fit;
|
|
lua_getfield(L, 1, "plates");
|
|
fit.plates = lua_toboolean(L, -1);
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 1, "axle");
|
|
if (!lua_isnil(L, -1)) fit.axle = static_cast<float>(luaL_checknumber(L, -1));
|
|
lua_pop(L, 1);
|
|
return result(L, game(plugin).board_fit(plugin.id(), fit));
|
|
}
|
|
// toss({bones}, {velocity, spin, ground}): a piece of the board comes off and falls.
|
|
static int board_toss(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "board", "reskate.board.toss");
|
|
auto bones = bone_list(L, 1);
|
|
luaL_checktype(L, 2, LUA_TTABLE);
|
|
lua_getfield(L, 2, "velocity");
|
|
const auto velocity = position_of(L, lua_gettop(L), "the piece's velocity");
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 2, "spin");
|
|
const auto spin = lua_isnil(L, -1) ? Vec3{} : position_of(L, lua_gettop(L), "the piece's spin");
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 2, "ground");
|
|
const double ground = luaL_checknumber(L, -1);
|
|
lua_pop(L, 1);
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).board_toss(plugin.id(), std::move(bones), velocity, spin, static_cast<float>(ground)));
|
|
}
|
|
// ---- outfit ------------------------------------------------------------------------------------------
|
|
// set(slot, item, {placement = {...}, covers = bool}): the item worn over the outfit in that slot.
|
|
static int outfit_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "outfit", "reskate.outfit.set");
|
|
const auto slot = checked_text(L, 1, 48, "a slot's name");
|
|
const auto item = checked_text(L, 2, 200, "an item's name");
|
|
// Only the face and skin layers, and eyewear (Extension/Skater/outfit_overlay.h).
|
|
std::string lowered = slot;
|
|
for (auto& c : lowered) c = static_cast<char>(c >= 'A' && c <= 'Z' ? c + 32 : c);
|
|
if (lowered.find("tattoo") == std::string::npos && lowered.find("moles") == std::string::npos && lowered.find("eyewear") == std::string::npos)
|
|
return result(L, "only tattoo, moles and eyewear slots");
|
|
std::vector<float> placement;
|
|
bool covers{};
|
|
if (!lua_isnoneornil(L, 3)) {
|
|
luaL_checktype(L, 3, LUA_TTABLE);
|
|
lua_getfield(L, 3, "covers");
|
|
covers = lua_toboolean(L, -1) != 0;
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 3, "placement");
|
|
if (!lua_isnil(L, -1)) {
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
const int count = lua_objlen(L, -1);
|
|
if (count > 8) luaL_error(L, "a placement is at most 8 numbers");
|
|
for (int i = 1; i <= count; ++i) {
|
|
lua_rawgeti(L, -1, i);
|
|
placement.push_back(static_cast<float>(luaL_checknumber(L, -1)));
|
|
lua_pop(L, 1);
|
|
}
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
if (!within(plugin.services_.now_ms(), plugin.control_second_, plugin.controls_, 4)) return result(L, "asked more than 4 times in a second");
|
|
return result(L, game(plugin).outfit_set(plugin.id(), slot, item, std::move(placement), covers));
|
|
}
|
|
static int outfit_clear(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "outfit", "reskate.outfit.clear");
|
|
game(plugin).outfit_clear(plugin.id(), lua_isnoneornil(L, 1) ? std::string{} : checked_text(L, 1, 48, "a slot's name"));
|
|
return 0;
|
|
}
|
|
// find(ending): the installed item whose name ends so (any case), or nil.
|
|
static int outfit_find(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "outfit", "reskate.outfit.find");
|
|
const auto name = game(plugin).outfit_find(checked_text(L, 1, 200, "an item's name"));
|
|
if (name.empty()) lua_pushnil(L);
|
|
else lua_pushlstring(L, name.data(), name.size());
|
|
return 1;
|
|
}
|
|
static int outfit_slots(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "outfit", "reskate.outfit.slots");
|
|
const auto slots = game(plugin).outfit_slots();
|
|
lua_createtable(L, static_cast<int>(slots.size()), 0);
|
|
for (std::size_t i = 0; i < slots.size(); ++i) {
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushlstring(L, slots[i].name.data(), slots[i].name.size());
|
|
lua_setfield(L, -2, "slot");
|
|
lua_pushlstring(L, slots[i].item.data(), slots[i].item.size());
|
|
lua_setfield(L, -2, "item");
|
|
lua_createtable(L, static_cast<int>(slots[i].parameters.size()), 0);
|
|
for (std::size_t k = 0; k < slots[i].parameters.size(); ++k) {
|
|
lua_pushnumber(L, std::isfinite(slots[i].parameters[k]) ? slots[i].parameters[k] : 0.0f);
|
|
lua_rawseti(L, -2, static_cast<int>(k + 1));
|
|
}
|
|
lua_setfield(L, -2, "parameters");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
return 1;
|
|
}
|
|
static int outfit_status(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "outfit", "reskate.outfit.status");
|
|
const auto status = game(plugin).outfit_status();
|
|
lua_pushlstring(L, status.data(), status.size());
|
|
return 1;
|
|
}
|
|
// ---- audio --------------------------------------------------------------------------------------------
|
|
static std::shared_ptr<const Wave> wave_at(lua_State* L, Plugin& plugin, const std::string& path) {
|
|
if (const auto found = plugin.waves_.find(path); found != plugin.waves_.end()) return found->second;
|
|
if (plugin.waves_.size() >= plugin.limits_.sounds) luaL_error(L, "a plugin plays at most %d different sounds", static_cast<int>(plugin.limits_.sounds));
|
|
std::string error;
|
|
const auto bytes = read_package_file(package_file(L, plugin, path), 8u << 20, error);
|
|
if (bytes.empty()) luaL_error(L, "the sound %s cannot be read: %s", path.c_str(), error.empty() ? "it is empty" : error.c_str());
|
|
auto wave = decode_wave(std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t*>(bytes.data()), bytes.size()), 30, error);
|
|
if (!wave) luaL_error(L, "the sound %s cannot be played: %s", path.c_str(), error.c_str());
|
|
return plugin.waves_.emplace(path, std::move(wave)).first->second;
|
|
}
|
|
// volume, pitch, range, throttle, speed, loop, position (a vector, or "skater").
|
|
static void sound_options(lua_State* L, int index, SoundStart* start, SoundChange* change) {
|
|
if (lua_isnoneornil(L, index)) return;
|
|
luaL_checktype(L, index, LUA_TTABLE);
|
|
const auto number = [&](const char* name, float least, float most, auto&& set) {
|
|
lua_getfield(L, index, name);
|
|
if (!lua_isnil(L, -1)) {
|
|
const double v = luaL_checknumber(L, -1);
|
|
if (!(v >= least && v <= most)) luaL_error(L, "%s is %g to %g", name, static_cast<double>(least), static_cast<double>(most));
|
|
set(static_cast<float>(v));
|
|
}
|
|
lua_pop(L, 1);
|
|
};
|
|
number("volume", 0, 1, [&](float v) { if (start) start->volume = v; else change->volume = v; });
|
|
number("pitch", 0.25f, 4, [&](float v) { if (start) start->pitch = v; else change->pitch = v; });
|
|
number("range", 1, 500, [&](float v) { if (start) start->range = v; else change->range = v; });
|
|
number("throttle", 0, 1, [&](float v) { if (start) start->throttle = v; else change->throttle = v; });
|
|
number("speed", 0, 200, [&](float v) { if (start) start->speed = v; else change->speed = v; });
|
|
if (start) {
|
|
lua_getfield(L, index, "loop");
|
|
start->loop = lua_toboolean(L, -1) != 0;
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_getfield(L, index, "position");
|
|
if (!lua_isnil(L, -1)) {
|
|
std::optional<Vec3> at;
|
|
if (lua_isstring(L, -1) && std::string_view(lua_tostring(L, -1)) == "skater") at = std::nullopt;
|
|
else at = position_of(L, lua_gettop(L), "a sound's position");
|
|
if (start) start->position = at;
|
|
else change->position = at;
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
// play(path or "motor", options): a sound in the game where it is, or at the skater; its id.
|
|
static int audio_play(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "audio", "reskate.audio.play");
|
|
const auto what = checked_text(L, 1, 200, "a sound");
|
|
SoundStart start;
|
|
if (what != "motor") start.wave = wave_at(L, plugin, what);
|
|
sound_options(L, 2, &start, nullptr);
|
|
if (!within(plugin.services_.now_ms(), plugin.wave_second_, plugin.wave_starts_, 20)) return result(L, "more than 20 sounds in a second");
|
|
const auto id = game(plugin).play_sound(plugin.id(), std::move(start));
|
|
if (!id) return result(L, "it plays 8 sounds already, or there is no level");
|
|
lua_pushnumber(L, static_cast<double>(id));
|
|
return 1;
|
|
}
|
|
static std::uint64_t sound_id(lua_State* L, int index) {
|
|
const double id = luaL_checknumber(L, index);
|
|
if (!(id >= 1 && id < 9007199254740992.0) || id != std::floor(id)) luaL_argerror(L, index, "not a sound's id");
|
|
return static_cast<std::uint64_t>(id);
|
|
}
|
|
static int audio_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "audio", "reskate.audio.set");
|
|
const auto id = sound_id(L, 1);
|
|
SoundChange change;
|
|
sound_options(L, 2, nullptr, &change);
|
|
lua_pushboolean(L, game(plugin).change_sound(plugin.id(), id, change));
|
|
return 1;
|
|
}
|
|
static int audio_stop(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "audio", "reskate.audio.stop");
|
|
game(plugin).stop_sound(plugin.id(), sound_id(L, 1));
|
|
return 0;
|
|
}
|
|
static int audio_playing(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "audio", "reskate.audio.playing");
|
|
lua_pushboolean(L, game(plugin).sound_playing(plugin.id(), sound_id(L, 1)));
|
|
return 1;
|
|
}
|
|
// ---- server: what only the session's authority does ----------------------------------------------
|
|
static Plugin& authority(lua_State* L, const char* what) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "server", what);
|
|
if (!plugin.services_.authority())
|
|
luaL_error(L, "%s is for the plugin's server copy (reskate.side is \"server\": \"side\" \"server\" or \"both\" in reskate-plugin.json)", what);
|
|
if (!within(plugin.services_.now_ms(), plugin.server_second_, plugin.server_calls_, 20)) luaL_error(L, "more than 20 server calls in a second");
|
|
return plugin;
|
|
}
|
|
static int server_kick(lua_State* L) {
|
|
auto& plugin = authority(L, "reskate.server.kick");
|
|
const auto player = player_of(L, 1);
|
|
const auto why = lua_isnoneornil(L, 2) ? std::string{} : checked_text(L, 2, 120, "why");
|
|
return result(L, game(plugin).kick(player, why));
|
|
}
|
|
static int server_say(lua_State* L) {
|
|
auto& plugin = authority(L, "reskate.server.say");
|
|
const auto text = checked_text(L, 1, 200, "a chat line");
|
|
if (text.empty() || text.front() == '/') luaL_argerror(L, 1, "a chat line, not empty and not a / command");
|
|
const double now = plugin.services_.now_ms();
|
|
if (now - plugin.chat_at_ < 1000) return result(L, "one line a second");
|
|
plugin.chat_at_ = now;
|
|
return result(L, game(plugin).announce(plugin.channel_name(), text));
|
|
}
|
|
static int server_tell(lua_State* L) {
|
|
auto& plugin = authority(L, "reskate.server.tell");
|
|
const auto player = player_of(L, 1);
|
|
const auto text = checked_text(L, 2, 200, "a chat line");
|
|
if (text.empty()) luaL_argerror(L, 2, "a chat line, not empty");
|
|
return result(L, game(plugin).tell(plugin.channel_name(), player, text));
|
|
}
|
|
static int server_move(lua_State* L) {
|
|
auto& plugin = authority(L, "reskate.server.move");
|
|
const auto player = player_of(L, 1);
|
|
return result(L, game(plugin).move_player(player, position_of(L, 2, "where the player goes")));
|
|
}
|
|
static int server_ban(lua_State* L) {
|
|
auto& plugin = authority(L, "reskate.server.ban");
|
|
return result(L, game(plugin).ban(player_of(L, 1)));
|
|
}
|
|
static int server_unban(lua_State* L) {
|
|
auto& plugin = authority(L, "reskate.server.unban");
|
|
return result(L, game(plugin).unban(player_of(L, 1)));
|
|
}
|
|
// A player's permissions: player.permissions is what the server holds for them now, read-only;
|
|
// permissions:clone() is a copy to change; player:apply_permissions(copy) makes it theirs.
|
|
static constexpr std::array<std::pair<const char*, bool Game::Permissions::*>, 6> permission_fields{{
|
|
{"noclip", &Game::Permissions::noclip}, {"nobail", &Game::Permissions::nobail}, {"boosts", &Game::Permissions::boosts},
|
|
{"tuning", &Game::Permissions::tuning}, {"objects", &Game::Permissions::objects}, {"ollies", &Game::Permissions::ollies}}};
|
|
static void push_permissions(lua_State* L, const Game::Permissions& permissions, bool readonly) {
|
|
lua_createtable(L, 0, static_cast<int>(permission_fields.size()));
|
|
for (const auto& [name, field] : permission_fields) {
|
|
lua_pushboolean(L, permissions.*field);
|
|
lua_setfield(L, -2, name);
|
|
}
|
|
lua_getfield(L, LUA_REGISTRYINDEX, "reskate.permissions");
|
|
lua_setmetatable(L, -2);
|
|
if (readonly) lua_setreadonly(L, -1, true);
|
|
}
|
|
// Every permission, each true or false, and nothing else: a clone, so none is left out by mistake.
|
|
static Game::Permissions permissions_of(lua_State* L, int index) {
|
|
luaL_checktype(L, index, LUA_TTABLE);
|
|
Game::Permissions permissions;
|
|
for (const auto& [name, field] : permission_fields) {
|
|
lua_getfield(L, index, name);
|
|
if (!lua_isboolean(L, -1)) luaL_error(L, "permission \"%s\" must be true or false (clone the player's permissions to change them)", name);
|
|
permissions.*field = lua_toboolean(L, -1);
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pushnil(L);
|
|
while (lua_next(L, index)) {
|
|
const char* key = lua_type(L, -2) == LUA_TSTRING ? lua_tostring(L, -2) : nullptr;
|
|
const bool known = key && std::any_of(permission_fields.begin(), permission_fields.end(),
|
|
[&](const auto& f) { return std::string_view(f.first) == key; });
|
|
if (!known) luaL_error(L, "there is no permission \"%s\"", key ? key : "(not a name)");
|
|
lua_pop(L, 1);
|
|
}
|
|
return permissions;
|
|
}
|
|
static int permissions_clone(lua_State* L) {
|
|
push_permissions(L, permissions_of(L, 1), false);
|
|
return 1;
|
|
}
|
|
static int player_apply_permissions(lua_State* L) {
|
|
auto& plugin = authority(L, "player:apply_permissions");
|
|
const auto player = player_of(L, 1);
|
|
return result(L, game(plugin).apply_permissions(plugin.id(), player, permissions_of(L, 2)));
|
|
}
|
|
// A player table's own names: permissions (read when asked, so always the server's now) and
|
|
// apply_permissions. Anything else a player table does not have is nil.
|
|
static int player_index(lua_State* L) {
|
|
const char* key = lua_type(L, 2) == LUA_TSTRING ? lua_tostring(L, 2) : "";
|
|
if (std::string_view(key) == "apply_permissions") {
|
|
lua_pushcfunction(L, player_apply_permissions, "apply_permissions");
|
|
return 1;
|
|
}
|
|
if (std::string_view(key) != "permissions") return 0;
|
|
auto& plugin = authority(L, "player.permissions");
|
|
const auto permissions = game(plugin).permissions(player_of(L, 1));
|
|
if (!permissions) return 0;
|
|
push_permissions(L, *permissions, true);
|
|
return 1;
|
|
}
|
|
// ---- throwdowns -------------------------------------------------------------------------------------
|
|
// host(mode, players, {max}): a drop where the skater stands for these players ("jam",
|
|
// "spot_battle" or "skate"); it starts once they all join. One every 5 s.
|
|
static int throwdown_host(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "throwdowns", "reskate.throwdowns.host");
|
|
const std::string mode = luaL_checkstring(L, 1);
|
|
const char* series = mode == "jam" || mode == "JamSession" ? "JamSession"
|
|
: mode == "spot_battle" || mode == "SpotBattle" ? "SpotBattle"
|
|
: mode == "skate" || mode == "ThrowdownSkate" ? "ThrowdownSkate" : nullptr;
|
|
if (!series) luaL_argerror(L, 1, "a throwdown is \"jam\", \"spot_battle\" or \"skate\"");
|
|
luaL_checktype(L, 2, LUA_TTABLE);
|
|
std::vector<std::uint64_t> players;
|
|
const int count = lua_objlen(L, 2);
|
|
if (count < 1 || count > 7) luaL_argerror(L, 2, "a drop is for 1 to 7 other players");
|
|
for (int i = 1; i <= count; ++i) {
|
|
lua_rawgeti(L, 2, i);
|
|
const auto player = player_of(L, lua_gettop(L));
|
|
lua_pop(L, 1);
|
|
if (std::find(players.begin(), players.end(), player) == players.end()) players.push_back(player);
|
|
}
|
|
int most = 0;
|
|
if (!lua_isnoneornil(L, 3)) {
|
|
luaL_checktype(L, 3, LUA_TTABLE);
|
|
lua_getfield(L, 3, "max");
|
|
if (!lua_isnil(L, -1)) most = static_cast<int>(luaL_checkinteger(L, -1));
|
|
lua_pop(L, 1);
|
|
if (most != 0 && (most < 2 || most > 8)) luaL_argerror(L, 3, "max is 2 to 8 players");
|
|
}
|
|
const double now = plugin.services_.now_ms();
|
|
if (now - plugin.host_at_ < 5000) return result(L, "one drop every 5 s");
|
|
plugin.host_at_ = now;
|
|
return result(L, game(plugin).host_throwdown(plugin.id(), series, std::move(players), most));
|
|
}
|
|
static int throwdown_cancel(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "throwdowns", "reskate.throwdowns.cancel");
|
|
game(plugin).cancel_throwdown(plugin.id());
|
|
return 0;
|
|
}
|
|
static int throwdown_hosting(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "throwdowns", "reskate.throwdowns.hosting");
|
|
const auto status = game(plugin).throwdown_status(plugin.id());
|
|
if (status.empty()) lua_pushnil(L);
|
|
else lua_pushlstring(L, status.data(), status.size());
|
|
return 1;
|
|
}
|
|
// mode{id, name, description, art, color, card, replaces, order, flag, cursor_height,
|
|
// players = {min, max, default}}: the mode's card on the Throwdowns page (cloned from the stock
|
|
// `card`: "skate", "spot" or "jam") and its native flag's preview prefab; cursor_height (metres)
|
|
// lets its flag's placement cursor rise onto anything that high. Its setup page is the plugin's
|
|
// `setup` document (throwdowns.ui). Again with the same id updates it.
|
|
static int int_field(lua_State* L, int table, const char* name, int fallback) {
|
|
lua_getfield(L, table, name);
|
|
const int value = lua_isnil(L, -1) ? fallback : static_cast<int>(luaL_checkinteger(L, -1));
|
|
lua_pop(L, 1);
|
|
return value;
|
|
}
|
|
static std::string text_field(lua_State* L, int table, const char* name) {
|
|
lua_getfield(L, table, name);
|
|
std::string value;
|
|
if (!lua_isnil(L, -1)) {
|
|
if (!lua_isstring(L, -1)) luaL_error(L, "%s is a string", name);
|
|
value = lua_tostring(L, -1);
|
|
}
|
|
lua_pop(L, 1);
|
|
return value;
|
|
}
|
|
static int throwdown_mode(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "throwdowns", "reskate.throwdowns.mode");
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
ThrowdownMode mode;
|
|
mode.id = text_field(L, 1, "id");
|
|
mode.name = text_field(L, 1, "name");
|
|
mode.description = text_field(L, 1, "description");
|
|
mode.art = text_field(L, 1, "art");
|
|
mode.flag = text_field(L, 1, "flag");
|
|
mode.replaces = text_field(L, 1, "replaces");
|
|
mode.card = text_field(L, 1, "card");
|
|
if (mode.card.empty()) mode.card = "spot";
|
|
mode.order = int_field(L, 1, "order", 0);
|
|
lua_getfield(L, 1, "color");
|
|
if (lua_isnumber(L, -1)) {
|
|
const double c = lua_tonumber(L, -1);
|
|
if (!(c >= 0 && c <= 4294967295.0)) luaL_argerror(L, 1, "color is 0xAABBGGRR");
|
|
mode.color = static_cast<std::uint32_t>(c);
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 1, "cursor_height");
|
|
if (!lua_isnil(L, -1)) {
|
|
const double h = luaL_checknumber(L, -1);
|
|
if (!(h >= 0 && h <= 100000)) luaL_argerror(L, 1, "cursor_height is 0 to 100000 metres");
|
|
mode.cursor_height = static_cast<float>(h);
|
|
}
|
|
lua_pop(L, 1);
|
|
const auto stock = [](const std::string& tile) { return tile == "skate" || tile == "spot" || tile == "spot_battle" || tile == "jam"; };
|
|
if (!mode.replaces.empty() && !stock(mode.replaces)) luaL_argerror(L, 1, "replaces is \"skate\", \"spot\" or \"jam\"");
|
|
if (!stock(mode.card)) luaL_argerror(L, 1, "card is \"skate\", \"spot\" or \"jam\"");
|
|
if (mode.flag.size() > 256 || mode.art.size() > 256) luaL_argerror(L, 1, "art and flag are at most 256 characters");
|
|
// An asset path: letters, digits, _ - . and /.
|
|
for (const char c : mode.flag)
|
|
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_' && c != '/' && c != '-' && c != '.')
|
|
luaL_argerror(L, 1, "flag is an asset path: letters, digits, _, -, . and /");
|
|
lua_getfield(L, 1, "players");
|
|
if (!lua_isnil(L, -1)) {
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
const int at = lua_gettop(L);
|
|
mode.min_players = int_field(L, at, "min", 1);
|
|
mode.max_players = int_field(L, at, "max", 6);
|
|
mode.players = int_field(L, at, "default", std::min(4, mode.max_players));
|
|
}
|
|
lua_pop(L, 1);
|
|
return result(L, game(plugin).set_throwdown_mode(plugin.id(), mode));
|
|
}
|
|
static int throwdown_remove_mode(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "throwdowns", "reskate.throwdowns.remove_mode");
|
|
game(plugin).remove_throwdown_mode(plugin.id(), luaL_checkstring(L, 1));
|
|
return 0;
|
|
}
|
|
static int throwdown_state(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "throwdowns", "reskate.throwdowns.state");
|
|
const auto t = game(plugin).throwdowns();
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushboolean(L, t.active);
|
|
lua_setfield(L, -2, "active");
|
|
lua_pushboolean(L, t.host);
|
|
lua_setfield(L, -2, "host");
|
|
lua_createtable(L, static_cast<int>(t.skate.size()), 0);
|
|
for (std::size_t i = 0; i < t.skate.size(); ++i) {
|
|
lua_createtable(L, 0, 4);
|
|
lua_pushlstring(L, t.skate[i].name.data(), t.skate[i].name.size());
|
|
lua_setfield(L, -2, "name");
|
|
lua_pushinteger(L, t.skate[i].letters);
|
|
lua_setfield(L, -2, "letters");
|
|
lua_pushboolean(L, t.skate[i].up);
|
|
lua_setfield(L, -2, "up");
|
|
lua_pushboolean(L, t.skate[i].out);
|
|
lua_setfield(L, -2, "out");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
lua_setfield(L, -2, "skate");
|
|
return 1;
|
|
}
|
|
// ---- music -------------------------------------------------------------------------------------------
|
|
static void push_track(lua_State* L, const Track& t) {
|
|
lua_createtable(L, 0, 5);
|
|
lua_pushlstring(L, t.app.data(), t.app.size());
|
|
lua_setfield(L, -2, "app");
|
|
lua_pushlstring(L, t.title.data(), t.title.size());
|
|
lua_setfield(L, -2, "title");
|
|
lua_pushlstring(L, t.artist.data(), t.artist.size());
|
|
lua_setfield(L, -2, "artist");
|
|
lua_pushlstring(L, t.album.data(), t.album.size());
|
|
lua_setfield(L, -2, "album");
|
|
lua_pushboolean(L, t.playing);
|
|
lua_setfield(L, -2, "playing");
|
|
}
|
|
static int current(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "music", "reskate.music.current");
|
|
const auto t = game(plugin).track();
|
|
if (!t.available) lua_pushnil(L);
|
|
else push_track(L, t);
|
|
return 1;
|
|
}
|
|
static int music(lua_State* L, const char* command) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "music", "reskate.music");
|
|
if (!within(plugin.services_.now_ms(), plugin.music_second_, plugin.musics_, 2)) return result(L, "more than 2 a second");
|
|
return result(L, game(plugin).music(command));
|
|
}
|
|
static int play(lua_State* L) { return music(L, "play"); }
|
|
static int pause(lua_State* L) { return music(L, "pause"); }
|
|
static int next(lua_State* L) { return music(L, "next"); }
|
|
// The game's own soundtrack: what it plays on the player's headphones, and a skip.
|
|
static int game_current(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "music", "reskate.music.game");
|
|
const auto t = game(plugin).game_track();
|
|
if (!t) lua_pushnil(L);
|
|
else push_track(L, *t);
|
|
return 1;
|
|
}
|
|
static int game_next(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "music", "reskate.music.game_next");
|
|
if (!within(plugin.services_.now_ms(), plugin.music_second_, plugin.musics_, 2)) return result(L, "asked more than twice in a second");
|
|
return result(L, game(plugin).game_music("next"));
|
|
}
|
|
static int previous(lua_State* L) { return music(L, "previous"); }
|
|
// ---- progression.read / settings.read -------------------------------------------------------------
|
|
static int progress(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "progression.read", "reskate.progression.get");
|
|
const auto p = game(plugin).progress();
|
|
if (!p.available) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 5);
|
|
lua_createtable(L, static_cast<int>(p.districts.size()), 0);
|
|
for (std::size_t i = 0; i < p.districts.size(); ++i) {
|
|
lua_createtable(L, 0, 3);
|
|
lua_pushlstring(L, p.districts[i].id.data(), p.districts[i].id.size());
|
|
lua_setfield(L, -2, "id");
|
|
lua_pushlstring(L, p.districts[i].name.data(), p.districts[i].name.size());
|
|
lua_setfield(L, -2, "name");
|
|
lua_pushinteger(L, p.districts[i].rank);
|
|
lua_setfield(L, -2, "rank");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
lua_setfield(L, -2, "districts");
|
|
lua_createtable(L, static_cast<int>(p.challenges.size()), 0);
|
|
for (std::size_t i = 0; i < p.challenges.size(); ++i) {
|
|
const auto& c = p.challenges[i];
|
|
lua_createtable(L, 0, 6);
|
|
lua_pushlstring(L, c.id.data(), c.id.size());
|
|
lua_setfield(L, -2, "id");
|
|
lua_pushlstring(L, c.type.data(), c.type.size());
|
|
lua_setfield(L, -2, "type");
|
|
lua_pushlstring(L, c.neighborhood.data(), c.neighborhood.size());
|
|
lua_setfield(L, -2, "neighborhood");
|
|
lua_pushinteger(L, c.attempts);
|
|
lua_setfield(L, -2, "attempts");
|
|
lua_pushinteger(L, c.completed_goals);
|
|
lua_setfield(L, -2, "completed_goals");
|
|
lua_pushinteger(L, c.goal_count);
|
|
lua_setfield(L, -2, "goal_count");
|
|
lua_rawseti(L, -2, static_cast<int>(i + 1));
|
|
}
|
|
lua_setfield(L, -2, "challenges");
|
|
lua_pushnumber(L, p.score);
|
|
lua_setfield(L, -2, "score");
|
|
lua_pushnumber(L, p.score_cap);
|
|
lua_setfield(L, -2, "score_cap");
|
|
lua_pushinteger(L, p.score_level);
|
|
lua_setfield(L, -2, "score_level");
|
|
return 1;
|
|
}
|
|
static int setting(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "settings.read", "reskate.settings.game");
|
|
const auto value = game(plugin).setting(checked_text(L, 1, 120, "a setting's name"));
|
|
if (value) lua_pushlstring(L, value->data(), value->size());
|
|
else lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
// settings.set(name, value): a game setting held at `value` while the plugin keeps it; nil lets go.
|
|
static int setting_set(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "settings.write", "reskate.settings.set");
|
|
const auto name = checked_text(L, 1, 120, "a setting's name");
|
|
std::optional<std::string> value;
|
|
if (lua_type(L, 2) == LUA_TBOOLEAN) value = lua_toboolean(L, 2) ? "true" : "false";
|
|
else if (lua_type(L, 2) == LUA_TNUMBER) value = std::format("{}", lua_tonumber(L, 2));
|
|
else if (!lua_isnoneornil(L, 2)) value = checked_text(L, 2, 200, "a setting's value");
|
|
return result(L, game(plugin).set_setting(plugin.id(), name, std::move(value)));
|
|
}
|
|
static int setting_names(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "settings.read", "reskate.settings.names");
|
|
const auto names = game(plugin).setting_names();
|
|
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;
|
|
}
|
|
// ---- input -------------------------------------------------------------------------------------------
|
|
// bind(action, "F7" | "Ctrl+K" | "Pad.LB+Pad.RB"): the "action" event names it on each press,
|
|
// while the game itself has the input (no menu, console or text box open).
|
|
static int bind(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.bind");
|
|
const auto action = checked_text(L, 1, 48, "an action's name");
|
|
const auto key = checked_text(L, 2, 60, "a key");
|
|
if (action.empty()) luaL_argerror(L, 1, "an action needs a name");
|
|
const auto combo = combo_of(key);
|
|
if (!combo) luaL_error(L, "\"%s\" is not a key: 0-9, A-Z, Space, F1-F12 with Ctrl, Shift or Alt; or Pad.A, Pad.LB+Pad.RB...", key.c_str());
|
|
if (!plugin.binds_.contains(action) && plugin.binds_.size() >= 16) luaL_error(L, "at most 16 actions");
|
|
if (const auto why = game(plugin).bind(plugin.id(), action, combo); !why.empty()) return result(L, why);
|
|
plugin.binds_.insert(action);
|
|
return result(L, {});
|
|
}
|
|
static int input_flight(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.flight");
|
|
const auto in = game(plugin).flight_input(plugin.id());
|
|
if (!in) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 6);
|
|
const std::pair<const char*, float> fields[] = {{"right", in->right}, {"up", in->up}, {"forward", in->forward},
|
|
{"look_x", in->look_x}, {"look_y", in->look_y}};
|
|
for (const auto& [name, value] : fields) {
|
|
lua_pushnumber(L, value);
|
|
lua_setfield(L, -2, name);
|
|
}
|
|
lua_pushboolean(L, in->boost);
|
|
lua_setfield(L, -2, "boost");
|
|
return 1;
|
|
}
|
|
static void push_buttons(lua_State* L, std::uint32_t bits, const std::pair<const char*, std::uint32_t>* names, std::size_t count) {
|
|
lua_createtable(L, 0, static_cast<int>(count));
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
lua_pushboolean(L, (bits & names[i].second) != 0);
|
|
lua_setfield(L, -2, names[i].first);
|
|
}
|
|
}
|
|
static int input_gamepad(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.gamepad");
|
|
const auto pad = game(plugin).gamepad();
|
|
if (!pad) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 10);
|
|
const std::pair<const char*, float> axes[] = {{"left_x", pad->left_x}, {"left_y", pad->left_y}, {"right_x", pad->right_x},
|
|
{"right_y", pad->right_y}, {"left_trigger", pad->left_trigger}, {"right_trigger", pad->right_trigger}};
|
|
for (const auto& [name, value] : axes) {
|
|
lua_pushnumber(L, value);
|
|
lua_setfield(L, -2, name);
|
|
}
|
|
// XInput's button bits.
|
|
static constexpr std::pair<const char*, std::uint32_t> names[] = {
|
|
{"up", 0x1}, {"down", 0x2}, {"left", 0x4}, {"right", 0x8}, {"start", 0x10}, {"back", 0x20}, {"left_stick", 0x40},
|
|
{"right_stick", 0x80}, {"lb", 0x100}, {"rb", 0x200}, {"a", 0x1000}, {"b", 0x2000}, {"x", 0x4000}, {"y", 0x8000}};
|
|
push_buttons(L, pad->buttons, names, std::size(names));
|
|
lua_setfield(L, -2, "buttons");
|
|
lua_pushboolean(L, pad->playstation);
|
|
lua_setfield(L, -2, "playstation");
|
|
return 1;
|
|
}
|
|
static int input_joystick(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.joystick");
|
|
const auto stick = game(plugin).joystick();
|
|
if (!stick) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 3);
|
|
lua_createtable(L, 6, 0);
|
|
for (int i = 0; i < 6; ++i) {
|
|
lua_pushnumber(L, stick->axes[static_cast<std::size_t>(i)]);
|
|
lua_rawseti(L, -2, i + 1);
|
|
}
|
|
lua_setfield(L, -2, "axes");
|
|
lua_createtable(L, 32, 0);
|
|
for (int i = 0; i < 32; ++i) {
|
|
lua_pushboolean(L, (stick->buttons >> i) & 1);
|
|
lua_rawseti(L, -2, i + 1);
|
|
}
|
|
lua_setfield(L, -2, "buttons");
|
|
lua_pushlstring(L, stick->name.data(), stick->name.size());
|
|
lua_setfield(L, -2, "name");
|
|
return 1;
|
|
}
|
|
static int input_capture(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.capture");
|
|
return result(L, game(plugin).capture_input(plugin.id(), lua_toboolean(L, 1) != 0));
|
|
}
|
|
// input.hide({left_trigger = true, right_trigger = true, left_x = "grinding"}): those kept from the
|
|
// game while asked; nil or {} gives them back.
|
|
static int input_hide(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.hide");
|
|
Game::PadHidden hidden;
|
|
if (!lua_isnoneornil(L, 1)) {
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
const auto flag = [&](const char* name) {
|
|
lua_getfield(L, 1, name);
|
|
const bool on = lua_toboolean(L, -1) != 0;
|
|
lua_pop(L, 1);
|
|
return on;
|
|
};
|
|
hidden.left_trigger = flag("left_trigger");
|
|
hidden.right_trigger = flag("right_trigger");
|
|
hidden.right_stick = flag("right_stick");
|
|
hidden.left_stick_click = flag("left_stick_click");
|
|
hidden.right_stick_click = flag("right_stick_click");
|
|
lua_getfield(L, 1, "left_x");
|
|
if (!lua_isnil(L, -1)) {
|
|
if (lua_type(L, -1) != LUA_TSTRING || std::string_view(lua_tostring(L, -1)) != "grinding")
|
|
luaL_error(L, "left_x is hidden only while grinding: left_x = \"grinding\"");
|
|
hidden.left_x_grinding = true;
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
return result(L, game(plugin).hide_pad(plugin.id(), hidden));
|
|
}
|
|
// screen.look({bend, fov, bars, hood, grade, soft, scanlines, grain, age, dropouts, head_switching,
|
|
// tracking, smear, iris, rolling, text}): a camcorder look over the game's picture for 0.3 s, so
|
|
// every tick; nil ends it. One plugin at a time.
|
|
static int screen_look(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "overlay", "reskate.screen.look");
|
|
if (lua_isnoneornil(L, 1)) return result(L, game(plugin).screen_look(plugin.id(), std::nullopt));
|
|
luaL_checktype(L, 1, LUA_TTABLE);
|
|
Game::ScreenLook look;
|
|
const auto flag = [&](const char* name) {
|
|
lua_getfield(L, 1, name);
|
|
const bool on = lua_toboolean(L, -1) != 0;
|
|
lua_pop(L, 1);
|
|
return on;
|
|
};
|
|
const auto number = [&](const char* name, double fallback, double low, double high, const char* why) {
|
|
lua_getfield(L, 1, name);
|
|
double v = fallback;
|
|
if (!lua_isnil(L, -1)) {
|
|
v = luaL_checknumber(L, -1);
|
|
if (!(v >= low && v <= high)) luaL_error(L, "%s", why);
|
|
}
|
|
lua_pop(L, 1);
|
|
return v;
|
|
};
|
|
look.bars = flag("bars"), look.hood = flag("hood"), look.grade = flag("grade"), look.soft = flag("soft");
|
|
look.scanlines = flag("scanlines"), look.grain = flag("grain"), look.dropouts = flag("dropouts");
|
|
look.head_switching = flag("head_switching"), look.tracking = flag("tracking"), look.smear = flag("smear"), look.iris = flag("iris");
|
|
look.bend = static_cast<float>(number("bend", 0, 0, 1, "a look's bend is 0 to 1"));
|
|
look.fov = static_cast<float>(number("fov", 60, 5, 170, "a look's fov is 5 to 170 degrees"));
|
|
look.age = static_cast<float>(number("age", 0, 0, 1, "a look's tape age is 0 to 1"));
|
|
look.rolling = number("rolling", -1, 0, 1e7, "a look's rolling is the seconds since the tape started, 0 or more");
|
|
lua_getfield(L, 1, "text");
|
|
if (!lua_isnil(L, -1)) {
|
|
luaL_checktype(L, -1, LUA_TTABLE);
|
|
if (lua_objlen(L, -1) > 5) luaL_error(L, "a look's text is at most 5 lines");
|
|
for (int line = 0; line < 5; ++line) {
|
|
lua_rawgeti(L, -1, line + 1);
|
|
if (!lua_isnil(L, -1)) {
|
|
std::string text = luaL_checkstring(L, -1);
|
|
if (text.size() > 12) luaL_error(L, "a look's text line is at most 12 characters");
|
|
for (auto& c : text) {
|
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
if (c < 0x20 || c > 0x5f) luaL_error(L, "a look's text is letters, digits, spaces and !\"#$%%&'()*+,-./:;<=>?@[\\]^_");
|
|
}
|
|
look.text[static_cast<std::size_t>(line)] = std::move(text);
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
}
|
|
lua_pop(L, 1);
|
|
return result(L, game(plugin).screen_look(plugin.id(), look));
|
|
}
|
|
static int screen_slap(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "overlay", "reskate.screen.slap");
|
|
return result(L, game(plugin).screen_slap(plugin.id()));
|
|
}
|
|
// ---- ui.screen ---------------------------------------------------------------------------------------
|
|
// screen(title, build): a full-screen screen, drawn while open() has it open; Esc closes it.
|
|
static int screen(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "ui.screen", "reskate.ui.screen");
|
|
std::size_t length = 0;
|
|
const char* text = luaL_checklstring(L, 1, &length);
|
|
const std::string title(text, length);
|
|
luaL_checktype(L, 2, LUA_TFUNCTION);
|
|
if (title.empty() || title.size() > 40) luaL_argerror(L, 1, "a screen's title is 1 to 40 characters");
|
|
if (plugin.screens_.size() >= 4) luaL_error(L, "at most 4 screens");
|
|
for (const auto& entry : plugin.screens_)
|
|
if (entry.title == title) luaL_error(L, "the screen \"%s\" is there already", title.c_str());
|
|
// {dock = "left" | "right", width = 0.2 to 0.6}: beside the game instead of over it.
|
|
int dock = 0;
|
|
float width = 0.3f;
|
|
if (!lua_isnoneornil(L, 3)) {
|
|
luaL_checktype(L, 3, LUA_TTABLE);
|
|
lua_getfield(L, 3, "dock");
|
|
if (!lua_isnil(L, -1)) {
|
|
const std::string side = luaL_checkstring(L, -1);
|
|
if (side == "left") dock = 1;
|
|
else if (side == "right") dock = 2;
|
|
else luaL_error(L, "a screen docks \"left\" or \"right\"");
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 3, "panel");
|
|
if (!lua_isnil(L, -1) && !lua_toboolean(L, -1)) {
|
|
if (dock) luaL_error(L, "a screen without a panel has no dock");
|
|
dock = 3;
|
|
}
|
|
lua_pop(L, 1);
|
|
lua_getfield(L, 3, "width");
|
|
if (!lua_isnil(L, -1)) {
|
|
const double w = luaL_checknumber(L, -1);
|
|
if (!(w >= 0.2 && w <= 0.6)) luaL_error(L, "a docked screen's width is 0.2 to 0.6 of the window");
|
|
width = static_cast<float>(w);
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
lua_pushvalue(L, 2);
|
|
plugin.screens_.push_back({title, lua_ref(L, -1), dock, width});
|
|
lua_pop(L, 1);
|
|
return 0;
|
|
}
|
|
// input.text(capture): what was typed since the last call while this plugin's screen is open; with
|
|
// capture, the typing stays the plugin's (and Esc with it) for 300 ms more.
|
|
static int input_text(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.text");
|
|
const bool capture = lua_isnone(L, 1) || lua_toboolean(L, 1) != 0;
|
|
const auto typed = plugin.open_screen_.empty() ? game(plugin).typed_text(false) : game(plugin).typed_text(capture);
|
|
if (plugin.open_screen_.empty()) {
|
|
lua_pushliteral(L, "");
|
|
return 1;
|
|
}
|
|
lua_pushlstring(L, typed.data(), typed.size());
|
|
return 1;
|
|
}
|
|
// room.open(), leave(), state(): the game menu's character room, the local skater shown in it.
|
|
static int room_open(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "room", "reskate.room.open");
|
|
return result(L, game(plugin).room_open(plugin.id()));
|
|
}
|
|
static int room_leave(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "room", "reskate.room.leave");
|
|
game(plugin).room_leave(plugin.id());
|
|
return 0;
|
|
}
|
|
static int room_state(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "room", "reskate.room.state");
|
|
const auto s = game(plugin).room_state(plugin.id());
|
|
lua_pushlstring(L, s.state.data(), s.state.size());
|
|
if (s.why.empty()) return 1;
|
|
lua_pushlstring(L, s.why.data(), s.why.size());
|
|
return 2;
|
|
}
|
|
// input.cursor(): the mouse over the game window while this plugin's screen is open, or nil.
|
|
static int input_cursor(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "input", "reskate.input.cursor");
|
|
const auto c = plugin.open_screen_.empty() ? std::nullopt : game(plugin).cursor();
|
|
if (!c) {
|
|
lua_pushnil(L);
|
|
return 1;
|
|
}
|
|
lua_createtable(L, 0, 7);
|
|
lua_pushvector(L, c->x, c->y, 0);
|
|
lua_setfield(L, -2, "position");
|
|
lua_pushboolean(L, c->left);
|
|
lua_setfield(L, -2, "left");
|
|
lua_pushboolean(L, c->right);
|
|
lua_setfield(L, -2, "right");
|
|
lua_pushboolean(L, c->middle);
|
|
lua_setfield(L, -2, "middle");
|
|
lua_pushnumber(L, c->wheel);
|
|
lua_setfield(L, -2, "wheel");
|
|
lua_pushboolean(L, c->over_screen);
|
|
lua_setfield(L, -2, "over_screen");
|
|
return 1;
|
|
}
|
|
static int open(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "ui.screen", "reskate.ui.open");
|
|
const std::string title = luaL_checkstring(L, 1);
|
|
if (std::none_of(plugin.screens_.begin(), plugin.screens_.end(), [&](const auto& e) { return e.title == title; }))
|
|
luaL_error(L, "there is no screen \"%s\"", title.c_str());
|
|
plugin.open_screen_ = title;
|
|
return 0;
|
|
}
|
|
static int close(lua_State* L) {
|
|
auto& plugin = plugin_of(L);
|
|
need(L, plugin, "ui.screen", "reskate.ui.close");
|
|
plugin.open_screen_.clear();
|
|
return 0;
|
|
}
|
|
|
|
static void install(Plugin& plugin, lua_State* L);
|
|
};
|
|
|
|
void install_world_api(Plugin& plugin, lua_State* L) { WorldApi::install(plugin, L); }
|
|
|
|
void WorldApi::install(Plugin& plugin, lua_State* L) {
|
|
(void)plugin;
|
|
// The metatables of player tables (push_player) and of permissions; their own stay hidden.
|
|
lua_createtable(L, 0, 2);
|
|
lua_pushcfunction(L, player_index, "player");
|
|
lua_setfield(L, -2, "__index");
|
|
lua_pushstring(L, "locked");
|
|
lua_setfield(L, -2, "__metatable");
|
|
lua_setfield(L, LUA_REGISTRYINDEX, "reskate.player");
|
|
lua_createtable(L, 0, 2);
|
|
lua_createtable(L, 0, 1);
|
|
lua_pushcfunction(L, permissions_clone, "clone");
|
|
lua_setfield(L, -2, "clone");
|
|
lua_setreadonly(L, -1, true);
|
|
lua_setfield(L, -2, "__index");
|
|
lua_pushstring(L, "locked");
|
|
lua_setfield(L, -2, "__metatable");
|
|
lua_setfield(L, LUA_REGISTRYINDEX, "reskate.permissions");
|
|
add_functions(L, "skater", {{"state", state}, {"noclip", noclip}, {"boost", boost}, {"freeze", freeze}, {"wipeout", wipeout}, {"xray", xray},
|
|
{"grind_exit", grind_exit}, {"reach", reach}, {"hand", hand}, {"joint", skater_joint}, {"pose", skater_pose}});
|
|
add_functions(L, "board", {{"motion", board_motion}, {"bones", board_bones}, {"set", board_set}, {"hide", board_hide}, {"clear", board_clear},
|
|
{"park", board_park}, {"fit", board_fit}, {"toss", board_toss}});
|
|
add_functions(L, "camera", {{"look", camera_look}, {"set", camera_set}, {"follow", camera_follow}, {"release", camera_release}, {"held", camera_held}, {"get", camera_get}});
|
|
add_functions(L, "trainer", {{"set_point", set_point}, {"back", back}});
|
|
add_functions(L, "hud", {{"hide", hide_hud}, {"corner", hud_corner}});
|
|
add_functions(L, "time", {{"slow", slow}, {"speed", speed}});
|
|
add_functions(L, "objects", {{"catalog", catalog}, {"list", objects}, {"place", place}, {"move", move}, {"remove", remove}});
|
|
add_functions(L, "entities", {{"set", entities_set}, {"remove", entities_remove}, {"clear", entities_clear}, {"status", entities_status},
|
|
{"ready", entities_ready}});
|
|
add_functions(L, "world", {{"set_time", set_time}, {"set_control", set_control}, {"set_park", set_park}});
|
|
add_functions(L, "travel", {{"locations", locations}, {"levels", levels}, {"go", go}, {"load", load}});
|
|
add_functions(L, "ai", {{"spawn", ai_spawn}, {"clear", ai_clear}, {"count", ai_count}});
|
|
add_functions(L, "session", {{"skaters", skaters}, {"tag", tag}, {"joint", session_joint}});
|
|
add_functions(L, "voice", {{"set_volume", voice_volume}});
|
|
add_functions(L, "chat", {{"send", chat_send}, {"notice", chat_local}});
|
|
add_functions(L, "animation", {{"play", animation_play}, {"stop", animation_stop}, {"playing", animation_playing}, {"replace", animation_replace},
|
|
{"record", animation_record}, {"recorded", animation_recorded}, {"forget", animation_forget}, {"trick", animation_trick},
|
|
{"seek", animation_seek}, {"pause", animation_pause}, {"time", animation_time}, {"info", animation_info},
|
|
{"sample", animation_sample}, {"joints", animation_joints}, {"trim", animation_trim}, {"edit", animation_edit},
|
|
{"save", animation_save}});
|
|
add_functions(L, "outfit", {{"set", outfit_set}, {"clear", outfit_clear}, {"find", outfit_find}, {"slots", outfit_slots}, {"status", outfit_status}});
|
|
add_functions(L, "audio", {{"play", audio_play}, {"set", audio_set}, {"stop", audio_stop}, {"playing", audio_playing}});
|
|
add_functions(L, "server", {{"kick", server_kick}, {"say", server_say}, {"tell", server_tell}, {"move", server_move},
|
|
{"ban", server_ban}, {"unban", server_unban}});
|
|
add_functions(L, "throwdowns", {{"state", throwdown_state}, {"host", throwdown_host}, {"cancel", throwdown_cancel}, {"hosting", throwdown_hosting},
|
|
{"mode", throwdown_mode}, {"remove_mode", throwdown_remove_mode}});
|
|
add_functions(L, "music", {{"current", current}, {"play", play}, {"pause", pause}, {"next", next}, {"previous", previous},
|
|
{"game", game_current}, {"game_next", game_next}});
|
|
add_functions(L, "progression", {{"get", progress}});
|
|
add_functions(L, "settings", {{"game", setting}, {"names", setting_names}, {"set", setting_set}});
|
|
add_functions(L, "input", {{"bind", bind}, {"flight", input_flight}, {"gamepad", input_gamepad}, {"joystick", input_joystick}, {"capture", input_capture},
|
|
{"hide", input_hide}, {"cursor", input_cursor}, {"text", input_text}});
|
|
add_functions(L, "room", {{"open", room_open}, {"leave", room_leave}, {"state", room_state}});
|
|
add_functions(L, "ui", {{"screen", screen}, {"open", open}, {"close", close}});
|
|
add_functions(L, "screen", {{"look", screen_look}, {"slap", screen_slap}});
|
|
}
|
|
|
|
std::uint32_t combo_of(std::string_view name) {
|
|
constexpr std::array<std::pair<std::string_view, std::uint32_t>, 16> pad{{
|
|
{"up", 0x1}, {"down", 0x2}, {"left", 0x4}, {"right", 0x8}, {"start", 0x10}, {"back", 0x20}, {"l3", 0x40}, {"r3", 0x80},
|
|
{"lb", 0x100}, {"rb", 0x200}, {"a", 0x1000}, {"b", 0x2000}, {"x", 0x4000}, {"y", 0x8000}, {"lt", 0x10000}, {"rt", 0x20000},
|
|
}};
|
|
std::array<std::uint64_t, 4> keys{};
|
|
std::uint32_t buttons = 0;
|
|
bool keyboard = false, controller = false;
|
|
std::size_t start = 0;
|
|
while (start <= name.size()) {
|
|
const auto end = std::min(name.find('+', start), name.size());
|
|
std::string part(name.substr(start, end - start));
|
|
for (auto& c : part) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + 32);
|
|
start = end + 1;
|
|
if (part.starts_with("pad.")) {
|
|
const auto button = std::string_view(part).substr(4);
|
|
const auto found = std::find_if(pad.begin(), pad.end(), [&](const auto& p) { return p.first == button; });
|
|
if (found == pad.end()) return 0;
|
|
buttons |= found->second;
|
|
controller = true;
|
|
continue;
|
|
}
|
|
unsigned key = 0;
|
|
if (part == "ctrl") key = 0x11;
|
|
else if (part == "shift") key = 0x10;
|
|
else if (part == "alt") key = 0x12;
|
|
else if (part == "space") key = 0x20;
|
|
else if (part.size() == 1 && ((part[0] >= 'a' && part[0] <= 'z') || (part[0] >= '0' && part[0] <= '9')))
|
|
key = part[0] >= 'a' ? static_cast<unsigned>(part[0] - 32) : static_cast<unsigned>(part[0]);
|
|
else if (part.size() >= 2 && part[0] == 'f') {
|
|
unsigned n = 0;
|
|
if (std::from_chars(part.data() + 1, part.data() + part.size(), n).ec != std::errc{} || n < 1 || n > 12) return 0;
|
|
key = 0x70 + n - 1;
|
|
} else return 0;
|
|
keys[key / 64] |= std::uint64_t{1} << (key % 64);
|
|
keyboard = true;
|
|
}
|
|
if (keyboard == controller) return 0; // a key or a pad combo, not both, and not nothing
|
|
if (controller) return buttons;
|
|
std::uint32_t value = 0x40000000u; // as controller_bindings.h's keyboard_binding_from_keys
|
|
unsigned count = 0;
|
|
const auto id_of = [](unsigned key) -> unsigned {
|
|
if (key >= '0' && key <= '9') return key - '0' + 1;
|
|
if (key >= 'A' && key <= 'Z') return key - 'A' + 11;
|
|
if (key == 0x20) return 37;
|
|
if (key >= 0x70 && key <= 0x7b) return key - 0x70 + 38;
|
|
if (key == 0x11) return 50;
|
|
if (key == 0x10) return 51;
|
|
if (key == 0x12) return 52;
|
|
return 0;
|
|
};
|
|
unsigned single = 0;
|
|
for (unsigned key = 0; key < 256; ++key) {
|
|
if (!(keys[key / 64] & (std::uint64_t{1} << (key % 64)))) continue;
|
|
if (count == 5) return 0;
|
|
single = key;
|
|
++count;
|
|
}
|
|
if (count == 1) return 0x80000000u | single;
|
|
// Chord ids go in ascending order.
|
|
std::vector<unsigned> ids;
|
|
for (unsigned key = 0; key < 256; ++key)
|
|
if (keys[key / 64] & (std::uint64_t{1} << (key % 64))) ids.push_back(id_of(key));
|
|
std::sort(ids.begin(), ids.end());
|
|
for (std::size_t i = 0; i < ids.size(); ++i) value |= ids[i] << (i * 6);
|
|
return value;
|
|
}
|
|
}
|