Throwdowns: fixes the docs review found in the kit and its services

- match.teleports lets through only the publishing plugin's own teleports, and only while its
  match runs (its HUD is active, or the native flag is one of its modes); it was merged across
  every plugin and checked no match.
- A mode's timer of 0 or past an hour no longer makes its card do nothing: the native flag takes
  it as 1 to 3600 s. flag.begin's seconds are checked as 1 to 3600, as the flag takes them.
- skater.push and hold(true) return false with a reason without the stock physics, where they
  were dropped unseen.
- telemetry's slam and struck are news since each plugin's own last read; reading them cleared
  them for every plugin.
- A banner or callout of a new match shows whatever serial it starts from, and a document without
  the slot no longer resets its serial.
- SmokeyDawg58's placement no longer divides input.wheel's whole notches by WHEEL_DELTA again.
This commit is contained in:
AyeZeeBB committed 2026-10-20 13:30:27 -05:00
1 parent efeae7e07d
commit bf693dbe5c
17 files changed
+81 -42

No files matched your search

+2 -2
View File
@@ -63,7 +63,7 @@ struct FakeGame final : Game {
void board_park(std::string_view plugin, std::vector<int> bones) override { calls.push_back(std::format("board park {} {}", plugin, bones.size())); }
// The general services a game layer in Lua reads and drives (plugin_api_services.cpp).
std::optional<SkaterTelemetry> telemetry;
std::optional<SkaterTelemetry> skater_telemetry() override { return telemetry; }
std::optional<SkaterTelemetry> 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<HttpResponse> 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);
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -134,7 +134,7 @@ struct KitApi {
if (!lua_isnoneornil(L, 3)) {
luaL_checktype(L, 3, LUA_TTABLE);
options.players = static_cast<unsigned>(number_field(L, 3, "players", options.players, 1, 10));
options.seconds = static_cast<unsigned>(number_field(L, 3, "seconds", options.seconds, 0, 3600));
options.seconds = static_cast<unsigned>(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<float>(number_field(L, 3, "cursor_height", options.cursor_height, 0, 100000));
}
+1 -1
View File
@@ -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");
+2 -1
View File
@@ -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<SkaterTelemetry> skater_telemetry() { return std::nullopt; }
// `plugin`: whose read it is (slam and struck are news since that plugin's last read).
virtual std::optional<SkaterTelemetry> 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<std::uint32_t> pad_bits(bool /*menu*/) { return std::nullopt; }
+17 -4
View File
@@ -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<SkaterTelemetry> skater_telemetry() override {
std::optional<SkaterTelemetry> 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<std::string> installed;
};
std::map<std::string, VariantRead, std::less<>> variant_reads_;
// The Bone Cam's slam and strike counts each plugin last read with its telemetry.
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>, std::less<>> telemetry_seen_;
using BindKey = std::pair<std::string, std::string>;
struct Bind {
std::uint32_t own{};
+4 -2
View File
@@ -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<SkaterState> 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<Vec3> raycast(const Vec3&, const Vec3&) { return std::nullopt; }
virtual std::optional<ScoreState> 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<SkaterState> 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<Vec3> raycast(const Vec3& from, const Vec3& to) override { return environment_.raycast(from, to); }
std::optional<ScoreState> score() override { return environment_.score(); }
void toast(std::string_view plugin, LogLevel level, std::string_view text) override { environment_.toast(plugin, level, text); }
+8 -3
View File
@@ -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<Vec3> raycast(const Vec3& from, const Vec3& to) override { return local_surface_hit(from, to); }
+2 -1
View File
@@ -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<SkaterState> 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<Vec3> raycast(const Vec3& from, const Vec3& to) = 0;
// The score HUD now, or empty without one.
+6 -6
View File
@@ -135,11 +135,11 @@ struct State {
// The hips' recent speeds, to notice the body stopping dead (the ground, a pole, a wall).
std::array<std::pair<std::uint64_t, float>, 24> hips_speeds{};
std::size_t hips_next{};
std::atomic<bool> slam_seen{};
std::atomic<std::uint64_t> 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<bool> struck_seen{};
std::atomic<std::uint64_t> strikes{};
std::uint64_t struck_time{};
std::array<std::uint8_t, watch_count> level{};
std::array<std::uint8_t, static_cast<std::size_t>(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) {
+7 -7
View File
@@ -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.
+6
View File
@@ -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();
+4
View File
@@ -222,6 +222,10 @@ std::optional<Documents> documents_for(std::string_view mode);
std::optional<Documents> 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 -----------------------------------------------------------------------------------
+12 -7
View File
@@ -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<std::string>{}(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<h.callout_until)piece("callout",[&]{kit_callout(c,hud);append(h.callout_item);});
if(hud.banner.serial!=h.banner_serial) {
if(!hud.banner.text.empty() && hud.banner.serial!=h.banner_serial) {
h.banner_serial=hud.banner.serial;
if(!hud.banner.text.empty())h.banner_until=now+(hud.banner.ms?hud.banner.ms:3000);
h.banner_until=now+(hud.banner.ms?hud.banner.ms:3000);
}
if(now<h.banner_until && !hud.banner.text.empty())piece("banner",[&]{kit_banner(c,hud);append(h.banner_item);});
if(editor)piece("editor",[&] {
@@ -252,11 +252,14 @@ void mode_open(const std::string& id) {
options.players=static_cast<unsigned>(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<unsigned>(std::max(0.f,setup->timer.value));
options.seconds=static_cast<unsigned>(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)});
@@ -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