mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-25 08:24:45 +01:00
Merge pull request #317 from ValveTextureFile/server-lua-api
Plugins: server plugins ban, unban; player permissions; reskate.Enums
This commit is contained in:
31 files changed
+657
-378
No files matched your search
@@ -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
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
@@ -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}});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}});
|
||||
}
|
||||
}
|
||||
@@ -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}});
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in new issue
Block a user