From 118c489fdb9efef7e834c49f9c19092e992ef028 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Thu, 22 Oct 2026 18:46:35 -0500 Subject: [PATCH] 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 --- CMakeLists.txt | 6 + .../Skater/Test/first_person_cycle_tests.cpp | 37 ++ Engine/Game/Skater/first_person_cycle.h | 39 +- Extension/Multiplayer/Hud/custom_nametags.h | 4 + .../Multiplayer/Remote/native_skater.cpp | 6 + Extension/Multiplayer/Remote/native_skater.h | 5 + .../Remote/native_skater_internal.h | 1 + Extension/Multiplayer/Session/session.cpp | 5 +- Extension/Plugins/Test/plugin_tests.cpp | 165 ++++++- Extension/Plugins/plugin_api.cpp | 4 +- Extension/Plugins/plugin_api_world.cpp | 160 +++++- Extension/Plugins/plugin_game.h | 43 +- Extension/Plugins/plugin_game_runtime.cpp | 92 +++- Extension/Plugins/plugin_host.cpp | 5 + Extension/Skater/Test/carry_tests.cpp | 178 +++++++ Extension/Skater/board_pose.cpp | 34 +- Extension/Skater/board_pose.h | 4 +- Extension/Skater/carry.cpp | 465 ++++++++++++++++++ Extension/Skater/carry.h | 82 +++ Extension/Skater/client_debug.cpp | 14 +- Extension/Skater/outfit_overlay.cpp | 4 +- Extension/Skater/outfit_overlay.h | 5 +- Extension/Skater/skater_animation.cpp | 159 ++++-- Extension/Skater/skater_animation.h | 20 +- cmake/Runtime.cmake | 1 + 25 files changed, 1449 insertions(+), 89 deletions(-) create mode 100644 Extension/Skater/Test/carry_tests.cpp create mode 100644 Extension/Skater/carry.cpp create mode 100644 Extension/Skater/carry.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b357227f..333517cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,6 +406,12 @@ if(DINGOSDK_BUILD_ANIMATION_STUDIO_TESTS) target_include_directories(dingosdk_arm_reach_tests PRIVATE "${PROJECT_SOURCE_DIR}") target_link_libraries(dingosdk_arm_reach_tests PRIVATE dingosdk_json) add_test(NAME arm_reach COMMAND dingosdk_arm_reach_tests) + # Something carried (reskate.skater.carry): held out, aimed, both hands, crouched with the feet kept. + add_executable(dingosdk_carry_tests Extension/Skater/Test/carry_tests.cpp Extension/Skater/carry.cpp Extension/Skater/arm_reach.cpp + Extension/AnimationStudio/anim_core.cpp) + target_include_directories(dingosdk_carry_tests PRIVATE "${PROJECT_SOURCE_DIR}") + target_link_libraries(dingosdk_carry_tests PRIVATE dingosdk_json) + add_test(NAME carry COMMAND dingosdk_carry_tests) endif() option(DINGOSDK_BUILD_REPLAY_KEYFRAMES_TESTS "Build replay keyframe smoothing regression tests" OFF) if(DINGOSDK_BUILD_REPLAY_KEYFRAMES_TESTS) diff --git a/Engine/Game/Skater/Test/first_person_cycle_tests.cpp b/Engine/Game/Skater/Test/first_person_cycle_tests.cpp index ea2f4748..52c9c077 100644 --- a/Engine/Game/Skater/Test/first_person_cycle_tests.cpp +++ b/Engine/Game/Skater/Test/first_person_cycle_tests.cpp @@ -58,12 +58,49 @@ void presses() { expect(high && !first_person, "the third press of each round returns to the high camera"); } } + +// With a plugin's camera in the cycle: high -> low -> first person -> the plugin's camera -> high, and +// without first person (or on foot, where it cannot start) high -> low -> the plugin's camera -> high. +void plugin() { + for (const bool with_first_person : {true, false}) { + bool high = true, first_person = false, on = false; + const auto press = [&] { + CycleState state{with_first_person, first_person, !first_person && !on, true, on, !on}; + const auto write = cycle_height_write(state, !high); + high = write.high; + if (write.step == CycleStep::enter) first_person = true; + if (write.step == CycleStep::leave) first_person = false; + if (write.plugin == CycleStep::enter) on = true; + if (write.plugin == CycleStep::leave) on = false; + }; + for (int round = 0; round < 3; ++round) { + expect(high && !first_person && !on, "each round starts on the high camera"); + press(); + expect(!high && !first_person && !on, "first press: low camera"); + if (with_first_person) { + press(); + expect(!high && first_person && !on, "second press: first person"); + } + press(); + expect(!high && !first_person && on, "then the plugin's camera, the low camera behind it"); + press(); + } + expect(high && !first_person && !on, "out of the plugin's camera: the high camera"); + } + // No plugin in the cycle: the first-person cycle as before. + expect(same(cycle_height_write({true, true, false, false, false, true}, true), true, CycleStep::leave), + "without a plugin's camera, first person -> high"); + // Turned on by the plugin itself over the high camera: the button still takes it out to high. + const auto out = cycle_height_write({false, false, true, true, true, false}, false); + expect(out.high && out.plugin == CycleStep::leave, "a plugin's camera on by itself: the button leaves it for high"); +} } // namespace int main() { off(); steps(); presses(); + plugin(); if (failures) { std::printf("%d first-person cycle test(s) failed\n", failures); return 1; diff --git a/Engine/Game/Skater/first_person_cycle.h b/Engine/Game/Skater/first_person_cycle.h index 8ee30393..00563bf1 100644 --- a/Engine/Game/Skater/first_person_cycle.h +++ b/Engine/Game/Skater/first_person_cycle.h @@ -1,4 +1,5 @@ #pragma once +#include #include namespace dingosdk::first_person { @@ -7,20 +8,46 @@ namespace dingosdk::first_person { // (true is the high camera). With the option on, ReSkate turns that into high -> low -> first // person -> high by changing the value written: the step into first person keeps the low camera // behind it, and the step out of first person always lands on the high camera. +// +// A plugin's camera can be in the cycle too, last (reskate.camera.cycle): high -> low -> first +// person -> the plugin's camera -> high (without first person in it, or where first person cannot +// start: high -> low -> the plugin's camera -> high). Its step keeps the low camera behind it as well. enum class CycleStep : std::uint8_t { none, enter, leave }; struct CycleState { bool enabled{}; // the option is on and the local skater is in control bool first_person{}; // first person has the camera bool can_enter{}; // first person can take the camera now: on the board, no Freecam + bool plugin_enabled{}; // a plugin's camera is in the cycle now + bool plugin{}; // that camera is on + bool plugin_can_enter{}; // that camera is off and can start }; struct CycleWrite { - bool high{}; // the height the game keeps - CycleStep step{}; // what first person does + bool high{}; // the height the game keeps + CycleStep step{}; // what first person does + CycleStep plugin{}; // what the plugin's camera does }; constexpr CycleWrite cycle_height_write(const CycleState& state, bool high) noexcept { - if (!state.enabled) return {high, CycleStep::none}; - if (state.first_person) return {true, CycleStep::leave}; - if (high && state.can_enter) return {false, CycleStep::enter}; - return {high, CycleStep::none}; + // The plugin's camera is last: out of it, the high camera. + if (state.plugin_enabled && state.plugin) return {true, CycleStep::none, CycleStep::leave}; + if (state.enabled && state.first_person) { + if (state.plugin_enabled && state.plugin_can_enter) return {false, CycleStep::leave, CycleStep::enter}; + return {true, CycleStep::leave, CycleStep::none}; + } + if (high) { // the game steps low -> high: first person, else the plugin's camera, comes in between + if (state.enabled && state.can_enter) return {false, CycleStep::enter, CycleStep::none}; + if (state.plugin_enabled && state.plugin_can_enter) return {false, CycleStep::none, CycleStep::enter}; + } + return {high, CycleStep::none, CycleStep::none}; } + +// The plugin's half of the link (the plugin runtime keeps it). Each tick the plugin in the cycle has +// its flags published; the height write (on the thread that handles the button) leaves the step it +// asks of the plugin's camera in `request`, taken on the plugin runtime's next tick. +enum PluginFlag : std::uint8_t { plugin_flag_enabled = 1, plugin_flag_on = 2, plugin_flag_can_enter = 4 }; +struct PluginLink { + std::atomic flags{}; + std::atomic request{}; // a CycleStep + std::atomic published{}; // GetTickCount64 when the flags were published +}; +inline PluginLink plugin_link{}; } diff --git a/Extension/Multiplayer/Hud/custom_nametags.h b/Extension/Multiplayer/Hud/custom_nametags.h index 578f2efd..d699540c 100644 --- a/Extension/Multiplayer/Hud/custom_nametags.h +++ b/Extension/Multiplayer/Hud/custom_nametags.h @@ -2,6 +2,7 @@ #include "Extension/UI/Overlay/overlay.h" #include "Extension/Multiplayer/developer_identity.h" #include +#include #include #include #include @@ -64,6 +65,9 @@ void publish_custom_nametags(std::uintptr_t base, std::vector pla bool show_bubbles, float bubble_distance, float name_distance = 120.f, bool dots = true) noexcept; void set_custom_nametags_enabled(bool enabled) noexcept; +// Held off for now (a plugin's reskate.session.nametags(false), filming): no names drawn over the +// players while it lasts, whatever the nametag setting is; chat bubbles still show. +inline std::atomic nametags_held_off{false}; bool custom_nametags_enabled() noexcept; // The overlay's feed (any thread): nothing while off, stale, or while the game hides nametags. overlay::Nametags custom_nametags(); diff --git a/Extension/Multiplayer/Remote/native_skater.cpp b/Extension/Multiplayer/Remote/native_skater.cpp index 5f298dee..abe724f2 100644 --- a/Extension/Multiplayer/Remote/native_skater.cpp +++ b/Extension/Multiplayer/Remote/native_skater.cpp @@ -556,6 +556,9 @@ NativeFrame capture_local(std::uintptr_t base, std::uintptr_t client, bool captu cache = {base, client, frame.context, player, frame.entity, frame.parent, component, frame.board_entity}; frame.ready = true; + // The pose as it is sent, after anyone who changes only what is drawn of the board has had it. + if (capture_pose) + if (const auto filter = shared().sent_pose_filter.load(std::memory_order_acquire)) filter(component, frame.pose); } catch (const std::exception &e) { cache = {}; frame.detail = e.what(); @@ -612,6 +615,9 @@ void set_render_pose_listener(RenderPosePublished listener) noexcept { void set_animation_evaluated_listener(AnimationEvaluated listener) noexcept { shared().evaluated_listener.store(listener, std::memory_order_release); } +void set_local_sent_pose_filter(LocalSentPoseFilter filter) noexcept { + shared().sent_pose_filter.store(filter, std::memory_order_release); +} void set_local_pose_filter(LocalPoseFilter filter) noexcept { shared().pose_filter.store(filter, std::memory_order_release); } diff --git a/Extension/Multiplayer/Remote/native_skater.h b/Extension/Multiplayer/Remote/native_skater.h index 868c5753..88ea2080 100644 --- a/Extension/Multiplayer/Remote/native_skater.h +++ b/Extension/Multiplayer/Remote/native_skater.h @@ -43,6 +43,11 @@ void set_animation_evaluated_listener(AnimationEvaluated listener) noexcept; // what other players receive of the local skater. using LocalPoseFilter = void (*)(std::uintptr_t component, std::vector &skater) noexcept; void set_local_pose_filter(LocalPoseFilter filter) noexcept; +// Then, with the whole pose capture_local returns (the skater and the board), so a change to what is +// drawn of the board (Extension/Skater/board_pose.h, a bone hidden from everyone) is in what the +// others see too. One listener; nullptr for none. Client thread. +using LocalSentPoseFilter = void (*)(std::uintptr_t component, Pose &pose) noexcept; +void set_local_sent_pose_filter(LocalSentPoseFilter filter) noexcept; // A pose override of the local skater (Extension/Skater/skater_animation.cpp): called after the // listener above, so it sees (and may change) the pose first person left. void set_pose_override_listener(AnimationEvaluated listener) noexcept; diff --git a/Extension/Multiplayer/Remote/native_skater_internal.h b/Extension/Multiplayer/Remote/native_skater_internal.h index 85d9476d..0aedebc5 100644 --- a/Extension/Multiplayer/Remote/native_skater_internal.h +++ b/Extension/Multiplayer/Remote/native_skater_internal.h @@ -58,6 +58,7 @@ struct HookState { std::atomic destroyed_listener{}; std::atomic evaluated_listener{}; std::atomic pose_filter{}; + std::atomic sent_pose_filter{}; // A pose override of the local skater (Extension/Skater/skater_animation.cpp), after the one above. std::atomic override_listener{}; std::atomic override_render_listener{}; diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index 6ab4bf37..93300b1c 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -485,7 +485,8 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui // each frame. std::vector nametags; const bool bubbles = s.chat_bubbles; - const bool labels = s.nametags || bubbles; + const bool names = s.nametags && !nametags_held_off.load(std::memory_order_relaxed); + const bool labels = s.nametags || bubbles; // (published even while names are held off, so none linger) if (labels) refresh_friends(s); // The newest chat lines from `sender` still inside the bubble duration, oldest first, // filtered the same way the chat panel filters them, each with the opacity it has left @@ -821,7 +822,7 @@ void render(Session &s, std::uintptr_t client, const NativeFrame &local, std::ui if (labels) publish_custom_nametags(s.base, std::move(nametags), local.ready ? std::optional(local.pose.root.position) : std::nullopt, - s.nametags, bubbles, s.chat_bubbles_distance, s.nametag_distance, + names, bubbles, s.chat_bubbles_distance, s.nametag_distance, s.nametag_dots); } } // namespace diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp index 203b3e76..701b9b3b 100644 --- a/Extension/Plugins/Test/plugin_tests.cpp +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -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 cycle_news; + std::vector 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> installed; // items found by their whole ending, as Pack/ 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 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 telemetry; @@ -150,8 +160,18 @@ struct FakeGame final : Game { return {}; } std::optional mouse_motion(std::string_view) override { return MouseMotion{3, -2, 1}; } - std::string skater_pose(std::string_view plugin, const std::vector& 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 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& 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 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(++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(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); diff --git a/Extension/Plugins/plugin_api.cpp b/Extension/Plugins/plugin_api.cpp index 609d5398..b2d618ab 100644 --- a/Extension/Plugins/plugin_api.cpp +++ b/Extension/Plugins/plugin_api.cpp @@ -17,7 +17,7 @@ namespace { struct Event { std::string_view name, permission; }; -constexpr std::array events{{ +constexpr std::array events{{ {"load", {}}, {"unload", {}}, {"tick", {}}, {"level_loaded", {}}, {"level_unloading", {}}, {"trick_started", "events.tricks"}, {"trick_landed", "events.tricks"}, {"line_broken", "events.tricks"}, {"draw", "overlay"}, {"player_joined", "session.read"}, {"player_left", "session.read"}, {"message", "net.session"}, @@ -25,7 +25,7 @@ constexpr std::array events{{ {"throwdown", "throwdowns"}, {"action", "input"}, {"screen_closed", "ui.screen"}, {"board_landed", "events.skater"}, {"body_hit", "events.skater"}, {"bail_end", "events.skater"}, {"physics_step", "events.physics"}, {"launch", "events.skater"}, {"trick_notes", "events.tricks"}, - {"throwdown_ui", "throwdowns"}, {"touch", "session.skaters"}, {"touch_end", "session.skaters"}, + {"throwdown_ui", "throwdowns"}, {"touch", "session.skaters"}, {"touch_end", "session.skaters"}, {"camera_cycle", "camera"}, }}; constexpr std::size_t maximum_key = 128; constexpr std::size_t maximum_command = 48; diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 7773a4ad..5af832ef 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -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(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, 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(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(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(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(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 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}}); diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index cb712075..c5563539 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -333,6 +333,8 @@ public: virtual std::vector hidden_players(std::string_view /*plugin*/) { return {}; } // A short tag beside a player's nametag, the plugin's own (empty text takes it away). virtual void set_nametag(std::string_view /*plugin*/, std::uint64_t, std::string, std::uint32_t) {} + // The names over the other players held off while any plugin asks (chat bubbles still show). + virtual void hide_nametags(std::string_view /*plugin*/, bool /*hidden*/) {} // 0 mutes, 1 is as heard, up to 10. The local player's ears only. virtual std::string voice_volume(std::uint64_t, float) { return unavailable; } // Chat lines after `sequence`, as this player sees them (filtered when they filter). @@ -564,6 +566,17 @@ public: virtual void release_camera(std::string_view /*plugin*/) {} // "" while the plugin's view is the one shown, otherwise why not. virtual std::string camera_status(std::string_view /*plugin*/) { return unavailable; } + // The plugin's camera as a step of the game's camera height button cycle (Engine/Game/Skater/ + // first_person_cycle.h): out of it, in it and off (ready to start), or in it and on. Held 300 ms, + // so said every tick; one plugin at a time. The steps the button asks for come back from + // take_camera_cycle, for the plugin in the cycle. + enum class CycleCamera : std::uint8_t { out, ready, active }; + virtual std::string camera_cycle(std::string_view /*plugin*/, CycleCamera) { return unavailable; } + struct CycleNews { + std::string plugin; + bool enter{}; + }; + virtual std::vector take_camera_cycle() { return {}; } // Holds the skater where it is (noclip, with no input), while any plugin asks. virtual std::string freeze(std::string_view /*plugin*/, bool) { return unavailable; } // ---- the session's authority (plugins' server copies) -------------------------------------------- @@ -628,6 +641,9 @@ public: virtual void pause_clip(std::string_view /*plugin*/, bool /*paused*/) {} virtual std::optional clip_time(std::string_view /*plugin*/) { return std::nullopt; } virtual bool clip_playing(std::string_view /*plugin*/) { return false; } + // Who sees a pose, a carry or a reach of the local skater (Extension/Skater/skater_animation.h + // Shown): everyone, only the other players (sent, not drawn), or only this player (drawn, not sent). + enum class Shown : std::uint8_t { everyone, others, me }; // An arm of the local skater reaching (Extension/Skater/arm_reach.h): its hand joint to `at` in the // world, turned to `rotation` when given, `weight` of the way; held 0.3 s, set every tick. One // plugin at a time; none ends that plugin's. Empty when done, else why not. @@ -636,18 +652,36 @@ public: Vec3 at{}; std::optional> rotation; float weight{1}; + Shown shown{}; }; virtual std::string skater_reach(std::string_view /*plugin*/, std::optional) { return unavailable; } + // Something held out in the local skater's hands and aimed (Extension/Skater/carry.h): its front + // to `front` in the world, pointing as `aim` says (its -Z the way it points); held 0.3 s, set every + // tick. One plugin at a time; none ends that plugin's. Empty when done, else why not. + struct Carry { + bool left{}, both{}; + Vec3 front{}; + std::array aim{0, 0, 0, 1}; + Vec3 front_from_grip{}; + // Worn on the hand: the item's rotation in the hand joint's frame, and its grip from the hand joint. + std::optional, Vec3>> worn; + float weight{1}, crouch{}, hunch{}, turn{}, step{}, step_phase{}; + bool still{}, push_straightens{}; + Shown shown{}; + }; + virtual std::string skater_carry(std::string_view /*plugin*/, std::optional) { return unavailable; } // Joints of the local skater turned (in their own frame) and scaled on top of the animation, in - // the pose drawn and sent (Extension/Skater/skater_animation.h pose); held 0.3 s, so set every - // tick; empty ends it. One plugin at a time. + // the pose drawn and sent (Extension/Skater/skater_animation.h pose) or one of them (`shown`); held + // 0.3 s, so set every tick; empty ends it. One plugin at a time. struct JointOffset { std::uint16_t joint{}; std::array rotation{0, 0, 0, 1}; float scale{1}; Vec3 offset{}; // added to its position, in its parent's frame }; - virtual std::string skater_pose(std::string_view /*plugin*/, const std::vector&, float /*weight*/) { return unavailable; } + virtual std::string skater_pose(std::string_view /*plugin*/, const std::vector&, float /*weight*/, Shown /*shown*/) { + return unavailable; + } // Where a hand joint of the local skater is in the world, as last evaluated (after any reach). struct HandPlace { Vec3 position{}; @@ -723,7 +757,8 @@ public: }; virtual std::optional> board_bones() { return std::nullopt; } virtual std::string board_set(std::string_view /*plugin*/, int, const BoardBone&, bool /*world*/) { return unavailable; } - virtual std::string board_hide(std::string_view /*plugin*/, int) { return unavailable; } + // `everyone`: hidden from the other players too, not only on this screen. + virtual std::string board_hide(std::string_view /*plugin*/, int, bool /*everyone*/) { return unavailable; } virtual void board_clear(std::string_view /*plugin*/, int /*bone, -1: all*/) {} virtual void board_park(std::string_view /*plugin*/, std::vector) {} virtual std::string board_toss(std::string_view /*plugin*/, std::vector, const Vec3&, const Vec3&, float) { return unavailable; } diff --git a/Extension/Plugins/plugin_game_runtime.cpp b/Extension/Plugins/plugin_game_runtime.cpp index fe713b79..69ae664d 100644 --- a/Extension/Plugins/plugin_game_runtime.cpp +++ b/Extension/Plugins/plugin_game_runtime.cpp @@ -25,6 +25,8 @@ #include "Extension/Skater/game_speed.h" #include "Extension/Skater/launches.h" #include "Extension/Skater/pad_hide.h" +#include "Engine/Game/Skater/first_person_cycle.h" +#include "Extension/Multiplayer/Hud/custom_nametags.h" #include "Extension/Skater/xray.h" #include "Extension/UI/hud_corner.h" #include "Extension/Throwdowns/custom_modes.h" @@ -376,6 +378,11 @@ public: void set_nametag(std::string_view plugin, std::uint64_t player, std::string text, std::uint32_t colour) override { plugin_feed::set_tag(std::string(plugin), player, std::move(text), colour); } + void hide_nametags(std::string_view plugin, bool hidden) override { + if (hidden) nametag_hiders_.emplace(plugin); + else if (const auto at = nametag_hiders_.find(plugin); at != nametag_hiders_.end()) nametag_hiders_.erase(at); + multiplayer::nametags_held_off.store(!nametag_hiders_.empty(), std::memory_order_relaxed); + } std::string voice_volume(std::uint64_t player, float volume) override { const auto id = std::to_string(player); const auto mute = multiplayer::command("voice-mute", id + (volume <= 0 ? " on" : " off")); @@ -456,7 +463,7 @@ public: // ---- input ------------------------------------------------------------------------------------------- std::string bind(std::string_view plugin, std::string_view action, std::uint32_t combo) override { - if (!valid_action_binding(combo)) return "that is not a key or pad combo"; + if (combo && !valid_action_binding(combo)) return "that is not a key or pad combo"; // (0: none of its own) binds_[{std::string(plugin), std::string(action)}].own = combo; publish_binds(); return {}; @@ -501,7 +508,7 @@ public: std::string board_set(std::string_view plugin, int bone, const BoardBone& value, bool world) override { return board_pose::set(plugin, bone, {value.position, value.rotation, value.scale}, world ? board_pose::Space::world : board_pose::Space::local); } - std::string board_hide(std::string_view plugin, int bone) override { return board_pose::hide(plugin, bone); } + std::string board_hide(std::string_view plugin, int bone, bool everyone) override { return board_pose::hide(plugin, bone, everyone); } void board_clear(std::string_view plugin, int bone) override { board_pose::clear(plugin, bone); } void board_park(std::string_view plugin, std::vector bones) override { board_pose::park(plugin, std::move(bones)); } std::string board_toss(std::string_view plugin, std::vector bones, const Vec3& velocity, const Vec3& spin, float ground) override { @@ -978,7 +985,10 @@ public: screen_look::slap(); return {}; } - std::string skater_pose(std::string_view plugin, const std::vector& offsets, float weight) override { + static skater_animation::Shown shown_of(Shown shown) { + return shown == Shown::others ? skater_animation::Shown::others : shown == Shown::me ? skater_animation::Shown::me : skater_animation::Shown::everyone; + } + std::string skater_pose(std::string_view plugin, const std::vector& offsets, float weight, Shown shown) override { const auto now = GetTickCount64(); if (!poser_.empty() && poser_ != plugin && now < pose_at_ + 300) return offsets.empty() ? std::string{} : poser_ + " poses the skater"; if (offsets.empty()) { @@ -992,7 +1002,65 @@ public: pose_at_ = now; std::vector out; for (const auto& o : offsets) out.push_back({o.joint, o.rotation, o.scale, o.offset}); - skater_animation::pose(std::move(out), weight); + skater_animation::pose(std::move(out), weight, shown_of(shown)); + return {}; + } + std::string camera_cycle(std::string_view plugin, CycleCamera state) override { + const auto now = GetTickCount64(); + auto& link = first_person::plugin_link; + if (!cycler_.empty() && cycler_ != plugin && now < cycle_at_ + 300) + return state == CycleCamera::out ? std::string{} : cycler_ + " is in the camera cycle"; + if (state == CycleCamera::out) { + if (cycler_ == plugin) { + link.flags.store(0, std::memory_order_release); + cycler_.clear(); + } + return {}; + } + if (cycler_ != plugin) link.request.store(0, std::memory_order_release); // (a step asked of another plugin) + cycler_ = plugin; + cycle_at_ = now; + link.flags.store(static_cast(first_person::plugin_flag_enabled | + (state == CycleCamera::active ? first_person::plugin_flag_on : first_person::plugin_flag_can_enter)), + std::memory_order_release); + link.published.store(now, std::memory_order_release); + return {}; + } + std::vector take_camera_cycle() override { + const auto step = static_cast(first_person::plugin_link.request.exchange(0, std::memory_order_acq_rel)); + if (step == first_person::CycleStep::none || cycler_.empty() || GetTickCount64() >= cycle_at_ + 300) return {}; + return {{cycler_, step == first_person::CycleStep::enter}}; + } + std::string skater_carry(std::string_view plugin, std::optional goal) override { + const auto now = GetTickCount64(); + if (!carrier_.empty() && carrier_ != plugin && now < carry_at_ + 300) { + if (!goal) return {}; + return carrier_ + " has the skater carrying"; + } + if (!goal) { + if (carrier_ == plugin) { + skater_animation::carry(std::nullopt); + carrier_.clear(); + } + return {}; + } + carrier_ = plugin; + carry_at_ = now; + carry::Goal g; + g.hand = goal->left ? arm_reach::Hand::left : arm_reach::Hand::right; + g.both = goal->both; + g.front = goal->front; + g.aim = goal->aim; + g.front_from_grip = goal->front_from_grip; + if (goal->worn) g.worn = carry::Grip{goal->worn->first, goal->worn->second}; + g.weight = goal->weight; + g.crouch = goal->crouch; + g.hunch = goal->hunch; + g.still = goal->still; + g.turn = goal->turn; + g.step = goal->step; + g.step_phase = goal->step_phase; + skater_animation::carry(g, goal->push_straightens, shown_of(goal->shown)); return {}; } std::string skater_reach(std::string_view plugin, std::optional goal) override { @@ -1015,7 +1083,7 @@ public: g.at = goal->at; if (goal->rotation) g.rotation = *goal->rotation; g.weight = goal->weight; - skater_animation::reach(g); + skater_animation::reach(g, shown_of(goal->shown)); return {}; } static Tuning tuning_of(const trainer::TuningValue& v) { @@ -1611,6 +1679,15 @@ public: skater_animation::pose({}, 1); poser_.clear(); } + if (carrier_ == plugin) { + skater_animation::carry(std::nullopt); + carrier_.clear(); + } + if (cycler_ == plugin) { + first_person::plugin_link.flags.store(0, std::memory_order_release); + cycler_.clear(); + } + hide_nametags(plugin, false); if (room_owner_ == plugin) room_leave(plugin); if (const auto seen = telemetry_seen_.find(plugin); seen != telemetry_seen_.end()) telemetry_seen_.erase(seen); plugin_feed::release_tags(std::string(plugin)); @@ -1645,6 +1722,11 @@ private: std::vector enabled_mods_; std::string reacher_; // the plugin holding the skater's arm (skater_reach) std::string poser_; // the plugin turning the skater's joints (skater_pose) + std::string carrier_; // the plugin having the skater carry something (skater_carry) + std::uint64_t carry_at_{}; + std::set> nametag_hiders_; // the plugins holding the nametags off (hide_nametags) + std::string cycler_; // the plugin whose camera is in the camera height cycle (camera_cycle) + std::uint64_t cycle_at_{}; std::string room_owner_; // the plugin in the character room (room_open) std::uint64_t pose_at_{}; std::string looker_; // the plugin whose look is on the screen (screen_look) diff --git a/Extension/Plugins/plugin_host.cpp b/Extension/Plugins/plugin_host.cpp index 5827101a..4a0cdf2e 100644 --- a/Extension/Plugins/plugin_host.cpp +++ b/Extension/Plugins/plugin_host.cpp @@ -316,6 +316,11 @@ void Host::follow_game() { } if (wants("events.skater")) for (const auto& launch : game.take_launches()) each([&](Plugin& plugin, const std::string&) { plugin.launched(launch); }); + // The camera height button asking the plugin in its cycle to start or stop its camera. + for (const auto& news : game.take_camera_cycle()) + each([&](Plugin& plugin, const std::string&) { + if (plugin.id() == news.plugin) plugin.game_event("camera_cycle", "camera", Json(news.enter ? "enter" : "leave")); + }); // Touching (reskate.session.touch): the bodies read once for every plugin that has it on. { std::optional> list; diff --git a/Extension/Skater/Test/carry_tests.cpp b/Extension/Skater/Test/carry_tests.cpp new file mode 100644 index 00000000..d59600c4 --- /dev/null +++ b/Extension/Skater/Test/carry_tests.cpp @@ -0,0 +1,178 @@ +// Something carried apart from the game: held out in front of the chest when it points level, higher +// when it points up, turned the way it is aimed when worn on the hand, the other hand under it, the +// hips lowered with the feet kept where they were, the body turned, and a broken pose left alone. +#include "Extension/Skater/carry.h" +#include +#include +#include +#include +#include +#include + +using namespace dingosdk; +using namespace dingosdk::carry; +namespace { +void check(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +float distance(const Vec3& a, const Vec3& b) { + return std::sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]) + (a[2] - b[2]) * (a[2] - b[2])); +} +bool same_turn(const Quat& a, const Quat& b) { return std::abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]) > 0.999f; } + +// A skater standing, facing +Z (its right at -X), arms hanging out to the sides: upper arms 0.3 m, +// forearms 0.28 m, thighs and shins 0.45 m, the knees a little forward. +constexpr float upper_length = 0.3f, fore_length = 0.28f; +std::vector standing() { + std::vector pose(395); + pose[0].position = {4, 0, -2}; + pose[1].position = {0, 0.95f, 0}; + for (const std::size_t j : {42u, 43u, 44u, 45u}) pose[j].position = {0, 0.1f, 0}; + pose[101].position = {0, 0.1f, 0}; + pose[102].position = {0, 0.05f, 0}; + pose[103].position = {0, 0.1f, 0}; + for (const auto hand : {Hand::right, Hand::left}) { + const auto joints = arm_reach::chain(hand); + const float x = hand == Hand::right ? -1.0f : 1.0f; + pose[joints[7]].position = {0.15f * x, 0, 0}; + pose[joints[8]].position = {0.05f * x, 0, 0}; + pose[joints[9]].position = {0, -upper_length, 0}; + pose[joints[10]].position = {0, -fore_length, 0}; + } + for (const auto& leg : {right_leg, left_leg}) { + const float x = leg[3] == 8 ? -1.0f : 1.0f; + pose[leg[3]].position = {0.1f * x, 0, 0}; + pose[leg[4]].position = {0, -0.45f, 0.03f}; + pose[leg[5]].position = {0, -0.45f, -0.03f}; + } + return pose; +} +std::array arm(const std::vector& pose, Hand hand) { + std::array w{}; + check(arm_reach::compose(pose, arm_reach::chain(hand), w), "The arm composes"); + return w; +} +Placed placed(const std::vector& pose, std::span path) { + std::array w{}; + check(arm_reach::compose(pose, path, w), "The joints compose"); + return w[path.size() - 1]; +} +// Pointing at +Z, tipped up by `up` radians. +Quat aimed(float up) { return anim_studio::multiply(anim_studio::axis_angle({0, 1, 0}, std::numbers::pi_v), anim_studio::axis_angle({1, 0, 0}, up)); } + +void held_level_in_front_of_the_chest() { + auto pose = standing(); + const Vec3 shoulder = arm(pose, Hand::right)[8].position; + Goal goal; + goal.front = {shoulder[0], shoulder[1], shoulder[2] + 2.0f}; // asked for far off: held where a person holds it + goal.aim = aimed(0); + goal.front_from_grip = {0, -0.143f, -0.248f}; + goal.worn = Grip{}; + const auto held = carry::carry(pose, goal); + check(held.valid, "Carried"); + const auto w = arm(pose, Hand::right); + check(held.front[2] - w[8].position[2] > 0.25f, "out in front of the shoulder"); + check(held.front[1] < w[8].position[1], "and below it"); + check(std::abs(distance(w[8].position, w[9].position) - upper_length) < 1e-4f, "The upper arm keeps its length"); + check(std::abs(distance(w[9].position, w[10].position) - fore_length) < 1e-4f, "and the forearm its length"); + check(same_turn(held.item.rotation, goal.aim), "Worn, it points the way it is aimed"); +} + +void aimed_up_it_comes_up() { + auto level = standing(), up = standing(); + Goal goal; + goal.front = {4, 1.4f, 0}; + goal.front_from_grip = {0, -0.143f, -0.248f}; + goal.aim = aimed(0); + const auto low = carry::carry(level, goal); + goal.aim = aimed(1.2f); + const auto high = carry::carry(up, goal); + check(low.valid && high.valid, "Carried both ways"); + check(high.front[1] > low.front[1] + 0.2f, "Aimed up, it is held higher"); +} + +void the_other_hand_holds_it_too() { + auto pose = standing(); + const auto before = arm(pose, Hand::left)[10].position; + Goal goal; + goal.front = {4, 1.3f, 0.5f}; + goal.aim = aimed(0); + goal.both = true; + const auto held = carry::carry(pose, goal); + check(held.valid, "Carried in both hands"); + const auto left = arm(pose, Hand::left)[10].position; + check(distance(left, before) > 0.2f, "The other hand comes up to it"); + check(distance(left, held.item.position) < 0.3f, "and holds it"); +} + +void the_feet_stay_as_the_hips_go_down() { + auto pose = standing(); + const auto right_foot = placed(pose, right_leg).position, left_foot = placed(pose, left_leg).position; + const float hips = placed(pose, std::span(right_leg).first(3)).position[1]; + Goal goal; + goal.front = {4, 1.2f, 0.5f}; + goal.aim = aimed(-0.3f); + goal.crouch = 0.2f; + goal.hunch = 0.8f; + check(carry::carry(pose, goal).valid, "Carried, crouched"); + check(std::abs(placed(pose, std::span(right_leg).first(3)).position[1] - (hips - 0.2f)) < 1e-3f, "The hips go down"); + check(distance(placed(pose, right_leg).position, right_foot) < 2e-3f, "the right foot stays where it was"); + check(distance(placed(pose, left_leg).position, left_foot) < 2e-3f, "and the left"); + const Vec3 head = placed(pose, head_chain).position; + check(head[2] > -2.0f + 0.05f, "The back curls over toward it"); +} + +void the_body_turns() { + auto pose = standing(); + Goal goal; + goal.front = {4, 1.3f, 0.5f}; + goal.aim = aimed(0); + goal.turn = std::numbers::pi_v / 2; + check(carry::carry(pose, goal).valid, "Carried, turned"); + const auto facing = body_facing(pose); + check(std::abs(facing[0]) > 0.99f, "The body turns a quarter round"); +} + +void no_weight_and_a_broken_pose() { + auto pose = standing(); + const auto before = pose; + Goal goal; + goal.front = {4, 1.3f, 0.5f}; + goal.aim = aimed(0); + goal.crouch = 0.3f; + goal.weight = 0; + check(carry::carry(pose, goal).valid, "Carried with no weight"); + for (std::size_t i = 0; i < pose.size(); ++i) + check(same_turn(pose[i].rotation, before[i].rotation) && distance(pose[i].position, before[i].position) < 1e-5f, "leaves the pose as it was"); + auto again = standing(); + goal.weight = 1; + goal.both = true; + goal.hunch = 0.6f; + goal.turn = 0.5f; + check(carry::carry(again, goal).valid, "Carried with everything"); + for (std::size_t i = 0; i < again.size(); ++i) + if (std::find(written.begin(), written.end(), i) == written.end()) + check(same_turn(again[i].rotation, before[i].rotation) && distance(again[i].position, before[i].position) < 1e-5f, + "A carry writes only the joints it says it writes"); + auto broken = standing(); + broken[48].rotation[0] = NAN; + check(!carry::carry(broken, goal).valid, "A broken arm carries nothing"); + goal.front = {NAN, 0, 0}; + auto fine = standing(); + check(!carry::carry(fine, goal).valid, "A point that is not a number is refused"); +} +} // namespace + +int main() { + try { + held_level_in_front_of_the_chest(); + aimed_up_it_comes_up(); + the_other_hand_holds_it_too(); + the_feet_stay_as_the_hips_go_down(); + the_body_turns(); + no_weight_and_a_broken_pose(); + } catch (const std::exception& error) { + std::cerr << "FAILED: " << error.what() << '\n'; + return 1; + } + std::cout << "carry: all passed\n"; + return 0; +} diff --git a/Extension/Skater/board_pose.cpp b/Extension/Skater/board_pose.cpp index 1690a096..2a8976f1 100644 --- a/Extension/Skater/board_pose.cpp +++ b/Extension/Skater/board_pose.cpp @@ -1,6 +1,7 @@ #include "board_pose.h" #include "Extension/AnimationStudio/anim_core.h" #include "Extension/Multiplayer/Remote/native_pose_layout.h" +#include "Extension/Multiplayer/Remote/native_skater.h" #include #include #include @@ -60,6 +61,7 @@ Quat about(const Vec3& axis, float angle) { // The pose a bone is written with, as the client tick asked it. struct Wanted { bool hidden{}; + bool everyone{}; // hidden from the other players too Quat rotation{0, 0, 0, 1}; Vec3 position{}; Space space{}; @@ -90,6 +92,8 @@ Shared& shared() { } std::atomic base_now{}, holder_now{}; std::atomic any{}; +// The bones hidden from everyone, as bits (bone b is bit b), for the sent pose filter without the lock. +std::atomic hidden_from_all{}; // How every board is fitted (fit): the baseplates held on the deck, and the wheels' spread. Read by // the render hook without the lock; the owner is under it. std::atomic fitted{}, fit_plates{}; @@ -117,7 +121,24 @@ bool store_float(std::uintptr_t address, float value) noexcept { return false; } } -void refresh_any(Shared& s) { any.store(!s.wanted.empty() || !s.parked.empty() || s.piece.has_value(), std::memory_order_release); } +void refresh_any(Shared& s) { + any.store(!s.wanted.empty() || !s.parked.empty() || s.piece.has_value(), std::memory_order_release); + std::uint32_t bits = 0; + for (const auto& [b, want] : s.wanted) + if (want.hidden && want.everyone) bits |= 1u << b; + hidden_from_all.store(bits, std::memory_order_release); +} +// The local skater's pose as it is sent: the bones hidden from everyone out of sight in it too, as they +// are drawn here. board[0] is the board entity's place in the world, then its rig's bones from 0. +void on_sent_pose(std::uintptr_t, multiplayer::Pose& pose) noexcept { + const auto bits = hidden_from_all.load(std::memory_order_acquire); + if (!bits) return; + for (int b = first_writable; b < bones; ++b) + if ((bits & (1u << b)) && static_cast(b + 1) < pose.board.size()) { + auto& at = pose.board[static_cast(b + 1)].position; + if (length(at) < 100.0f) at[1] -= 2000.0f; + } +} std::string take(Shared& s, std::string_view owner) { if (!s.owner.empty() && s.owner != owner) return s.owner + " holds the board"; s.owner = owner; @@ -175,16 +196,16 @@ std::string set(std::string_view owner, int bone, const Bone& value, Space space auto& s = shared(); std::lock_guard lock(s.mutex); if (auto why = take(s, owner); !why.empty()) return why; - s.wanted[bone] = {false, normalised(value.rotation), value.position, space}; + s.wanted[bone] = {false, false, normalised(value.rotation), value.position, space}; refresh_any(s); return {}; } -std::string hide(std::string_view owner, int bone) { +std::string hide(std::string_view owner, int bone, bool everyone) { if (bone < first_writable || bone >= bones) return "a board's bone is 2 to 15"; auto& s = shared(); std::lock_guard lock(s.mutex); if (auto why = take(s, owner); !why.empty()) return why; - s.wanted[bone] = {true}; + s.wanted[bone] = {true, everyone}; refresh_any(s); return {}; } @@ -265,6 +286,11 @@ void tick(std::uintptr_t base, std::uintptr_t component) noexcept { } catch (...) {} base_now.store(base, std::memory_order_release); holder_now.store(holder, std::memory_order_release); + static bool sending{}; + if (!sending) { + multiplayer::set_local_sent_pose_filter(&on_sent_pose); + sending = true; + } } void on_render_pose(std::uintptr_t animation_interface) noexcept { diff --git a/Extension/Skater/board_pose.h b/Extension/Skater/board_pose.h index 404f396c..2e8a37a2 100644 --- a/Extension/Skater/board_pose.h +++ b/Extension/Skater/board_pose.h @@ -28,7 +28,9 @@ inline constexpr int bones = 16, first_writable = 2; // Client thread. One owner holds the local board's bones at a time; empty when taken, else why not. std::string set(std::string_view owner, int bone, const Bone& value, Space space); -std::string hide(std::string_view owner, int bone); +// `everyone`: hidden from the other players too (the board as you are sent: Extension/Multiplayer/ +// Remote/native_skater.h's sent pose filter), not only on this screen. +std::string hide(std::string_view owner, int bone, bool everyone = false); void clear(std::string_view owner, int bone = -1); // These bones go out of sight on every board, other players' included (a breakable deck's // stretches, which the game would draw as cracks). diff --git a/Extension/Skater/carry.cpp b/Extension/Skater/carry.cpp new file mode 100644 index 00000000..8ecf9c75 --- /dev/null +++ b/Extension/Skater/carry.cpp @@ -0,0 +1,465 @@ +#include "carry.h" +#include +#include + +namespace dingosdk::carry { +namespace { +using anim_studio::conjugate; +using anim_studio::multiply; +using anim_studio::normalised; +using anim_studio::rotate; +using arm_reach::chain; +using arm_reach::compose; + +Vec3 add(const Vec3& a, const Vec3& b) { return {a[0] + b[0], a[1] + b[1], a[2] + b[2]}; } +Vec3 sub(const Vec3& a, const Vec3& b) { return {a[0] - b[0], a[1] - b[1], a[2] - b[2]}; } +Vec3 scaled(const Vec3& a, float k) { return {a[0] * k, a[1] * k, a[2] * k}; } +float dot(const Vec3& a, const Vec3& b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } +Vec3 cross(const Vec3& a, const Vec3& b) { return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; } +float length(const Vec3& a) { return std::sqrt(dot(a, a)); } +Vec3 unit(const Vec3& a, const Vec3& fallback) { + const float l = length(a); + return l > 1e-6f && std::isfinite(l) ? scaled(a, 1.0f / l) : fallback; +} +bool finite(const Vec3& v) { return std::isfinite(v[0]) && std::isfinite(v[1]) && std::isfinite(v[2]); } +bool finite(const Quat& q) { return std::isfinite(q[0]) && std::isfinite(q[1]) && std::isfinite(q[2]) && std::isfinite(q[3]); } +// The shortest turn taking direction `from` onto direction `to`. +Quat arc(const Vec3& from, const Vec3& to) { + const Vec3 a = unit(from, {0, 0, 1}), b = unit(to, {0, 0, 1}); + const float d = dot(a, b); + if (d < -0.9999f) { // half a turn: about any axis square to `from` + Vec3 axis = cross({1, 0, 0}, a); + if (length(axis) < 1e-3f) axis = cross({0, 1, 0}, a); + axis = unit(axis, {0, 1, 0}); + return {axis[0], axis[1], axis[2], 0.0f}; + } + const Vec3 c = cross(a, b); + return normalised({c[0], c[1], c[2], 1.0f + d}); +} +// Part way from a to b, the short way round. +Quat blend(const Quat& a, Quat b, float t) { + t = std::clamp(t, 0.0f, 1.0f); + if (a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] < 0.0f) + for (auto& v : b) v = -v; + return normalised({a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3] + (b[3] - a[3]) * t}); +} +Quat about_up(float radians) { return {0.0f, std::sin(radians / 2), 0.0f, std::cos(radians / 2)}; } +// The world turn `world` as the local rotation of a joint whose parent is placed at `parent`. +Quat local_of(const Placed& parent, const Quat& world) { return normalised(multiply(conjugate(parent.rotation), world)); } +// The way an aimed thing faces round the upright (level, unit), steady however steeply it points up or +// down: straight down, its forward has no level part left, but its up does (the top of it is the way +// it faces). +Vec3 level_heading(const Quat& aim) { + const Quat q = normalised(aim); + const Vec3 forward = rotate(q, {0.0f, 0.0f, -1.0f}), up = rotate(q, {0.0f, 1.0f, 0.0f}); + const float sin_p = std::clamp(forward[1], -1.0f, 1.0f), cos_p = std::sqrt(std::max(0.0f, 1.0f - sin_p * sin_p)); + return unit(Vec3{forward[0] * cos_p - up[0] * sin_p, 0.0f, forward[2] * cos_p - up[2] * sin_p}, {0.0f, 0.0f, 1.0f}); +} + +// In an arm's chain: chest, shoulder, upper arm, forearm, hand. +constexpr std::size_t chest = 6, shoulder = 7, upper = 8, fore = 9, hand_at = 10; +// Held, not worn: from the wrist joint to the middle of the palm along the hand. +constexpr float palm_from_wrist = 0.075f; +// Where the other hand takes it, in the item's own frame (right, up, back; the grip at the origin), for +// a right-hand grip: the left side of it, underneath. Mirrored for a left grip. +constexpr Vec3 support_from_grip{-0.08f, -0.16f, -0.04f}; +// The body as a column the arm keeps out of: about the hips' and chest's line, this far out at most +// (and inside the shoulders). +constexpr float body_radius = 0.24f; +// Its front is held at least this far out in front of the shoulder (along the aim), and at least this +// far below it. +constexpr float min_reach_out = 0.4f, below_shoulder = 0.05f; +// Toward the holding hand's own side it comes up this much (all the way square to that side). +constexpr float side_lift = 0.2f; + +// One foot lifted `up` metres and moved by `move` (a step), the knee bending to it the way it already +// bends; the foot keeps its turn. +bool step_foot(std::span pose, std::span path, float up, const Vec3& move) noexcept { + std::array now{}; + if (!compose(pose, path, now)) return false; + const Vec3 hip = now[3].position, knee = now[4].position; + const Vec3 ankle = add(now[5].position, add(Vec3{0.0f, up, 0.0f}, move)); + const float a = length(sub(knee, hip)), b = length(sub(now[5].position, knee)); + if (!(a > 0.1f && b > 0.1f)) return true; + const float d = std::clamp(length(sub(ankle, hip)), std::abs(a - b) + 0.01f, (a + b) * 0.999f); + const Vec3 along = unit(sub(ankle, hip), {0, -1, 0}); + Vec3 pole = sub(knee, add(hip, scaled(along, dot(sub(knee, hip), along)))); + pole = unit(pole, Vec3{0.0f, 0.0f, 1.0f}); + const float cos_a = std::clamp((a * a + d * d - b * b) / (2.0f * a * d), -1.0f, 1.0f); + const float sin_a = std::sqrt(std::max(0.0f, 1.0f - cos_a * cos_a)); + const Vec3 new_knee = add(hip, add(scaled(along, a * cos_a), scaled(pole, a * sin_a))); + const Quat foot_world = now[5].rotation; + const Quat thigh_world = normalised(multiply(arc(sub(knee, hip), sub(new_knee, hip)), now[3].rotation)); + pose[path[3]].rotation = local_of(now[2], thigh_world); + if (!compose(pose, path, now)) return false; + const Quat shin_world = normalised(multiply(arc(sub(now[5].position, now[4].position), sub(ankle, now[4].position)), now[4].rotation)); + pose[path[4]].rotation = local_of(now[3], shin_world); + if (!compose(pose, path, now)) return false; + pose[path[5]].rotation = local_of(now[4], foot_world); + return true; +} + +struct One { + Hand hand = Hand::right; + Vec3 front{}; + Quat aim{0, 0, 0, 1}; + Vec3 front_from_grip{}; + std::optional worn; + float weight = 1.0f; + // The front reached for where it is (no holding it out in front and below the shoulder): the + // supporting hand's reach. + bool free{}; +}; + +// One arm holding the item (its upper arm and forearm, and the hand when it is worn). +Held hold_one(std::span pose, const One& goal) noexcept { + const auto joints = chain(goal.hand); + std::array w{}; + if (!compose(pose, joints, w)) return {}; + const Quat aim = normalised(goal.aim); + const Vec3 s = w[upper].position, e = w[fore].position, h = w[hand_at].position; + const float a = length(sub(e, s)), b = length(sub(h, e)); + if (!(a > 0.05f && b > 0.05f && a < 1.0f && b < 1.0f)) return {}; + const float scale = std::max(w[hand_at].scale, 0.1f); + // Where its front is shown held: where it was asked for, but out in front of the shoulder along + // the aim and no higher than the shoulder, as a person holds a thing they point (a front up by the + // head or tucked against the body would fold the arm up into the face). + Vec3 front = goal.front; + float to_own_side = 0.0f; + if (!goal.free) { + const Vec3 ahead = level_heading(aim); + const float out = dot(sub(front, s), ahead); + // Toward the holding hand's own side the arm would stretch across to its full length: it comes + // in closer and up a little there, the elbow bent. + Vec3 own_side = sub(s, w[chest].position); + own_side[1] = 0.0f; + own_side = unit(own_side, ahead); + const float toward_side = std::clamp(dot(ahead, own_side), 0.0f, 1.0f); + to_own_side = toward_side; + // Where it is held follows the aim in one smooth sweep: level, out in front at the chest, under + // the eyes; aimed down, lower and a little closer, all the way down to the stomach; aimed up, + // higher and closer to the face, up over the head. + const float pointing_y = std::clamp(rotate(aim, {0.0f, 0.0f, -1.0f})[1], -1.0f, 1.0f); + const float up_part = std::max(pointing_y, 0.0f), down_part = std::max(-pointing_y, 0.0f); + const float height = -below_shoulder - 0.13f * (1.0f - up_part) + 0.55f * up_part - 0.45f * down_part; + const float reach_out = (min_reach_out + 0.08f * (1.0f - up_part)) * (1.0f - 0.35f * toward_side) * (1.0f - 0.55f * up_part) * + (1.0f - 0.15f * down_part); + const Vec3 across = cross({0.0f, 1.0f, 0.0f}, ahead); + const float lateral = std::clamp(dot(sub(front, s), across), -0.3f, 0.3f); + const float out_now = std::max(out, 0.0f); + // (Further out than that only toward the other side, across the body.) + const float out_held = toward_side > 0.0f && out_now > reach_out + 0.1f ? out_now - (out_now - reach_out - 0.1f) * toward_side : reach_out; + front = add(add(s, scaled(ahead, out_held)), scaled(across, lateral)); + front[1] = s[1] + height + side_lift * toward_side; + } + const Grip worn = goal.worn.value_or(Grip{}); + const Quat hand_world = normalised(multiply(aim, conjugate(worn.item))); + const Vec3 grip_offset = scaled(rotate(hand_world, worn.from_wrist), scale); + Vec3 palm_goal = goal.worn ? sub(sub(front, rotate(aim, goal.front_from_grip)), grip_offset) : sub(front, rotate(aim, goal.front_from_grip)); + if (!goal.free) { + // The hand itself out in front of the shoulder and not up over it: with the hand tucked in over + // the shoulder the arm can only fold up, the elbow rising by the head. Aimed up it comes up and + // in a little, smoothly with the tilt. + const Vec3 forward = level_heading(aim); + const float up_part = std::clamp(rotate(aim, {0.0f, 0.0f, -1.0f})[1], 0.0f, 1.0f); + const float out_min = (0.24f - 0.06f * up_part) * (1.0f - 0.35f * to_own_side); + const float out_now = dot(sub(palm_goal, s), forward); + if (out_now < out_min) palm_goal = add(palm_goal, scaled(forward, out_min - out_now)); + const float top = s[1] + 0.04f + 0.22f * up_part + side_lift * to_own_side; + if (palm_goal[1] > top) palm_goal[1] = top; + } + { + // The body as a column through the hips and chest: the hand's target, and the straight reach to + // it from the shoulder, kept out of it (pushed out along the way they came closest). + const Vec3 low = w[2].position, high = w[chest].position; + const float bottom = low[1] - 0.35f, top = high[1] + 0.15f; + const auto axis_at = [&](float y) { + const float t = std::clamp((y - low[1]) / std::max(high[1] - low[1], 0.05f), 0.0f, 1.0f); + return Vec3{low[0] + (high[0] - low[0]) * t, y, low[2] + (high[2] - low[2]) * t}; + }; + // As wide as the body is: inside the shoulders (the arm hangs from the column's edge). + Vec3 shoulder_out = sub(s, axis_at(s[1])); + shoulder_out[1] = 0.0f; + const float radius = std::min(body_radius, length(shoulder_out) * 0.85f); + for (int round = 0; round < 3; ++round) { + float nearest = radius; + Vec3 out_dir{}; + for (int k = 3; k <= 8; ++k) { + const Vec3 p = add(s, scaled(sub(palm_goal, s), static_cast(k) / 8.0f)); + if (p[1] < bottom || p[1] > top) continue; + Vec3 radial = sub(p, axis_at(p[1])); + radial[1] = 0.0f; + const float r = length(radial); + if (r < nearest) { + nearest = r; + Vec3 side_out = sub(s, w[chest].position); + side_out[1] = 0.0f; + out_dir = unit(radial, unit(side_out, {goal.hand == Hand::left ? -1.0f : 1.0f, 0, 0})); + } + } + if (nearest >= radius) break; + palm_goal = add(palm_goal, scaled(out_dir, radius - nearest + 0.01f)); + } + } + const float palm = goal.worn ? 0.0f : palm_from_wrist * scale, b_palm = b + palm; + // Never held at full stretch: the elbow keeps a bend (it comes in a little rather than the arm + // locking straight). + const float reach = (a + b_palm) * (goal.free ? 0.995f : 0.93f); + const float wanted = length(sub(palm_goal, s)); + const float d = std::clamp(wanted, std::abs(a - b_palm) + 0.02f, reach); + const Vec3 direction = unit(sub(palm_goal, s), {0, -1, 0}); + const Vec3 target = add(s, scaled(direction, d)); + // The elbow out from the body and a little down and back: the shoulder's side of the chest. + Vec3 side = sub(s, w[chest].position); + side[1] = 0.0f; + side = unit(side, {goal.hand == Hand::left ? -1.0f : 1.0f, 0, 0}); + // (Forward for the elbow: the way the arm reaches, level; reaching nearly straight up it has almost + // none, and would flip about, so it leans on the way the item points then.) + const Vec3 forward = level_heading(aim); + Vec3 ahead{direction[0], 0.0f, direction[2]}; + ahead = add(ahead, scaled(forward, std::max(0.0f, 0.6f - length(ahead)))); + ahead = unit(ahead, forward); + Vec3 pole = add(add(scaled(side, 0.8f), Vec3{0.0f, -0.45f, 0.0f}), scaled(ahead, -0.35f)); + { + // Raised (up to the eye, up over the head): the elbow comes round to the front and a little out, + // as an arm does lifting something up in front of the face, gradually with the aim; never back + // behind the shoulder, never winged straight out. + const float lifted = std::clamp((rotate(aim, {0.0f, 0.0f, -1.0f})[1] - 0.1f) / 0.8f, 0.0f, 1.0f); + const float ease_in = lifted * lifted * (3.0f - 2.0f * lifted); + if (ease_in > 0.0f) { + const Vec3 lifted_pole = add(add(scaled(side, 0.85f), Vec3{0.0f, -0.4f, 0.0f}), scaled(forward, 0.35f)); + pole = add(scaled(pole, 1.0f - ease_in), scaled(lifted_pole, ease_in)); + } + } + pole = sub(pole, scaled(direction, dot(pole, direction))); + { + // The elbow hangs: whatever way the arm reaches, it bends with the elbow below the line from + // shoulder to hand (and out to the side), never folding up over the shoulder. (Reaching up high, + // straight down from that line would put it back behind the shoulder: there it only comes down + // as far as it can without going back.) + Vec3 hang = sub(Vec3{0.0f, -1.0f, 0.0f}, scaled(direction, -direction[1])); + const float back = dot(hang, forward); + if (back < 0.0f) hang = sub(hang, scaled(forward, back)); + hang = sub(hang, scaled(direction, dot(hang, direction))); + const float hang_len = length(hang); + if (hang_len > 0.05f) pole = add(unit(pole, side), scaled(hang, 0.6f / hang_len)); + } + pole = unit(pole, side); + const float cos_a = std::clamp((a * a + d * d - b_palm * b_palm) / (2.0f * a * d), -1.0f, 1.0f); + const float sin_a = std::sqrt(std::max(0.0f, 1.0f - cos_a * cos_a)); + const Vec3 elbow = add(s, add(scaled(direction, a * cos_a), scaled(pole, a * sin_a))); + const Vec3 wrist = add(elbow, scaled(unit(sub(target, elbow), direction), b)); + // The upper arm turned onto the new elbow, then the forearm (as the upper arm left it) onto the wrist. + const Quat turn_upper = arc(sub(e, s), sub(elbow, s)); + const Quat upper_world = normalised(multiply(turn_upper, w[upper].rotation)); + const Quat turn_fore = arc(rotate(turn_upper, sub(h, e)), sub(wrist, elbow)); + const Quat fore_world = normalised(multiply(turn_fore, multiply(turn_upper, w[fore].rotation))); + // Back to each joint's own rotation, part way by the weight. + const float weight = std::clamp(goal.weight, 0.0f, 1.0f); + auto& upper_bone = pose[joints[upper]]; + auto& fore_bone = pose[joints[fore]]; + upper_bone.rotation = blend(upper_bone.rotation, normalised(multiply(conjugate(w[shoulder].rotation), upper_world)), weight); + fore_bone.rotation = blend(fore_bone.rotation, normalised(multiply(conjugate(upper_world), fore_world)), weight); + if (goal.worn) { + // The hand, from the forearm as it now is, turned so the worn item points along the aim. + if (!compose(pose, joints, w)) return {}; + auto& hand_bone = pose[joints[hand_at]]; + hand_bone.rotation = blend(hand_bone.rotation, normalised(multiply(conjugate(w[fore].rotation), hand_world)), weight); + } + // Where the hand ended up, from the pose as it now is. + if (!compose(pose, joints, w)) return {}; + Held held; + if (goal.worn) { + held.item.rotation = normalised(multiply(w[hand_at].rotation, worn.item)); + held.item.position = add(w[hand_at].position, scaled(rotate(w[hand_at].rotation, worn.from_wrist), scale)); + } else { + // Held: the grip lies on the forearm's line, past the wrist. + const Vec3 along = unit(sub(w[hand_at].position, w[fore].position), {0, -1, 0}); + held.item.position = add(w[hand_at].position, scaled(along, palm_from_wrist * scale)); + held.item.rotation = aim; + } + held.front = add(held.item.position, rotate(held.item.rotation, goal.front_from_grip)); + held.valid = finite(held.item.position) && finite(held.front); + held.reached = wanted <= reach + 0.01f; + return held; +} +} // namespace + +bool crouch(std::span pose, float depth, float twist) noexcept { + if (!((depth > 0.001f || std::abs(twist) > 0.001f) && std::isfinite(depth) && std::isfinite(twist))) return true; + depth = std::clamp(depth, 0.0f, 0.6f); + struct Leg { + std::span path; + std::array before{}; + }; + std::array legs{{{right_leg}, {left_leg}}}; + for (auto& leg : legs) + if (!compose(pose, leg.path, leg.before)) return false; + // The hips down, in their parent's frame (the skater's placement). + const auto& parent = legs[0].before[1]; + auto& hips = pose[hips_joint]; + const Vec3 drop = rotate(conjugate(parent.rotation), Vec3{0.0f, -depth, 0.0f}); + hips.position = add(hips.position, scaled(drop, 1.0f / std::max(parent.scale, 0.1f))); + // Turned round the upright, the feet staying where they are. + if (std::abs(twist) > 0.001f) { + std::array now{}; + if (!compose(pose, legs[0].path, now)) return false; + hips.rotation = local_of(parent, normalised(multiply(about_up(twist), now[2].rotation))); + } + for (auto& leg : legs) { + std::array now{}; + if (!compose(pose, leg.path, now)) return false; + const Vec3 hip = now[3].position, knee = leg.before[4].position, ankle = leg.before[5].position; + const float a = length(sub(leg.before[4].position, leg.before[3].position)), b = length(sub(ankle, knee)); + if (!(a > 0.1f && b > 0.1f)) continue; + const float d = std::clamp(length(sub(ankle, hip)), std::abs(a - b) + 0.01f, (a + b) * 0.999f); + const Vec3 along = unit(sub(ankle, hip), {0, -1, 0}); + // The knee bends the way it already does (forward of the hip-to-ankle line). + const Vec3 old_line = unit(sub(ankle, leg.before[3].position), along); + const Vec3 old_mid = add(leg.before[3].position, scaled(old_line, dot(sub(knee, leg.before[3].position), old_line))); + Vec3 pole = sub(knee, old_mid); + pole = sub(pole, scaled(along, dot(pole, along))); + pole = unit(pole, Vec3{0.0f, 0.0f, 1.0f}); + const float cos_a = std::clamp((a * a + d * d - b * b) / (2.0f * a * d), -1.0f, 1.0f); + const float sin_a = std::sqrt(std::max(0.0f, 1.0f - cos_a * cos_a)); + const Vec3 new_knee = add(hip, add(scaled(along, a * cos_a), scaled(pole, a * sin_a))); + // Thigh, then shin, turned onto the new knee and the ankle; the foot keeps its turn in the world. + const Quat thigh_world = normalised(multiply(arc(sub(now[4].position, hip), sub(new_knee, hip)), now[3].rotation)); + pose[leg.path[3]].rotation = local_of(now[2], thigh_world); + if (!compose(pose, leg.path, now)) return false; + const Quat shin_world = normalised(multiply(arc(sub(now[5].position, now[4].position), sub(ankle, now[4].position)), now[4].rotation)); + pose[leg.path[4]].rotation = local_of(now[3], shin_world); + if (!compose(pose, leg.path, now)) return false; + pose[leg.path[5]].rotation = local_of(now[4], leg.before[5].rotation); + } + return true; +} + +bool hunch(std::span pose, const Vec3& toward, float angle, float look, float twist) noexcept { + if (!(std::abs(angle) + std::abs(look) + std::abs(twist) > 1e-4f) || !finite(toward)) return true; + Vec3 level{toward[0], 0.0f, toward[2]}; + if (length(level) < 1e-4f) return true; + level = unit(level, {0, 0, 1}); + // Turning about this bends the top of the body over toward `level`. + const Vec3 axis = unit(cross(Vec3{0.0f, 1.0f, 0.0f}, level), {1, 0, 0}); + // Spine, Spine1, Spine2, Spine3 share the back's angle; Neck, Neck1 and Head the look. + constexpr std::array share{0.15f, 0.25f, 0.3f, 0.3f, 0.35f, 0.25f, 0.4f}; + constexpr std::array twist_share{0.1f, 0.1f, 0.15f, 0.2f, 0.15f, 0.1f, 0.2f}; + for (std::size_t i = 0; i < back_joints.size(); ++i) { + std::array w{}; + if (!compose(pose, head_chain, w)) return false; + const std::size_t at = 3 + i; // its place in head_chain + const float turn = (i < 4 ? angle : look) * share[i]; + const Quat bend{axis[0] * std::sin(turn / 2), axis[1] * std::sin(turn / 2), axis[2] * std::sin(turn / 2), std::cos(turn / 2)}; + const Quat world = normalised(multiply(bend, multiply(about_up(twist * twist_share[i]), w[at].rotation))); + pose[back_joints[i]].rotation = local_of(w[at - 1], world); + } + return true; +} + +Held carry(std::span pose, const Goal& goal) noexcept { + if (pose.size() <= 343 || !finite(goal.front) || !finite(goal.aim) || !finite(goal.front_from_grip)) return {}; + const float body_weight = std::clamp(goal.weight, 0.0f, 1.0f); + if (std::abs(goal.turn) > 1e-4f && std::isfinite(goal.turn)) { + // The whole body turned round the upright through the hips: the hips (everything hangs from + // them) turned in place. + std::array w3{}; + if (!compose(pose, std::span(right_leg).first(3), w3)) return {}; + pose[hips_joint].rotation = local_of(w3[1], normalised(multiply(about_up(goal.turn * body_weight), w3[2].rotation))); + } + // Turning on the spot: the feet step round with it, one then the other, each lifted a little and + // set down a little in under the hips. + if (goal.step > 0.01f && std::isfinite(goal.step_phase)) { + const float phase = goal.step_phase - std::floor(goal.step_phase); + const bool right_foot = phase < 0.5f; + const float half = right_foot ? phase * 2.0f : (phase - 0.5f) * 2.0f; + const float lift = std::sin(half * 3.14159265f) * 0.09f * std::clamp(goal.step, 0.0f, 1.0f) * body_weight; + std::array leg{}; + const auto path = right_foot ? std::span(right_leg) : std::span(left_leg); + if (lift > 0.002f && compose(pose, path, leg)) { + Vec3 in = sub(leg[2].position, leg[5].position); + in[1] = 0.0f; + const float inward = std::min(length(in), 0.06f) * std::sin(half * 3.14159265f); + if (!step_foot(pose, path, lift, scaled(unit(in, {0, 0, 0}), inward * 0.5f))) return {}; + } + } + // Which way it points against the way the body faces (round the upright), for the body turning to + // it: the hips a quarter, the back and shoulders and head the rest. + float full = 0.0f, pitch = 0.0f; + Vec3 facing{0, 0, 1}; + if (goal.hunch > 0.0f || goal.crouch > 0.0f) { + facing = body_facing(pose); + const Vec3 level = level_heading(goal.aim); + full = std::clamp(std::atan2(dot(cross(facing, level), {0.0f, 1.0f, 0.0f}), dot(facing, level)), -1.6f, 1.6f); + pitch = std::asin(std::clamp(rotate(normalised(goal.aim), {0.0f, 0.0f, -1.0f})[1], -1.0f, 1.0f)); // down is negative + } + if (goal.still) + for (std::size_t i = 0; i < back_joints.size(); ++i) + pose[back_joints[i]].rotation = blend(pose[back_joints[i]].rotation, standing_back[i], body_weight); + constexpr float hips_share = 0.25f; + const bool turning = goal.hunch > 0.0f; + if ((goal.crouch > 0.0f || turning) && !crouch(pose, goal.crouch * body_weight, turning ? full * hips_share * body_weight : 0.0f)) return {}; + if (turning) { + // Over it, the way the body faces: toward the aim, but no more than 60 degrees off the body's own + // facing (never folded backwards). + const Vec3 toward = rotate(about_up(std::clamp(full, -1.05f, 1.05f)), facing); + // Pointed well up, it is held up to the eye instead: no curl over it, the head tilted back to + // it (eased in with the tilt). + const float up_t = std::clamp((pitch - 0.3f) / 0.6f, 0.0f, 1.0f); + // The back over it as it goes down with the aim, a little at level, more aimed down; the head + // down to it the same way, a touch at level, more as it goes down; aimed up, the head back. + const float amount = goal.hunch * std::clamp(0.45f - pitch * (pitch < 0.0f ? 0.55f : 0.7f), 0.2f, 1.2f) * body_weight * (1.0f - up_t); + const float look = std::clamp(0.2f - pitch * (pitch < 0.0f ? 0.18f : 0.45f), 0.1f, 0.65f) * body_weight * (1.0f - up_t) - + up_t * std::min(pitch, 1.2f) * 0.45f * body_weight; + // Up to the eye: the shoulders turn a little, the holding arm's shoulder coming forward, so the + // arm reaches out in front of the face. + float shoulders = 0.0f; + if (up_t > 0.0f) { + std::array arm{}; + if (compose(pose, chain(goal.hand), arm)) { + const Vec3 r = sub(arm[upper].position, arm[chest].position); + const float way = dot(cross({0.0f, 1.0f, 0.0f}, r), facing) >= 0.0f ? 1.0f : -1.0f; + shoulders = way * 0.35f * up_t * body_weight; + } + } + // The shoulders and head turn the rest of the way to it off to the side. + if (!hunch(pose, toward, amount, look, full * (1.0f - hips_share) * body_weight + shoulders)) return {}; + } + One first; + first.hand = goal.hand; + first.front = goal.front; + first.aim = goal.aim; + first.front_from_grip = goal.front_from_grip; + first.worn = goal.worn; + first.weight = goal.weight; + const auto held = hold_one(pose, first); + if (!held.valid || !goal.both) return held; + // The other hand to its side, underneath, where the first one put it. + Vec3 support = support_from_grip; + if (goal.hand == Hand::left) support[0] = -support[0]; + const Vec3 point = add(held.item.position, rotate(held.item.rotation, support)); + One other; + other.hand = goal.hand == Hand::left ? Hand::right : Hand::left; + other.aim = normalised(goal.aim); + other.weight = goal.weight; + other.free = true; + other.front = point; // (front_from_grip 0: its grip goes to `point`) + (void)hold_one(pose, other); + return held; +} + +float pushing(std::span pose) noexcept { + std::array r{}, l{}; + if (!compose(pose, right_leg, r) || !compose(pose, left_leg, l)) return 0.0f; + const float apart = std::abs(r[5].position[1] - l[5].position[1]); + return std::clamp((apart - 0.04f) / 0.06f, 0.0f, 1.0f); +} + +Vec3 body_facing(std::span pose) noexcept { + std::array right_thigh{}, left_thigh{}; + if (!compose(pose, right_leg, right_thigh) || !compose(pose, left_leg, left_thigh)) return {0, 0, 1}; + Vec3 across = sub(left_thigh[3].position, right_thigh[3].position); + across[1] = 0.0f; + return unit(cross(across, {0.0f, 1.0f, 0.0f}), {0, 0, 1}); +} +} diff --git a/Extension/Skater/carry.h b/Extension/Skater/carry.h new file mode 100644 index 00000000..8dcb2931 --- /dev/null +++ b/Extension/Skater/carry.h @@ -0,0 +1,82 @@ +#pragma once +#include "Extension/Skater/arm_reach.h" +#include +#include +#include +#include + +// Something held out in the hands and aimed, the way a person holds a camera or a phone they point: +// level, out in front at the chest; aimed down, lower and closer, down to the stomach; aimed up, up to +// the eye and over the head. The elbows keep a bend and the arms go round the body; the other hand can +// hold it too, from the side and underneath. The body can crouch over it (the feet kept where they +// are), curl its back over it and look down at it, turn on the spot after it and step round as it does. +// Pure maths on the 395-joint AnimBase pose (arm_reach.h), made from the Filmer's arm holding its +// camera (PR 343, filmer_hold.cpp); skater_animation.cpp applies it as the pose is evaluated, drawn and sent. +namespace dingosdk::carry { +using anim_studio::Joint; +using anim_studio::Quat; +using anim_studio::Vec3; +using arm_reach::Hand; +using arm_reach::Placed; + +// The legs from the root: the last three are thigh, shin and foot. The hips (7) carry them and the spine. +inline constexpr std::array right_leg{0, 1, 7, 8, 9, 10}; +inline constexpr std::array left_leg{0, 1, 7, 341, 342, 343}; +inline constexpr std::uint16_t hips_joint = 7; +// The back and head from the root: Spine, Spine1, Spine2, Spine3, Neck, Neck1, Head. +inline constexpr std::array head_chain{0, 1, 7, 42, 43, 44, 45, 101, 102, 103}; +inline constexpr std::array back_joints{42, 43, 44, 45, 101, 102, 103}; +// The back, neck and head as the skeleton stands (each joint's own turn, from its bind pose). +inline constexpr std::array standing_back{{{0.0f, 0.0f, 0.16592f, 0.98614f}, + {0.0f, 0.0f, -0.19038f, 0.98171f}, + {0.0f, 0.0f, -0.10393f, 0.99458f}, + {0.0f, 0.0f, 0.22027f, 0.97544f}, + {0.0f, 0.0f, 0.11357f, 0.99353f}, + {0.0f, 0.0f, -0.19607f, 0.98059f}, + {0.0f, 0.0f, -0.00959f, 0.99995f}}}; +// Every joint carry() may write, lowest first: the hips and legs, the back and head, both arms. +inline constexpr std::array written{7, 8, 9, 10, 42, 43, 44, 45, 46, 47, 48, 49, 101, 102, 103, 275, 276, 277, 278, 341, 342, 343}; + +// Worn on the hand (a cosmetic weighted wholly to the hand): the item's rows (right, up, back) in the +// hand joint's own frame, and where its grip is from the hand joint, in that frame, both from the +// skeleton's bind pose. The hand turns so the item points along the aim. +struct Grip { + Quat item{0, 0, 0, 1}; + Vec3 from_wrist{}; +}; +struct Goal { + Hand hand = Hand::right; + Vec3 front{}; // where the item's front (a lens, a screen) should be, in the world + Quat aim{0, 0, 0, 1}; // the item's rotation in the world: its -Z the way it points, +Y its top + Vec3 front_from_grip{}; // its front from where the hand grips it, in its own frame + std::optional worn; + float weight = 1.0f; // 0: the body as the animation has it, 1: carrying + bool both{}; // the other hand holds it too, from the side and underneath + float crouch{}; // how far the hips go down (metres), the feet kept where they are + float hunch{}; // how far the back curls over it (radians), the head looking down at it + bool still{}; // the back, neck and head from a still standing pose (no idle fidgets) + float turn{}; // the whole body turned round the upright through the hips (radians) + float step{}, step_phase{}; // turning on the spot: how much the feet step (0..1), where in the stride (one per 1) +}; +struct Held { + bool valid{}; + Placed item; // where the item's grip is, turned the way it points + Vec3 front{}; // where its front ended up (short of the goal when out of reach) + bool reached{}; +}; +// Bends the pose (in place) to carry the item as the goal says. Leaves the pose alone and returns +// invalid when the joints are not there, or the arm's bones are not an arm's length. +Held carry(std::span pose, const Goal& goal) noexcept; +// How much a pose is pushing on a board (0 to 1): one foot down on the ground well below the other. +float pushing(std::span pose) noexcept; +// Which way the body faces (level, in the world), from the line across its hips; {0, 0, 1} when the +// legs are not there. +Vec3 body_facing(std::span pose) noexcept; +// Lowers the hips by `depth` (metres) and turns them round the upright by `twist` (radians), bending +// the knees so each foot stays where it was, turned as it was. False when the legs are not there. +bool crouch(std::span pose, float depth, float twist = 0.0f) noexcept; +// Curls the back over toward `toward` (a direction in the world, level) by `angle` radians spread +// down the spine, and the neck and head on down by `look` more; first turns the upper body and the +// head round the upright by `twist` radians (most of it in the neck and head). +bool hunch(std::span pose, const Vec3& toward, float angle, float look, float twist = 0.0f) noexcept; +} diff --git a/Extension/Skater/client_debug.cpp b/Extension/Skater/client_debug.cpp index b748b7bc..dfb62b53 100644 --- a/Extension/Skater/client_debug.cpp +++ b/Extension/Skater/client_debug.cpp @@ -1276,12 +1276,22 @@ constexpr ULONGLONG cycle_flags_lifetime_ms = 250; bool first_person_camera_height_write(bool high) noexcept { const auto published = cycle_published.load(std::memory_order_acquire); const auto flags = cycle_flags.load(std::memory_order_acquire); + // A plugin's camera in the cycle (reskate.camera.cycle), last; its flags come from the plugin runtime. + auto& plugin = first_person::plugin_link; + const auto plugin_published = plugin.published.load(std::memory_order_acquire); + const auto plugin_flags = plugin.flags.load(std::memory_order_acquire); const auto now = GetTickCount64(); - if (now < published || now - published > cycle_flags_lifetime_ms) return high; + const bool fresh = now >= published && now - published <= cycle_flags_lifetime_ms; + const bool plugin_fresh = now >= plugin_published && now - plugin_published <= cycle_flags_lifetime_ms; + if (!fresh && !plugin_fresh) return high; const auto write = first_person::cycle_height_write( - {(flags & cycle_enabled) != 0, (flags & cycle_first_person) != 0, (flags & cycle_can_enter) != 0}, high); + {fresh && (flags & cycle_enabled) != 0, fresh && (flags & cycle_first_person) != 0, fresh && (flags & cycle_can_enter) != 0, + plugin_fresh && (plugin_flags & first_person::plugin_flag_enabled) != 0, plugin_fresh && (plugin_flags & first_person::plugin_flag_on) != 0, + plugin_fresh && (plugin_flags & first_person::plugin_flag_can_enter) != 0}, + high); if (write.step != first_person::CycleStep::none) cycle_request.store(static_cast(write.step), std::memory_order_release); + if (write.plugin != first_person::CycleStep::none) plugin.request.store(static_cast(write.plugin), std::memory_order_release); return write.high; } void first_person_cycle_tick(SourceTrial& trial, std::uintptr_t client, bool can_control, bool phase, DWORD error) { diff --git a/Extension/Skater/outfit_overlay.cpp b/Extension/Skater/outfit_overlay.cpp index 00d8f541..11afd8f9 100644 --- a/Extension/Skater/outfit_overlay.cpp +++ b/Extension/Skater/outfit_overlay.cpp @@ -24,7 +24,7 @@ namespace dingosdk::outfit_overlay { namespace { using skater_items::SkaterItems; using skater_items::lower; -constexpr std::string_view allowed[] = {"tattoo", "moles", "eyewear"}; +constexpr std::string_view allowed[] = {"tattoo", "moles", "eyewear", "hand"}; struct Wanted { std::string owner, item; @@ -387,7 +387,7 @@ bool allowed_slot(std::string_view slot) noexcept { std::string set(std::string owner, std::string slot, std::string item, std::vector placement, bool covers) { auto& s = state(); slot = lower(slot); - if (!allowed_slot(slot)) return "only tattoo, moles and eyewear slots"; + if (!allowed_slot(slot)) return "only tattoo, moles, eyewear and hand wear slots"; if (const auto taken = s.wanted.find(slot); taken != s.wanted.end() && taken->second.owner != owner) return taken->second.owner + " has that slot"; if (!content_cache::catalogs().available) return "the paid items are not known yet; try again in a moment"; if (profile_runtime::reserved_cosmetic(item)) return "that is a paid store item"; diff --git a/Extension/Skater/outfit_overlay.h b/Extension/Skater/outfit_overlay.h index a0a9d0ab..b55d533e 100644 --- a/Extension/Skater/outfit_overlay.h +++ b/Extension/Skater/outfit_overlay.h @@ -9,8 +9,9 @@ // owner puts an item in a slot of the skater's recipe (by the slot's name: "tattoo_legl", "moles", // "eyewear"); the skater wears them over the outfit through the game's own recipe copy, and is given // back to the game while one of its menus is up (the menus change the outfit), when nothing is worn, -// and when a plugin stops. Only the face and skin layers (tattoos, moles) and eyewear, and never a -// paid store item. Others see what the skater wears. Client thread throughout. +// and when a plugin stops. Only the face and skin layers (tattoos, moles), eyewear and hand wear (the +// game has no menu for it: a camera in the hand), and never a paid store item. Others see what the +// skater wears. Client thread throughout. namespace dingosdk::outfit_overlay { // The slots items may go in: a slot's name holds one of these. bool allowed_slot(std::string_view slot) noexcept; diff --git a/Extension/Skater/skater_animation.cpp b/Extension/Skater/skater_animation.cpp index 2cf706d5..a817ceec 100644 --- a/Extension/Skater/skater_animation.cpp +++ b/Extension/Skater/skater_animation.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include // The pose paths of Animation Studio (PR 297, anim_studio.cpp: on_animation, on_render, @@ -62,6 +63,8 @@ double now_seconds() noexcept { } // ---- transforms ------------------------------------------------------------------------------------ +// The smallest scale a posed joint is given: as small as hiding the skater makes its placement joint. +constexpr float smallest_scale = 0.001f; // One transform after another (parent, then child in the parent's frame); uniform scale. Joint compose(const Joint& parent, const Joint& child) noexcept { const float k = (parent.scale[0] + parent.scale[1] + parent.scale[2]) / 3.0f; @@ -265,10 +268,16 @@ struct Plan { std::uint64_t record_serial{}; std::optional reach; double reach_until{}, joints_until{}; // now_seconds() until which the reach holds, and the joints are read + Shown reach_shown{}; + std::optional carry; // carry(), until carry_until + double carry_until{}; + bool carry_push{}; + Shown carry_shown{}; std::vector joints_read; // joint(): the joints asked for, in order std::vector offsets; // pose(), until offsets_until float offsets_weight{1}; double offsets_until{}; + Shown offsets_shown{}; }; // The game menu's character room (PR 297's studio room): the spot of the menu's skater, how far from // it a rig counts as in the room, and the room's floor (measured 2026-10-10 on Stadium 1 and San @@ -310,6 +319,15 @@ struct Live { std::atomic base{}, component{}, board_holder{}; std::vector bones; std::vector pose, custom, scratch; + std::vector sent; // the pose the others are sent, when it is not the one drawn (Shown) + float carry_push{}; // how much a foot is down pushing, eased (carry's push_straightens) + // The game's own scale of each joint a pose() scaled in the pose drawn. The game never sets a joint's + // scale again, so the scale the last render wrote is still in the buffer at the next update: pose() + // scaled it again on top (a forearm at 0.001 was at 1e-6 the next frame, and on to 0), and it stayed + // when the pose ended (Filmer's own-view forearm, 2026-10-22: the arms gone after Filmer stopped, + // and on foot the body physics, which read the buffer, lost the skater). Put back at every update. + std::map> own_scale; + std::uintptr_t own_scale_buffer{}; // In the room: the placement written for the last render, and the game's own one before it. Joint room_written, game_placement; bool room_written_valid{}, game_placement_valid{}; @@ -544,8 +562,10 @@ void on_animation(std::uintptr_t component) noexcept { const bool reaching = snap->reach && now < snap->reach_until; const bool joints_wanted = now < snap->joints_until && !snap->joints_read.empty(); const bool posing = !snap->offsets.empty() && now < snap->offsets_until; + const bool carrying = snap->carry && now < snap->carry_until; const bool in_room = room_shown.load(std::memory_order_acquire); - if (!busy && !reaching && !joints_wanted && !posing && !in_room && (!snap->allowed || (!snap->play && snap->replacements.empty()))) { + if (!busy && !reaching && !carrying && !joints_wanted && !posing && !in_room && l.own_scale.empty() && + (!snap->allowed || (!snap->play && snap->replacements.empty()))) { std::lock_guard lock(pending().mutex); pending().bones.clear(); SetLastError(error); @@ -584,6 +604,18 @@ void on_animation(std::uintptr_t component) noexcept { l.game_placement_valid = true; } if (!room_shown.load(std::memory_order_acquire)) l.room_written_valid = false; + // Scaled joints get the game's own scale back first, in the buffer too (the body physics read it + // after this update; only the render gets the posed scale), and are posed again from it. + if (l.own_scale_buffer != layout.buffer) { + l.own_scale.clear(); // another skater's buffer: the game's scales + l.own_scale_buffer = layout.buffer; + } + for (const auto& [i, scale] : l.own_scale) { + auto& b = l.bones[i]; + if (std::memcmp(b.scale.data(), scale.data(), sizeof(scale)) == 0) continue; + std::memcpy(b.scale.data(), scale.data(), sizeof(scale)); + (void)poke(layout.buffer + i * sizeof(NativeBone), b.scale.data(), sizeof(scale)); + } l.pose.resize(layout.count); for (std::size_t i = 0; i < layout.count; ++i) l.pose[i] = joint_of(l.bones[i]); if (snap->play) { @@ -724,39 +756,78 @@ void on_animation(std::uintptr_t component) noexcept { first = 2; last = layout.count; } - // Joints turned and scaled, after the clip and before any reach: they join what is written. - if (posing) { + // [first, last) of a pose grown to take in [from, to). + const auto widen = [](std::size_t& first, std::size_t& last, std::size_t from, std::size_t to) { + if (last > first) { + first = std::min(first, from); + last = std::max(last, to); + } else { + first = from; + last = to; + } + }; + // Joints turned and scaled, after the clip; then something carried; then an arm reaching. Each + // goes into the pose drawn, the pose sent, or both (Shown), and its joints join what is written. + std::vector scaled_now; // joints scaled in the pose drawn this update + const auto pose_joints = [&](std::vector& pose, std::size_t& first, std::size_t& last) { const float w = std::clamp(snap->offsets_weight, 0.0f, 1.0f); for (const auto& o : snap->offsets) { if (o.joint < 2 || o.joint >= layout.count) continue; - auto& j = l.pose[o.joint]; + auto& j = pose[o.joint]; j.rotation = normalised(multiply(j.rotation, slerp(Quat{0, 0, 0, 1}, o.rotation, w))); - const float s = 1.0f + (o.scale - 1.0f) * w; + // Never quite 0: a joint at scale 0 has no inverse. This small, it is as good as gone. + const float s = std::max(1.0f + (o.scale - 1.0f) * w, smallest_scale); + if (s != 1.0f && &pose == &l.pose) { + l.own_scale.try_emplace(o.joint, j.scale); + scaled_now.push_back(o.joint); + } for (auto& k : j.scale) k *= s; for (std::size_t a = 0; a < 3; ++a) j.position[a] += o.offset[a] * w; - if (last > first) { - first = std::min(first, o.joint); - last = std::max(last, o.joint + 1u); - } else { - first = o.joint; - last = o.joint + 1u; - } + widen(first, last, o.joint, o.joint + 1u); } + }; + if (carrying && snap->carry_push) { + // Pushing on the board: the knees mostly straighten while the foot is down (eased, from the + // pose as the animation left it). + const float pushing = carry::pushing(std::span(l.pose.data(), l.pose.size())); + l.carry_push += (pushing - l.carry_push) * 0.256f; + } else { + l.carry_push = 0.0f; } - // An arm reaching, after the clip: its joints join what is written. - if (reaching) { + const auto carry_item = [&](std::vector& pose, std::size_t& first, std::size_t& last) { + auto goal = *snap->carry; + goal.crouch *= 1.0f - 0.65f * l.carry_push; + if (carry::carry(std::span(pose.data(), pose.size()), goal).valid) + widen(first, last, carry::written.front(), carry::written.back() + 1u); + }; + const auto reach_arm = [&](std::vector& pose, std::size_t& first, std::size_t& last) { const auto& goal = *snap->reach; - if (arm_reach::reach(std::span(l.pose.data(), l.pose.size()), goal).valid) { - const std::size_t from = arm_reach::first_written(goal.hand), to = arm_reach::last_written(goal.hand) + 1u; - if (last > first) { - first = std::min(first, from); - last = std::max(last, to); - } else { - first = from; - last = to; - } + if (arm_reach::reach(std::span(pose.data(), pose.size()), goal).valid) + widen(first, last, arm_reach::first_written(goal.hand), arm_reach::last_written(goal.hand) + 1u); + }; + const auto for_everyone = [](bool on, Shown shown) { return on && shown == Shown::everyone; }; + if (for_everyone(posing, snap->offsets_shown)) pose_joints(l.pose, first, last); + if (for_everyone(carrying, snap->carry_shown)) carry_item(l.pose, first, last); + if (for_everyone(reaching, snap->reach_shown)) reach_arm(l.pose, first, last); + // What only the others see goes on a copy that is sent; what only you see, on the pose drawn. + const bool split = (posing && snap->offsets_shown != Shown::everyone) || (carrying && snap->carry_shown != Shown::everyone) || + (reaching && snap->reach_shown != Shown::everyone); + std::size_t sent_from = first, sent_to = last; + if (split) { + l.sent = l.pose; + const auto apply = [&](Shown who, bool on, Shown shown, const auto& step) { + if (!on || shown != who) return; + if (who == Shown::others) step(l.sent, sent_from, sent_to); + else step(l.pose, first, last); + }; + for (const auto who : {Shown::others, Shown::me}) { + apply(who, posing, snap->offsets_shown, pose_joints); + apply(who, carrying, snap->carry_shown, carry_item); + apply(who, reaching, snap->reach_shown, reach_arm); } } + // A joint no longer scaled has had its own scale back since the top of this update: forgotten. + std::erase_if(l.own_scale, [&](const auto& entry) { return std::ranges::find(scaled_now, entry.first) == scaled_now.end(); }); if (joints_wanted) { const std::span pose(l.pose.data(), l.pose.size()); thread_local std::vector> placed; @@ -821,21 +892,27 @@ void on_animation(std::uintptr_t component) noexcept { pr.last_size = size; Probe::step(l.pose, pr.last_out, pr.out_turn, pr.out_move); } - // Other players are never sent the room: from joint 2 on only. - const std::size_t sent_first = std::max(first, 2); - auto& sp = sent_pose(); - std::lock_guard sent_lock(sp.mutex); - sp.component = component; - sp.first = static_cast(sent_first); - sp.last = static_cast(std::max(last, sent_first)); - sp.joints.assign(l.pose.begin() + static_cast(sent_first), - l.pose.begin() + static_cast(std::max(last, sent_first))); - sp.at = at; } else { auto& p = pending(); std::lock_guard lock(p.mutex); p.bones.clear(); } + // What the others are sent: the pose drawn, or its own copy (Shown). Other players are never + // sent the room: from joint 2 on only. + const auto& sent = split ? l.sent : l.pose; + if (!split) sent_from = first, sent_to = last; + if (sent_to > sent_from) { + for (std::size_t i = sent_from; i < sent_to; ++i) + if (!finite(sent[i])) throw std::runtime_error("a pose for the others was broken; nothing was sent"); + const std::size_t sent_first = std::max(sent_from, 2); + auto& sp = sent_pose(); + std::lock_guard sent_lock(sp.mutex); + sp.component = component; + sp.first = static_cast(sent_first); + sp.last = static_cast(std::max(sent_to, sent_first)); + sp.joints.assign(sent.begin() + static_cast(sent_first), sent.begin() + static_cast(std::max(sent_to, sent_first))); + sp.at = at; + } auto& pb = pending_board(); std::lock_guard lock(pb.mutex); pb.weight = board_weight > 0 && finite(board_target) ? board_weight : 0; @@ -1074,17 +1151,26 @@ void pause(bool paused) { bool playing() noexcept { return plan_draft.play.has_value() && finished_serial.load(std::memory_order_acquire) != plan_draft.serial; } -void pose(std::vector offsets, float weight) { +void pose(std::vector offsets, float weight, Shown shown) { plan_draft.offsets = std::move(offsets); plan_draft.offsets_weight = weight; + plan_draft.offsets_shown = shown; plan_draft.offsets_until = plan_draft.offsets.empty() ? 0 : now_seconds() + 0.3; publish(); } -void reach(std::optional goal) { +void reach(std::optional goal, Shown shown) { plan_draft.reach = std::move(goal); + plan_draft.reach_shown = shown; plan_draft.reach_until = plan_draft.reach ? now_seconds() + 0.3 : 0; publish(); } +void carry(std::optional goal, bool push_straightens, Shown shown) { + plan_draft.carry = std::move(goal); + plan_draft.carry_push = push_straightens; + plan_draft.carry_shown = shown; + plan_draft.carry_until = plan_draft.carry ? now_seconds() + 0.3 : 0; + publish(); +} std::optional joint(std::uint16_t index) { if (index >= 395) return std::nullopt; const double now = now_seconds(); @@ -1181,7 +1267,8 @@ void tick(std::uintptr_t base, std::uintptr_t component, bool allowed) { } const double now = now_seconds(); const bool wanted = plan_draft.play || !plan_draft.replacements.empty() || recording() || - (plan_draft.reach && now < plan_draft.reach_until) || now < plan_draft.joints_until || + (plan_draft.reach && now < plan_draft.reach_until) || (plan_draft.carry && now < plan_draft.carry_until) || + now < plan_draft.joints_until || (!plan_draft.offsets.empty() && now < plan_draft.offsets_until) || room_watch.load(std::memory_order_acquire) || room_shown.load(std::memory_order_acquire) || now < room_restore_until.load(std::memory_order_acquire); if (wanted && !hooked && base) { diff --git a/Extension/Skater/skater_animation.h b/Extension/Skater/skater_animation.h index 3634b814..2404fef0 100644 --- a/Extension/Skater/skater_animation.h +++ b/Extension/Skater/skater_animation.h @@ -1,6 +1,7 @@ #pragma once #include "Extension/AnimationStudio/anim_core.h" #include "Extension/Skater/arm_reach.h" +#include "Extension/Skater/carry.h" #include #include #include @@ -55,19 +56,26 @@ void set_replacements(std::vector list); bool record(float seconds); bool recording() noexcept; std::shared_ptr take_recording(); -// An arm reaching (arm_reach.h), after any clip, in the pose drawn and sent to the other players; -// none ends it. It holds for 0.3 s: set it every tick. Unlike clips it is allowed in a throwdown. -void reach(std::optional goal); +// Who sees a pose, a carry or a reach: everyone (the pose drawn here and sent), only the other players +// (sent, not drawn: you look through a camera your skater is seen holding), or only you (drawn, not sent). +enum class Shown : std::uint8_t { everyone, others, me }; +// An arm reaching (arm_reach.h), after any clip and carry; none ends it. It holds for 0.3 s: set it +// every tick. Unlike clips it is allowed in a throwdown. +void reach(std::optional goal, Shown shown = Shown::everyone); +// Something held out in the hands (carry.h), after any pose and before any reach; none ends it. It +// holds for 0.3 s: set it every tick. `push_straightens`: riding, the knees straighten (most of the +// crouch let go) while a foot is down pushing. +void carry(std::optional goal, bool push_straightens = false, Shown shown = Shown::everyone); // Joints turned (in their own frame) and scaled on top of the animation and any clip, before any -// reach, in the pose drawn and sent; `weight` 0..1 of the way. Joints 0 and 1 (the skater's world -// placement) are left alone. It holds for 0.3 s: set it every tick; empty ends it. +// carry and reach; `weight` 0..1 of the way. Joints 0 and 1 (the skater's world placement) are left +// alone. It holds for 0.3 s: set it every tick; empty ends it. struct PoseOffset { std::uint16_t joint{}; anim_studio::Quat rotation{0, 0, 0, 1}; float scale{1}; anim_studio::Vec3 offset{}; // added to its position, in its parent's frame }; -void pose(std::vector offsets, float weight); +void pose(std::vector offsets, float weight, Shown shown = Shown::everyone); // Where a joint (of the 395) was in the world in the last pose evaluated, after any clip and reach; // empty without a skater. Asking keeps that joint read for two seconds, so the first ask comes back // empty; at most 32 joints are read at once. diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 76efb470..470fc0e3 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -151,6 +151,7 @@ add_library(dingosdk_runtime SHARED Extension/Skater/pad_hide.cpp Extension/Skater/trick_rewrite.cpp Extension/Skater/arm_reach.cpp + Extension/Skater/carry.cpp Extension/UI/hud_corner.cpp Extension/Assets/morph_memory_pool.cpp Extension/Assets/native_render_resource_pool.cpp