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

+162 -3
View File
@@ -45,6 +45,13 @@ struct FakeGame final : Game {
return {};
}
void outfit_clear(std::string_view plugin, std::string_view slot) override { calls.push_back(std::format("outfit clear {} {}", plugin, slot)); }
std::string camera_cycle(std::string_view plugin, CycleCamera place) override {
calls.push_back(std::format("cycle {} {}", plugin, place == CycleCamera::out ? "out" : place == CycleCamera::ready ? "ready" : "active"));
return {};
}
std::vector<CycleNews> cycle_news;
std::vector<CycleNews> take_camera_cycle() override { return std::exchange(cycle_news, {}); }
void hide_nametags(std::string_view plugin, bool hidden) override { calls.push_back(std::format("nametags {} {}", plugin, hidden ? "off" : "on")); }
std::set<std::string, std::less<>> installed; // items found by their whole ending, as Pack/<ending>
std::string outfit_find(std::string_view ending) override {
if (installed.contains(ending)) return "Pack/" + std::string(ending);
@@ -60,7 +67,10 @@ struct FakeGame final : Game {
calls.push_back(std::format("board {} {} {:.1f} {}", plugin, bone, value.position[1], world ? "world" : "local"));
return {};
}
std::string board_hide(std::string_view plugin, int bone) override { calls.push_back(std::format("board hide {} {}", plugin, bone)); return {}; }
std::string board_hide(std::string_view plugin, int bone, bool everyone) override {
calls.push_back(std::format("board hide {} {}{}", plugin, bone, everyone ? " everyone" : ""));
return {};
}
void board_park(std::string_view plugin, std::vector<int> bones) override { calls.push_back(std::format("board park {} {}", plugin, bones.size())); }
// The general services a game layer in Lua reads and drives (plugin_api_services.cpp).
std::optional<SkaterTelemetry> telemetry;
@@ -150,8 +160,18 @@ struct FakeGame final : Game {
return {};
}
std::optional<MouseMotion> mouse_motion(std::string_view) override { return MouseMotion{3, -2, 1}; }
std::string skater_pose(std::string_view plugin, const std::vector<JointOffset>& offsets, float weight) override {
static const char* shown_name(Shown shown) { return shown == Shown::others ? "others" : shown == Shown::me ? "me" : "everyone"; }
std::string skater_carry(std::string_view plugin, std::optional<Carry> c) override {
calls.push_back(c ? std::format("carry {} {}{} {:.1f} {:.1f} {:.1f} front {:.2f} grip {} w {:.2f} crouch {:.2f} hunch {:.2f} turn {:.2f} step {:.2f}{}{} {}",
plugin, c->left ? "left" : "right", c->both ? " both" : "", c->front[0], c->front[1], c->front[2],
c->front_from_grip[2], c->worn.has_value(), c->weight, c->crouch, c->hunch, c->turn, c->step,
c->still ? " still" : "", c->push_straightens ? " push" : "", shown_name(c->shown))
: std::format("carry {} none", plugin));
return {};
}
std::string skater_pose(std::string_view plugin, const std::vector<JointOffset>& offsets, float weight, Shown shown) override {
std::string text = std::format("pose {} {:.2f}", plugin, weight);
if (shown != Shown::everyone) text += std::format(" {}", shown_name(shown));
auto sorted = offsets;
std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { return a.joint < b.joint; });
for (const auto& o : sorted) text += std::format(" {}:{:.2f},{:.2f},{:.2f},{:.2f}x{:.2f}", o.joint, o.rotation[0], o.rotation[1], o.rotation[2], o.rotation[3], o.scale);
@@ -181,7 +201,8 @@ struct FakeGame final : Game {
}
std::string skater_reach(std::string_view plugin, std::optional<Reach> r) override {
calls.push_back(r ? std::format("reach {} {} {:.1f} {:.1f} {:.1f} {} {:.2f}", plugin, r->left ? "left" : "right", r->at[0], r->at[1], r->at[2],
r->rotation.has_value(), r->weight)
r->rotation.has_value(), r->weight) +
(r->shown != Shown::everyone ? std::format(" {}", shown_name(r->shown)) : std::string{})
: std::format("reach {} none", plugin));
return {};
}
@@ -1357,6 +1378,71 @@ void plugins_reach_cruise_and_read_the_mouse() {
"skater.animation, skater.read, skater.control, input and overlay are needed\n" + environment.all());
}
// What the Filmer's port needs besides: something carried (seen by the others only), a pose only you
// see, a step in the camera height cycle and its event, the nametags held off, the board hidden from
// everyone, and a camcorder in the hand wear slot.
void plugins_carry_and_take_a_step_in_the_camera_cycle() {
TestEnvironment environment;
auto& game = environment.fake;
Host host(environment, Side::client);
const std::string main = R"(
local steps = {}
reskate.on("camera_cycle", function(step) table.insert(steps, step) end)
assert(reskate.skater.carry({at = vector.create(1, 2, 3), rotation = {0, 1, 0, 0}, both = true, front = vector.create(0, -0.143, -0.248),
grip = {rotation = {0.5, -0.5, 0.5, 0.5}, offset = vector.create(0.12, 0.04, -0.03)}, crouch = 0.22, hunch = 1.0, turn = 0.5,
step = 0.5, step_phase = 1.25, still = true, push_straightens = true, weight = 0.75, shown = "others"}))
assert(reskate.skater.carry({hand = "left", at = vector.create(0, 1, 0), rotation = {0, 0, 0, 1}}))
assert(reskate.skater.carry(nil))
assert(not pcall(reskate.skater.carry, {at = vector.create(0, 1, 0)}), "a carry needs a rotation")
assert(not pcall(reskate.skater.carry, {at = vector.create(0, 1, 0), rotation = {0, 0, 0, 1}, crouch = 1}), "a crouch is 0 to 0.6 m")
assert(not pcall(reskate.skater.carry, {at = vector.create(0, 1, 0), rotation = {0, 0, 0, 1}, shown = "them"}), "shown is everyone, others or me")
assert(reskate.skater.pose({[48] = {scale = 0}}, 1, {shown = "me"}))
assert(reskate.skater.reach({at = vector.create(1, 2, 3), shown = "others"}))
assert(reskate.camera.cycle("ready") and reskate.camera.cycle("active") and reskate.camera.cycle(nil))
assert(not pcall(reskate.camera.cycle, "on"), "ready or active")
reskate.session.nametags(false)
reskate.session.nametags(true)
assert(reskate.board.hide(2, {others = true}))
assert(reskate.outfit.set("handwear_l", "Own_HandwearGlove_Gen_GloveFingerless_00001"))
local ok, why = reskate.outfit.set("shoes", "Own_Shoes_00001")
assert(not ok and why == "only tattoo, moles, eyewear and hand wear slots", why)
assert(reskate.input.bind("toggle") and reskate.input.bind("slap", "Pad.LB"), "an action with no key of its own, and one with")
reskate.on("tick", function()
if #steps > 0 then reskate.log("steps " .. table.concat(steps, " ")) steps = {} end
end)
reskate.log("carried")
)";
host.apply({package("filmer", {{manifest_file, manifest(R"("skater.animation", "camera", "session.nametags", "board", "outfit", "input")")},
{"plugin/main.lua", main}})});
ticks(host, environment, 1);
running(host, "filmer", environment);
check(environment.logged("[filmer] carried"), "The calls work\n" + environment.all());
check(game.called("carry filmer right both 1.0 2.0 3.0 front -0.25 grip true w 0.75 crouch 0.22 hunch 1.00 turn 0.50 step 0.50 still push others") &&
game.called("carry filmer left 0.0 1.0 0.0 front 0.00 grip false w 1.00 crouch 0.00 hunch 0.00 turn 0.00 step 0.00 everyone") &&
game.called("carry filmer none"),
"Carried in both hands for the others, in one, and let go\n" + environment.all());
check(game.called("pose filmer 1.00 me 48:0.00,0.00,0.00,1.00x0.00") && game.called("reach filmer right 1.0 2.0 3.0 false 1.00 others"),
"A pose only you see, a reach only the others see\n" + environment.all());
check(game.called("cycle filmer ready") && game.called("cycle filmer active") && game.called("cycle filmer out"), "In the cycle, on, and out");
check(game.called("nametags filmer off") && game.called("nametags filmer on"), "The nametags held off and given back");
check(game.called("board hide filmer 2 everyone"), "The board hidden from everyone");
check(game.called("outfit filmer handwear_l Own_HandwearGlove_Gen_GloveFingerless_00001 0 false"), "A camcorder in the hand wear slot");
check(game.binds.contains("filmer/toggle") && game.binds["filmer/toggle"] == 0 && game.binds["filmer/slap"] == combo_of("Pad.LB"),
"An action the player puts on a key themselves");
// The button's steps reach the plugin in the cycle, and nobody else.
game.cycle_news = {{"filmer", true}, {"someone", false}, {"filmer", false}};
ticks(host, environment, 2);
check(environment.logged("[filmer] steps enter leave"), "The camera cycle's steps as events\n" + environment.all());
// Each needs its permission.
host.apply({package("sneaky", {{manifest_file, manifest()}, {"plugin/main.lua", R"(
reskate.log(tostring(pcall(reskate.skater.carry, {at = vector.create(0, 0, 0), rotation = {0, 0, 0, 1}})) .. " " ..
tostring(pcall(reskate.camera.cycle, "ready")) .. " " .. tostring(pcall(reskate.session.nametags, false)) .. " " ..
tostring(pcall(reskate.on, "camera_cycle", function() end)))
)"}})});
ticks(host, environment, 1);
check(environment.logged("[sneaky] false false false false"), "skater.animation, camera and session.nametags are needed\n" + environment.all());
}
// Reading a skater's joints, and the physics tuning read and set.
void plugins_read_joints_and_tune_physics() {
TestEnvironment environment;
@@ -3227,6 +3313,77 @@ void the_throwdowns_page_lists_the_stock_cards_and_plugin_modes() {
check(again.revision() > before && again.definitions().size() == 3 && again.definitions()[0].id == "native.skate", "S.K.A.T.E. is back when it goes");
}
// The Filmer (contrib/plugins/filmer): an AI filmer follows a skater riding a carving line, then you film
// someone, with your skater carrying the camcorder for the others; all within the 2 ms a tick a plugin has.
void filmer_follows_and_films() {
TestEnvironment environment;
auto& game = environment.fake;
const std::string plugin = "filmer";
Host host(environment, Side::client);
host.apply({contributed(plugin)});
running(host, plugin, environment);
SkaterTelemetry t;
t.skater = true;
t.physics_state = 100;
t.speed = 6;
game.telemetry = t;
environment.local_skater = SkaterState{{0, 0.9f, 0}, 0, 6, 0, false, 0};
host.set_level("sanvan");
const auto ride = [&](int count) {
for (int i = 0; i < count; ++i) {
auto& skater = *environment.local_skater;
static int step = 0;
skater.heading = 0.4f * std::sin(static_cast<float>(++step) * 0.012f);
skater.position[0] += std::sin(skater.heading) * 6.0f * 0.0167f;
skater.position[2] += std::cos(skater.heading) * 6.0f * 0.0167f;
game.telemetry->position = skater.position;
game.telemetry->heading = skater.heading * 57.29578f;
ticks(host, environment, 1);
}
};
check(host.run_console("filmer mode follow"), "filmer mode follow");
ride(900);
running(host, plugin, environment);
check(!environment.logged(".lua:"), "The Filmer follows without an error\n" + environment.all());
check(game.called("camera filmer"), "It shows its camera\n" + environment.all());
check(game.called("look filmer 1.00 100"), "with the VX look over the fisheye\n" + environment.all());
check(game.called("cycle filmer active"), "in the camera height cycle, on");
{
// Where the AI filmer's lens is: behind the skater (never in front), a filmer's distance off, low.
float x{}, y{}, z{};
bool found = false;
for (auto it = game.calls.rbegin(); it != game.calls.rend() && !found; ++it)
if (it->starts_with("camera filmer ")) found = static_cast<bool>(std::istringstream(it->substr(14)) >> x >> y >> z);
const auto& at = environment.local_skater->position;
const float heading = environment.local_skater->heading;
const float dx = x - at[0], dz = z - at[2];
const float away = std::sqrt(dx * dx + dz * dz), along = dx * std::sin(heading) + dz * std::cos(heading);
std::cout << "Filmer follow: the lens " << away << " m from the skater, " << along << " m along, " << y << " m up\n";
check(found && away > 0.8f && away < 6.0f && along < 0.0f && y > 0.0f && y < 1.5f,
"The AI filmer rides behind the skater, a filmer's distance off, low\n" + environment.all());
}
const auto follow_ms = status_of(host, plugin).tick_ms;
std::cout << "Filmer follow: " << follow_ms << " ms a tick (its budget is 2 ms)\n";
check(follow_ms < 2.0, "The Filmer follows within its 2 ms a tick");
// You film the skater ahead of you, holding the camcorder out for the others to see.
environment.lines.clear();
check(host.run_console("filmer mode film"), "filmer mode film");
ride(600);
running(host, plugin, environment);
check(!environment.logged(".lua:"), "You film without an error\n" + environment.all());
check(game.called("carry filmer right both") && game.called(" push others") && game.called("hide skater filmer on") &&
game.called("board hide filmer 2"),
"Your skater carries the camcorder for the others, and you, looking through it, do not see your skater or board\n" + environment.all());
check(game.called("hide filmer false false false stick true false true"), "The right stick and its click are the camera's");
const auto film_ms = status_of(host, plugin).tick_ms;
std::cout << "Filmer film: " << film_ms << " ms a tick\n";
check(film_ms < 2.0, "You film within the 2 ms a tick");
check(host.run_console("filmer slap") && environment.printed("Clip 1 marked"), "A slap marks a clip\n" + printed_lines(environment));
check(host.run_console("filmer mode off"), "filmer mode off");
ticks(host, environment, 30);
check(game.called("carry filmer none") && game.called("camera release filmer"), "Off: the camcorder put down and the camera given back\n" + environment.all());
}
void smokey_throwdowns_register_and_play_one_up() {
namespace kit = dingosdk::throwdowns::kit;
TestEnvironment environment;
@@ -3665,6 +3822,7 @@ int main() {
run(plugins_place_world_entities);
run(plugins_have_what_a_prop_hunt_needs);
run(plugins_reach_cruise_and_read_the_mouse);
run(plugins_carry_and_take_a_step_in_the_camera_cycle);
run(plugins_read_joints_and_tune_physics);
run(plugins_type_and_use_the_room);
run(plugins_touch_hide_and_ray_at_players);
@@ -3697,6 +3855,7 @@ int main() {
run(hall_of_meat_scores_a_bail);
run(grind_exits_pick_the_side);
run(smokey_throwdowns_register_and_play_one_up);
run(filmer_follows_and_films);
run(plugins_drive_the_throwdown_kit);
run(fit_check_turns_the_board_and_saves_it);
run(item_tabs_keep_tabs_and_wear_on_top);