Files
ReSkate/Extension/Plugins/plugin_vm.h
T
AyeZeeBB f37abef426 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.
2026-10-19 12:43:44 -05:00

198 lines
8.4 KiB
C++

#pragma once
#include "plugin_manifest.h"
#include "Engine/Core/Json/json.h"
#include <array>
#include <chrono>
#include <cstddef>
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
struct lua_State;
// One plugin: its own Luau VM, the reskate API registered in it, and the limits that keep it from
// harming the game. Nothing a plugin does reaches the game except through the API, and every
// limit it goes over stops it, not the game.
//
// Not thread safe: a plugin is loaded, ticked and unloaded on one thread (the client tick).
namespace dingosdk::plugins {
enum class LogLevel { info, warn, error };
struct Limits {
std::size_t memory = 64u << 20; // the VM's heap
double call_ms = 50; // one call into the plugin (load, a handler, a command)
double unload_ms = 20; // its unload handlers, together
double tick_ms = 2; // the average time a tick, over a window of 1 s...
int over_windows = 3; // ...over it this many windows in a row stops it
int errors = 5; // more errors than this...
double error_window_ms = 10'000; // ...in this long stops it
int log_lines_per_second = 50;
std::size_t storage_bytes = 1u << 20;
std::size_t file_bytes = 16u << 20; // a package file read
std::size_t source_bytes = 1u << 20; // a source file
std::size_t handlers = 256; // per event
std::size_t timers = 1024;
std::size_t commands = 64;
int raycasts_per_tick = 64;
float raycast_metres = 1000; // the longest ray
int teleports_per_second = 2;
};
// The game, as plugins see it (Environment, plugin_host.h, gives the host these).
using Vec3 = std::array<float, 3>;
struct SkaterState {
Vec3 position{};
// Radians about world up, -pi to pi: 0 faces +Z, pi/2 faces +X, and a higher heading is turned
// to the skater's left (checked in game). Teleports take it the same way.
float heading{};
float speed{}; // metres per second over the ground
float vertical{}; // metres per second, up
bool airborne{};
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 {
public:
virtual ~Services() = default;
virtual void log(std::string_view plugin, LogLevel level, std::string_view text) = 0;
virtual void console(std::string_view text) = 0;
virtual double now_ms() = 0; // a steady clock, for reskate.time and the windows
virtual std::string level() = 0; // the level being played, or empty
virtual std::filesystem::path storage_file(std::string_view plugin) = 0;
// Empty when the plugin may have the console command; otherwise why not.
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;
// 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 };
class Plugin {
public:
// `directory` is the mod folder; `id` its folder name.
Plugin(std::string id, std::filesystem::path directory, Manifest manifest, Services& services, Limits limits);
~Plugin();
Plugin(const Plugin&) = delete;
Plugin& operator=(const Plugin&) = delete;
// Creates the VM, runs the entry (or `entry` in its place, for the test runner), then the load
// handlers. False when it stopped; reason() says why.
bool load(std::string entry = {});
// The unload handlers (while it runs), its storage saved, the VM closed. Safe to call twice.
void unload();
// Stops it at the next safe point: the VM is closed by close_if_stopped().
void stop(std::string reason);
void close_if_stopped();
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();
const std::string& id() const noexcept { return id_; }
const std::filesystem::path& directory() const noexcept { return directory_; }
const Manifest& manifest() const noexcept { return manifest_; }
State state() const noexcept { return state_; }
const std::string& reason() const noexcept { return reason_; }
std::size_t memory() const noexcept { return memory_; }
double tick_ms() const noexcept { return last_window_ms_; }
// Its console commands and their help.
std::vector<std::pair<std::string, std::string>> commands() const;
bool has(std::string_view permission) const { return manifest_.permissions.contains(permission); }
// Over a limit or stopped: the API refuses every call until the VM is closed.
bool halted() const noexcept { return halt_ != Halt::none || memory_exceeded_; }
private:
friend struct Api;
friend struct ApiFunctions;
struct Command { std::string help; int function{}; };
struct Timer { double due{}, every{}; int function{}; };
using Clock = std::chrono::steady_clock;
enum class Halt { none, time, memory, stopped };
static void* allocate(void* ud, void* ptr, std::size_t old_size, std::size_t new_size);
static void interrupt(lua_State* L, int gc);
// Calls the function under the `count` arguments on top of the main stack; false (and the
// error reported) when it raised one. Stops the plugin past its limits.
bool call(int count, std::string_view what, double limit_ms);
void report(std::string_view what, std::string_view message);
// reskate.log and console.print: at most limits_.log_lines_per_second lines between them.
bool count_line();
void log(LogLevel level, std::string_view text);
void print(std::string_view text);
// 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_;
std::filesystem::path directory_;
std::string code_root_; // the entry's folder in the package: where require looks
Manifest manifest_;
Services& services_;
Limits limits_;
lua_State* L_{};
int script_thread_{}, modules_{};
State state_{State::loading};
std::string reason_;
std::size_t memory_{};
Halt halt_{Halt::none};
bool memory_exceeded_{};
Clock::time_point deadline_{};
bool timed_{};
unsigned interrupts_{};
double spent_ms_{};
bool window_started_{};
double window_start_{};
double window_spent_{};
int window_ticks_{}, over_windows_{};
double last_window_ms_{};
std::vector<double> errors_;
double log_second_{};
int log_lines_{};
int raycasts_{}; // since the last tick
double teleport_second_{};
int teleports_{};
std::map<std::string, std::vector<int>, std::less<>> handlers_;
std::map<int, Timer> timers_;
int next_timer_{1};
std::map<std::string, Command, std::less<>> commands_;
std::set<std::string, std::less<>> loading_modules_;
Json storage_ = Json::object();
bool storage_loaded_{}, storage_dirty_{};
};
}