Plugins: what the Filmer needs as a plugin: carry, who sees a pose, a step in the camera cycle

PR 343's Filmer becomes a Luau plugin mod (kept in contrib/plugins/filmer) on general APIs instead of
its own C++:

- reskate.skater.carry: something held out in one or both hands and aimed, the body crouched (feet
  kept) and hunched over it, turned on the spot after it (Extension/Skater/carry.cpp, from the PR's
  filmer_hold.cpp, with tests)
- shown = "everyone" | "others" | "me" on skater.pose, reach and carry: the pose sent to the other
  players and the one drawn here are split when asked
- reskate.camera.cycle and the camera_cycle event: a plugin's camera as the last step of the camera
  height button's cycle, after first person
- reskate.session.nametags(false) holds the names over the other players off
- board.hide(bone, {others = true}) hides it in the board the others are sent too (a sent-pose filter)
- outfit.set takes hand wear slots; input.bind takes an action with no key of its own
This commit is contained in:
AyeZeeBB committed 2026-10-22 18:46:49 -05:00
1 parent 8ede361755
commit 118c489fdb
25 files changed
+1449 -89

No files matched your search

+146 -14
View File
@@ -225,6 +225,20 @@ struct WorldApi {
need(L, plugin, "camera", "reskate.camera.held");
return result(L, game(plugin).camera_status(plugin.id()));
}
// camera.cycle(state): the plugin's camera as a step of the camera height button's cycle: nil (out
// of it), "ready" (in it, off) or "active" (in it, on). For 300 ms, so every tick.
static int camera_cycle(lua_State* L) {
auto& plugin = plugin_of(L);
need(L, plugin, "camera", "reskate.camera.cycle");
auto state = Game::CycleCamera::out;
if (!lua_isnoneornil(L, 1)) {
const std::string_view s = luaL_checkstring(L, 1);
if (s == "ready") state = Game::CycleCamera::ready;
else if (s == "active") state = Game::CycleCamera::active;
else luaL_argerror(L, 1, "a camera's place in the cycle is nil, \"ready\" or \"active\"");
}
return result(L, game(plugin).camera_cycle(plugin.id(), state));
}
static int camera_get(lua_State* L) {
auto& plugin = plugin_of(L);
need(L, plugin, "camera", "reskate.camera.get");
@@ -727,6 +741,15 @@ struct WorldApi {
game(plugin).set_nametag(plugin.id(), player, std::move(text), colour_at(L, 3, 0xffffffff));
return 0;
}
// session.nametags(shown): false holds the names over the other players off (until true, or the
// plugin stops); chat bubbles still show.
static int nametags(lua_State* L) {
auto& plugin = plugin_of(L);
need(L, plugin, "session.nametags", "reskate.session.nametags");
luaL_checktype(L, 1, LUA_TBOOLEAN);
game(plugin).hide_nametags(plugin.id(), !lua_toboolean(L, 1));
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;
@@ -1223,9 +1246,100 @@ struct WorldApi {
}
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.
// A `shown` field: who sees a pose, a carry or a reach ("everyone", "others" or "me").
static Game::Shown shown_field(lua_State* L, int table, const char* what) {
lua_getfield(L, table, "shown");
Game::Shown shown = Game::Shown::everyone;
if (!lua_isnil(L, -1)) {
const std::string_view s = luaL_checkstring(L, -1);
if (s == "others") shown = Game::Shown::others;
else if (s == "me") shown = Game::Shown::me;
else if (s != "everyone") luaL_error(L, "%s's shown is \"everyone\", \"others\" or \"me\"", what);
}
lua_pop(L, 1);
return shown;
}
// A number field from 0 or `low` to `high`, `fallback` when not given.
static float number_field(lua_State* L, int table, const char* key, float fallback, double low, double high, const char* message) {
lua_getfield(L, table, key);
float value = fallback;
if (!lua_isnil(L, -1)) {
const double v = luaL_checknumber(L, -1);
if (!(v >= low && v <= high)) luaL_error(L, "%s", message);
value = static_cast<float>(v);
}
lua_pop(L, 1);
return value;
}
// skater.carry({at = vector, rotation = {x, y, z, w}, hand, both, front, grip, weight, crouch, hunch,
// still, turn, step, step_phase, push_straightens, shown}): something held out in the hands and
// aimed, its front at `at`; for 0.3 s, so every tick. nil lets it go.
static int carry(lua_State* L) {
auto& plugin = plugin_of(L);
need(L, plugin, "skater.animation", "reskate.skater.carry");
if (lua_isnoneornil(L, 1)) return result(L, game(plugin).skater_carry(plugin.id(), std::nullopt));
luaL_checktype(L, 1, LUA_TTABLE);
Game::Carry 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 carry's hand is \"right\" or \"left\"");
goal.left = hand == "left";
}
lua_pop(L, 1);
lua_getfield(L, 1, "both");
goal.both = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, 1, "at");
goal.front = position_of(L, lua_gettop(L), "the carry's point");
lua_pop(L, 1);
lua_getfield(L, 1, "rotation");
if (lua_isnil(L, -1)) luaL_error(L, "a carry needs a rotation {x, y, z, w}: the way the thing held points");
goal.aim = quaternion_at(L, lua_gettop(L));
lua_pop(L, 1);
lua_getfield(L, 1, "front");
if (!lua_isnil(L, -1)) {
goal.front_from_grip = position_of(L, lua_gettop(L), "a carry's front");
for (const auto v : goal.front_from_grip)
if (std::abs(v) > 1) luaL_error(L, "a carry's front is at most 1 m from its grip on each axis");
}
lua_pop(L, 1);
lua_getfield(L, 1, "grip");
if (!lua_isnil(L, -1)) {
luaL_checktype(L, -1, LUA_TTABLE);
const int grip = lua_gettop(L);
std::pair<std::array<float, 4>, Vec3> worn{{0, 0, 0, 1}, {}};
lua_getfield(L, grip, "rotation");
if (!lua_isnil(L, -1)) worn.first = quaternion_at(L, lua_gettop(L));
lua_pop(L, 1);
lua_getfield(L, grip, "offset");
if (!lua_isnil(L, -1)) {
worn.second = position_of(L, lua_gettop(L), "a grip's offset");
for (const auto v : worn.second)
if (std::abs(v) > 0.5f) luaL_error(L, "a grip's offset is at most 0.5 m from the hand on each axis");
}
lua_pop(L, 1);
goal.worn = worn;
}
lua_pop(L, 1);
goal.weight = number_field(L, 1, "weight", 1, 0, 1, "a carry's weight is 0 to 1");
goal.crouch = number_field(L, 1, "crouch", 0, 0, 0.6, "a carry's crouch is 0 to 0.6 m");
goal.hunch = number_field(L, 1, "hunch", 0, 0, 1.2, "a carry's hunch is 0 to 1.2 radians");
goal.turn = number_field(L, 1, "turn", 0, -3.2, 3.2, "a carry's turn is -pi to pi radians");
goal.step = number_field(L, 1, "step", 0, 0, 1, "a carry's step is 0 to 1");
goal.step_phase = number_field(L, 1, "step_phase", 0, -1e6, 1e6, "a carry's step_phase is a finite number");
lua_getfield(L, 1, "still");
goal.still = lua_toboolean(L, -1);
lua_pop(L, 1);
lua_getfield(L, 1, "push_straightens");
goal.push_straightens = lua_toboolean(L, -1);
lua_pop(L, 1);
goal.shown = shown_field(L, 1, "a carry");
return result(L, game(plugin).skater_carry(plugin.id(), goal));
}
// skater.reach({hand = "right"|"left", at = vector, rotation = {x, y, z, w}?, weight = 0..1, shown}):
// the hand joint taken to `at` in the world (turned to `rotation`), in the pose drawn and sent (or
// one of them); 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");
@@ -1252,6 +1366,7 @@ struct WorldApi {
goal.weight = static_cast<float>(w);
}
lua_pop(L, 1);
goal.shown = shown_field(L, 1, "a reach");
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.
@@ -1334,7 +1449,13 @@ struct WorldApi {
}
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)));
// options = {shown = "everyone" | "others" | "me"}
auto shown = Game::Shown::everyone;
if (!lua_isnoneornil(L, 3)) {
luaL_checktype(L, 3, LUA_TTABLE);
shown = shown_field(L, 3, "a pose");
}
return result(L, game(plugin).skater_pose(plugin.id(), offsets, static_cast<float>(weight), shown));
}
// skater.joint(joint): {position, rotation, scale} of one of the local skater's joints, or nil.
static int skater_joint(lua_State* L) {
@@ -1540,7 +1661,15 @@ struct WorldApi {
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)));
// options = {others = true}: hidden from the other players too
bool everyone = false;
if (!lua_isnoneornil(L, 2)) {
luaL_checktype(L, 2, LUA_TTABLE);
lua_getfield(L, 2, "others");
everyone = lua_toboolean(L, -1);
lua_pop(L, 1);
}
return result(L, game(plugin).board_hide(plugin.id(), bone_number(L, 1), everyone));
}
static int board_clear(lua_State* L) {
auto& plugin = plugin_of(L);
@@ -1596,11 +1725,13 @@ struct WorldApi {
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).
// Only the face and skin layers, eyewear and hand wear (Extension/Skater/outfit_overlay.h): the
// game has no menu for hand wear, so a camera or a glove a plugin puts in the hand goes there.
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");
if (lowered.find("tattoo") == std::string::npos && lowered.find("moles") == std::string::npos && lowered.find("eyewear") == std::string::npos &&
lowered.find("hand") == std::string::npos)
return result(L, "only tattoo, moles, eyewear and hand wear slots");
std::vector<float> placement;
bool covers{};
if (!lua_isnoneornil(L, 3)) {
@@ -2179,10 +2310,11 @@ struct WorldApi {
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());
// nil: no key of its own; the player can put it on one in the Binds page.
const auto key = lua_isnoneornil(L, 2) ? std::string{} : checked_text(L, 2, 60, "a key");
const auto combo = key.empty() ? 0u : combo_of(key);
if (!key.empty() && !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);
@@ -2500,10 +2632,10 @@ void WorldApi::install(Plugin& plugin, lua_State* L) {
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},
{"shape_launch", shape_launch}, {"hide", hide_skater}, {"reach", reach}, {"hand", hand}, {"joint", skater_joint}, {"pose", skater_pose}});
{"shape_launch", shape_launch}, {"hide", hide_skater}, {"reach", reach}, {"carry", carry}, {"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, "camera", {{"look", camera_look}, {"set", camera_set}, {"follow", camera_follow}, {"release", camera_release}, {"held", camera_held}, {"get", camera_get}, {"cycle", camera_cycle}});
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}});
@@ -2514,7 +2646,7 @@ void WorldApi::install(Plugin& plugin, lua_State* L) {
add_functions(L, "world", {{"set_time", set_time}, {"set_control", set_control}, {"set_park", set_park}, {"pick", world_pick}});
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}, {"touch", session_touch},
add_functions(L, "session", {{"skaters", skaters}, {"tag", tag}, {"nametags", nametags}, {"joint", session_joint}, {"touch", session_touch},
{"touching", session_touching}, {"hide", session_hide}, {"hidden", session_hidden},
{"raycast", session_raycast}});
add_functions(L, "voice", {{"set_volume", voice_volume}});