mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 16:25:58 +02:00
Plugins: trick events (trick_started, trick_landed, line_broken) and reskate.score.current
- With events.tricks a plugin hears the local skater's tricks: trick_started (where), trick_landed (points, multiplier, where it started and landed, how long it took) and line_broken (a slam or a wipeout); reskate.score.current() is the score HUD's line, multiplier and whether a trick is going on. Without the permission the events cannot be registered. - The score HUD is read into the trick watch (Extension/Skater/score_hud.h, trick_watch.h) on the client tick only while a running plugin has events.tricks; a new level starts a new line. plugins::start takes the image base for it. - reskate-test has a tricks module (skipped until a trick lands) and ptest trick, which waits 30 s for one and prints what its events said; reskate-hostile has trick-loop. Not yet checked against real tricks in game.
This commit is contained in:
1 parent
373c141381
commit
f37abef426
13 files changed
+333
-15
No files matched your search
@@ -31,6 +31,8 @@ struct TestEnvironment final : Environment {
|
||||
std::vector<std::pair<Vec3, float>> teleports;
|
||||
int rays{};
|
||||
std::optional<SkaterState> skater() override { return local_skater; }
|
||||
std::optional<ScoreState> score_now;
|
||||
std::optional<ScoreState> score() override { return score_now; }
|
||||
Teleport teleport(const Vec3& position, float heading) override {
|
||||
if (teleport_forbidden) return Teleport::forbidden;
|
||||
if (teleport_unavailable || !local_skater) return Teleport::unavailable;
|
||||
@@ -455,6 +457,53 @@ void the_skater_can_be_read_moved_and_the_world_probed() {
|
||||
running(host, "mover", environment);
|
||||
}
|
||||
|
||||
void tricks_reach_plugins_that_ask_for_them() {
|
||||
TestEnvironment environment;
|
||||
Host host(environment, Side::client);
|
||||
host.apply({script("bare", R"(
|
||||
local ok, err = pcall(reskate.on, "trick_landed", function() end)
|
||||
assert(not ok and err:find("events.tricks"), err)
|
||||
ok, err = pcall(reskate.score.current)
|
||||
assert(not ok and err:find("events.tricks"), err)
|
||||
reskate.log("bare refused")
|
||||
)")});
|
||||
check(environment.logged("bare refused") && !host.wants_tricks(), "Tricks need events.tricks, and none is read without it");
|
||||
host.apply({script("bare", "reskate.log('bare')"), script("tricks", R"(
|
||||
reskate.on("trick_started", function(e) reskate.log(string.format("started at %.0f %.0f %.0f", e.at.x, e.at.y, e.at.z)) end)
|
||||
reskate.on("trick_landed", function(e)
|
||||
reskate.log(string.format("landed %.0f x%.1f from %.0f to %.0f in %.0f ms", e.points, e.multiplier, e.started_at.x, e.landed_at.x, e.ms))
|
||||
end)
|
||||
reskate.on("line_broken", function(e) reskate.log("broken by a " .. e.reason) end)
|
||||
reskate.console.command("score", "x", function()
|
||||
local s = reskate.score.current()
|
||||
reskate.log(s and string.format("score %.0f x%.1f %s", s.line, s.multiplier, tostring(s.in_trick)) or "no score")
|
||||
end)
|
||||
)", R"("console", "events.tricks")")});
|
||||
check(host.wants_tricks(), "With a plugin that has events.tricks, tricks are read");
|
||||
TrickNews started;
|
||||
started.started_at = {1, 2, 3};
|
||||
host.trick(started);
|
||||
TrickNews landed;
|
||||
landed.kind = TrickNews::Kind::landed;
|
||||
landed.points = 250;
|
||||
landed.multiplier = 2;
|
||||
landed.started_at = {1, 2, 3};
|
||||
landed.landed_at = {9, 2, 3};
|
||||
landed.ms = 1200;
|
||||
host.trick(landed);
|
||||
TrickNews broken;
|
||||
broken.kind = TrickNews::Kind::broken;
|
||||
broken.reason = "slam";
|
||||
host.trick(broken);
|
||||
check(environment.logged("[tricks] started at 1 2 3") && environment.logged("[tricks] landed 250 x2.0 from 1 to 9 in 1200 ms") &&
|
||||
environment.logged("[tricks] broken by a slam"), "Each trick event reaches it, with its values\n" + environment.all());
|
||||
host.run_console("score");
|
||||
environment.score_now = ScoreState{708, 2, true};
|
||||
host.run_console("score");
|
||||
check(environment.logged("[tricks] no score") && environment.logged("[tricks] score 708 x2.0 true"), "and the score HUD as it is");
|
||||
running(host, "tricks", environment);
|
||||
}
|
||||
|
||||
void level_events_reach_plugins() {
|
||||
TestEnvironment environment;
|
||||
Host host(environment, Side::client);
|
||||
@@ -554,7 +603,7 @@ void reskate_test_passes_its_own_checks() {
|
||||
check(host.run_console("ptest all"), "ptest is its command");
|
||||
ticks(host, environment, 120); // its later checks run on timers
|
||||
const auto output = printed_lines(environment);
|
||||
check(output.find("ptest: 46 passed, 0 failed, 4 skipped") != std::string::npos &&
|
||||
check(output.find("ptest: 47 passed, 0 failed, 7 skipped") != std::string::npos &&
|
||||
output.find("FAIL") == std::string::npos, "Every reskate-test check passes:\n" + output + environment.all());
|
||||
check(host.run_console("ptest echo a b") && environment.printed("ptest echo: a b"), "and its echo command answers");
|
||||
// With a skater on the ground, its skater checks run too.
|
||||
@@ -563,6 +612,16 @@ void reskate_test_passes_its_own_checks() {
|
||||
check(host.run_console("ptest skater"), "ptest skater");
|
||||
check(environment.printed("ptest: 5 passed, 0 failed (skater 5/5)"), "The skater checks pass with a skater:\n" + printed_lines(environment));
|
||||
check(host.run_console("ptest tp") && environment.teleports.size() == 1, "and ptest tp teleports it");
|
||||
// ptest trick waits for a trick and says what its events held.
|
||||
check(host.run_console("ptest trick"), "ptest trick");
|
||||
host.trick({TrickNews::Kind::started, 0, 0, {1, 0, 1}});
|
||||
host.trick({TrickNews::Kind::landed, 250, 2, {1, 0, 1}, {4, 0, 5}, 900});
|
||||
check(environment.printed("ptest trick: started at (1.0, 0.0, 1.0)") &&
|
||||
environment.printed("ptest trick: landed 250 points at x2.0 in 900 ms, from (1.0, 0.0, 1.0) to (4.0, 0.0, 5.0)"),
|
||||
"ptest trick prints the trick");
|
||||
environment.lines.clear();
|
||||
check(host.run_console("ptest tricks") && environment.printed("ptest: 3 passed, 0 failed, 1 skipped"),
|
||||
"and the tricks checks pass once one landed");
|
||||
running(host, "reskate-test", environment);
|
||||
}
|
||||
|
||||
@@ -571,17 +630,18 @@ void reskate_hostile_is_stopped_or_refused_every_time() {
|
||||
Host host(environment, Side::client);
|
||||
host.apply({contributed("reskate-hostile")});
|
||||
running(host, "reskate-hostile", environment);
|
||||
struct Case { const char* attack; bool stops; int ticks; };
|
||||
struct Case { const char* attack; bool stops; int ticks; bool trick = false; };
|
||||
const Case cases[] = {
|
||||
{"loop", true, 0}, {"caught-loop", true, 0}, {"tick-loop", true, 1}, {"memory", true, 0},
|
||||
{"caught-memory", true, 0}, {"huge-string", true, 0}, {"buffer", true, 0}, {"coroutines", true, 0},
|
||||
{"errors", true, 10}, {"slow", true, 60 * 5}, {"log-flood", false, 1}, {"timers", false, 1},
|
||||
{"recursion", false, 1}, {"escapes", false, 1}, {"teleport-spam", false, 1}, {"raycast-flood", false, 1},
|
||||
{"recursion", false, 1}, {"escapes", false, 1}, {"teleport-spam", false, 1}, {"raycast-flood", false, 1}, {"trick-loop", true, 0, true},
|
||||
};
|
||||
for (const auto& item : cases) {
|
||||
check(host.reload("reskate-hostile").find("reloaded") != std::string::npos, std::string("reloads before ") + item.attack);
|
||||
check(host.run_console(std::string("hostile ") + item.attack), item.attack);
|
||||
ticks(host, environment, item.ticks);
|
||||
if (item.trick) host.trick({TrickNews::Kind::landed, 100, 1});
|
||||
const auto state = status_of(host, "reskate-hostile").state;
|
||||
check((state == "stopped") == item.stops, std::string(item.attack) + " left it " + state + "\n" + environment.all());
|
||||
}
|
||||
@@ -611,6 +671,7 @@ int main() {
|
||||
a_plugin_over_its_time_budget_is_stopped();
|
||||
a_plugin_reloads_when_its_files_change();
|
||||
the_skater_can_be_read_moved_and_the_world_probed();
|
||||
tricks_reach_plugins_that_ask_for_them();
|
||||
level_events_reach_plugins();
|
||||
timers_run_when_due();
|
||||
a_package_with_a_program_or_on_the_wrong_side_is_not_loaded();
|
||||
|
||||
@@ -13,7 +13,14 @@
|
||||
// luaL_error, or a C++ exception, which Luau turns into a Lua error with its message.
|
||||
namespace dingosdk::plugins {
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 5> events{"load", "unload", "tick", "level_loaded", "level_unloading"};
|
||||
// Events and the permission each needs (none for the base set).
|
||||
struct Event {
|
||||
std::string_view name, permission;
|
||||
};
|
||||
constexpr std::array<Event, 8> events{{
|
||||
{"load", {}}, {"unload", {}}, {"tick", {}}, {"level_loaded", {}}, {"level_unloading", {}},
|
||||
{"trick_started", "events.tricks"}, {"trick_landed", "events.tricks"}, {"line_broken", "events.tricks"},
|
||||
}};
|
||||
constexpr std::size_t maximum_key = 128;
|
||||
constexpr std::size_t maximum_command = 48;
|
||||
constexpr std::size_t maximum_listing = 1000;
|
||||
@@ -197,8 +204,10 @@ struct ApiFunctions {
|
||||
auto& plugin = plugin_of(L);
|
||||
const std::string event = luaL_checkstring(L, 1);
|
||||
luaL_checktype(L, 2, LUA_TFUNCTION);
|
||||
if (std::find(events.begin(), events.end(), event) == events.end())
|
||||
luaL_error(L, "there is no event \"%s\" in this ReSkate", event.c_str());
|
||||
const auto known = std::find_if(events.begin(), events.end(), [&](const Event& e) { return e.name == event; });
|
||||
if (known == events.end()) luaL_error(L, "there is no event \"%s\" in this ReSkate", event.c_str());
|
||||
if (!known->permission.empty() && !plugin.has(known->permission))
|
||||
luaL_error(L, "the %s event needs the \"%s\" permission in reskate-plugin.json", event.c_str(), std::string(known->permission).c_str());
|
||||
auto& list = plugin.handlers_[event];
|
||||
if (list.size() >= plugin.limits_.handlers) luaL_error(L, "more than %d handlers for one event", static_cast<int>(plugin.limits_.handlers));
|
||||
lua_pushvalue(L, 2);
|
||||
@@ -493,6 +502,24 @@ struct ApiFunctions {
|
||||
else lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
// events.tricks: the score HUD now (the line's score, its multiplier, a trick going on), or nil.
|
||||
static int score(lua_State* L) {
|
||||
auto& plugin = plugin_of(L);
|
||||
need(L, plugin, "events.tricks", "reskate.score.current");
|
||||
const auto now = plugin.services_.score();
|
||||
if (!now) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
lua_createtable(L, 0, 3);
|
||||
lua_pushnumber(L, now->line);
|
||||
lua_setfield(L, -2, "line");
|
||||
lua_pushnumber(L, now->multiplier);
|
||||
lua_setfield(L, -2, "multiplier");
|
||||
lua_pushboolean(L, now->in_trick);
|
||||
lua_setfield(L, -2, "in_trick");
|
||||
return 1;
|
||||
}
|
||||
// Modules come from the plugin's own package, beside its entry: require("./rules") or
|
||||
// require("rules") is <entry folder>/rules.luau or rules.lua. Each runs once.
|
||||
static int require(lua_State* L) {
|
||||
@@ -620,6 +647,11 @@ void Api::install(Plugin& plugin, lua_State* L) {
|
||||
readonly();
|
||||
lua_setfield(L, -2, "skater");
|
||||
|
||||
lua_createtable(L, 0, 1);
|
||||
set_function(L, "current", ApiFunctions::score);
|
||||
readonly();
|
||||
lua_setfield(L, -2, "score");
|
||||
|
||||
lua_createtable(L, 0, 1);
|
||||
set_function(L, "raycast", ApiFunctions::raycast);
|
||||
readonly();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "plugin_host.h"
|
||||
#include "plugin_api.h"
|
||||
#include "Engine/Core/Console/console_core.h"
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
|
||||
@@ -130,6 +131,19 @@ void Host::set_level(std::string level) {
|
||||
}
|
||||
}
|
||||
|
||||
void Host::trick(const TrickNews& news) {
|
||||
for (auto& [id, entry] : entries_) {
|
||||
if (!entry.plugin || !entry.plugin->has("events.tricks")) continue;
|
||||
entry.plugin->trick(news);
|
||||
collect(entry);
|
||||
}
|
||||
}
|
||||
|
||||
bool Host::wants_tricks() const {
|
||||
return std::any_of(entries_.begin(), entries_.end(),
|
||||
[](const auto& item) { return item.second.plugin && item.second.plugin->has("events.tricks"); });
|
||||
}
|
||||
|
||||
bool Host::run_console(std::string_view text) {
|
||||
if (commands_.empty()) return false;
|
||||
const auto parsed = parse_console_command(text);
|
||||
|
||||
@@ -35,6 +35,7 @@ public:
|
||||
virtual std::optional<SkaterState> skater() { return std::nullopt; }
|
||||
virtual Teleport teleport(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; }
|
||||
};
|
||||
|
||||
struct Status {
|
||||
@@ -58,6 +59,10 @@ public:
|
||||
void apply(const std::vector<Candidate>& found);
|
||||
void tick(double dt_ms);
|
||||
void set_level(std::string level);
|
||||
// A trick of the local skater, to every plugin with events.tricks.
|
||||
void trick(const TrickNews& news);
|
||||
// Whether a running plugin has events.tricks: the score HUD is read only then.
|
||||
bool wants_tricks() const;
|
||||
// Runs a plugin's console command when the text starts with one; false when none matched.
|
||||
bool run_console(std::string_view text);
|
||||
std::vector<Status> status() const;
|
||||
@@ -91,6 +96,7 @@ private:
|
||||
std::optional<SkaterState> skater() override { return environment_.skater(); }
|
||||
Teleport teleport(const Vec3& position, float heading) override { return environment_.teleport(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(); }
|
||||
|
||||
Environment& environment_;
|
||||
Side side_;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "Engine/Game/Multiplayer/session_tools.h"
|
||||
#include "Extension/Console/commands.h"
|
||||
#include "Extension/Profile/local_profile_runtime.h"
|
||||
#include "Extension/Skater/score_hud.h"
|
||||
#include "Extension/Skater/trick_watch.h"
|
||||
#include "Extension/Trainer/trainer.h"
|
||||
#include <Windows.h>
|
||||
#include <ShlObj.h>
|
||||
@@ -53,6 +55,8 @@ public:
|
||||
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); }
|
||||
std::optional<ScoreState> score() override { return score_now; }
|
||||
std::optional<ScoreState> score_now; // read each tick while a plugin wants tricks
|
||||
|
||||
private:
|
||||
std::filesystem::path storage_;
|
||||
@@ -68,6 +72,55 @@ std::vector<Candidate> found; // under found_mutex
|
||||
std::uint64_t found_generation{}; // under found_mutex
|
||||
std::uint64_t applied_generation{};
|
||||
std::chrono::steady_clock::time_point last_tick{};
|
||||
std::uintptr_t base{};
|
||||
// The local skater's tricks (Extension/Skater/trick_watch.h), and where and when the one going on began.
|
||||
tricks::TrickWatch trick_watch;
|
||||
Vec3 started_at{};
|
||||
std::chrono::steady_clock::time_point started_time{};
|
||||
std::string watched_level;
|
||||
|
||||
// The score HUD into the trick watch, and what it saw to the plugins. Client thread.
|
||||
void read_tricks() {
|
||||
const auto hud = tricks::read_score_hud(base);
|
||||
if (!hud) {
|
||||
environment->score_now.reset();
|
||||
return;
|
||||
}
|
||||
const auto skater = trainer::telemetry();
|
||||
tricks::TrickSignals signals;
|
||||
signals.action = hud->action;
|
||||
signals.line_score = hud->line_score;
|
||||
signals.multiplier = hud->multiplier;
|
||||
signals.slam = hud->slam;
|
||||
// A wipeout is physics states 300-399 (as Domination reads it).
|
||||
signals.wipeout = skater.skater && skater.physics_state >= 300 && skater.physics_state < 400;
|
||||
const auto event = trick_watch.update(signals);
|
||||
environment->score_now = ScoreState{hud->line_score, hud->multiplier, trick_watch.in_trick()};
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
TrickNews news;
|
||||
switch (event.kind) {
|
||||
case tricks::TrickEvent::Kind::none: return;
|
||||
case tricks::TrickEvent::Kind::started:
|
||||
started_at = skater.position;
|
||||
started_time = now;
|
||||
news.kind = TrickNews::Kind::started;
|
||||
news.started_at = started_at;
|
||||
break;
|
||||
case tricks::TrickEvent::Kind::landed:
|
||||
news.kind = TrickNews::Kind::landed;
|
||||
news.points = event.points;
|
||||
news.multiplier = event.multiplier;
|
||||
news.started_at = started_at;
|
||||
news.landed_at = skater.position;
|
||||
news.ms = std::chrono::duration<double, std::milli>(now - started_time).count();
|
||||
break;
|
||||
case tricks::TrickEvent::Kind::broken:
|
||||
news.kind = TrickNews::Kind::broken;
|
||||
news.reason = signals.slam ? "slam" : "wipeout";
|
||||
break;
|
||||
}
|
||||
host->trick(news);
|
||||
}
|
||||
|
||||
void watch() {
|
||||
FileChecks checks;
|
||||
@@ -96,9 +149,10 @@ std::filesystem::path local_storage() {
|
||||
}
|
||||
}
|
||||
|
||||
bool start() noexcept {
|
||||
bool start(std::uintptr_t image_base) noexcept {
|
||||
try {
|
||||
if (host) return true;
|
||||
base = image_base;
|
||||
std::array<wchar_t, 32768> path{};
|
||||
const auto length = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
|
||||
if (!length || length >= path.size()) return false;
|
||||
@@ -136,6 +190,8 @@ void on_client_tick() noexcept {
|
||||
const double dt = last_tick == std::chrono::steady_clock::time_point{} ? 0
|
||||
: std::chrono::duration<double, std::milli>(now - last_tick).count();
|
||||
last_tick = now;
|
||||
if (host->wants_tricks()) read_tricks();
|
||||
else environment->score_now.reset();
|
||||
host->tick(dt);
|
||||
} catch (const std::exception& error) {
|
||||
logging::log(logging::Level::error, logging::Channel::plugins, "Plugin tick failed: {}", error.what());
|
||||
@@ -144,7 +200,14 @@ void on_client_tick() noexcept {
|
||||
|
||||
void set_level(std::string_view level) noexcept {
|
||||
if (!host) return;
|
||||
try { host->set_level(std::string(level)); }
|
||||
try {
|
||||
// A new level starts a new line: a trick going on before it scores nothing.
|
||||
if (level != watched_level) {
|
||||
watched_level = std::string(level);
|
||||
trick_watch = tricks::TrickWatch{};
|
||||
}
|
||||
host->set_level(std::string(level));
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
logging::log(logging::Level::error, logging::Channel::plugins, "Plugin level change failed: {}", error.what());
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -8,8 +9,9 @@
|
||||
//
|
||||
// Every function but start is for the client thread, the one that runs console commands.
|
||||
namespace dingosdk::plugins {
|
||||
// Once, at startup. Plugins load at the first client tick after discovery finds them.
|
||||
bool start() noexcept;
|
||||
// Once, at startup, with the game's image base (the score HUD is read from it). Plugins load at the
|
||||
// first client tick after discovery finds them.
|
||||
bool start(std::uintptr_t image_base) noexcept;
|
||||
void on_client_tick() noexcept;
|
||||
// The level being played, or empty without one: plugins hear level_loaded and level_unloading.
|
||||
void set_level(std::string_view level) noexcept;
|
||||
|
||||
@@ -281,6 +281,13 @@ void Plugin::unload() {
|
||||
}
|
||||
|
||||
void Plugin::fire(std::string_view event, const std::vector<std::string>& arguments) {
|
||||
fire_with(event, [&](lua_State* L) {
|
||||
for (const auto& argument : arguments) lua_pushlstring(L, argument.data(), argument.size());
|
||||
return static_cast<int>(arguments.size());
|
||||
});
|
||||
}
|
||||
|
||||
void Plugin::fire_with(std::string_view event, const std::function<int(lua_State*)>& push) {
|
||||
if (state_ != State::running) return;
|
||||
const auto found = handlers_.find(event);
|
||||
if (found == handlers_.end()) return;
|
||||
@@ -289,8 +296,54 @@ void Plugin::fire(std::string_view event, const std::vector<std::string>& argume
|
||||
for (const int handler : handlers) {
|
||||
if (state_ != State::running) return;
|
||||
lua_getref(L_, handler);
|
||||
for (const auto& argument : arguments) lua_pushlstring(L_, argument.data(), argument.size());
|
||||
call(static_cast<int>(arguments.size()), what, limits_.call_ms);
|
||||
int count = 0;
|
||||
try {
|
||||
count = push(L_);
|
||||
} catch (const std::exception& error) {
|
||||
// Out of memory building the event: the VM is closed next, so its stack is left as it is.
|
||||
stop(std::string("its event could not be handed over: ") + error.what());
|
||||
return;
|
||||
}
|
||||
call(count, what, limits_.call_ms);
|
||||
}
|
||||
}
|
||||
|
||||
void Plugin::trick(const TrickNews& news) {
|
||||
if (state_ != State::running || !has("events.tricks")) return;
|
||||
const auto vector = [](lua_State* L, const Vec3& v) { lua_pushvector(L, v[0], v[1], v[2]); };
|
||||
switch (news.kind) {
|
||||
case TrickNews::Kind::started:
|
||||
fire_with("trick_started", [&](lua_State* L) {
|
||||
lua_createtable(L, 0, 1);
|
||||
vector(L, news.started_at);
|
||||
lua_setfield(L, -2, "at");
|
||||
return 1;
|
||||
});
|
||||
break;
|
||||
case TrickNews::Kind::landed:
|
||||
fire_with("trick_landed", [&](lua_State* L) {
|
||||
lua_createtable(L, 0, 5);
|
||||
lua_pushnumber(L, news.points);
|
||||
lua_setfield(L, -2, "points");
|
||||
lua_pushnumber(L, news.multiplier);
|
||||
lua_setfield(L, -2, "multiplier");
|
||||
vector(L, news.started_at);
|
||||
lua_setfield(L, -2, "started_at");
|
||||
vector(L, news.landed_at);
|
||||
lua_setfield(L, -2, "landed_at");
|
||||
lua_pushnumber(L, news.ms);
|
||||
lua_setfield(L, -2, "ms");
|
||||
return 1;
|
||||
});
|
||||
break;
|
||||
case TrickNews::Kind::broken:
|
||||
fire_with("line_broken", [&](lua_State* L) {
|
||||
lua_createtable(L, 0, 1);
|
||||
lua_pushlstring(L, news.reason.data(), news.reason.size());
|
||||
lua_setfield(L, -2, "reason");
|
||||
return 1;
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
@@ -55,6 +56,18 @@ struct SkaterState {
|
||||
float air_time{}; // seconds into the jump going on
|
||||
};
|
||||
enum class Teleport { sent, unavailable, forbidden };
|
||||
// A trick of the local skater, read from the score HUD (Extension/Skater/trick_watch.h).
|
||||
struct TrickNews {
|
||||
enum class Kind { started, landed, broken } kind{Kind::started};
|
||||
float points{}, multiplier{}; // landed
|
||||
Vec3 started_at{}, landed_at{};
|
||||
double ms{}; // landed: from its start
|
||||
std::string reason; // broken: "slam" or "wipeout"
|
||||
};
|
||||
struct ScoreState {
|
||||
float line{}, multiplier{};
|
||||
bool in_trick{};
|
||||
};
|
||||
|
||||
// What a plugin's VM can reach outside itself. The host implements it.
|
||||
class Services {
|
||||
@@ -72,6 +85,8 @@ public:
|
||||
virtual Teleport teleport(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.
|
||||
virtual std::optional<ScoreState> score() = 0;
|
||||
};
|
||||
|
||||
enum class State { loading, running, stopped };
|
||||
@@ -95,6 +110,8 @@ public:
|
||||
|
||||
void tick(double dt_ms);
|
||||
void level_changed(const std::string& previous, const std::string& current);
|
||||
// trick_started, trick_landed or line_broken, to a plugin with events.tricks.
|
||||
void trick(const TrickNews& news);
|
||||
// A console command it registered: the words after the command's name.
|
||||
void run_command(const std::string& name, const std::vector<std::string>& arguments);
|
||||
void save_storage();
|
||||
@@ -133,6 +150,8 @@ private:
|
||||
// The compiled chunk of a source file on the main stack, or false and the error on it.
|
||||
bool load_chunk(lua_State* L, const std::string& relative, const std::filesystem::path& file);
|
||||
void fire(std::string_view event, const std::vector<std::string>& arguments);
|
||||
// Each handler of `event` with what `push` puts on the stack (it returns how many values).
|
||||
void fire_with(std::string_view event, const std::function<int(lua_State*)>& push);
|
||||
void close();
|
||||
|
||||
std::string id_;
|
||||
|
||||
Reference in new issue
Block a user