diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp index c0b206b4..ae318f18 100644 --- a/Extension/Plugins/Test/plugin_tests.cpp +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -63,7 +63,7 @@ struct FakeGame final : Game { 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; - std::optional skater_telemetry() override { return telemetry; } + std::optional skater_telemetry(std::string_view) override { return telemetry; } std::uint64_t world_id() override { return 77; } std::string world_map() override { return "test_map"; } std::string pad_flight(std::string_view plugin, bool on) override { @@ -398,7 +398,7 @@ struct TestEnvironment final : Environment { std::vector take_http_responses() override { return std::exchange(responses, {}); } FakeGame fake; Game& game() override { return fake; } - Teleport teleport(const Vec3& position, float heading) override { + Teleport teleport(std::string_view, const Vec3& position, float heading) override { if (teleport_forbidden) return Teleport::forbidden; if (teleport_unavailable || !local_skater) return Teleport::unavailable; teleports.emplace_back(position, heading); diff --git a/Extension/Plugins/plugin_api.cpp b/Extension/Plugins/plugin_api.cpp index e5c24325..3ad1197b 100644 --- a/Extension/Plugins/plugin_api.cpp +++ b/Extension/Plugins/plugin_api.cpp @@ -526,7 +526,7 @@ struct ApiFunctions { } const char* refusal = nullptr; if (++plugin.teleports_ > plugin.limits_.teleports_per_second) refusal = "asked more than twice in a second"; - else switch (plugin.services_.teleport(at, heading)) { + else switch (plugin.services_.teleport(plugin.id(), at, heading)) { case Teleport::sent: break; case Teleport::forbidden: refusal = "the host has turned teleporting off"; break; case Teleport::unavailable: refusal = "teleporting is unavailable now (no skater, or a load)"; break; diff --git a/Extension/Plugins/plugin_api_services.cpp b/Extension/Plugins/plugin_api_services.cpp index 395328ce..78907cb0 100644 --- a/Extension/Plugins/plugin_api_services.cpp +++ b/Extension/Plugins/plugin_api_services.cpp @@ -61,7 +61,7 @@ void field_vec(lua_State* L, const char* name, const Vec3& v) { // skater.telemetry(): the trainer's telemetry, its last jump, the physics watch and the bail's body. int telemetry(lua_State* L) { auto& plugin = allowed(L, "skater.read", "reskate.skater.telemetry"); - const auto t = game_of(plugin).skater_telemetry(); + const auto t = game_of(plugin).skater_telemetry(plugin.id()); if (!t) { lua_pushnil(L); return 1; diff --git a/Extension/Plugins/plugin_api_throwdowns.cpp b/Extension/Plugins/plugin_api_throwdowns.cpp index dbc8f9ae..e9e3c599 100644 --- a/Extension/Plugins/plugin_api_throwdowns.cpp +++ b/Extension/Plugins/plugin_api_throwdowns.cpp @@ -134,7 +134,7 @@ struct KitApi { if (!lua_isnoneornil(L, 3)) { luaL_checktype(L, 3, LUA_TTABLE); options.players = static_cast(number_field(L, 3, "players", options.players, 1, 10)); - options.seconds = static_cast(number_field(L, 3, "seconds", options.seconds, 0, 3600)); + options.seconds = static_cast(number_field(L, 3, "seconds", options.seconds, 1, 3600)); options.setup_page = bool_field(L, 3, "setup_page", options.setup_page); options.cursor_height = static_cast(number_field(L, 3, "cursor_height", options.cursor_height, 0, 100000)); } diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index a4e70d39..10d4213d 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -279,7 +279,7 @@ struct WorldApi { if (!plugin.trainer_point_) return result(L, "no save point is set"); if (!within(plugin.services_.now_ms(), plugin.teleport_second_, plugin.teleports_, plugin.limits_.teleports_per_second)) return result(L, "asked more than twice in a second"); - switch (plugin.services_.teleport(plugin.trainer_point_->first, plugin.trainer_point_->second)) { + switch (plugin.services_.teleport(plugin.id(), plugin.trainer_point_->first, plugin.trainer_point_->second)) { case Teleport::sent: return result(L, {}); case Teleport::forbidden: return result(L, "the host has turned teleporting off"); default: return result(L, "teleporting is unavailable now"); diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index 10ae927a..0c615fd5 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -319,7 +319,8 @@ public: virtual throwdowns::kit::flag::State throwdown_flag_state() { return {}; } virtual void throwdown_flag_leave(std::string_view /*plugin*/) {} // ---- general services for a game layer in Lua (plugin_api_services.cpp) ---------------------- - virtual std::optional skater_telemetry() { return std::nullopt; } + // `plugin`: whose read it is (slam and struck are news since that plugin's last read). + virtual std::optional skater_telemetry(std::string_view /*plugin*/) { return std::nullopt; } // The pad's XInput button bits, LT 0x10000 and RT 0x20000 (empty: no pad). `menu`: read while // ReSkate's menu is open too. virtual std::optional pad_bits(bool /*menu*/) { return std::nullopt; } diff --git a/Extension/Plugins/plugin_game_runtime.cpp b/Extension/Plugins/plugin_game_runtime.cpp index bb690c7d..9b70a0bf 100644 --- a/Extension/Plugins/plugin_game_runtime.cpp +++ b/Extension/Plugins/plugin_game_runtime.cpp @@ -568,9 +568,12 @@ public: void dialog_close(std::string_view plugin, std::string_view id) override { overlay::close_plugin_dialog(plugin, id); } // ---- skater.push / hold -------------------------------------------------------------------------- // A velocity the next on-board physics step writes (Extension/Skater/skater_push.h), and a hold - // that keeps the board still. + // that keeps the board still. Both are written on the game's stock physics only (client_noclip.cpp): + // without it they would be dropped unseen, so they say so. + static constexpr char needs_stock_physics[] = "needs the game's stock physics: reskate.physics.competition(true) first"; std::string skater_push(std::string_view, const Vec3& velocity) override { if (!::dingosdk::skater_push::valid(velocity)) return "a push is horizontal (y 0) and at most 120 m/s"; + if (!competition_stock_physics()) return std::string("a push ") + needs_stock_physics; ::dingosdk::skater_push::request(velocity); return {}; } @@ -583,6 +586,7 @@ public: return {}; } std::string skater_hold(std::string_view plugin, bool on) override { + if (on && !competition_stock_physics()) return std::string("a hold ") + needs_stock_physics; if (on) hold_owner_ = plugin; else if (hold_owner_ != plugin) return {}; else hold_owner_.clear(); @@ -738,7 +742,7 @@ public: skater_animation::tick(base, local_skater_component(base, client), allowed); } // ---- general services for a game layer in Lua ------------------------------------------------------ - std::optional skater_telemetry() override { + std::optional skater_telemetry(std::string_view plugin) override { const auto t = trainer::telemetry(); const auto w = watched_physics_state(); SkaterTelemetry m; @@ -757,8 +761,14 @@ public: j.spin, j.spin_rate, j.flip, j.board_turn, j.state, j.takeoff, j.landing}; m.watch = {w.valid, w.ragdoll_known, w.ragdoll, w.state, w.previous, w.previous_seconds, w.changes, w.wipeouts, w.runouts}; m.teleport_pending = local_skater_teleport_pending(); - m.slam = bonecam::bone_cam_slam(); - m.struck = bonecam::bone_cam_struck(); + // Each plugin hears of a slam or a strike once, whoever else reads the telemetry; its first + // read only takes its place. + const auto slams = bonecam::bone_cam_slams(), strikes = bonecam::bone_cam_strikes(); + auto seen = telemetry_seen_.find(plugin); + if (seen == telemetry_seen_.end()) seen = telemetry_seen_.emplace(std::string(plugin), std::pair{slams, strikes}).first; + m.slam = slams != seen->second.first; + m.struck = strikes != seen->second.second; + seen->second = {slams, strikes}; m.body = bonecam::bail_body_position(); return m; } @@ -967,6 +977,7 @@ public: hud_corner::release(plugin); game_speed::ask(plugin, 1.0f); grind_exit::release(plugin); + if (const auto seen = telemetry_seen_.find(plugin); seen != telemetry_seen_.end()) telemetry_seen_.erase(seen); plugin_feed::release_tags(std::string(plugin)); if (std::erase_if(binds_, [&](const auto& item) { return item.first.first == plugin; })) publish_binds(); } @@ -995,6 +1006,8 @@ private: std::vector installed; }; std::map> variant_reads_; + // The Bone Cam's slam and strike counts each plugin last read with its telemetry. + std::map, std::less<>> telemetry_seen_; using BindKey = std::pair; struct Bind { std::uint32_t own{}; diff --git a/Extension/Plugins/plugin_host.h b/Extension/Plugins/plugin_host.h index 5cc64b9e..a15ab408 100644 --- a/Extension/Plugins/plugin_host.h +++ b/Extension/Plugins/plugin_host.h @@ -33,7 +33,7 @@ public: virtual bool builtin_command(std::string_view word) = 0; // The game (Services, plugin_vm.h). Where there is none, there is no skater and nothing to hit. virtual std::optional skater() { return std::nullopt; } - virtual Teleport teleport(const Vec3&, float) { return Teleport::unavailable; } + virtual Teleport teleport(std::string_view /*plugin*/, const Vec3&, float) { return Teleport::unavailable; } virtual std::optional raycast(const Vec3&, const Vec3&) { return std::nullopt; } virtual std::optional score() { return std::nullopt; } virtual void toast(std::string_view /*plugin*/, LogLevel, std::string_view) {} @@ -126,7 +126,9 @@ private: std::filesystem::path storage_file(std::string_view plugin) override; std::string claim_command(std::string_view plugin, std::string_view name) override; std::optional skater() override { return environment_.skater(); } - Teleport teleport(const Vec3& position, float heading) override { return environment_.teleport(position, heading); } + Teleport teleport(std::string_view plugin, const Vec3& position, float heading) override { + return environment_.teleport(plugin, position, heading); + } std::optional raycast(const Vec3& from, const Vec3& to) override { return environment_.raycast(from, to); } std::optional score() override { return environment_.score(); } void toast(std::string_view plugin, LogLevel level, std::string_view text) override { environment_.toast(plugin, level, text); } diff --git a/Extension/Plugins/plugin_runtime.cpp b/Extension/Plugins/plugin_runtime.cpp index 4595e0e6..29cc8ec0 100644 --- a/Extension/Plugins/plugin_runtime.cpp +++ b/Extension/Plugins/plugin_runtime.cpp @@ -73,10 +73,15 @@ public: return SkaterState{t.position, heading, t.speed, t.vertical, t.airborne, t.air_time}; } // As the console's tp: refused in a session whose host has turned teleporting (and noclip) off. - Teleport teleport(const Vec3& position, float heading) override { + Teleport teleport(std::string_view plugin, const Vec3& position, float heading) override { // A throwdown mode's own moves (line-ups, recoveries, returns to the start) are the game's: - // allowed wherever its match runs and asks for them (the kit's `match.teleports`). - if (!session_noclip_allowed() && !throwdowns::kit::match().teleports) return Teleport::forbidden; + // allowed while that plugin's own match runs and asks for them (the kit's `match.teleports`), + // never for another plugin's calls. + if (!session_noclip_allowed()) { + const auto flag = throwdowns::kit::flag::state(); + const bool mine = flag.owned && flag.mode.size() > plugin.size() && flag.mode.starts_with(plugin) && flag.mode[plugin.size()] == '/'; + if (!throwdowns::kit::allows_teleports(plugin, mine)) return Teleport::forbidden; + } return teleport_local_skater_heading(position, heading) ? Teleport::sent : Teleport::unavailable; } std::optional raycast(const Vec3& from, const Vec3& to) override { return local_surface_hit(from, to); } diff --git a/Extension/Plugins/plugin_vm.h b/Extension/Plugins/plugin_vm.h index 6ce7e653..19ece0bf 100644 --- a/Extension/Plugins/plugin_vm.h +++ b/Extension/Plugins/plugin_vm.h @@ -135,7 +135,8 @@ public: virtual std::string claim_command(std::string_view plugin, std::string_view name) = 0; // The local skater, or empty without one (the menu, a load). virtual std::optional skater() = 0; - virtual Teleport teleport(const Vec3& position, float heading) = 0; + // `plugin`: whose call it is (a throwdown match of its own may let it through, kit::allows_teleports). + virtual Teleport teleport(std::string_view plugin, const Vec3& position, float heading) = 0; // Where the line first meets the world's collision, or empty when it is clear (or not loaded). virtual std::optional raycast(const Vec3& from, const Vec3& to) = 0; // The score HUD now, or empty without one. diff --git a/Extension/Skater/bone_cam.cpp b/Extension/Skater/bone_cam.cpp index 7a68c624..6ebac8fe 100644 --- a/Extension/Skater/bone_cam.cpp +++ b/Extension/Skater/bone_cam.cpp @@ -135,11 +135,11 @@ struct State { // The hips' recent speeds, to notice the body stopping dead (the ground, a pole, a wall). std::array, 24> hips_speeds{}; std::size_t hips_next{}; - std::atomic slam_seen{}; + std::atomic slams{}; std::uint64_t slam_time{}; // The head, chest, shoulders or hips striking an object at speed (bone_cam_struck), and when // any part last did. - std::atomic struck_seen{}; + std::atomic strikes{}; std::uint64_t struck_time{}; std::array level{}; std::array(BoneSprite::count)> hurt{}; @@ -336,7 +336,7 @@ void find_impacts(State &s, std::uint64_t now, bool injuring = true) { // the body hit something. Once in a while at most: on foot the measured speed // jitters, and one slam must not be counted thirty times. if (fastest >= 8.0f && speed < 2.5f && body_stopped && now - s.slam_time > 1500) { - s.slam_seen.store(true); + s.slams.fetch_add(1); s.slam_time = now; } s.hips_speeds[s.hips_next++ % s.hips_speeds.size()] = {now, speed}; @@ -360,7 +360,7 @@ void find_impacts(State &s, std::uint64_t now, bool injuring = true) { // only counts once a bail is on). const auto j = watches[w].joint; const bool body = j == joint::head || j == joint::spine2 || j == joint::hips || j == joint::right_arm || j == joint::left_arm; - if (body && now - s.struck_time > 1000) s.struck_seen.store(true); + if (body && now - s.struck_time > 1000) s.strikes.fetch_add(1); s.struck_time = now; } const bool touching = on_ground || against || struck; @@ -606,9 +606,9 @@ void tick_bone_cam(std::uintptr_t base, std::uintptr_t client, bool playing) noe void bone_cam_bail() noexcept { state().bail_pending.store(true); } -bool bone_cam_slam() noexcept { return state().slam_seen.exchange(false); } +std::uint64_t bone_cam_slams() noexcept { return state().slams.load(); } -bool bone_cam_struck() noexcept { return state().struck_seen.exchange(false); } +std::uint64_t bone_cam_strikes() noexcept { return state().strikes.load(); } std::string bone_cam_setting() { switch (state().setting) { diff --git a/Extension/Skater/bone_cam.h b/Extension/Skater/bone_cam.h index 2a4d38c9..af377558 100644 --- a/Extension/Skater/bone_cam.h +++ b/Extension/Skater/bone_cam.h @@ -70,13 +70,13 @@ void bone_cam_bail() noexcept; // and strikes are followed then even with the Bone Cam off. An owner's ask goes with release. void bone_cam_game(std::string_view owner, bool on); void bone_cam_release(std::string_view owner); -// Whether the skater's body stopped dead (lost 7 m/s within a third of a second) since the last -// call: the ground, a pole, a wall. Only followed while the Bone Cam would show or a game asks. -bool bone_cam_slam() noexcept; -// Whether the skater's head, chest, shoulders or hips struck an object at speed (a pole, a tree, a -// rail: nearly stopped while the body was still flying) since the last call. Followed as -// bone_cam_slam is. -bool bone_cam_struck() noexcept; +// How many times the skater's body has stopped dead (lost 7 m/s within a third of a second): the +// ground, a pole, a wall. A count, so each reader keeps its own place (a reader that cleared a flag +// hid the slam from every other). Only followed while the Bone Cam would show or a game asks. +std::uint64_t bone_cam_slams() noexcept; +// How many times the skater's head, chest, shoulders or hips struck an object at speed (a pole, a +// tree, a rail: nearly stopped while the body was still flying). Followed as bone_cam_slams is. +std::uint64_t bone_cam_strikes() noexcept; // The X-ray's snapshot, with the latest camera (any thread). BoneCam bone_cam(); // Fresh native hips position while the normal on-board telemetry is absent. diff --git a/Extension/Throwdowns/throwdown_kit.cpp b/Extension/Throwdowns/throwdown_kit.cpp index 537a299e..ef35b5fb 100644 --- a/Extension/Throwdowns/throwdown_kit.cpp +++ b/Extension/Throwdowns/throwdown_kit.cpp @@ -516,6 +516,12 @@ Match match() noexcept { } return out; } +bool allows_teleports(std::string_view plugin, bool flag_mine) noexcept { + auto& s = store(); + std::lock_guard lock(s.mutex); + const auto entry = s.plugins.find(plugin); + return entry != s.plugins.end() && entry->second.documents.match.teleports && (entry->second.documents.hud.active || flag_mine); +} bool hidden(std::uint64_t player) noexcept { if (!player) return false; auto& s = store(); diff --git a/Extension/Throwdowns/throwdown_kit.h b/Extension/Throwdowns/throwdown_kit.h index fcb2563a..1edda3fd 100644 --- a/Extension/Throwdowns/throwdown_kit.h +++ b/Extension/Throwdowns/throwdown_kit.h @@ -222,6 +222,10 @@ std::optional documents_for(std::string_view mode); std::optional active(); // What every running match asks, merged (any plugin's `true` holds). Match match() noexcept; +// Whether `plugin`'s own match lets its teleports through where the session turned noclip off: its +// `match.teleports`, while that match runs (its HUD is up, or `flag_mine`: the native flag is one of +// its modes, which the caller knows from flag::state()). +bool allows_teleports(std::string_view plugin, bool flag_mine) noexcept; bool hidden(std::uint64_t player) noexcept; // ---- events ----------------------------------------------------------------------------------- diff --git a/Extension/UI/NativeMenu/throwdown_kit_hud.inl b/Extension/UI/NativeMenu/throwdown_kit_hud.inl index a529c447..d101aab7 100644 --- a/Extension/UI/NativeMenu/throwdown_kit_hud.inl +++ b/Extension/UI/NativeMenu/throwdown_kit_hud.inl @@ -1138,8 +1138,12 @@ void kit_hud(const Context& c) { auto& s=state();auto& h=s.kit_hud;const auto now=GetTickCount64(); const auto docs=kit::active(); const auto generation=docs && docs->hud.active?std::hash{}(docs->plugin)|1:0; - // A match that ended or another plugin's: everything goes, including a pending GO. - if(h.generation!=generation) {kit_hud_publish(c,{});h.generation=generation;h.go_until=0;h.was_counting=false;} + // A match that ended or another plugin's: everything goes, including a pending GO, and a banner + // or callout of the new one shows whatever serial it starts from. + if(h.generation!=generation) { + kit_hud_publish(c,{});h.generation=generation;h.go_until=0;h.was_counting=false; + h.callout_serial=h.banner_serial=~0ULL;h.callout_until=h.banner_until=0; + } if(!generation) {s.hud_doc.reset();kit_hud_publish(c,{});s.hud_ready.store(false);return;} const auto& hud=docs->hud; s.hud_doc=hud; @@ -1199,15 +1203,16 @@ void kit_hud(const Context& c) { if(hud.list.present && !hud.list.items.empty())piece("list",[&]{kit_list(c,hud,styled);append(h.list_item);}); } if(!hud.warning.text.empty())piece("warning",[&]{kit_warning(c,hud);append(h.warning_item);}); - // A callout and a banner each show for their time after a new serial. - if(hud.callout.serial!=h.callout_serial) { + // A callout and a banner each show for their time after a new serial. A document without the + // slot leaves the serial as it was: the same one coming back is not news. + if(!hud.callout.text.empty() && hud.callout.serial!=h.callout_serial) { h.callout_serial=hud.callout.serial; - if(!hud.callout.text.empty())h.callout_until=now+(hud.callout.ms?hud.callout.ms:2500),h.callout_text=hud.callout.text; + h.callout_until=now+(hud.callout.ms?hud.callout.ms:2500);h.callout_text=hud.callout.text; } if(now(std::max(1,d->players)); options.setup_page=true; options.cursor_height=d->cursor_height; // the registration's, e.g. a flag that may stand on anything + // The native registration's limit is 1 s to an hour. A mode's timer of 0 (none) or past an hour + // keeps its card working: the plugin runs its own clock. if(const auto setup=current_setup(d->id);setup && setup->timer.present) - options.seconds=static_cast(std::max(0.f,setup->timer.value)); + options.seconds=static_cast(std::clamp(std::round(setup->timer.value),1.f,3600.f)); kit::set_current(d->id); if(!kit::flag::begin(d->id,game,options)) { - logging::log(logging::Level::info,logging::Channel::ui,"Throwdowns: {} not opened, another flag is busy.",d->id); + logging::log(logging::Level::info,logging::Channel::ui, + "Throwdowns: {} not opened: there is no world yet, or another flag is being placed or waits.",d->id); return; } kit::post(d->id,"open",{std::to_string(game)}); diff --git a/contrib/plugins/SmokeyDawg58-ReSkate_Throwdown/plugin/td/placing.lua b/contrib/plugins/SmokeyDawg58-ReSkate_Throwdown/plugin/td/placing.lua index 3f9163d8..60d36325 100644 --- a/contrib/plugins/SmokeyDawg58-ReSkate_Throwdown/plugin/td/placing.lua +++ b/contrib/plugins/SmokeyDawg58-ReSkate_Throwdown/plugin/td/placing.lua @@ -9,12 +9,11 @@ local RA = require("td/h/race_arrow_placement") -- game_modes.cpp:1445. local pad_up, pad_down, pad_left, pad_right = 0x1, 0x2, 0x4, 0x8 --- Windows virtual keys (WinUser.h) and WHEEL_DELTA. +-- Windows virtual keys (WinUser.h). local VK_LBUTTON, VK_BACK, VK_RETURN, VK_ESCAPE = 0x01, 0x08, 0x0D, 0x1B local VK_PRIOR, VK_NEXT, VK_END = 0x21, 0x22, 0x23 local VK_OEM_4, VK_OEM_6 = 0xDB, 0xDD local KEY_F, KEY_G, KEY_R, KEY_X, KEY_Z = 0x46, 0x47, 0x52, 0x58, 0x5A -local WHEEL_DELTA = 120 local band, bor, bnot = bit32.band, bit32.bor, bit32.bnot @@ -200,7 +199,7 @@ function M.run_placing(s, now) local mouse = s.free_place local keys = {held(VK_RETURN) or (mouse and held(VK_LBUTTON)), held(VK_BACK), held(VK_END) or (mouse and held(KEY_F)), held(VK_ESCAPE)} local wheel = reskate.input.wheel() -- only gathered while the free camera flies - local notches = wheel / WHEEL_DELTA + local notches = wheel -- reskate.input.wheel counts whole notches already local struck = {false, false, false, false} for i = 1, #keys do struck[i] = keys[i] and not s.keys_previous[i] end s.keys_previous = keys