Merge pull request #317 from ValveTextureFile/server-lua-api

Plugins: server plugins ban, unban; player permissions; reskate.Enums
This commit is contained in:
zeex64 authored and GitHub committed 2026-10-20 13:24:17 -05:00
commit 5da1344805
31 files changed
+657 -378

No files matched your search

+4
View File
@@ -38,3 +38,7 @@ analysis/raw/**/*.res
!/contrib/plugins/SmokeyDawg58-ReSkate_Throwdown/plugin/
!/contrib/plugins/SmokeyDawg58-ReSkate_Throwdown/reskate-plugin.json
/contrib/plugins/*.fbmod
# vtf's linux development configuration
.vscode/
.clangd
+2
View File
@@ -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};
+6 -3
View File
@@ -3,24 +3,27 @@
#include "session_competition.h"
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<bool> noclip{true}, no_bail{true}, boosts{true};
inline std::atomic<bool> noclip{true}, no_bail{true}, boosts{true}, ollies{true};
inline std::atomic<bool> session{};
}
inline bool session_noclip_allowed() noexcept { return !competition_stock_physics() && session_tools_detail::noclip.load(std::memory_order_acquire); }
inline bool session_no_bail_allowed() noexcept { return !competition_stock_physics() && session_tools_detail::no_bail.load(std::memory_order_acquire); }
inline bool session_boosts_allowed() noexcept { return !competition_stock_physics() && session_tools_detail::boosts.load(std::memory_order_acquire); }
// A lock, not a tool: a competition's stock physics leaves it as the server set it.
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
+2 -2
View File
@@ -766,7 +766,7 @@ std::vector<std::uint8_t> encode(const Packet &p, bool compact_pose, std::uint32
w.integer(static_cast<std::uint16_t>(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<unsigned>(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U), 1);
(static_cast<unsigned>(p.server_votes) << 3) | (p.enforce_tuning ? 64U : 0U) | (p.guest_ollies ? 128U : 0U), 1);
if (p.server_polls > static_cast<std::uint8_t>(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<Packet> decode(std::span<const std::uint8_t> bytes) noexcept {
p.voice_range = static_cast<float>(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<std::uint8_t>((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<std::uint8_t>(ServerPolls::everyone) || custom > server_custom_vote_limit) return {};
p.server_polls = static_cast<std::uint8_t>(polls);
+4 -3
View File
@@ -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)
+2 -2
View File
@@ -106,7 +106,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();
@@ -125,7 +125,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;
@@ -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 {
@@ -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);
@@ -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);
}
@@ -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;
@@ -941,6 +941,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;
+77 -88
View File
@@ -31,18 +31,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<Plugin*>(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<ApiFunction> 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) {
@@ -155,11 +149,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);
@@ -182,8 +171,50 @@ 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<ApiFunction> functions) {
lua_getfield(L, -1, field);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_createtable(L, 0, static_cast<int>(functions.size()));
lua_pushvalue(L, -1);
// The parent may be a read-only table itself (reskate.throwdowns, for its flag).
const bool locked = lua_getreadonly(L, -3) != 0;
lua_setreadonly(L, -3, false);
lua_setfield(L, -3, field);
lua_setreadonly(L, -2, locked);
}
// 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<ApiFunction> functions) { set_functions(L, functions); }
void push_functions(lua_State* L, std::initializer_list<ApiFunction> functions) {
lua_createtable(L, 0, static_cast<int>(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<Plugin*>(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<int>(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)
@@ -191,7 +222,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); }
@@ -643,80 +673,37 @@ 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");
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, 5);
set_function(L, "read", ApiFunctions::read);
set_function(L, "read_json", ApiFunctions::read_json);
set_function(L, "list", ApiFunctions::list);
set_function(L, "variants", ApiFunctions::variants);
set_function(L, "choose", ApiFunctions::choose);
readonly();
lua_setfield(L, -2, "package");
// 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);
set_function(L, "encode", ApiFunctions::encode);
set_function(L, "decode", ApiFunctions::decode);
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, "json");
lua_createtable(L, 0, 2);
set_function(L, "command", ApiFunctions::command);
lua_pushcfunction(L, ApiFunctions::console_print, "print");
lua_setfield(L, -2, "print");
lua_setfield(L, -2, "Side");
lua_createtable(L, 0, static_cast<int>(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, "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");
lua_setfield(L, -2, "Event");
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");
lua_setfield(L, -2, "Enums");
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}, {"variants", F::variants}, {"choose", F::choose}});
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);
@@ -744,5 +731,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);
}
}
+35
View File
@@ -1,5 +1,7 @@
#pragma once
#include "plugin_vm.h"
#include "lua.h"
#include <initializer_list>
// Inside the plugin host only: the reskate API a plugin's VM gets.
namespace dingosdk::plugins {
@@ -21,6 +23,39 @@ void install_services_api(Plugin& plugin, lua_State* L);
// The throwdown kit's documents and native flag, the plugin's data files, overlay dialogs and the
// skater's push (with its wait and reset) and hold (plugin_api_throwdowns.cpp).
void install_kit_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.<field>, the reskate table being on top of the stack (or to a table
// inside it, such as reskate.throwdowns, when that is on top). 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<ApiFunction> functions);
// Adds the functions to the reskate table itself (on top of the stack).
void add_functions(lua_State* L, std::initializer_list<ApiFunction> 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<ApiFunction> 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 = "<SteamID64>", 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`
+3 -25
View File
@@ -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<Plugin*>(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<int>(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}});
}
}
+6 -28
View File
@@ -17,11 +17,6 @@ struct ServicesApi {
};
namespace {
Game& game_of(Plugin& plugin) { return ServicesApi::game(plugin); }
Plugin& plugin_of(lua_State* L) {
auto& plugin = *static_cast<Plugin*>(lua_callbacks(L)->userdata);
if (plugin.halted()) luaL_error(L, "the plugin was stopped");
return plugin;
}
Plugin& allowed(lua_State* L, const char* permission, const char* what) {
auto& plugin = plugin_of(L);
if (!plugin.has(permission)) luaL_error(L, "%s needs the \"%s\" permission in reskate-plugin.json", what, permission);
@@ -279,31 +274,14 @@ int gesture(lua_State* L) {
return 1;
}
// Adds the functions to reskate.<table> (on top of the stack), making the table when there is none.
void into_table(lua_State* L, const char* table, std::initializer_list<std::pair<const char*, lua_CFunction>> functions) {
lua_getfield(L, -1, table);
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
lua_createtable(L, 0, static_cast<int>(functions.size()));
lua_pushvalue(L, -1);
lua_setfield(L, -3, table);
}
lua_setreadonly(L, -1, false);
for (const auto& [name, f] : functions) {
lua_pushcfunction(L, f, name);
lua_setfield(L, -2, name);
}
lua_setreadonly(L, -1, true);
lua_pop(L, 1);
}
} // namespace
void install_services_api(Plugin&, lua_State* L) {
into_table(L, "skater", {{"telemetry", telemetry}, {"goofy", goofy}, {"bone_cam", bone_cam}});
into_table(L, "input", {{"pad_bits", pad_bits}, {"key", key}, {"wheel", wheel}, {"game_buttons", game_buttons}, {"allowed", input_allowed}, {"focus", focus}});
into_table(L, "session", {{"spectate", spectate}, {"world", world}});
into_table(L, "camera", {{"free", free_camera}, {"pad_flight", pad_flight}});
into_table(L, "physics", {{"competition", competition}, {"stock_ready", stock_ready}, {"prepare", prepare}});
into_table(L, "tricks", {{"feed", trick_feed}, {"gesture", gesture}});
add_functions(L, "skater", {{"telemetry", telemetry}, {"goofy", goofy}, {"bone_cam", bone_cam}});
add_functions(L, "input", {{"pad_bits", pad_bits}, {"key", key}, {"wheel", wheel}, {"game_buttons", game_buttons}, {"allowed", input_allowed}, {"focus", focus}});
add_functions(L, "session", {{"spectate", spectate}, {"world", world}});
add_functions(L, "camera", {{"free", free_camera}, {"pad_flight", pad_flight}});
add_functions(L, "physics", {{"competition", competition}, {"stock_ready", stock_ready}, {"prepare", prepare}});
add_functions(L, "tricks", {{"feed", trick_feed}, {"gesture", gesture}});
}
} // namespace dingosdk::plugins
+5 -42
View File
@@ -13,14 +13,6 @@
// permission and its arguments; the game (plugin_game.h) does the rest.
namespace dingosdk::plugins {
namespace {
Plugin& plugin_of(lua_State* L) {
auto& plugin = *static_cast<Plugin*>(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);
}
int result(lua_State* L, const std::string& why) {
lua_pushboolean(L, why.empty());
if (why.empty()) return 1;
@@ -40,12 +32,6 @@ bool within(double now, double& second, int& count, int most) {
}
return ++count <= most;
}
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<int>(most));
return std::string(text, length);
}
// A string field of the table at `table` (nil: empty), at most `most` long.
std::string text_field(lua_State* L, int table, const char* name, std::size_t most) {
lua_getfield(L, table, name);
@@ -366,37 +352,14 @@ struct KitApi {
void install_kit_api(Plugin& plugin, lua_State* L) { KitApi::install(plugin, L); }
namespace {
// Adds the functions to the table at `field` of the table on top of the stack, making it when
// there is none yet (a table made read-only earlier in the install is opened for this).
void into(lua_State* L, const char* field, std::initializer_list<std::pair<const char*, lua_CFunction>> functions) {
lua_getfield(L, -1, field);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_createtable(L, 0, static_cast<int>(functions.size()));
lua_pushvalue(L, -1);
const bool locked = lua_getreadonly(L, -3) != 0;
lua_setreadonly(L, -3, false);
lua_setfield(L, -3, field);
lua_setreadonly(L, -2, locked);
}
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);
}
} // namespace
void KitApi::install(Plugin&, lua_State* L) {
into(L, "throwdowns", {{"ui", ui}});
add_functions(L, "throwdowns", {{"ui", ui}});
lua_getfield(L, -1, "throwdowns");
into(L, "flag", {{"begin", flag_begin}, {"state", flag_state}, {"leave", flag_leave}});
add_functions(L, "flag", {{"begin", flag_begin}, {"state", flag_state}, {"leave", flag_leave}});
lua_pop(L, 1);
into(L, "files", {{"list", files_list}, {"read", files_read}, {"write", files_write}, {"remove", files_remove}});
into(L, "ui", {{"dialog", dialog}, {"close_dialog", close_dialog}});
into(L, "skater", {{"push", push}, {"push_wait", push_wait}, {"push_reset", push_reset}, {"hold", hold}});
add_functions(L, "files", {{"list", files_list}, {"read", files_read}, {"write", files_write}, {"remove", files_remove}});
add_functions(L, "ui", {{"dialog", dialog}, {"close_dialog", close_dialog}});
add_functions(L, "skater", {{"push", push}, {"push_wait", push_wait}, {"push_reset", push_reset}, {"hold", hold}});
}
}
+14 -89
View File
@@ -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<Plugin*>(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<int>(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}});
}
}
+115 -72
View File
@@ -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<Plugin*>(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<int>(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());
@@ -1141,6 +1120,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<std::pair<const char*, bool Game::Permissions::*>, 6> permission_fields{{
{"noclip", &Game::Permissions::noclip}, {"nobail", &Game::Permissions::nobail}, {"boosts", &Game::Permissions::boosts},
{"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<int>(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.
@@ -1561,59 +1607,56 @@ 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<std::pair<const char*, lua_CFunction>> functions) {
lua_getfield(L, -1, field);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_createtable(L, 0, static_cast<int>(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;
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}, {"fit", board_fit}, {"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, "entities", {{"set", entities_set}, {"remove", entities_remove}, {"clear", entities_clear}, {"status", entities_status},
{"ready", entities_ready}});
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}});
into(L, "throwdowns", {{"state", throwdown_state}, {"host", throwdown_host}, {"cancel", throwdown_cancel}, {"hosting", throwdown_hosting},
{"mode", throwdown_mode}, {"remove_mode", throwdown_remove_mode}});
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}});
// The metatables of player tables (push_player) and of permissions; their own stay hidden.
lua_createtable(L, 0, 2);
lua_pushcfunction(L, player_index, "player");
lua_setfield(L, -2, "__index");
lua_pushstring(L, "locked");
lua_setfield(L, -2, "__metatable");
lua_setfield(L, LUA_REGISTRYINDEX, "reskate.player");
lua_createtable(L, 0, 2);
lua_createtable(L, 0, 1);
lua_pushcfunction(L, permissions_clone, "clone");
lua_setfield(L, -2, "clone");
lua_setreadonly(L, -1, true);
lua_setfield(L, -2, "__index");
lua_pushstring(L, "locked");
lua_setfield(L, -2, "__metatable");
lua_setfield(L, LUA_REGISTRYINDEX, "reskate.permissions");
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}, {"fit", board_fit}, {"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, "entities", {{"set", entities_set}, {"remove", entities_remove}, {"clear", entities_clear}, {"status", entities_status},
{"ready", entities_ready}});
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}});
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) {
+10
View File
@@ -412,6 +412,16 @@ public:
virtual std::string announce(std::string_view /*plugin*/, std::string_view) { return unavailable; }
virtual std::string tell(std::string_view /*plugin*/, std::uint64_t, std::string_view) { return unavailable; }
virtual std::string move_player(std::uint64_t, const Vec3&) { return unavailable; }
// Bans a player by SteamID64, connected or not (a dedicated server's only); lifts a ban.
virtual std::string ban(std::uint64_t) { return unavailable; }
virtual std::string unban(std::uint64_t) { return unavailable; }
// 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, ollies = true;
};
virtual std::optional<Permissions> 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.
+7 -2
View File
@@ -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 <windows.h>
#include <array>
@@ -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<unsigned char, 32> 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{};
+263
View File
@@ -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/<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::<area>::<name>` |
| `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:
```
<plugin>/
├── 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`.
+2 -2
View File
@@ -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
+3 -1
View File
@@ -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, 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");
// ---- An admin's locks stay with the player when they leave and join again -----------------
c.leave();
+13 -3
View File
@@ -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<std::uint8_t> tools = lock ? std::nullopt : std::optional<std::uint8_t>(lock_all);
std::optional<std::uint8_t> tools = lock ? std::nullopt : std::optional<std::uint8_t>(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 <player|SteamID64> <tool[,tool...]|all> (tools: noclip, nobail, boosts, tuning)"
return lock ? "lock <player|SteamID64> <tool[,tool...]|all> (tools: noclip, nobail, boosts, tuning, ollies, objects)"
: "unlock <player|SteamID64> [<tool[,tool...]|all>]";
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 <tool[,tool...]|all>|off (now " + (config_.throwdown_locks ? lock_tools_text(config_.throwdown_locks) : std::string("off")) + ")";
const auto tools = lower(argument) == "off" ? std::optional<std::uint8_t>(0) : parse_lock_tools(argument);
if (!tools) return "throwdown-locks <tool[,tool...]|all>|off (tools: noclip, nobail, boosts, tuning)";
if (!tools) return "throwdown-locks <tool[,tool...]|all>|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).
@@ -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];
+5 -2
View File
@@ -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,8 @@ std::optional<std::uint8_t> 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;
}
@@ -725,7 +727,8 @@ std::optional<std::uint8_t> 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_ollies, "ollies"}, std::pair{lock_objects, "objects"}})
if (tools & bit) text += (text.empty() ? "" : ", ") + std::string(name);
return text.empty() ? "none" : text;
}
+4 -3
View File
@@ -88,9 +88,10 @@ 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;
inline constexpr std::uint8_t lock_movement = lock_noclip | lock_nobail | lock_boosts; // what the role's "movement" gives
// 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, 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;
+5 -3
View File
@@ -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<Variant, std::set<std::uint64_t>> variants;
std::set<std::uint64_t> 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);
@@ -1524,7 +1526,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();
+5 -1
View File
@@ -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<std::array<float, 3>> 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`.
+30 -2
View File
@@ -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> 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), !(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<std::uint8_t>((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.ollies ? 0 : lock_ollies));
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));
@@ -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 <text>", function(args)
if args[1] ~= "say" or not args[2] then return reskate.console.print("starter say <text>") end
reskate.server.say(table.concat(args, " ", 2))
end)
reskate.log("server-starter loaded")
@@ -0,0 +1,6 @@
{
"api": 1,
"entry": "plugin/main.lua",
"side": "server",
"permissions": ["console", "storage", "session.read", "server"]
}