From 02d34012aeac2b3fb1df5ba9105f896ec14dee52 Mon Sep 17 00:00:00 2001 From: vtf Date: Mon, 19 Oct 2026 20:09:30 -0400 Subject: [PATCH 1/5] Plugins: server plugins ban, unban; player permissions; reskate.Enums --- .gitignore | 4 + Extension/Plugins/plugin_api.cpp | 21 +++++ Extension/Plugins/plugin_api_world.cpp | 86 ++++++++++++++++++- Extension/Plugins/plugin_game.h | 10 +++ Server/Test/server_lock_tests.cpp | 4 +- Server/server_commands.cpp | 16 +++- Server/server_config.cpp | 4 +- Server/server_config.h | 5 +- Server/server_host.cpp | 2 +- Server/server_host.h | 6 +- Server/server_plugins.cpp | 32 ++++++- .../plugins/server-starter/plugin/main.lua | 14 +++ .../server-starter/reskate-plugin.json | 6 ++ 13 files changed, 198 insertions(+), 12 deletions(-) create mode 100644 contrib/plugins/server-starter/plugin/main.lua create mode 100644 contrib/plugins/server-starter/reskate-plugin.json diff --git a/.gitignore b/.gitignore index a31f8b1c..1f79901d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ analysis/raw/**/*.res /assets/models/fpv/fpv_drone.glb /contrib/plugins/SmokeyDawg58-ReSkate_Throwdown /contrib/plugins/*.fbmod + +# vtf's linux development configuration +.vscode/ +.clangd \ No newline at end of file diff --git a/Extension/Plugins/plugin_api.cpp b/Extension/Plugins/plugin_api.cpp index 242811c3..5efff252 100644 --- a/Extension/Plugins/plugin_api.cpp +++ b/Extension/Plugins/plugin_api.cpp @@ -600,6 +600,25 @@ void Api::install(Plugin& plugin, lua_State* L) { // "client" for the copy on a player's game, "server" for the session authority's copy. lua_pushstring(L, plugin.services_.authority() ? "server" : "client"); lua_setfield(L, -2, "side"); + // Named constants, read-only and built from the C++ lists so they cannot drift: + // reskate.Enums.Side.server is "server", reskate.Enums.Event.tick is "tick". Plain strings still work. + lua_createtable(L, 0, 2); + lua_createtable(L, 0, 3); + for (const auto side : {Side::client, Side::server, Side::both}) { + lua_pushstring(L, side_name(side)); + lua_setfield(L, -2, side_name(side)); + } + readonly(); + lua_setfield(L, -2, "Side"); + lua_createtable(L, 0, static_cast(events.size())); + for (const auto& event : events) { + lua_pushlstring(L, event.name.data(), event.name.size()); + lua_setfield(L, -2, std::string(event.name).c_str()); + } + readonly(); + lua_setfield(L, -2, "Event"); + readonly(); + lua_setfield(L, -2, "Enums"); set_function(L, "log", ApiFunctions::log); set_function(L, "on", ApiFunctions::on); @@ -697,5 +716,7 @@ void push_player(lua_State* L, const SessionPlayer& player) { lua_setfield(L, -2, "host"); lua_pushboolean(L, player.server); lua_setfield(L, -2, "server"); + lua_getfield(L, LUA_REGISTRYINDEX, "reskate.player"); // its permissions (plugin_api_world.cpp) + lua_setmetatable(L, -2); } } diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index d0cd307a..557737a9 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -1006,6 +1006,73 @@ struct WorldApi { const auto player = player_of(L, 1); return result(L, game(plugin).move_player(player, position_of(L, 2, "where the player goes"))); } + static int server_ban(lua_State* L) { + auto& plugin = authority(L, "reskate.server.ban"); + return result(L, game(plugin).ban(player_of(L, 1))); + } + static int server_unban(lua_State* L) { + auto& plugin = authority(L, "reskate.server.unban"); + return result(L, game(plugin).unban(player_of(L, 1))); + } + // A player's permissions: player.permissions is what the server holds for them now, read-only; + // permissions:clone() is a copy to change; player:apply_permissions(copy) makes it theirs. + static constexpr std::array, 5> permission_fields{{ + {"noclip", &Game::Permissions::noclip}, {"nobail", &Game::Permissions::nobail}, {"boosts", &Game::Permissions::boosts}, + {"tuning", &Game::Permissions::tuning}, {"objects", &Game::Permissions::objects}}}; + static void push_permissions(lua_State* L, const Game::Permissions& permissions, bool readonly) { + lua_createtable(L, 0, static_cast(permission_fields.size())); + for (const auto& [name, field] : permission_fields) { + lua_pushboolean(L, permissions.*field); + lua_setfield(L, -2, name); + } + lua_getfield(L, LUA_REGISTRYINDEX, "reskate.permissions"); + lua_setmetatable(L, -2); + if (readonly) lua_setreadonly(L, -1, true); + } + // Every permission, each true or false, and nothing else: a clone, so none is left out by mistake. + static Game::Permissions permissions_of(lua_State* L, int index) { + luaL_checktype(L, index, LUA_TTABLE); + Game::Permissions permissions; + for (const auto& [name, field] : permission_fields) { + lua_getfield(L, index, name); + if (!lua_isboolean(L, -1)) luaL_error(L, "permission \"%s\" must be true or false (clone the player's permissions to change them)", name); + permissions.*field = lua_toboolean(L, -1); + lua_pop(L, 1); + } + lua_pushnil(L); + while (lua_next(L, index)) { + const char* key = lua_type(L, -2) == LUA_TSTRING ? lua_tostring(L, -2) : nullptr; + const bool known = key && std::any_of(permission_fields.begin(), permission_fields.end(), + [&](const auto& f) { return std::string_view(f.first) == key; }); + if (!known) luaL_error(L, "there is no permission \"%s\"", key ? key : "(not a name)"); + lua_pop(L, 1); + } + return permissions; + } + static int permissions_clone(lua_State* L) { + push_permissions(L, permissions_of(L, 1), false); + return 1; + } + static int player_apply_permissions(lua_State* L) { + auto& plugin = authority(L, "player:apply_permissions"); + const auto player = player_of(L, 1); + return result(L, game(plugin).apply_permissions(plugin.id(), player, permissions_of(L, 2))); + } + // A player table's own names: permissions (read when asked, so always the server's now) and + // apply_permissions. Anything else a player table does not have is nil. + static int player_index(lua_State* L) { + const char* key = lua_type(L, 2) == LUA_TSTRING ? lua_tostring(L, 2) : ""; + if (std::string_view(key) == "apply_permissions") { + lua_pushcfunction(L, player_apply_permissions, "apply_permissions"); + return 1; + } + if (std::string_view(key) != "permissions") return 0; + auto& plugin = authority(L, "player.permissions"); + const auto permissions = game(plugin).permissions(player_of(L, 1)); + if (!permissions) return 0; + push_permissions(L, *permissions, true); + return 1; + } // ---- throwdowns ------------------------------------------------------------------------------------- // host(mode, players, {max}): a drop where the skater stands for these players ("jam", // "spot_battle" or "skate"); it starts once they all join. One every 5 s. @@ -1478,6 +1545,22 @@ static void into(lua_State* L, const char* field, std::initializer_list permissions(std::uint64_t) { return std::nullopt; } + virtual std::string apply_permissions(std::string_view /*plugin*/, std::uint64_t, const Permissions&) { return unavailable; } // ---- skater.animation: clips on the local skater's body (Extension/Skater/skater_animation.h) ------ // One plugin plays at a time; none while a throwdown runs. A replacement plays when the game does // `trick` ("kickflip") with that pop ("" any, "low", "mid", "high"); a null clip takes it away. diff --git a/Server/Test/server_lock_tests.cpp b/Server/Test/server_lock_tests.cpp index 638e3b4a..a9b81510 100644 --- a/Server/Test/server_lock_tests.cpp +++ b/Server/Test/server_lock_tests.cpp @@ -331,7 +331,9 @@ int run() { check(has(b.chat, "The server unlocked for you: nobail, boosts."), "Bailey was not told of the unlock"); command("unlock Dana all"); check(free(d) && d.permissions && (*d.permissions & role_movement), "Dana did not get their roster and movement back"); - check(command("unlock Dana") == "Dana has none of noclip, nobail, boosts, tuning locked.", "unlock with nothing locked"); + check(command("unlock Dana") == "Dana has none of noclip, nobail, boosts, tuning, objects locked.", "unlock with nothing locked"); + check(command("lock Dana objects") == "Locked for Dana: objects.", "objects did not lock by name"); + check(command("unlock Dana") == "Unlocked for Dana: objects.", "a bare unlock did not take back objects"); // ---- An admin's locks stay with the player when they leave and join again ----------------- c.leave(); diff --git a/Server/server_commands.cpp b/Server/server_commands.cpp index 7e8dc6f0..f0dad033 100644 --- a/Server/server_commands.cpp +++ b/Server/server_commands.cpp @@ -947,14 +947,14 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { // last word, so a name may have spaces in it. const bool lock = name == "lock"; auto who = trim(argument); - std::optional tools = lock ? std::nullopt : std::optional(lock_all); + std::optional tools = lock ? std::nullopt : std::optional(lock_all | lock_objects); if (const auto space = who.find_last_of(" \t"); space != std::string_view::npos) if (const auto named = parse_lock_tools(who.substr(space + 1))) { tools = named; who = trim(who.substr(0, space)); } if (who.empty() || !tools) - return lock ? "lock (tools: noclip, nobail, boosts, tuning)" + return lock ? "lock (tools: noclip, nobail, boosts, tuning, objects)" : "unlock []"; auto *guest = target(who); if (guest && !guest->handshaken) guest = nullptr; @@ -996,7 +996,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { if (argument.empty()) return "throwdown-locks |off (now " + (config_.throwdown_locks ? lock_tools_text(config_.throwdown_locks) : std::string("off")) + ")"; const auto tools = lower(argument) == "off" ? std::optional(0) : parse_lock_tools(argument); - if (!tools) return "throwdown-locks |off (tools: noclip, nobail, boosts, tuning)"; + if (!tools) return "throwdown-locks |off (tools: noclip, nobail, boosts, tuning, objects)"; // The players of a running throwdown get the new set at once: what it no longer locks // unlocked, what it now locks locked (none of it when it is turned off or on: then // update_throwdown_locks ends or starts theirs). @@ -1025,6 +1025,16 @@ std::string Host::lock_name(std::uint64_t id, const Guest *guest) const { if (const auto seen = seen_names_.find(id); seen != seen_names_.end()) return seen->second; return std::to_string(id); } +std::uint8_t Host::set_locks(std::uint64_t id, std::uint8_t locks, const std::string &source) { + auto *guest = find(id); + if (guest && !guest->handshaken) guest = nullptr; + // Locked as an admin locks, so a throwdown ending does not lift what was asked for here. + const auto found = locks_.find(id); + const std::uint8_t manual = found == locks_.end() ? 0 : found->second.manual; + const auto now = locks_of(id); + return change_locks(id, guest, locks & ~manual, true, LockBy::admin, source) | + change_locks(id, guest, now & ~locks, false, LockBy::admin, source); +} std::uint8_t Host::change_locks(std::uint64_t id, Guest *guest, std::uint8_t tools, bool lock, LockBy by, const std::string &source) { const bool automatic = by != LockBy::admin; auto &locks = locks_[id]; diff --git a/Server/server_config.cpp b/Server/server_config.cpp index 2c5c7d41..e62fb9f0 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -716,6 +716,7 @@ std::optional parse_lock_tools(std::string_view text) { else if (lower == "nobail") tools |= lock_nobail; else if (lower == "boosts") tools |= lock_boosts; else if (lower == "tuning") tools |= lock_tuning; + else if (lower == "objects") tools |= lock_objects; else if (lower == "all") tools |= lock_all; else return std::nullopt; } @@ -725,7 +726,8 @@ std::optional parse_lock_tools(std::string_view text) { std::string lock_tools_text(std::uint8_t tools) { std::string text; for (const auto &[bit, name] : {std::pair{lock_noclip, "noclip"}, std::pair{lock_nobail, "nobail"}, - std::pair{lock_boosts, "boosts"}, std::pair{lock_tuning, "tuning"}}) + std::pair{lock_boosts, "boosts"}, std::pair{lock_tuning, "tuning"}, + std::pair{lock_objects, "objects"}}) if (tools & bit) text += (text.empty() ? "" : ", ") + std::string(name); return text.empty() ? "none" : text; } diff --git a/Server/server_config.h b/Server/server_config.h index 8407d407..d29aafc2 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -88,8 +88,9 @@ inline constexpr unsigned max_spawned_parks = 16; // back, so a restart keeps it. inline constexpr unsigned dedicated_tps = 20; // The tools the server can take away from single players (lock, throwdown_locks): noclip with -// teleporting, No Bail, the boosts, and their own physics tuning. -inline constexpr std::uint8_t lock_noclip = 1, lock_nobail = 2, lock_boosts = 4, lock_tuning = 8; +// teleporting, No Bail, the boosts, their own physics tuning, and placing objects. "all" is the +// first four, as throwdown_locks has always read it: objects are only ever locked by name. +inline constexpr std::uint8_t lock_noclip = 1, lock_nobail = 2, lock_boosts = 4, lock_tuning = 8, lock_objects = 16; inline constexpr std::uint8_t lock_movement = lock_noclip | lock_nobail | lock_boosts; // what the role's "movement" gives inline constexpr std::uint8_t lock_all = lock_movement | lock_tuning; struct ServerConfig { diff --git a/Server/server_host.cpp b/Server/server_host.cpp index a1584f18..4d3d6eb0 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -1524,7 +1524,7 @@ void Host::sync_objects() { // Guests upload their layouts; the server alone decides what everyone else // sees, and freezes the layouts of players who may not build right now. for (auto &[id, guest] : guests_) - if (guest->handshaken && guest->objects.revision() != guest->shared_from && now_ >= guest->objects_held_until && + if (guest->handshaken && !(locks_of(id) & lock_objects) && guest->objects.revision() != guest->shared_from && now_ >= guest->objects_held_until && (config_.object_placement == ObjectPlacement::everyone || (config_.object_placement == ObjectPlacement::host_only && has(id, multiplayer::role_objects)))) { auto layout = guest->objects.layout(); diff --git a/Server/server_host.h b/Server/server_host.h index f39a39a0..b7e76a63 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -76,6 +76,11 @@ class Host { void send_player_chat(std::uint64_t steam_id, std::string_view text); void broadcast_player_chat(std::string_view text); bool teleport_player(std::uint64_t steam_id, float x, float y, float z); + bool is_banned(std::uint64_t id) const; + // A player's locks (lock_*, server_config.h), and making them exactly `locks` as an admin's + // lock and unlock would (logged, and the player told): the tools that changed. + std::uint8_t player_locks(std::uint64_t id) const { return locks_of(id); } + std::uint8_t set_locks(std::uint64_t id, std::uint8_t locks, const std::string &source); std::optional> player_position(std::uint64_t steam_id) const; void drop(std::uint64_t id, const std::string &reason, const std::string &detail = {}); void change_map(std::string_view map); // a level name, level path or destination @@ -434,7 +439,6 @@ class Host { void send_words(Guest &); // the server's own filtered chat words (server_words.h) void send_next_maps(Guest &); // the maps the vote for the next one will offer void roles_changed(); // after any change to the roles or who has them - bool is_banned(std::uint64_t id) const; void save(); // `detail` is for the log alone: the player is told `reason`. diff --git a/Server/server_plugins.cpp b/Server/server_plugins.cpp index 0af1305a..edd1a643 100644 --- a/Server/server_plugins.cpp +++ b/Server/server_plugins.cpp @@ -46,7 +46,7 @@ std::string fingerprint_of(const std::filesystem::path &directory) { // The server's game, for plugins: its players and what the server does to them. class ServerGame final : public Game { public: - explicit ServerGame(Host &host) : host_(host) {} + ServerGame(Host &host, ServerPlugins::Log log) : host_(host), log_(std::move(log)) {} std::string kick(std::uint64_t player, std::string_view why) override { if (!present(player)) return "that player is not on the server"; host_.drop(player, why.empty() ? std::string("Removed from the server.") : "Removed from the server: " + std::string(why), @@ -65,6 +65,33 @@ class ServerGame final : public Game { std::string move_player(std::uint64_t player, const Vec3 &at) override { return host_.teleport_player(player, at[0], at[1], at[2]) ? std::string{} : "that player is not on the server"; } + // Permissions are the player's locks turned around; any SteamID64, here or not, as `lock` takes. + std::optional permissions(std::uint64_t player) override { + if (!multiplayer::individual_steam_id(player)) return std::nullopt; + const auto locks = host_.player_locks(player); + return Permissions{!(locks & lock_noclip), !(locks & lock_nobail), !(locks & lock_boosts), !(locks & lock_tuning), + !(locks & lock_objects)}; + } + std::string apply_permissions(std::string_view plugin, std::uint64_t player, const Permissions &p) override { + if (!multiplayer::individual_steam_id(player)) return "not a player's SteamID64"; + const auto locks = static_cast((p.noclip ? 0 : lock_noclip) | (p.nobail ? 0 : lock_nobail) | (p.boosts ? 0 : lock_boosts) | + (p.tuning ? 0 : lock_tuning) | (p.objects ? 0 : lock_objects)); + host_.set_locks(player, locks, "plugin " + std::string(plugin)); + return {}; + } + // Through the console's own ban and unban, so they keep its checks and save the bans file. + std::string ban(std::uint64_t player) override { + const auto answer = host_.command("ban " + std::to_string(player)); + if (!host_.is_banned(player)) return answer; + log_("[Plugins] " + answer); // the console's ban leaves logging to the console + return {}; + } + std::string unban(std::uint64_t player) override { + const auto answer = host_.command("unban " + std::to_string(player)); + if (host_.is_banned(player)) return answer; + log_("[Plugins] " + answer); + return {}; + } private: bool present(std::uint64_t player) const { @@ -73,12 +100,13 @@ class ServerGame final : public Game { return false; } Host &host_; + ServerPlugins::Log log_; }; class ServerEnvironment final : public Environment { public: ServerEnvironment(Host &host, std::filesystem::path storage, ServerPlugins::Log log) - : host_(host), storage_(std::move(storage)), log_(std::move(log)), game_(host) {} + : host_(host), storage_(std::move(storage)), log_(log), game_(host, log) {} void log(LogLevel level, std::string_view text) override { log_(std::string(level == LogLevel::error ? "[Plugins] error: " : level == LogLevel::warn ? "[Plugins] warning: " : "[Plugins] ") + std::string(text)); diff --git a/contrib/plugins/server-starter/plugin/main.lua b/contrib/plugins/server-starter/plugin/main.lua new file mode 100644 index 00000000..037c3d29 --- /dev/null +++ b/contrib/plugins/server-starter/plugin/main.lua @@ -0,0 +1,14 @@ +-- A starting point for a server plugin: greets each player who joins and counts their visits, +-- and gives the server console a `starter` command. Copy the folder into the server's Plugins. +reskate.on("player_joined", function(player) + local visits = (reskate.storage.get("visits." .. player.id) or 0) + 1 + reskate.storage.set("visits." .. player.id, visits) + reskate.server.tell(player.id, "Welcome, " .. player.name .. " (visit " .. visits .. ")") +end) + +reskate.console.command("starter", "Server starter plugin: starter say ", function(args) + if args[1] ~= "say" or not args[2] then return reskate.console.print("starter say ") end + reskate.server.say(table.concat(args, " ", 2)) +end) + +reskate.log("server-starter loaded") diff --git a/contrib/plugins/server-starter/reskate-plugin.json b/contrib/plugins/server-starter/reskate-plugin.json new file mode 100644 index 00000000..b6713ddc --- /dev/null +++ b/contrib/plugins/server-starter/reskate-plugin.json @@ -0,0 +1,6 @@ +{ + "api": 1, + "entry": "plugin/main.lua", + "side": "server", + "permissions": ["console", "storage", "session.read", "server"] +} From c02b4001a76361c1f8db3a0e94acfda47e197587 Mon Sep 17 00:00:00 2001 From: wagu Date: Tue, 20 Oct 2026 09:53:25 -0400 Subject: [PATCH 2/5] Plugins: add_functions, one line to add to the reskate table The shared API helpers (plugin_of, need, checked_text, position_of) are in plugin_api.h, defined once. The API plugins see is unchanged. Co-Authored-By: Claude Opus 5.5 --- Extension/Plugins/plugin_api.cpp | 146 ++++++++++--------------- Extension/Plugins/plugin_api.h | 34 ++++++ Extension/Plugins/plugin_api_more.cpp | 28 +---- Extension/Plugins/plugin_api_ui.cpp | 103 +++-------------- Extension/Plugins/plugin_api_world.cpp | 101 +++++------------ 5 files changed, 137 insertions(+), 275 deletions(-) diff --git a/Extension/Plugins/plugin_api.cpp b/Extension/Plugins/plugin_api.cpp index 5efff252..ae6109de 100644 --- a/Extension/Plugins/plugin_api.cpp +++ b/Extension/Plugins/plugin_api.cpp @@ -30,18 +30,12 @@ constexpr std::size_t maximum_key = 128; constexpr std::size_t maximum_command = 48; constexpr std::size_t maximum_listing = 1000; -// A plugin that caught its own memory or time error in a pcall gets no further: nothing it asks -// for is done, and the next safe point stops it. -Plugin& plugin_of(lua_State* L) { - auto& plugin = *static_cast(lua_callbacks(L)->userdata); - if (plugin.halted()) luaL_error(L, "the plugin was stopped"); - return plugin; -} - // The table being filled is on top of the stack. -void set_function(lua_State* L, const char* name, lua_CFunction function) { - lua_pushcfunction(L, function, name); - lua_setfield(L, -2, name); +void set_functions(lua_State* L, std::initializer_list functions) { + for (const auto& [name, function] : functions) { + lua_pushcfunction(L, function, name); + lua_setfield(L, -2, name); + } } std::string text_of(lua_State* L, int index) { @@ -148,11 +142,6 @@ void push_json_value(lua_State* L, const Json& value, int depth) { } } -void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) { - if (!plugin.has(permission)) - luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission); -} - std::string key_of(lua_State* L, int index) { std::size_t length = 0; const char* key = luaL_checklstring(L, index, &length); @@ -175,8 +164,46 @@ std::filesystem::path package_path(lua_State* L, Plugin& plugin, std::string_vie if (code || (!relative.empty() && (inside.empty() || *inside.begin() == ".."))) luaL_error(L, "that path leads outside the package"); return full; } +} -// A position a plugin passes: a vector, finite and within 100 km of the map's centre. +void add_functions(lua_State* L, const char* field, std::initializer_list functions) { + lua_getfield(L, -1, field); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + lua_createtable(L, 0, static_cast(functions.size())); + lua_pushvalue(L, -1); + lua_setfield(L, -3, field); + } + // A table made read-only earlier in the install is opened for this, and closed again. + lua_setreadonly(L, -1, false); + set_functions(L, functions); + lua_setreadonly(L, -1, true); + lua_pop(L, 1); +} +void add_functions(lua_State* L, std::initializer_list functions) { set_functions(L, functions); } +void push_functions(lua_State* L, std::initializer_list functions) { + lua_createtable(L, 0, static_cast(functions.size())); + set_functions(L, functions); + lua_setreadonly(L, -1, true); +} + +// A plugin that caught its own memory or time error in a pcall gets no further: nothing it asks +// for is done, and the next safe point stops it. +Plugin& plugin_of(lua_State* L) { + auto& plugin = *static_cast(lua_callbacks(L)->userdata); + if (plugin.halted()) luaL_error(L, "the plugin was stopped"); + return plugin; +} +void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) { + if (!plugin.has(permission)) + luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission); +} +std::string checked_text(lua_State* L, int index, std::size_t most, const char* what) { + std::size_t length = 0; + const char* text = luaL_checklstring(L, index, &length); + if (length > most) luaL_error(L, "%s is longer than %d characters", what, static_cast(most)); + return std::string(text, length); +} Vec3 position_of(lua_State* L, int index, const char* what) { const float* v = luaL_checkvector(L, index); for (int i = 0; i < 3; ++i) @@ -184,7 +211,6 @@ Vec3 position_of(lua_State* L, int index, const char* what) { return {v[0], v[1], v[2]}; } void push_position(lua_State* L, const Vec3& v) { lua_pushvector(L, v[0], v[1], v[2]); } -} std::filesystem::path package_file(lua_State* L, Plugin& plugin, std::string_view path) { return package_path(L, plugin, path, false); } @@ -619,78 +645,18 @@ void Api::install(Plugin& plugin, lua_State* L) { lua_setfield(L, -2, "Event"); readonly(); lua_setfield(L, -2, "Enums"); - set_function(L, "log", ApiFunctions::log); - set_function(L, "on", ApiFunctions::on); - - lua_createtable(L, 0, 4); - set_function(L, "now", ApiFunctions::now); - set_function(L, "after", ApiFunctions::after); - set_function(L, "every", ApiFunctions::every); - set_function(L, "cancel", ApiFunctions::cancel); - readonly(); - lua_setfield(L, -2, "time"); - - lua_createtable(L, 0, 1); - set_function(L, "current", ApiFunctions::level); - readonly(); - lua_setfield(L, -2, "level"); - - lua_createtable(L, 0, 3); - set_function(L, "read", ApiFunctions::read); - set_function(L, "read_json", ApiFunctions::read_json); - set_function(L, "list", ApiFunctions::list); - readonly(); - lua_setfield(L, -2, "package"); - - lua_createtable(L, 0, 2); - set_function(L, "encode", ApiFunctions::encode); - set_function(L, "decode", ApiFunctions::decode); - readonly(); - lua_setfield(L, -2, "json"); - - lua_createtable(L, 0, 2); - set_function(L, "command", ApiFunctions::command); - lua_pushcfunction(L, ApiFunctions::console_print, "print"); - lua_setfield(L, -2, "print"); - readonly(); - lua_setfield(L, -2, "console"); - - lua_createtable(L, 0, 4); - lua_pushcfunction(L, ApiFunctions::storage_get, "get"); - lua_setfield(L, -2, "get"); - lua_pushcfunction(L, ApiFunctions::storage_set, "set"); - lua_setfield(L, -2, "set"); - lua_pushcfunction(L, ApiFunctions::storage_add, "add"); - lua_setfield(L, -2, "add"); - lua_pushcfunction(L, ApiFunctions::storage_keys, "keys"); - lua_setfield(L, -2, "keys"); - readonly(); - lua_setfield(L, -2, "storage"); - - lua_createtable(L, 0, 6); - set_function(L, "get", ApiFunctions::skater_get); - lua_pushcfunction(L, ApiFunctions::skater_position, "position"); - lua_setfield(L, -2, "position"); - lua_pushcfunction(L, ApiFunctions::skater_heading, "heading"); - lua_setfield(L, -2, "heading"); - lua_pushcfunction(L, ApiFunctions::skater_speed, "speed"); - lua_setfield(L, -2, "speed"); - lua_pushcfunction(L, ApiFunctions::skater_on_ground, "on_ground"); - lua_setfield(L, -2, "on_ground"); - lua_pushcfunction(L, ApiFunctions::skater_teleport, "teleport"); - lua_setfield(L, -2, "teleport"); - 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(); - lua_setfield(L, -2, "world"); + using F = ApiFunctions; + add_functions(L, {{"log", F::log}, {"on", F::on}}); + add_functions(L, "time", {{"now", F::now}, {"after", F::after}, {"every", F::every}, {"cancel", F::cancel}}); + add_functions(L, "level", {{"current", F::level}}); + add_functions(L, "package", {{"read", F::read}, {"read_json", F::read_json}, {"list", F::list}}); + add_functions(L, "json", {{"encode", F::encode}, {"decode", F::decode}}); + add_functions(L, "console", {{"command", F::command}, {"print", F::console_print}}); + add_functions(L, "storage", {{"get", F::storage_get}, {"set", F::storage_set}, {"add", F::storage_add}, {"keys", F::storage_keys}}); + add_functions(L, "skater", {{"get", F::skater_get}, {"position", F::skater_position}, {"heading", F::skater_heading}, + {"speed", F::skater_speed}, {"on_ground", F::skater_on_ground}, {"teleport", F::skater_teleport}}); + add_functions(L, "score", {{"current", F::score}}); + add_functions(L, "world", {{"raycast", F::raycast}}); install_ui_api(plugin, L); install_more_api(plugin, L); diff --git a/Extension/Plugins/plugin_api.h b/Extension/Plugins/plugin_api.h index 127a557e..a404726f 100644 --- a/Extension/Plugins/plugin_api.h +++ b/Extension/Plugins/plugin_api.h @@ -1,5 +1,7 @@ #pragma once #include "plugin_vm.h" +#include "lua.h" +#include // Inside the plugin host only: the reskate API a plugin's VM gets. namespace dingosdk::plugins { @@ -14,6 +16,38 @@ void install_ui_api(Plugin& plugin, lua_State* L); void install_more_api(Plugin& plugin, lua_State* L); // The wider set's game parts (plugin_api_world.cpp), into the same table. void install_world_api(Plugin& plugin, lua_State* L); + +// ---- adding to the reskate table --------------------------------------------------------------- +// A new part of the API is a file of `static int name(lua_State*)` functions and an install_*_api +// that lists them, called from Api::install: +// +// add_functions(L, "skater", {{"get", skater_get}, {"teleport", skater_teleport}}); // reskate.skater.* +// add_functions(L, {{"log", log}, {"on", on}}); // reskate.* +// +// Each function checks its own permission (need) and arguments, as the rest of the API does. +struct ApiFunction { + const char* name; + lua_CFunction function; +}; +// Adds the functions to reskate., the reskate table being on top of the stack. The table is +// made when there is none yet, and another file may add to one already there; it is left read-only. +void add_functions(lua_State* L, const char* field, std::initializer_list functions); +// Adds the functions to the reskate table itself (on top of the stack). +void add_functions(lua_State* L, std::initializer_list functions); +// Pushes a new read-only table of the functions, kept outside reskate (the draw and ui handles). +void push_functions(lua_State* L, std::initializer_list functions); + +// ---- what most API functions start with -------------------------------------------------------- +// The plugin calling, or a Lua error once it has been stopped. +Plugin& plugin_of(lua_State* L); +// A Lua error naming `what` unless the plugin's reskate-plugin.json grants `permission`. +void need(lua_State* L, Plugin& plugin, const char* permission, const char* what); +// A string argument of at most `most` bytes. +std::string checked_text(lua_State* L, int index, std::size_t most, const char* what); +// A vector argument, finite and within 100 km of the map's centre. +Vec3 position_of(lua_State* L, int index, const char* what); +void push_position(lua_State* L, const Vec3& v); + // A player as plugins get one: {id = "", name, me, host}. void push_player(lua_State* L, const SessionPlayer& player); // A file's bytes when it is a regular file no larger than `most`; otherwise empty, and `error` diff --git a/Extension/Plugins/plugin_api_more.cpp b/Extension/Plugins/plugin_api_more.cpp index efff9a87..63236cc9 100644 --- a/Extension/Plugins/plugin_api_more.cpp +++ b/Extension/Plugins/plugin_api_more.cpp @@ -8,21 +8,6 @@ // plugin_api.cpp, every value is checked before it leaves the VM. namespace dingosdk::plugins { namespace { -Plugin& plugin_of(lua_State* L) { - auto& plugin = *static_cast(lua_callbacks(L)->userdata); - if (plugin.halted()) luaL_error(L, "the plugin was stopped"); - return plugin; -} -void set_function(lua_State* L, const char* name, lua_CFunction function) { - lua_pushcfunction(L, function, name); - lua_setfield(L, -2, name); -} -std::string checked_text(lua_State* L, int index, std::size_t most, const char* what) { - std::size_t length = 0; - const char* text = luaL_checklstring(L, index, &length); - if (length > most) luaL_error(L, "%s is longer than %d characters", what, static_cast(most)); - return std::string(text, length); -} // A name another plugin calls or listens for: letters, digits and _ . -, 1 to 48 of them. std::string identifier(lua_State* L, int index, const char* what) { auto name = checked_text(L, index, 48, what); @@ -194,15 +179,8 @@ void install_more_api(Plugin& plugin, lua_State* L) { MoreApi::install(plugin, L void MoreApi::install(Plugin& plugin, lua_State* L) { (void)plugin; // The reskate table is on top of the stack. - set_function(L, "expose", MoreApi::expose); - set_function(L, "require_plugin", MoreApi::require_plugin); - set_function(L, "emit", MoreApi::emit); - set_function(L, "on_plugin", MoreApi::on_plugin); - - lua_createtable(L, 0, 1); - lua_pushcfunction(L, MoreApi::http_request, "request"); - lua_setfield(L, -2, "request"); - lua_setreadonly(L, -1, true); - lua_setfield(L, -2, "http"); + using F = MoreApi; + add_functions(L, {{"expose", F::expose}, {"require_plugin", F::require_plugin}, {"emit", F::emit}, {"on_plugin", F::on_plugin}}); + add_functions(L, "http", {{"request", F::http_request}}); } } diff --git a/Extension/Plugins/plugin_api_ui.cpp b/Extension/Plugins/plugin_api_ui.cpp index d8aa489e..c200c4ba 100644 --- a/Extension/Plugins/plugin_api_ui.cpp +++ b/Extension/Plugins/plugin_api_ui.cpp @@ -13,24 +13,6 @@ // plugin_api.cpp, every value is checked before it leaves the VM. namespace dingosdk::plugins { namespace { -Plugin& plugin_of(lua_State* L) { - auto& plugin = *static_cast(lua_callbacks(L)->userdata); - if (plugin.halted()) luaL_error(L, "the plugin was stopped"); - return plugin; -} -void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) { - if (!plugin.has(permission)) luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission); -} -void set_function(lua_State* L, const char* name, lua_CFunction function) { - lua_pushcfunction(L, function, name); - lua_setfield(L, -2, name); -} -std::string checked_text(lua_State* L, int index, std::size_t most, const char* what) { - std::size_t length = 0; - const char* text = luaL_checklstring(L, index, &length); - if (length > most) luaL_error(L, "%s is longer than %d characters", what, static_cast(most)); - return std::string(text, length); -} float finite(lua_State* L, int index, const char* what) { const double value = luaL_checknumber(L, index); if (!std::isfinite(value) || std::fabs(value) > 1e6) luaL_error(L, "%s must be a finite number", what); @@ -646,82 +628,25 @@ void install_ui_api(Plugin& plugin, lua_State* L) { UiApi::install(plugin, L); } void UiApi::install(Plugin& plugin, lua_State* L) { // The reskate table is on top of the stack. - const auto readonly = [&] { lua_setreadonly(L, -1, true); }; - - lua_createtable(L, 0, 10); - set_function(L, "text", UiApi::text); - set_function(L, "rect", UiApi::rect); - set_function(L, "line", UiApi::line); - set_function(L, "circle", UiApi::circle); - set_function(L, "poly", UiApi::poly); - set_function(L, "world_text", UiApi::world_text); - set_function(L, "world_line", UiApi::world_line); - set_function(L, "world_circle", UiApi::world_circle); - set_function(L, "image", UiApi::image); - set_function(L, "world_image", UiApi::world_image); - set_function(L, "game_image", UiApi::game_image); - set_function(L, "gradient", UiApi::gradient); - set_function(L, "skeleton", UiApi::skeleton); - set_function(L, "size", UiApi::size); - set_function(L, "world_to_screen", UiApi::world_to_screen); - readonly(); + using F = UiApi; + push_functions(L, {{"text", F::text}, {"rect", F::rect}, {"line", F::line}, {"circle", F::circle}, {"poly", F::poly}, + {"world_text", F::world_text}, {"world_line", F::world_line}, {"world_circle", F::world_circle}, + {"image", F::image}, {"world_image", F::world_image}, {"game_image", F::game_image}, + {"gradient", F::gradient}, {"skeleton", F::skeleton}, {"size", F::size}, {"world_to_screen", F::world_to_screen}}); plugin.draw_table_ = lua_ref(L, -1); lua_pop(L, 1); - lua_createtable(L, 0, 8); - set_function(L, "heading", UiApi::heading); - set_function(L, "label", UiApi::label); - set_function(L, "separator", UiApi::separator); - set_function(L, "button", UiApi::button); - set_function(L, "toggle", UiApi::toggle); - set_function(L, "slider", UiApi::slider); - set_function(L, "dropdown", UiApi::dropdown); - set_function(L, "bar", UiApi::bar); - set_function(L, "card", UiApi::card); - set_function(L, "note", UiApi::note); - set_function(L, "warn", UiApi::warn); - set_function(L, "info", UiApi::info); - set_function(L, "tabs", UiApi::tabs); - readonly(); + push_functions(L, {{"heading", F::heading}, {"label", F::label}, {"separator", F::separator}, {"button", F::button}, + {"toggle", F::toggle}, {"slider", F::slider}, {"dropdown", F::dropdown}, {"bar", F::bar}, {"card", F::card}, + {"note", F::note}, {"warn", F::warn}, {"info", F::info}, {"tabs", F::tabs}}); plugin.ui_table_ = lua_ref(L, -1); lua_pop(L, 1); - lua_createtable(L, 0, 1); - set_function(L, "toast", UiApi::toast); - readonly(); - lua_setfield(L, -2, "ui"); - - lua_createtable(L, 0, 1); - set_function(L, "panel", UiApi::panel); - set_function(L, "page", UiApi::page); - readonly(); - lua_setfield(L, -2, "menu"); - - lua_createtable(L, 0, 1); - set_function(L, "play_ui", UiApi::play_ui); - readonly(); - lua_setfield(L, -2, "audio"); - - lua_createtable(L, 0, 2); - set_function(L, "installed", UiApi::installed); - lua_pushcfunction(L, UiApi::mods_list, "list"); - lua_setfield(L, -2, "list"); - readonly(); - lua_setfield(L, -2, "mods"); - - lua_createtable(L, 0, 5); - set_function(L, "active", UiApi::active); - lua_pushcfunction(L, UiApi::dedicated, "is_dedicated"); - lua_setfield(L, -2, "is_dedicated"); - set_function(L, "players", UiApi::players); - set_function(L, "me", UiApi::me); - set_function(L, "host", UiApi::host); - readonly(); - lua_setfield(L, -2, "session"); - - lua_createtable(L, 0, 1); - set_function(L, "send", UiApi::send); - readonly(); - lua_setfield(L, -2, "net"); + add_functions(L, "ui", {{"toast", F::toast}}); + add_functions(L, "menu", {{"panel", F::panel}, {"page", F::page}}); + add_functions(L, "audio", {{"play_ui", F::play_ui}}); + add_functions(L, "mods", {{"installed", F::installed}, {"list", F::mods_list}}); + add_functions(L, "session", {{"active", F::active}, {"is_dedicated", F::dedicated}, {"players", F::players}, {"me", F::me}, {"host", F::host}}); + add_functions(L, "net", {{"send", F::send}}); } } diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 557737a9..7bf62d41 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -16,27 +16,6 @@ // under the game's own rules (a session's host, the park editor's limits). namespace dingosdk::plugins { namespace { -Plugin& plugin_of(lua_State* L) { - auto& plugin = *static_cast(lua_callbacks(L)->userdata); - if (plugin.halted()) luaL_error(L, "the plugin was stopped"); - return plugin; -} -void need(lua_State* L, Plugin& plugin, const char* permission, const char* what) { - if (!plugin.has(permission)) luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission); -} -std::string checked_text(lua_State* L, int index, std::size_t most, const char* what) { - std::size_t length = 0; - const char* text = luaL_checklstring(L, index, &length); - if (length > most) luaL_error(L, "%s is longer than %d characters", what, static_cast(most)); - return std::string(text, length); -} -Vec3 position_of(lua_State* L, int index, const char* what) { - const float* v = luaL_checkvector(L, index); - for (int i = 0; i < 3; ++i) - if (!std::isfinite(v[i]) || std::fabs(v[i]) > 1e5f) luaL_error(L, "%s must be a finite position within 100 km", what); - return {v[0], v[1], v[2]}; -} -void push_position(lua_State* L, const Vec3& v) { lua_pushvector(L, v[0], v[1], v[2]); } // ok, or false and why: what most of these return. int result(lua_State* L, const std::string& why) { lua_pushboolean(L, why.empty()); @@ -1523,26 +1502,6 @@ struct WorldApi { void install_world_api(Plugin& plugin, lua_State* L) { WorldApi::install(plugin, L); } -// Adds the functions to the table at `field` of the reskate table on top of the stack, making it -// when there is none yet. -static void into(lua_State* L, const char* field, std::initializer_list> functions) { - lua_getfield(L, -1, field); - if (lua_isnil(L, -1)) { - lua_pop(L, 1); - lua_createtable(L, 0, static_cast(functions.size())); - lua_pushvalue(L, -1); - lua_setfield(L, -3, field); - } - // A table made read-only earlier in the install is opened for this, and closed again. - lua_setreadonly(L, -1, false); - for (const auto& [name, function] : functions) { - lua_pushcfunction(L, function, name); - lua_setfield(L, -2, name); - } - lua_setreadonly(L, -1, true); - lua_pop(L, 1); -} - void WorldApi::install(Plugin& plugin, lua_State* L) { (void)plugin; // The metatables of player tables (push_player) and of permissions; their own stay hidden. @@ -1561,36 +1520,36 @@ void WorldApi::install(Plugin& plugin, lua_State* L) { lua_pushstring(L, "locked"); lua_setfield(L, -2, "__metatable"); lua_setfield(L, LUA_REGISTRYINDEX, "reskate.permissions"); - into(L, "skater", {{"state", state}, {"noclip", noclip}, {"boost", boost}, {"freeze", freeze}, {"wipeout", wipeout}, {"xray", xray}, - {"grind_exit", grind_exit}}); - into(L, "board", {{"motion", board_motion}, {"bones", board_bones}, {"set", board_set}, {"hide", board_hide}, {"clear", board_clear}, - {"park", board_park}, {"toss", board_toss}}); - into(L, "camera", {{"look", camera_look}, {"set", camera_set}, {"follow", camera_follow}, {"release", camera_release}, {"held", camera_held}, {"get", camera_get}}); - into(L, "trainer", {{"set_point", set_point}, {"back", back}}); - into(L, "hud", {{"hide", hide_hud}, {"corner", hud_corner}}); - into(L, "time", {{"slow", slow}, {"speed", speed}}); - into(L, "objects", {{"catalog", catalog}, {"list", objects}, {"place", place}, {"move", move}, {"remove", remove}}); - into(L, "world", {{"set_time", set_time}, {"set_control", set_control}, {"set_park", set_park}}); - into(L, "travel", {{"locations", locations}, {"levels", levels}, {"go", go}, {"load", load}}); - into(L, "ai", {{"spawn", ai_spawn}, {"clear", ai_clear}, {"count", ai_count}}); - into(L, "session", {{"skaters", skaters}, {"tag", tag}}); - into(L, "voice", {{"set_volume", voice_volume}}); - into(L, "chat", {{"send", chat_send}, {"notice", chat_local}}); - into(L, "animation", {{"play", animation_play}, {"stop", animation_stop}, {"playing", animation_playing}, {"replace", animation_replace}, - {"record", animation_record}, {"recorded", animation_recorded}, {"forget", animation_forget}, {"trick", animation_trick}}); - into(L, "outfit", {{"set", outfit_set}, {"clear", outfit_clear}, {"find", outfit_find}, {"slots", outfit_slots}, {"status", outfit_status}}); - into(L, "audio", {{"play", audio_play}, {"set", audio_set}, {"stop", audio_stop}, {"playing", audio_playing}}); - into(L, "server", {{"kick", server_kick}, {"say", server_say}, {"tell", server_tell}, {"move", server_move}, - {"ban", server_ban}, {"unban", server_unban}}); - into(L, "throwdowns", {{"state", throwdown_state}, {"host", throwdown_host}, {"cancel", throwdown_cancel}, {"hosting", throwdown_hosting}, - {"mode", throwdown_mode}, {"remove_mode", throwdown_remove_mode}, {"release_flag", throwdown_release_flag}}); - into(L, "music", {{"current", current}, {"play", play}, {"pause", pause}, {"next", next}, {"previous", previous}, - {"game", game_current}, {"game_next", game_next}}); - into(L, "progression", {{"get", progress}}); - into(L, "settings", {{"game", setting}, {"names", setting_names}}); - into(L, "input", {{"bind", bind}, {"flight", input_flight}, {"gamepad", input_gamepad}, {"joystick", input_joystick}, {"capture", input_capture}, - {"hide", input_hide}}); - into(L, "ui", {{"screen", screen}, {"open", open}, {"close", close}}); + add_functions(L, "skater", {{"state", state}, {"noclip", noclip}, {"boost", boost}, {"freeze", freeze}, {"wipeout", wipeout}, {"xray", xray}, + {"grind_exit", grind_exit}}); + add_functions(L, "board", {{"motion", board_motion}, {"bones", board_bones}, {"set", board_set}, {"hide", board_hide}, {"clear", board_clear}, + {"park", board_park}, {"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, "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}}); + add_functions(L, "objects", {{"catalog", catalog}, {"list", objects}, {"place", place}, {"move", move}, {"remove", remove}}); + add_functions(L, "world", {{"set_time", set_time}, {"set_control", set_control}, {"set_park", set_park}}); + 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}}); + add_functions(L, "voice", {{"set_volume", voice_volume}}); + add_functions(L, "chat", {{"send", chat_send}, {"notice", chat_local}}); + add_functions(L, "animation", {{"play", animation_play}, {"stop", animation_stop}, {"playing", animation_playing}, {"replace", animation_replace}, + {"record", animation_record}, {"recorded", animation_recorded}, {"forget", animation_forget}, {"trick", animation_trick}}); + add_functions(L, "outfit", {{"set", outfit_set}, {"clear", outfit_clear}, {"find", outfit_find}, {"slots", outfit_slots}, {"status", outfit_status}}); + add_functions(L, "audio", {{"play", audio_play}, {"set", audio_set}, {"stop", audio_stop}, {"playing", audio_playing}}); + add_functions(L, "server", {{"kick", server_kick}, {"say", server_say}, {"tell", server_tell}, {"move", server_move}, + {"ban", server_ban}, {"unban", server_unban}}); + add_functions(L, "throwdowns", {{"state", throwdown_state}, {"host", throwdown_host}, {"cancel", throwdown_cancel}, {"hosting", throwdown_hosting}, + {"mode", throwdown_mode}, {"remove_mode", throwdown_remove_mode}, {"release_flag", throwdown_release_flag}}); + add_functions(L, "music", {{"current", current}, {"play", play}, {"pause", pause}, {"next", next}, {"previous", previous}, + {"game", game_current}, {"game_next", game_next}}); + add_functions(L, "progression", {{"get", progress}}); + add_functions(L, "settings", {{"game", setting}, {"names", setting_names}}); + add_functions(L, "input", {{"bind", bind}, {"flight", input_flight}, {"gamepad", input_gamepad}, {"joystick", input_joystick}, {"capture", input_capture}, + {"hide", input_hide}}); + add_functions(L, "ui", {{"screen", screen}, {"open", open}, {"close", close}}); } std::uint32_t combo_of(std::string_view name) { From 67e8e439f7a65470b4d438151df6c8b66a80c5c2 Mon Sep 17 00:00:00 2001 From: wagu Date: Tue, 20 Oct 2026 09:53:31 -0400 Subject: [PATCH 3/5] Server: lock ollies for a player A lock tool like noclip and boosts. The player's game enforces it: the roster carries guest_ollies (protocol 53), and the jump hook gives a locked ollie no upward speed. Plugins get player.permissions.ollies. Co-Authored-By: Claude Opus 5.5 --- Engine/Game/Multiplayer/session_tools.h | 8 +++++--- Extension/Multiplayer/Net/protocol.cpp | 4 ++-- Extension/Multiplayer/Net/protocol.h | 7 ++++--- Extension/Multiplayer/Session/session.cpp | 4 ++-- Extension/Multiplayer/Session/session_commands.cpp | 5 +++-- Extension/Multiplayer/Session/session_internal.h | 3 ++- Extension/Multiplayer/Session/session_receive.cpp | 4 ++-- Extension/Multiplayer/Test/protocol_tests.cpp | 7 +++++++ Extension/Plugins/plugin_api_world.cpp | 4 ++-- Extension/Plugins/plugin_game.h | 2 +- Extension/Trainer/trainer_jump.cpp | 9 +++++++-- Server/Test/server_config_tests.cpp | 4 ++-- Server/Test/server_lock_tests.cpp | 2 +- Server/server_commands.cpp | 4 ++-- Server/server_config.cpp | 5 +++-- Server/server_config.h | 4 ++-- Server/server_host.cpp | 6 ++++-- Server/server_plugins.cpp | 4 ++-- 18 files changed, 53 insertions(+), 33 deletions(-) diff --git a/Engine/Game/Multiplayer/session_tools.h b/Engine/Game/Multiplayer/session_tools.h index 3363ff62..4048e405 100644 --- a/Engine/Game/Multiplayer/session_tools.h +++ b/Engine/Game/Multiplayer/session_tools.h @@ -2,24 +2,26 @@ #include namespace dingosdk { -// Noclip (and teleporting), No Bail and the forward / up boosts as the session's host lets +// Noclip (and teleporting), No Bail, the forward / up boosts and ollies as the session's host lets // the local player use them. All are allowed outside a session, and always for the host // itself and a dedicated server's admins. namespace session_tools_detail { -inline std::atomic noclip{true}, no_bail{true}, boosts{true}; +inline std::atomic noclip{true}, no_bail{true}, boosts{true}, ollies{true}; inline std::atomic session{}; } inline bool session_noclip_allowed() noexcept { return session_tools_detail::noclip.load(std::memory_order_acquire); } inline bool session_no_bail_allowed() noexcept { return session_tools_detail::no_bail.load(std::memory_order_acquire); } inline bool session_boosts_allowed() noexcept { return session_tools_detail::boosts.load(std::memory_order_acquire); } +inline bool session_ollies_allowed() noexcept { return session_tools_detail::ollies.load(std::memory_order_acquire); } // Hosting or joined to a multiplayer session (set by the session every tick). Any thread. inline bool multiplayer_session_active() noexcept { return session_tools_detail::session.load(std::memory_order_acquire); } inline void set_multiplayer_session_active(bool active) noexcept { session_tools_detail::session.store(active, std::memory_order_release); } -inline void set_session_tools_allowed(bool noclip, bool no_bail, bool boosts) noexcept { +inline void set_session_tools_allowed(bool noclip, bool no_bail, bool boosts, bool ollies) noexcept { session_tools_detail::noclip.store(noclip, std::memory_order_release); session_tools_detail::no_bail.store(no_bail, std::memory_order_release); session_tools_detail::boosts.store(boosts, std::memory_order_release); + session_tools_detail::ollies.store(ollies, std::memory_order_release); } } // namespace dingosdk diff --git a/Extension/Multiplayer/Net/protocol.cpp b/Extension/Multiplayer/Net/protocol.cpp index edf875a8..274fe8a2 100644 --- a/Extension/Multiplayer/Net/protocol.cpp +++ b/Extension/Multiplayer/Net/protocol.cpp @@ -766,7 +766,7 @@ std::vector encode(const Packet &p, bool compact_pose, std::uint32 w.integer(static_cast(std::lround(p.voice_range)), 2); if (p.server_votes > 7) throw std::invalid_argument("Invalid server votes"); w.integer((p.guest_noclip ? 1U : 0U) | (p.guest_no_bail ? 2U : 0U) | (p.guest_boosts ? 4U : 0U) | - (static_cast(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U), 1); + (static_cast(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U) | (p.guest_ollies ? 128U : 0U), 1); if (p.server_polls > static_cast(ServerPolls::everyone) || p.server_custom_votes.size() > server_custom_vote_limit || !std::all_of(p.server_custom_votes.begin(), p.server_custom_votes.end(), [](const auto &v) { return valid_server_custom_vote(v); })) throw std::invalid_argument("Invalid server votes"); @@ -1223,12 +1223,12 @@ std::optional decode(std::span bytes) noexcept { p.voice_range = static_cast(r.integer(2)); if (!valid_voice_range(p.voice_range)) return {}; const auto tools = r.integer(1); - if (tools > 127) return {}; p.guest_noclip = (tools & 1) != 0; p.guest_no_bail = (tools & 2) != 0; p.guest_boosts = (tools & 4) != 0; p.server_votes = static_cast((tools >> 3) & 7); p.enforce_tuning = (tools & 64) != 0; + p.guest_ollies = (tools & 128) != 0; const auto polls = r.integer(1), custom = r.integer(1); if (polls > static_cast(ServerPolls::everyone) || custom > server_custom_vote_limit) return {}; p.server_polls = static_cast(polls); diff --git a/Extension/Multiplayer/Net/protocol.h b/Extension/Multiplayer/Net/protocol.h index 99be6cf3..a9581f7d 100644 --- a/Extension/Multiplayer/Net/protocol.h +++ b/Extension/Multiplayer/Net/protocol.h @@ -27,7 +27,7 @@ namespace dingosdk::multiplayer { constexpr std::size_t max_skater_bones = 512, max_board_bones = 64; constexpr std::size_t max_packet = 24576; constexpr std::size_t packet_header_size = 64; -constexpr std::uint16_t protocol_version = 52; +constexpr std::uint16_t protocol_version = 53; // How long a receiver keeps typing dots after the last "still typing" notice. The sender // clears them after three seconds without input; this only covers a lost "stopped" packet. constexpr std::uint64_t typing_hold_us = 4000000; @@ -349,8 +349,9 @@ struct Packet { std::string text; float voice_range = default_voice_range; // roster: how far the host forwards proximity voice // roster: whether guests may use noclip (and teleport) / No Bail / the forward and up boosts - // (the host and a dedicated server's admins always may) - bool guest_noclip = true, guest_no_bail = true, guest_boosts = true; + // (the host and a dedicated server's admins always may); and ollies, which only a dedicated + // server's lock takes away + bool guest_noclip = true, guest_no_bail = true, guest_boosts = true, guest_ollies = true; // roster: guests skate with the host's physics tuning (a dedicated server's: the game's own) bool enforce_tuning = true; // roster: the votes a dedicated server lets players start (server_vote_* bits) diff --git a/Extension/Multiplayer/Session/session.cpp b/Extension/Multiplayer/Session/session.cpp index a85d8136..bb858f17 100644 --- a/Extension/Multiplayer/Session/session.cpp +++ b/Extension/Multiplayer/Session/session.cpp @@ -103,7 +103,7 @@ void stop(Session &s, std::string reason) { s.server_map_rotation = 0; set_lobby_object_placement_allowed(true); set_lobby_object_scaling_allowed(true); - s.guest_noclip = s.guest_no_bail = s.guest_boosts = true; + s.guest_noclip = s.guest_no_bail = s.guest_boosts = s.guest_ollies = true; s.enforce_tuning = true; s.score_check = true; s.scoring_sent.reset(); @@ -122,7 +122,7 @@ void stop(Session &s, std::string reason) { s.server_votes = 0; s.server_polls = 0; s.server_custom_votes.clear(); - set_session_tools_allowed(true, true, true); + set_session_tools_allowed(true, true, true, true); s.object_clears.reset(); s.clear_pending = false; s.force_world_layers = false; diff --git a/Extension/Multiplayer/Session/session_commands.cpp b/Extension/Multiplayer/Session/session_commands.cpp index 2ae2f3f9..7fc0429f 100644 --- a/Extension/Multiplayer/Session/session_commands.cpp +++ b/Extension/Multiplayer/Session/session_commands.cpp @@ -51,12 +51,13 @@ void apply_nametags(const Session &s) { set_native_nametags_enabled(false); set_native_compass_enabled(!s.nametags); } -void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts) { +void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts, bool ollies) { s.guest_noclip = noclip; s.guest_no_bail = no_bail; s.guest_boosts = boosts; + s.guest_ollies = ollies; const bool exempt = s.mode == Mode::host || (s.role_permissions & role_movement); - set_session_tools_allowed(noclip || exempt, no_bail || exempt, boosts || exempt); + set_session_tools_allowed(noclip || exempt, no_bail || exempt, boosts || exempt, ollies || exempt); } } // namespace session_detail namespace { diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index 1c8fd018..7d18344e 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -173,6 +173,7 @@ struct Session { bool sync_effects{true}; // the dedicated server has players see each other's skater effects // What guests may use: the host's choice, or the host's roster for a guest. bool guest_noclip = true, guest_no_bail = true, guest_boosts = true; + bool guest_ollies = true; // a dedicated server's lock only: a lobby host has no such choice // Host: guests skate with its physics tuning. Guest: the host's roster says so (a // dedicated server's guests skate with the game's own). bool enforce_tuning = true; @@ -588,7 +589,7 @@ void apply_object_limit(Session &s, unsigned limit); void apply_object_scaling(Session &s, bool allowed); // Stores what guests may use and applies it to the local player (the host and a dedicated // server's admins are exempt). -void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts); +void apply_guest_tools(Session &s, bool noclip, bool no_bail, bool boosts, bool ollies = true); // Shows the chosen nametags: ReSkate's (and none of the game's nametags or compass // arrows), the game's own, or none. void apply_nametags(const Session &s); diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index ebc078db..b776143e 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -286,7 +286,7 @@ void apply_roster(Session &s, const Packet &p, std::uint64_t now) { apply_object_limit(s, p.object_limit); // after server_admin, which exempts an admin apply_object_scaling(s, !dedicated_host(s) || p.object_scaling); s.sync_effects = !dedicated_host(s) || p.sync_effects; - apply_guest_tools(s, p.guest_noclip, p.guest_no_bail, p.guest_boosts); + apply_guest_tools(s, p.guest_noclip, p.guest_no_bail, p.guest_boosts, p.guest_ollies); s.enforce_tuning = p.enforce_tuning; s.server_votes = dedicated_host(s) ? p.server_votes : 0; s.server_polls = dedicated_host(s) ? p.server_polls : 0; @@ -970,7 +970,7 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { apply_object_limit(s, s.object_limit); apply_object_placement(s, s.object_placement); apply_object_scaling(s, s.object_scaling); - apply_guest_tools(s, s.guest_noclip, s.guest_no_bail, s.guest_boosts); + apply_guest_tools(s, s.guest_noclip, s.guest_no_bail, s.guest_boosts, s.guest_ollies); publish(s); publish_chat(s); } diff --git a/Extension/Multiplayer/Test/protocol_tests.cpp b/Extension/Multiplayer/Test/protocol_tests.cpp index f331c2c4..40ba4aad 100644 --- a/Extension/Multiplayer/Test/protocol_tests.cpp +++ b/Extension/Multiplayer/Test/protocol_tests.cpp @@ -928,6 +928,13 @@ void dedicated_server_codec() { auto no_boosts = roster; no_boosts.guest_boosts = false; check(decode(encode(no_boosts)) && !decode(encode(no_boosts))->guest_boosts, "Guest boosts permission lost"); + auto no_ollies = voting; + no_ollies.guest_ollies = false; + const auto ollies_decoded = decode(encode(no_ollies)); + check(ollies_decoded && !ollies_decoded->guest_ollies && ollies_decoded->enforce_tuning && + ollies_decoded->server_votes == (server_vote_map | server_vote_time), + "Guest ollies permission lost, or it spilled into the other roster bits"); + check(decoded && decoded->guest_ollies, "Guest ollies should be allowed by default"); check(decoded && decoded->enforce_tuning, "Physics tuning enforcement lost"); auto own_tuning = voting; own_tuning.enforce_tuning = false; diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 7bf62d41..4365ae1b 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -995,9 +995,9 @@ struct WorldApi { } // A player's permissions: player.permissions is what the server holds for them now, read-only; // permissions:clone() is a copy to change; player:apply_permissions(copy) makes it theirs. - static constexpr std::array, 5> permission_fields{{ + static constexpr std::array, 6> permission_fields{{ {"noclip", &Game::Permissions::noclip}, {"nobail", &Game::Permissions::nobail}, {"boosts", &Game::Permissions::boosts}, - {"tuning", &Game::Permissions::tuning}, {"objects", &Game::Permissions::objects}}}; + {"tuning", &Game::Permissions::tuning}, {"objects", &Game::Permissions::objects}, {"ollies", &Game::Permissions::ollies}}}; static void push_permissions(lua_State* L, const Game::Permissions& permissions, bool readonly) { lua_createtable(L, 0, static_cast(permission_fields.size())); for (const auto& [name, field] : permission_fields) { diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index 8b007bd9..c2ac1993 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -292,7 +292,7 @@ public: // A player's permissions, true where they may: what the server holds for them now, or nothing // for one it cannot hold any for. Applying makes them exactly these. struct Permissions { - bool noclip = true, nobail = true, boosts = true, tuning = true, objects = true; + bool noclip = true, nobail = true, boosts = true, tuning = true, objects = true, ollies = true; }; virtual std::optional permissions(std::uint64_t) { return std::nullopt; } virtual std::string apply_permissions(std::string_view /*plugin*/, std::uint64_t, const Permissions&) { return unavailable; } diff --git a/Extension/Trainer/trainer_jump.cpp b/Extension/Trainer/trainer_jump.cpp index f62ae921..3f80391d 100644 --- a/Extension/Trainer/trainer_jump.cpp +++ b/Extension/Trainer/trainer_jump.cpp @@ -1,6 +1,7 @@ #include "trainer_jump.h" #include "Engine/Core/Hooks/hooks.h" #include "Engine/Core/Log/logging.h" +#include "Engine/Game/Multiplayer/session_tools.h" #include "Extension/Skater/no_bail.h" #include #include @@ -15,8 +16,9 @@ namespace { // the vector at parameters + 0x80. Measured with a scripted controller: // - a no comply calls it from 0x482f646 on entering the pop (state 200) after a crouch // (state 103) of about 0.20 s; an ollie crouches for 0.08 s and launches elsewhere; -// - a boneless calls it from 0x47691f8 while the foot is planted (state 602). -constexpr std::uintptr_t set_trajectory = 0x4752aa0, no_comply_call = 0x482f646, boneless_call = 0x47691f8; +// - a boneless calls it from 0x47691f8 while the foot is planted (state 602); +// - a flat-ground ollie calls it from 0x4824f7f (Engine/Game/Build/20260929/grind_exit.h). +constexpr std::uintptr_t set_trajectory = 0x4752aa0, no_comply_call = 0x482f646, boneless_call = 0x47691f8, ollie_call = 0x4824f7f; constexpr std::array set_trajectory_prefix{0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x80, 0xb9, 0x88, 0xbb, 0x00, 0x00, 0x00, 0x48, 0x8b, 0xd9, 0x0f, 0x85, 0xf6, 0x00, 0x00, 0x00, 0xc5, 0xfc, 0x28, 0x05, 0x62, 0x7d, 0xe8, 0x01, 0xc5, 0xfc}; @@ -53,6 +55,9 @@ void set_trajectory_hook(std::uintptr_t trajectory, float *parameters) { parameters[velocity_y + 1] = velocity[2]; } } + // A dedicated server's ollie lock (session_tools.h): the ollie launches with no upward speed, + // so the skater stays on the ground. + if (parameters && caller == ollie_call + 5 && !session_ollies_allowed() && parameters[velocity_y] > 0) parameters[velocity_y] = 0; if (parameters && (caller == no_comply_call + 5 || caller == boneless_call + 5)) { const auto watch = watched_physics_state(); // the local skater's Trick trick{}; diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index fd878eae..4df8a2d0 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -183,10 +183,10 @@ int run() { check(load_config(file).throwdown_locks == (lock_nobail | lock_tuning), "Throwdown locks lost"); std::ofstream(file, std::ios::binary) << R"({"players": {"throwdown_locks": ["boosts", "jetpack", 3, "teleport"]}})"; check(load_config(file).throwdown_locks == (lock_boosts | lock_noclip), "Bad throwdown locks not cleaned"); - check(parse_lock_tools("all") == lock_all && parse_lock_tools(" NoBail , tuning") == (lock_nobail | lock_tuning) && + check(parse_lock_tools("all") == lock_all && parse_lock_tools(" NoBail , tuning") == (lock_nobail | lock_tuning) && parse_lock_tools("ollies") == lock_ollies && !parse_lock_tools("") && !parse_lock_tools("nobail,") && !parse_lock_tools("fly"), "Lock tool names not read"); - check(lock_tools_text(lock_all) == "noclip, nobail, boosts, tuning" && lock_tools_text(0) == "none", "Lock tool names not written"); + check(lock_tools_text(lock_all) == "noclip, nobail, boosts, tuning, ollies" && lock_tools_text(0) == "none", "Lock tool names not written"); // A config from before the sections: every setting is read from where it was, under the name // it had, and the file is written back in sections, in reading order, with nothing called new diff --git a/Server/Test/server_lock_tests.cpp b/Server/Test/server_lock_tests.cpp index a9b81510..195b603f 100644 --- a/Server/Test/server_lock_tests.cpp +++ b/Server/Test/server_lock_tests.cpp @@ -331,7 +331,7 @@ int run() { check(has(b.chat, "The server unlocked for you: nobail, boosts."), "Bailey was not told of the unlock"); command("unlock Dana all"); check(free(d) && d.permissions && (*d.permissions & role_movement), "Dana did not get their roster and movement back"); - check(command("unlock Dana") == "Dana has none of noclip, nobail, boosts, tuning, objects locked.", "unlock with nothing locked"); + check(command("unlock Dana") == "Dana has none of noclip, nobail, boosts, tuning, ollies, objects locked.", "unlock with nothing locked"); check(command("lock Dana objects") == "Locked for Dana: objects.", "objects did not lock by name"); check(command("unlock Dana") == "Unlocked for Dana: objects.", "a bare unlock did not take back objects"); diff --git a/Server/server_commands.cpp b/Server/server_commands.cpp index f0dad033..15280049 100644 --- a/Server/server_commands.cpp +++ b/Server/server_commands.cpp @@ -954,7 +954,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { who = trim(who.substr(0, space)); } if (who.empty() || !tools) - return lock ? "lock (tools: noclip, nobail, boosts, tuning, objects)" + return lock ? "lock (tools: noclip, nobail, boosts, tuning, ollies, objects)" : "unlock []"; auto *guest = target(who); if (guest && !guest->handshaken) guest = nullptr; @@ -996,7 +996,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { if (argument.empty()) return "throwdown-locks |off (now " + (config_.throwdown_locks ? lock_tools_text(config_.throwdown_locks) : std::string("off")) + ")"; const auto tools = lower(argument) == "off" ? std::optional(0) : parse_lock_tools(argument); - if (!tools) return "throwdown-locks |off (tools: noclip, nobail, boosts, tuning, objects)"; + if (!tools) return "throwdown-locks |off (tools: noclip, nobail, boosts, tuning, ollies, objects)"; // The players of a running throwdown get the new set at once: what it no longer locks // unlocked, what it now locks locked (none of it when it is turned off or on: then // update_throwdown_locks ends or starts theirs). diff --git a/Server/server_config.cpp b/Server/server_config.cpp index e62fb9f0..8a432c69 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -132,7 +132,7 @@ Layout layout(const ServerConfig &c) { players.set("sync_effects", c.sync_effects); players.set("announce_throwdowns", c.announce_throwdowns); auto locks = Json::array(); - for (const auto bit : {lock_noclip, lock_nobail, lock_boosts, lock_tuning}) + for (const auto bit : {lock_noclip, lock_nobail, lock_boosts, lock_tuning, lock_ollies}) if (c.throwdown_locks & bit) locks.push_back(lock_tools_text(bit)); players.set("throwdown_locks", std::move(locks)); @@ -716,6 +716,7 @@ std::optional parse_lock_tools(std::string_view text) { else if (lower == "nobail") tools |= lock_nobail; else if (lower == "boosts") tools |= lock_boosts; else if (lower == "tuning") tools |= lock_tuning; + else if (lower == "ollies") tools |= lock_ollies; else if (lower == "objects") tools |= lock_objects; else if (lower == "all") tools |= lock_all; else return std::nullopt; @@ -727,7 +728,7 @@ std::string lock_tools_text(std::uint8_t tools) { std::string text; for (const auto &[bit, name] : {std::pair{lock_noclip, "noclip"}, std::pair{lock_nobail, "nobail"}, std::pair{lock_boosts, "boosts"}, std::pair{lock_tuning, "tuning"}, - std::pair{lock_objects, "objects"}}) + std::pair{lock_ollies, "ollies"}, std::pair{lock_objects, "objects"}}) if (tools & bit) text += (text.empty() ? "" : ", ") + std::string(name); return text.empty() ? "none" : text; } diff --git a/Server/server_config.h b/Server/server_config.h index d29aafc2..65e47662 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -90,8 +90,8 @@ inline constexpr unsigned dedicated_tps = 20; // The tools the server can take away from single players (lock, throwdown_locks): noclip with // teleporting, No Bail, the boosts, their own physics tuning, and placing objects. "all" is the // first four, as throwdown_locks has always read it: objects are only ever locked by name. -inline constexpr std::uint8_t lock_noclip = 1, lock_nobail = 2, lock_boosts = 4, lock_tuning = 8, lock_objects = 16; -inline constexpr std::uint8_t lock_movement = lock_noclip | lock_nobail | lock_boosts; // what the role's "movement" gives +inline constexpr std::uint8_t lock_noclip = 1, lock_nobail = 2, lock_boosts = 4, lock_tuning = 8, lock_objects = 16, lock_ollies = 32; +inline constexpr std::uint8_t lock_movement = lock_noclip | lock_nobail | lock_boosts | lock_ollies; // what the role's "movement" gives inline constexpr std::uint8_t lock_all = lock_movement | lock_tuning; struct ServerConfig { std::filesystem::path file; diff --git a/Server/server_host.cpp b/Server/server_host.cpp index 4d3d6eb0..bbef2e72 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -814,10 +814,10 @@ void Host::send_roster() { // locked tools is told those are off for them. Everyone else gets the same roster; players // whose roster differs in the same way share one. struct Variant { - bool noclip{}, no_bail{}, boosts{}, enforce_tuning{}, announcement{}; + bool noclip{}, no_bail{}, boosts{}, ollies{}, enforce_tuning{}, announcement{}; auto operator<=>(const Variant &) const = default; }; - const Variant common{config_.noclip, config_.no_bail, config_.boosts, config_.enforce_tuning, !announcement_for_}; + const Variant common{config_.noclip, config_.no_bail, config_.boosts, true, config_.enforce_tuning, !announcement_for_}; std::map> variants; std::set others; for (const auto &[id, guest] : guests_) { @@ -831,6 +831,7 @@ void Host::send_roster() { variant.noclip = !(locks & lock_noclip) && (config_.noclip || movement); variant.no_bail = !(locks & lock_nobail) && (config_.no_bail || movement); variant.boosts = !(locks & lock_boosts) && (config_.boosts || movement); + variant.ollies = !(locks & lock_ollies); // no setting turns ollies off: only a lock } variant.enforce_tuning = config_.enforce_tuning || (locks & lock_tuning); } @@ -845,6 +846,7 @@ void Host::send_roster() { p.guest_noclip = variant.noclip; p.guest_no_bail = variant.no_bail; p.guest_boosts = variant.boosts; + p.guest_ollies = variant.ollies; p.enforce_tuning = variant.enforce_tuning; p.announcement = variant.announcement ? shown : multiplayer::ServerAnnouncement{}; broadcast(p, true, false, 0, &players); diff --git a/Server/server_plugins.cpp b/Server/server_plugins.cpp index edd1a643..790bfa60 100644 --- a/Server/server_plugins.cpp +++ b/Server/server_plugins.cpp @@ -70,12 +70,12 @@ class ServerGame final : public Game { if (!multiplayer::individual_steam_id(player)) return std::nullopt; const auto locks = host_.player_locks(player); return Permissions{!(locks & lock_noclip), !(locks & lock_nobail), !(locks & lock_boosts), !(locks & lock_tuning), - !(locks & lock_objects)}; + !(locks & lock_objects), !(locks & lock_ollies)}; } std::string apply_permissions(std::string_view plugin, std::uint64_t player, const Permissions &p) override { if (!multiplayer::individual_steam_id(player)) return "not a player's SteamID64"; const auto locks = static_cast((p.noclip ? 0 : lock_noclip) | (p.nobail ? 0 : lock_nobail) | (p.boosts ? 0 : lock_boosts) | - (p.tuning ? 0 : lock_tuning) | (p.objects ? 0 : lock_objects)); + (p.tuning ? 0 : lock_tuning) | (p.objects ? 0 : lock_objects) | (p.ollies ? 0 : lock_ollies)); host_.set_locks(player, locks, "plugin " + std::string(plugin)); return {}; } From 70228af281152a4b6b75419a5804c7d4600abd76 Mon Sep 17 00:00:00 2001 From: wagu Date: Tue, 20 Oct 2026 09:54:45 -0400 Subject: [PATCH 4/5] Docs: project structure for clarity --- STRUCTURE.md | 263 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 STRUCTURE.md diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 00000000..50259a91 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,263 @@ +# ReSkate structure + +How the repository is laid out, what each part builds into, and where a feature's code lives. +For building and using ReSkate, see [README.md](README.md). + +## What gets built + +| Binary | Source | What it is | +|---|---|---| +| `ReSkateLauncher.exe` | `Launcher/` | the launcher: updates, Steam sign-in and game download, mods and profiles, starts the game, crash reporting | +| `ReSkate.dll` | `Runtime/` + `Extension/` + `Engine/` (+ `SpotifyHelper/`) | the runtime the launcher loads into `Skate.exe` | +| `ReSkateServer.exe` / `ReSkateServer` | `Server/` | the headless dedicated server (Windows and native Linux) | +| `ReSkateEmotePacker.exe` | `EmotePacker/` | packs `assets/emotes` into the chat emote pack | +| `dingosdk_builtin_packer` | `BuiltinPacker/` | packs `assets/builtin` into the built-in packs | +| `dingosdk_*_tests` | each module's `Test/` | regression tests, run with CTest | + +Internally the project is still called **DingoSDK** (CMake options, target names). + +## Layering + +``` + Launcher/ ──launches──▶ Skate.exe ◀──injected── ReSkate.dll + │ + Runtime/ entry point, startup, per-frame tick, exports + │ + Extension/ ReSkate's features and UI + │ + Engine/Game/ native ABI, data models, per-build addresses + Engine/Vfs/ mods, merging, content cache + Engine/Resource/ Frostbite formats + Engine/Core/ logging, console, hooks, crash reports + │ + External/ third-party libraries + + Server/ (ReSkateServer) reuses Engine/Core and the multiplayer protocol, not the game. +``` + +Feature code never holds game addresses: everything tied to one `Skate.exe` build lives in +`Engine/Game/Build//` (see [Engine/Game](#enginegame)). + +## Top level + +``` +ReSkate/ +├── CMakeLists.txt, CMakePresets.json build entry; presets vs2022-x64, linux-x64, release, linux-release +├── cmake/ the build, split by area +├── Engine/ engine-level code shared by the DLL, launcher and server +├── Extension/ ReSkate's features (in ReSkate.dll) +├── Runtime/ ReSkate.dll entry point and lifecycle +├── Launcher/ ReSkateLauncher.exe +├── Server/ ReSkateServer (dedicated server) +├── SpotifyHelper/ Rust static library: the game as a Spotify Connect device +├── EmotePacker/ emote pack tool +├── BuiltinPacker/ built-in pack tool +├── External/ vendored third-party code, pinned and hashed +├── assets/ files embedded into the binaries +├── config/defaults/ default profile/settings JSON embedded into ReSkate.dll +├── contrib/ plugin mods, server hosting files, design notes, release script +├── README.md, MACOS-SETUP.md, MUSIC-SETUP.md, LICENSE +└── .clangd, .gitattributes, .gitignore +``` + +## `cmake/` + +| File | Covers | +|---|---| +| `ProjectOptions.cmake` | `DINGOSDK_*` options, version stamping (`reskate_version.h`, `version.rc`) | +| `Dependencies.cmake` | the `External/` libraries as static targets | +| `Core.cmake` | `Engine/` libraries: hooks, console core, launcher support, supported build | +| `Features.cmake` | `Extension/` feature libraries | +| `UI.cmake` | the ImGui overlay and PlayStation controller input | +| `Runtime.cmake` | `ReSkate.dll` (`dingosdk_runtime`), including the cargo build of `SpotifyHelper/` | +| `Apps.cmake` | launcher, server, packer tools, and most test executables | +| `CrashReport.cmake` | Sentry crash report client/upload, wired into the DLL, launcher and server | +| `ProfileData.cmake`, `CustomScriptData.cmake` | embed `config/defaults/` and the custom Lua entry | +| `templates/` | generated headers: version, `version.rc`, crash/update config, profile defaults | + +## `Engine/` + +Loosely follows Frostbite's own layout. + +### `Engine/Core/` +| Folder | Contents | +|---|---| +| `Console/` | command registry and console core | +| `Debug/` | crash reports: minidumps, report envelope, trace, upload | +| `Hooks/` | Detours hook helpers | +| `Json/` | JSON helpers over RapidJSON | +| `Log/` | spdlog-based logger, log paths, crash/event logging | +| `Platform/` | process injection, PE helpers, memory and path utilities | +| `Profiling/` | profiler and sampler | +| `Storage/` | SQLite save database | +| `Text/` | chat word filter | + +### `Engine/Resource/` +Frostbite data formats: binary bundles, TOC, CAS codec (Oodle/zstd/LZ4), EBX documents (read, write, merge, +fixup repair), mesh sets, skeletons, textures, material grids, shader lookup. + +### `Engine/Vfs/` +The virtual file system and modding pipeline: +- `initfs`, `game_archives`, `game_bundles`, `game_textures`, `native_db` — reading the game's data. +- `mod_list`, `mod_catalog`, `mod_item_sources`, `mod_conflicts`, `mod_scoring` — the `Mods/` folder and `mods.json`. +- `mod_merge*` — merging mods into the game's data (overrides, combine, grid, textures, loading screens, CAS store, scan cache, build stamp). +- `content_cache*`, `content_catalogs` — the merged-content cache and its install. +- `thunderstore_package`, `https_download` — Thunderstore packages. +- `mod_music`, `item_thumbnails`, `world_layer_scan`, `builtin_packs`. + +### `Engine/Scripting/` +Custom Lua script support (currently switched off for users). + +### `Engine/Game/` +The game's native ABI and plain data models, no hooks: + +| Folder | Contents | +|---|---| +| `Abi/` | native strings, data layouts, transforms, callback queue | +| `Build/` | `supported_build.h` (hashes, Steam depot/manifest pins), `fingerprint.h`, `image_identity`, `addresses.h` | +| `Build/20260929/` | one header per area with every address, fingerprint and patch for this `Skate.exe` build, as `addr::::` | +| `Input/` | controller bindings, PlayStation reports, voice input | +| `Multiplayer/` | session model and limits, tick/voice/distance settings, role commands, chat rate | +| `Profile/` | progression, player card, object persistence models | +| `Rendering/` | draw packets, graphics controls | +| `Settings/` | named settings, simulation time, multiplayer settings lock | +| `Skater/` | first person, velocity boost, skater body | +| `UI/` | game view, menu entries and scale, loading screen selection | +| `World/` | world model, layers, parks (rotation, randomization, editor), travel, custom level manifest | + +## `Extension/` + +ReSkate's features. Each folder is one area; most have a `console_commands.cpp` for its console +commands and a `Test/` folder. + +| Folder | Feature | +|---|---| +| `Boot/` | offline boot, offline Steam, blocking EA app/services, user-data redirect, startup interventions, Discord presence, fatal dumps | +| `Assets/` | live mods (apply without restart), loose files, mod layers, multiplayer map download, map icons | +| `Console/` | built-in console commands, perf commands, memory ledger | +| `Customization/` | cosmetics catalog, item browser, local customization, player card | +| `Multiplayer/` | Steam lobbies, sessions, parties, voice and text chat (see below) | +| `Music/` | local music, playlists and artwork, external players: Spotify, YouTube Music, Plex | +| `News/` | launcher/game news feed | +| `Objects/` | object placement and the Park Editor (`ParkEditor/`), park object sets, park mods | +| `Plugins/` | the Luau plugin host and its `reskate.*` API (see below) | +| `Profile/` | offline profile storage: skater, progress, missions, objects, preferences | +| `Progression/` | challenges, entitlements, neighborhood unlocks, fast travel unlock, rip score, board wear | +| `Rendering/` | graphics controls and labels, display startup, replay export | +| `ReplayKeyframes/` | replay keyframe curves and settings | +| `Scripting/` | Lua startup, custom script loader, native blocking | +| `Settings/` | gameplay/native/user settings, input bindings, engine tweaks, replay drafts | +| `Skater/` | first person, noclip, free flight, physics tuning, no-bail, AI skaters, x-ray, breakable deck, grind exits, trick watch | +| `Throwdowns/` | native throwdowns, custom (plugin) modes, relay and wire format | +| `Trainer/` | trick trainer: waypoints, jumps, landings, presets | +| `Travel/` | location travel | +| `UI/` | the in-game UI (see below) | +| `World/` | level loading, loading screens, time/atmosphere, population, park rotation, world layers, engine guards | +| `AnimationStudio/` | animation core | + +### `Extension/Multiplayer/` +| Folder | Contents | +|---|---| +| `Net/` | protocol, wire/delta/pose/sound codecs | +| `Steam/` | lobbies, server browser, transport and lanes, friend join, social | +| `Session/` | session state, send/receive, parties, commands, rooms, passwords | +| `Remote/` | remote skaters: native skater spawn, cosmetics, poses, audio, VFX, collision | +| `Hud/` | nametags, indicators, player UI | +| `Voice/` | voice capture and playback | + +### `Extension/UI/` +| Folder | Contents | +|---|---| +| `Overlay/` | the ImGui overlay: the ReSkate menu (Insert) and its pages, console, chat and emotes, Park Editor panels, plugin pages, perf overlay, theme | +| `NativeMenu/` | pages built into the game's own menu (hub, multiplayer, throwdowns) | +| `Startup/` | startup window/splash | + +### `Extension/Plugins/` +The Luau plugin host. A plugin is a folder in `Mods/` with a `reskate-plugin.json`. +- `plugin_discovery`, `plugin_manifest` — find and read plugins. +- `plugin_vm`, `plugin_runtime`, `plugin_host` — the sandboxed Luau VM and plugin lifecycle. +- `plugin_api*.cpp` — the `reskate.*` API (game, world, UI, more); `plugin_game*`, `plugin_camera`, `plugin_joystick`. +- `plugin_ui`, `plugin_sound`, `plugin_wave`, `plugin_http`, `plugin_session_feed`, `plugin_wire` — pages, audio, HTTP, multiplayer messages. +- `plugin_commands` — the `plugin` console command. + +## `Runtime/` + +`ReSkate.dll`'s lifecycle: `exports.cpp` (DLL exports), `bootstrap`, `runtime_init`, `runtime_state`, +`client_context`, `client_tick` (per-frame), `frame_timing`, `level_requests`, `request_scheduler`, +`menu_entry_hook`, `overlay_callbacks`, `console_bridge`. + +## `Launcher/` + +| Files | Contents | +|---|---| +| `main.cpp`, `launch` | entry point and starting/injecting the game | +| `updater` | ReSkate self-update from GitHub releases | +| `depot_output`, `game_settings` | supported game build download/verification and game settings | +| `mod_manager`, `mod_profiles`, `thunderstore`, `official_mods` | mods, profiles (r2modman-compatible codes and `.r2z`), Thunderstore | +| `gui*.cpp` | the window: home, mods (browse, profiles), settings, sign-in, changelog, legal, gamepad | +| `crash_reporter` | crash reporting host | +| `Test/` | launcher tests | + +## `Server/` + +| Files | Contents | +|---|---| +| `main.cpp`, `server_engine`, `server_host`, `steam_server` | the server loop and Steam game server | +| `server_config` | `ReSkateServer.json` | +| `server_commands`, `server_votes`, `server_party`, `server_parks`, `server_map_update` | admin commands, votes, parties, parks, map updates | +| `roles.h`, `global_bans`, `server_words` | roles/permissions, bans, word filter | +| `board_check.h`, `speed_check.h` | anti-cheat checks | +| `server_plugins` | server-side plugins | +| `server_update`, `server_release` | self-update | +| `GUI/` | optional server window | +| `README.txt`, `README-linux.md`, `README-roles.txt`, `ROLES.md` | server docs | +| `Test/` | server tests and fixtures | + +## `SpotifyHelper/` + +Rust crate (`reskate-spotify`, librespot-based) built by cargo from `cmake/Runtime.cmake` as a static +library linked into `ReSkate.dll`. It makes the game a Spotify Connect device. Not built in Debug. + +## `External/` + +Vendored libraries, versions and hashes in `External/manifest.json` and [External/README.md](External/README.md): +Dear ImGui, Microsoft Detours, Luau, RapidJSON, spdlog, SQLite, LZ4, Zstandard, miniz, bcdec, +Valve's Steam networking headers, and fonts (Montserrat, Permanent Marker). + +## `assets/` and `config/` + +- `assets/launcher/` — launcher background and icons. +- `assets/emotes/` — chat emotes (`emotes.json`, `emotes.png`). +- `assets/builtin/` — built-in packs (`throwdown_menu.zip`). +- `assets/models/fpv/` — FPV drone model. +- `assets/*.jpg|png` — splash, music artwork. +- `config/defaults/` — default `profile.json`, `progress.json`, `settings.json`, `options.json`, + `challenges.json`, `location-travel.json`, `object-dropper.json`, `news.json`, `manifest.json`. + +## `contrib/` + +| Path | Contents | +|---|---| +| `plugins/` | plugin mods: ports of former built-in features (`hall-of-meat`, `grind-exits`, `road-rash`, `board-break`, `fpv-drone`), test plugins (`reskate-test`, `reskate-hostile`, `throwdown-modes-test`), `server-starter`, and `tools/`. See [contrib/plugins/README.md](contrib/plugins/README.md) | +| `pterodactyl/` | Pterodactyl egg for hosting the server | +| `reskate-server.service`, `setup-linux-server-libs.sh` | Linux server setup | +| `package_release_zip.py` | packs release zips | +| `*.md` | design notes for individual features | +| `Test/` | tests for contrib scripts | + +Each plugin folder looks like: + +``` +/ +├── reskate-plugin.json plugin manifest +├── manifest.json Thunderstore manifest +├── plugin/main.lua entry point (+ other modules, tests.lua) +└── images/, sounds/, data/ optional assets +``` + +## Tests + +Tests live in a `Test/` folder beside the code they cover (about 115 files) and build as +`dingosdk_*_tests` executables. Turn groups on with `DINGOSDK_BUILD_MULTIPLAYER_TESTS`, +`DINGOSDK_BUILD_LAUNCHER_TESTS`, `DINGOSDK_BUILD_PARK_EDITOR_TESTS`, `DINGOSDK_BUILD_CRASH_REPORT_TESTS` +and `DINGOSDK_BUILD_PLUGIN_TESTS`, then run `ctest --test-dir build/vs2022-x64 -C Release`. From ded49756ea4f3790ef0f80f440777829c2f118a4 Mon Sep 17 00:00:00 2001 From: wagu Date: Tue, 20 Oct 2026 09:54:45 -0400 Subject: [PATCH 5/5] Docs: project structure for clarity --- Engine/Game/Multiplayer/session_model.h | 2 + .../Multiplayer/Session/session_view.cpp | 1 + STRUCTURE.md | 263 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 STRUCTURE.md diff --git a/Engine/Game/Multiplayer/session_model.h b/Engine/Game/Multiplayer/session_model.h index cf5c1519..f9046790 100644 --- a/Engine/Game/Multiplayer/session_model.h +++ b/Engine/Game/Multiplayer/session_model.h @@ -179,6 +179,8 @@ struct MultiplayerModel { bool object_scaling{true}; // Host setting: whether guests may use noclip / No Bail (the host and server admins always may). bool guest_noclip{true}, guest_no_bail{true}, guest_boosts{true}; + // Whether this player may ollie: false only while a dedicated server locks it for them. + bool guest_ollies{true}; // Host setting: guests skate with the host's physics tuning (on a dedicated server: the // game's own). Guest: what that does here, while enforced. bool enforce_tuning{true}; diff --git a/Extension/Multiplayer/Session/session_view.cpp b/Extension/Multiplayer/Session/session_view.cpp index 88a8d07e..f58e5491 100644 --- a/Extension/Multiplayer/Session/session_view.cpp +++ b/Extension/Multiplayer/Session/session_view.cpp @@ -158,6 +158,7 @@ void publish(Session &s, const NativeFrame *local) { view.guest_noclip = s.guest_noclip; view.guest_no_bail = s.guest_no_bail; view.guest_boosts = s.guest_boosts; + view.guest_ollies = s.guest_ollies; view.enforce_tuning = s.enforce_tuning; view.tuning_status = s.mode == Mode::join && s.enforce_tuning ? physics_tuning::status() : std::string{}; view.force_world_layers = s.force_world_layers; diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 00000000..50259a91 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,263 @@ +# ReSkate structure + +How the repository is laid out, what each part builds into, and where a feature's code lives. +For building and using ReSkate, see [README.md](README.md). + +## What gets built + +| Binary | Source | What it is | +|---|---|---| +| `ReSkateLauncher.exe` | `Launcher/` | the launcher: updates, Steam sign-in and game download, mods and profiles, starts the game, crash reporting | +| `ReSkate.dll` | `Runtime/` + `Extension/` + `Engine/` (+ `SpotifyHelper/`) | the runtime the launcher loads into `Skate.exe` | +| `ReSkateServer.exe` / `ReSkateServer` | `Server/` | the headless dedicated server (Windows and native Linux) | +| `ReSkateEmotePacker.exe` | `EmotePacker/` | packs `assets/emotes` into the chat emote pack | +| `dingosdk_builtin_packer` | `BuiltinPacker/` | packs `assets/builtin` into the built-in packs | +| `dingosdk_*_tests` | each module's `Test/` | regression tests, run with CTest | + +Internally the project is still called **DingoSDK** (CMake options, target names). + +## Layering + +``` + Launcher/ ──launches──▶ Skate.exe ◀──injected── ReSkate.dll + │ + Runtime/ entry point, startup, per-frame tick, exports + │ + Extension/ ReSkate's features and UI + │ + Engine/Game/ native ABI, data models, per-build addresses + Engine/Vfs/ mods, merging, content cache + Engine/Resource/ Frostbite formats + Engine/Core/ logging, console, hooks, crash reports + │ + External/ third-party libraries + + Server/ (ReSkateServer) reuses Engine/Core and the multiplayer protocol, not the game. +``` + +Feature code never holds game addresses: everything tied to one `Skate.exe` build lives in +`Engine/Game/Build//` (see [Engine/Game](#enginegame)). + +## Top level + +``` +ReSkate/ +├── CMakeLists.txt, CMakePresets.json build entry; presets vs2022-x64, linux-x64, release, linux-release +├── cmake/ the build, split by area +├── Engine/ engine-level code shared by the DLL, launcher and server +├── Extension/ ReSkate's features (in ReSkate.dll) +├── Runtime/ ReSkate.dll entry point and lifecycle +├── Launcher/ ReSkateLauncher.exe +├── Server/ ReSkateServer (dedicated server) +├── SpotifyHelper/ Rust static library: the game as a Spotify Connect device +├── EmotePacker/ emote pack tool +├── BuiltinPacker/ built-in pack tool +├── External/ vendored third-party code, pinned and hashed +├── assets/ files embedded into the binaries +├── config/defaults/ default profile/settings JSON embedded into ReSkate.dll +├── contrib/ plugin mods, server hosting files, design notes, release script +├── README.md, MACOS-SETUP.md, MUSIC-SETUP.md, LICENSE +└── .clangd, .gitattributes, .gitignore +``` + +## `cmake/` + +| File | Covers | +|---|---| +| `ProjectOptions.cmake` | `DINGOSDK_*` options, version stamping (`reskate_version.h`, `version.rc`) | +| `Dependencies.cmake` | the `External/` libraries as static targets | +| `Core.cmake` | `Engine/` libraries: hooks, console core, launcher support, supported build | +| `Features.cmake` | `Extension/` feature libraries | +| `UI.cmake` | the ImGui overlay and PlayStation controller input | +| `Runtime.cmake` | `ReSkate.dll` (`dingosdk_runtime`), including the cargo build of `SpotifyHelper/` | +| `Apps.cmake` | launcher, server, packer tools, and most test executables | +| `CrashReport.cmake` | Sentry crash report client/upload, wired into the DLL, launcher and server | +| `ProfileData.cmake`, `CustomScriptData.cmake` | embed `config/defaults/` and the custom Lua entry | +| `templates/` | generated headers: version, `version.rc`, crash/update config, profile defaults | + +## `Engine/` + +Loosely follows Frostbite's own layout. + +### `Engine/Core/` +| Folder | Contents | +|---|---| +| `Console/` | command registry and console core | +| `Debug/` | crash reports: minidumps, report envelope, trace, upload | +| `Hooks/` | Detours hook helpers | +| `Json/` | JSON helpers over RapidJSON | +| `Log/` | spdlog-based logger, log paths, crash/event logging | +| `Platform/` | process injection, PE helpers, memory and path utilities | +| `Profiling/` | profiler and sampler | +| `Storage/` | SQLite save database | +| `Text/` | chat word filter | + +### `Engine/Resource/` +Frostbite data formats: binary bundles, TOC, CAS codec (Oodle/zstd/LZ4), EBX documents (read, write, merge, +fixup repair), mesh sets, skeletons, textures, material grids, shader lookup. + +### `Engine/Vfs/` +The virtual file system and modding pipeline: +- `initfs`, `game_archives`, `game_bundles`, `game_textures`, `native_db` — reading the game's data. +- `mod_list`, `mod_catalog`, `mod_item_sources`, `mod_conflicts`, `mod_scoring` — the `Mods/` folder and `mods.json`. +- `mod_merge*` — merging mods into the game's data (overrides, combine, grid, textures, loading screens, CAS store, scan cache, build stamp). +- `content_cache*`, `content_catalogs` — the merged-content cache and its install. +- `thunderstore_package`, `https_download` — Thunderstore packages. +- `mod_music`, `item_thumbnails`, `world_layer_scan`, `builtin_packs`. + +### `Engine/Scripting/` +Custom Lua script support (currently switched off for users). + +### `Engine/Game/` +The game's native ABI and plain data models, no hooks: + +| Folder | Contents | +|---|---| +| `Abi/` | native strings, data layouts, transforms, callback queue | +| `Build/` | `supported_build.h` (hashes, Steam depot/manifest pins), `fingerprint.h`, `image_identity`, `addresses.h` | +| `Build/20260929/` | one header per area with every address, fingerprint and patch for this `Skate.exe` build, as `addr::::` | +| `Input/` | controller bindings, PlayStation reports, voice input | +| `Multiplayer/` | session model and limits, tick/voice/distance settings, role commands, chat rate | +| `Profile/` | progression, player card, object persistence models | +| `Rendering/` | draw packets, graphics controls | +| `Settings/` | named settings, simulation time, multiplayer settings lock | +| `Skater/` | first person, velocity boost, skater body | +| `UI/` | game view, menu entries and scale, loading screen selection | +| `World/` | world model, layers, parks (rotation, randomization, editor), travel, custom level manifest | + +## `Extension/` + +ReSkate's features. Each folder is one area; most have a `console_commands.cpp` for its console +commands and a `Test/` folder. + +| Folder | Feature | +|---|---| +| `Boot/` | offline boot, offline Steam, blocking EA app/services, user-data redirect, startup interventions, Discord presence, fatal dumps | +| `Assets/` | live mods (apply without restart), loose files, mod layers, multiplayer map download, map icons | +| `Console/` | built-in console commands, perf commands, memory ledger | +| `Customization/` | cosmetics catalog, item browser, local customization, player card | +| `Multiplayer/` | Steam lobbies, sessions, parties, voice and text chat (see below) | +| `Music/` | local music, playlists and artwork, external players: Spotify, YouTube Music, Plex | +| `News/` | launcher/game news feed | +| `Objects/` | object placement and the Park Editor (`ParkEditor/`), park object sets, park mods | +| `Plugins/` | the Luau plugin host and its `reskate.*` API (see below) | +| `Profile/` | offline profile storage: skater, progress, missions, objects, preferences | +| `Progression/` | challenges, entitlements, neighborhood unlocks, fast travel unlock, rip score, board wear | +| `Rendering/` | graphics controls and labels, display startup, replay export | +| `ReplayKeyframes/` | replay keyframe curves and settings | +| `Scripting/` | Lua startup, custom script loader, native blocking | +| `Settings/` | gameplay/native/user settings, input bindings, engine tweaks, replay drafts | +| `Skater/` | first person, noclip, free flight, physics tuning, no-bail, AI skaters, x-ray, breakable deck, grind exits, trick watch | +| `Throwdowns/` | native throwdowns, custom (plugin) modes, relay and wire format | +| `Trainer/` | trick trainer: waypoints, jumps, landings, presets | +| `Travel/` | location travel | +| `UI/` | the in-game UI (see below) | +| `World/` | level loading, loading screens, time/atmosphere, population, park rotation, world layers, engine guards | +| `AnimationStudio/` | animation core | + +### `Extension/Multiplayer/` +| Folder | Contents | +|---|---| +| `Net/` | protocol, wire/delta/pose/sound codecs | +| `Steam/` | lobbies, server browser, transport and lanes, friend join, social | +| `Session/` | session state, send/receive, parties, commands, rooms, passwords | +| `Remote/` | remote skaters: native skater spawn, cosmetics, poses, audio, VFX, collision | +| `Hud/` | nametags, indicators, player UI | +| `Voice/` | voice capture and playback | + +### `Extension/UI/` +| Folder | Contents | +|---|---| +| `Overlay/` | the ImGui overlay: the ReSkate menu (Insert) and its pages, console, chat and emotes, Park Editor panels, plugin pages, perf overlay, theme | +| `NativeMenu/` | pages built into the game's own menu (hub, multiplayer, throwdowns) | +| `Startup/` | startup window/splash | + +### `Extension/Plugins/` +The Luau plugin host. A plugin is a folder in `Mods/` with a `reskate-plugin.json`. +- `plugin_discovery`, `plugin_manifest` — find and read plugins. +- `plugin_vm`, `plugin_runtime`, `plugin_host` — the sandboxed Luau VM and plugin lifecycle. +- `plugin_api*.cpp` — the `reskate.*` API (game, world, UI, more); `plugin_game*`, `plugin_camera`, `plugin_joystick`. +- `plugin_ui`, `plugin_sound`, `plugin_wave`, `plugin_http`, `plugin_session_feed`, `plugin_wire` — pages, audio, HTTP, multiplayer messages. +- `plugin_commands` — the `plugin` console command. + +## `Runtime/` + +`ReSkate.dll`'s lifecycle: `exports.cpp` (DLL exports), `bootstrap`, `runtime_init`, `runtime_state`, +`client_context`, `client_tick` (per-frame), `frame_timing`, `level_requests`, `request_scheduler`, +`menu_entry_hook`, `overlay_callbacks`, `console_bridge`. + +## `Launcher/` + +| Files | Contents | +|---|---| +| `main.cpp`, `launch` | entry point and starting/injecting the game | +| `updater` | ReSkate self-update from GitHub releases | +| `depot_output`, `game_settings` | supported game build download/verification and game settings | +| `mod_manager`, `mod_profiles`, `thunderstore`, `official_mods` | mods, profiles (r2modman-compatible codes and `.r2z`), Thunderstore | +| `gui*.cpp` | the window: home, mods (browse, profiles), settings, sign-in, changelog, legal, gamepad | +| `crash_reporter` | crash reporting host | +| `Test/` | launcher tests | + +## `Server/` + +| Files | Contents | +|---|---| +| `main.cpp`, `server_engine`, `server_host`, `steam_server` | the server loop and Steam game server | +| `server_config` | `ReSkateServer.json` | +| `server_commands`, `server_votes`, `server_party`, `server_parks`, `server_map_update` | admin commands, votes, parties, parks, map updates | +| `roles.h`, `global_bans`, `server_words` | roles/permissions, bans, word filter | +| `board_check.h`, `speed_check.h` | anti-cheat checks | +| `server_plugins` | server-side plugins | +| `server_update`, `server_release` | self-update | +| `GUI/` | optional server window | +| `README.txt`, `README-linux.md`, `README-roles.txt`, `ROLES.md` | server docs | +| `Test/` | server tests and fixtures | + +## `SpotifyHelper/` + +Rust crate (`reskate-spotify`, librespot-based) built by cargo from `cmake/Runtime.cmake` as a static +library linked into `ReSkate.dll`. It makes the game a Spotify Connect device. Not built in Debug. + +## `External/` + +Vendored libraries, versions and hashes in `External/manifest.json` and [External/README.md](External/README.md): +Dear ImGui, Microsoft Detours, Luau, RapidJSON, spdlog, SQLite, LZ4, Zstandard, miniz, bcdec, +Valve's Steam networking headers, and fonts (Montserrat, Permanent Marker). + +## `assets/` and `config/` + +- `assets/launcher/` — launcher background and icons. +- `assets/emotes/` — chat emotes (`emotes.json`, `emotes.png`). +- `assets/builtin/` — built-in packs (`throwdown_menu.zip`). +- `assets/models/fpv/` — FPV drone model. +- `assets/*.jpg|png` — splash, music artwork. +- `config/defaults/` — default `profile.json`, `progress.json`, `settings.json`, `options.json`, + `challenges.json`, `location-travel.json`, `object-dropper.json`, `news.json`, `manifest.json`. + +## `contrib/` + +| Path | Contents | +|---|---| +| `plugins/` | plugin mods: ports of former built-in features (`hall-of-meat`, `grind-exits`, `road-rash`, `board-break`, `fpv-drone`), test plugins (`reskate-test`, `reskate-hostile`, `throwdown-modes-test`), `server-starter`, and `tools/`. See [contrib/plugins/README.md](contrib/plugins/README.md) | +| `pterodactyl/` | Pterodactyl egg for hosting the server | +| `reskate-server.service`, `setup-linux-server-libs.sh` | Linux server setup | +| `package_release_zip.py` | packs release zips | +| `*.md` | design notes for individual features | +| `Test/` | tests for contrib scripts | + +Each plugin folder looks like: + +``` +/ +├── reskate-plugin.json plugin manifest +├── manifest.json Thunderstore manifest +├── plugin/main.lua entry point (+ other modules, tests.lua) +└── images/, sounds/, data/ optional assets +``` + +## Tests + +Tests live in a `Test/` folder beside the code they cover (about 115 files) and build as +`dingosdk_*_tests` executables. Turn groups on with `DINGOSDK_BUILD_MULTIPLAYER_TESTS`, +`DINGOSDK_BUILD_LAUNCHER_TESTS`, `DINGOSDK_BUILD_PARK_EDITOR_TESTS`, `DINGOSDK_BUILD_CRASH_REPORT_TESTS` +and `DINGOSDK_BUILD_PLUGIN_TESTS`, then run `ctest --test-dir build/vs2022-x64 -C Release`.