Merge pull request #348 from moelrobi/plugins/server-roles

Plugins: server plugins know each player's role
This commit is contained in:
zeex64 authored and GitHub committed 2026-10-23 15:50:02 -05:00
commit e34c4ab94f
6 files changed
+118 -5

No files matched your search

+39
View File
@@ -546,6 +546,10 @@ struct FakeGame final : Game {
calls.push_back(std::format("announce {} {}", plugin, text));
return {};
}
std::optional<Role> role(std::uint64_t player) override {
if (player == 1) return Role{"moderator", 50, true};
return std::nullopt;
}
std::vector<ServerPark> server_parks() override { return {{"street", "San Vansterdam", true, true, 12, 5}, {"plaza", "Isle of Grom", false, false, 0, 0}}; }
std::string spawn_park(std::string_view plugin, std::string_view name, unsigned minutes) override {
calls.push_back(std::format("park spawn {} {} {}", plugin, name, minutes));
@@ -2108,6 +2112,40 @@ void plugins_server_copies_spawn_park_mods() {
"A plugin without server.parks used the park mods\n" + blind.all());
}
// A player's role, for a server copy with server: read-only, nil for one with none. Not for a copy
// without the permission, nor for a player's copy.
void plugins_server_copies_see_roles() {
TestEnvironment server, player, blind;
for (auto* environment : {&server, &player, &blind}) environment->session_now = {true, false, {{1, "Ann", false, false}, {2, "Bob", false, false}}};
const std::string main = R"(
reskate.console.command("roles", "x", function()
local ann, bob = reskate.session.players()[1], reskate.session.players()[2]
local role = ann.role
assert(role and role.name == "moderator" and role.rank == 50 and role.staff == true)
assert(not pcall(function() role.rank = 1000 end), "read-only")
assert(bob.role == nil, "nil with no role")
reskate.log("roles done")
end)
)";
const auto roles = [&](const char* id, const char* permissions, const char* side) {
return package(id, {{manifest_file, manifest(permissions, side)}, {"plugin/main.lua", main}});
};
Host server_host(server, Side::server), player_host(player, Side::client), blind_host(blind, Side::server);
server_host.apply({roles("roles-s", R"("console", "session.read", "server")", "server")});
player_host.apply({roles("roles-p", R"("console", "session.read", "server")", "client")});
blind_host.apply({roles("roles-b", R"("console", "session.read")", "server")});
// a tick first: the host takes the session's players then (Host::follow_session), so they are there to ask
ticks(server_host, server, 1);
ticks(player_host, player, 1);
ticks(blind_host, blind, 1);
for (auto* host : {&server_host, &player_host, &blind_host}) host->run_console("roles");
check(server.logged("[roles-s] roles done"), "A server copy could not read the players' roles\n" + server.all());
check(!player.logged("roles done") && player.all().find("server copy") != std::string::npos,
"A player's copy read the players' roles\n" + player.all());
check(!blind.logged("roles done") && blind.all().find("\"server\"") != std::string::npos,
"A plugin without server read the players' roles\n" + blind.all());
}
// Clips on the skater's body: from the package or recorded, played, replacing a trick.
void plugins_animate_the_skater() {
TestEnvironment environment;
@@ -3870,6 +3908,7 @@ int main() {
run(animator_studio_records_and_saves);
run(plugins_server_copies_are_the_authority);
run(plugins_server_copies_spawn_park_mods);
run(plugins_server_copies_see_roles);
run(plugins_animate_the_skater);
run(plugins_play_their_own_sounds);
run(plugins_have_menu_panels);
+18 -2
View File
@@ -2152,14 +2152,30 @@ struct WorldApi {
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.
// player.role: {name, rank, staff}, read-only, or nil for a player with no role.
static int player_role(lua_State* L) {
auto& plugin = authority(L, "player.role");
const auto role = game(plugin).role(player_of(L, 1));
if (!role) return 0;
lua_createtable(L, 0, 3);
lua_pushlstring(L, role->name.data(), role->name.size());
lua_setfield(L, -2, "name");
lua_pushinteger(L, role->rank);
lua_setfield(L, -2, "rank");
lua_pushboolean(L, role->staff);
lua_setfield(L, -2, "staff");
lua_setreadonly(L, -1, true);
return 1;
}
// A player table's own names: permissions and role (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) == "role") return player_role(L);
if (std::string_view(key) != "permissions") return 0;
auto& plugin = authority(L, "player.permissions");
const auto permissions = game(plugin).permissions(player_of(L, 1));
+9
View File
@@ -596,6 +596,15 @@ public:
};
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; }
// A player's role on a dedicated server (data/roles.json): its name, its rank (0 to 1000), and
// whether it gives any of the server's commands. Nothing for a player with no role, nor where
// there are no roles (a lobby's host).
struct Role {
std::string name;
int rank{};
bool staff{};
};
virtual std::optional<Role> role(std::uint64_t) { return std::nullopt; }
// A dedicated server's park mods (server.parks): those it has set up, whether each fits the map
// running now and is spawned (its objects, and minutes left; 0 until removed). Spawning one (for
// `minutes`, 0 until removed) or removing it goes as the server's own park-mod does.
+43 -3
View File
@@ -1,5 +1,6 @@
// The dedicated server's plugins (server_plugins.cpp) with the server's host, over a transport
// that is not Steam's and simulated players: what a server plugin sees of the players' skaters.
// that is not Steam's and simulated players: what a server plugin sees of the players' skaters
// and roles.
//
// dingosdk_server_plugin_tests <Server/Test/fixtures/park_mods>
#include "Server/server_host.h"
@@ -177,6 +178,7 @@ int run(const std::filesystem::path &fixtures) {
config.auto_update = false;
config.activity_log = false;
config.roles = default_roles();
config.roles.players[76561198000000001ULL] = "moderator"; // the second player has the default role
config.park_mods = {{"street", "popular skate 2 street park", "San Vansterdam", "bam"}, {"plaza", "mixed park", "Isle of Grom", "grom"}};
check(config_error(config).empty(), "The test's config was refused: " + config_error(config));
@@ -251,6 +253,32 @@ int run(const std::filesystem::path &fixtures) {
end)
)");
// One that says each player's role, and one without server that may not know them.
write(root / "Plugins" / "ranks" / "reskate-plugin.json",
R"({"api": 1, "entry": "plugin/main.lua", "side": "server", "permissions": ["console", "session.read", "server"]})");
write(root / "Plugins" / "ranks" / "plugin" / "main.lua", R"(
reskate.console.command("ranks", "players' roles, for the test", function()
for _, p in ipairs(reskate.session.players()) do
local role = p.role
if role then
reskate.console.print(string.format("role %s %s rank %d staff %s", p.id, role.name, role.rank, tostring(role.staff)))
reskate.console.print(pcall(function() role.rank = 1000 end) and "role changed" or "role read-only")
elseif not p.server then
reskate.console.print("role " .. p.id .. " none")
end
end
end)
)");
write(root / "Plugins" / "rankless" / "reskate-plugin.json",
R"({"api": 1, "entry": "plugin/main.lua", "side": "server", "permissions": ["console", "session.read"]})");
write(root / "Plugins" / "rankless" / "plugin" / "main.lua", R"(
reskate.console.command("rankless", "roles without server", function()
local p = reskate.session.players()[1]
local ok, why = pcall(function() return p.role end)
reskate.console.print(ok and "rankless saw a role" or "rankless refused: " .. tostring(why))
end)
)");
std::vector<std::string> log;
SteamTransport transport;
Host host(config, transport, [&](const std::string &line) { log.push_back(line); });
@@ -320,12 +348,24 @@ int run(const std::filesystem::path &fixtures) {
run_for(1100000);
check(has(log, "skater 76561198000000001 at 10.0 2.0 -30.0 facing 1.00, stale"), "A skater's age did not grow without poses");
// Park mods: listed with whether they fit this map; spawned and removed through park-mod's own
// checks, logged by the plugin's name.
// Roles: the one given (moderator), the server's default (player) for the other, read-only;
// and nothing for a plugin without server.
const auto plugin_command = [&](std::string_view line) {
log.clear();
check(host.command(line) == "Ran by a plugin.", std::string(line) + " did not reach the plugin");
};
plugin_command("ranks");
check(has(log, "role 76561198000000001 moderator rank 50 staff true"), "The moderator's role is not what the server gave them");
check(has(log, "role 76561198000000002 player rank 0 staff false"), "A player given no role does not have the default role");
check(has(log, "role read-only") && !has(log, "role changed"), "A plugin changed a player's role table");
host.command("role 76561198000000002 admin");
plugin_command("ranks");
check(has(log, "role 76561198000000002 admin rank 100 staff true"), "A role given while they are on was not seen");
plugin_command("rankless");
check(has(log, "rankless refused: ") && has(log, "server"), "A plugin without server saw a role");
// Park mods: listed with whether they fit this map; spawned and removed through park-mod's own
// checks, logged by the plugin's name.
plugin_command("parker list");
check(has(log, "park street on San Vansterdam, here true, spawned false, 0 objects, 0 min left"), "street is not listed as it is");
check(has(log, "park plaza on Isle of Grom, here false, spawned false"), "plaza is not listed as on another map");
+2
View File
@@ -85,6 +85,8 @@ class Host {
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;
// The role a player has (data/roles.json: given them, or the default), here or not; or none.
const Role *player_role(std::uint64_t id) const { return role_of(id); }
// Where each player in the world last said their skater was (their latest pose's root), and
// how long ago that arrived: what the server's plugins see of the skaters.
struct SkaterPose {
+7
View File
@@ -80,6 +80,13 @@ class ServerGame final : public Game {
host_.set_locks(player, locks, "plugin " + std::string(plugin));
return {};
}
// The role they have (the one given them, or the server's default role), here or not.
std::optional<Role> role(std::uint64_t player) override {
if (!multiplayer::individual_steam_id(player)) return std::nullopt; // the server itself has none
const auto *held = host_.player_role(player);
if (!held) return std::nullopt;
return Role{held->name, held->rank, server::staff(held)};
}
std::vector<ServerPark> server_parks() override {
std::vector<ServerPark> result;
for (auto &p : host_.park_status()) result.push_back({std::move(p.name), std::move(p.map), p.here, p.spawned, p.objects, p.minutes_left});