From fd5f95467f93fcfcbb6084732f6c8d27de4fbc1d Mon Sep 17 00:00:00 2001 From: Robin Moeller Date: Thu, 22 Oct 2026 20:38:22 +0200 Subject: [PATCH] Plugins: server plugins know the map and hear it change On a dedicated server reskate.level.current() was empty and level_loaded and level_unloading never fired, so a server plugin could not tell which map ran or that it had changed. The server's plugins are now told the map's level each tick, named as players' games name it (a destination's level, after its "|"). When the server changes the map, they hear level_unloading for the old one and level_loaded for the new one, as a game's plugins do; players load it after. dingosdk_server_plugin_tests checks level.current() and both events across a map change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Hs7xxJDf6SDcQUaXnzmtL5 --- Server/Test/server_plugin_tests.cpp | 25 +++++++++++++++++++++++++ Server/server_host.cpp | 5 +++++ Server/server_host.h | 3 +++ Server/server_plugins.cpp | 3 +++ 4 files changed, 36 insertions(+) diff --git a/Server/Test/server_plugin_tests.cpp b/Server/Test/server_plugin_tests.cpp index 416317b7..39fc2059 100644 --- a/Server/Test/server_plugin_tests.cpp +++ b/Server/Test/server_plugin_tests.cpp @@ -206,6 +206,19 @@ int run() { end) )"); + // One that says what the level is, and when it changes. + write(root / "Plugins" / "follower" / "reskate-plugin.json", + R"({"api": 1, "entry": "plugin/main.lua", "side": "server", "permissions": ["console"]})"); + write(root / "Plugins" / "follower" / "plugin" / "main.lua", R"( + reskate.on("level_unloading", function(name) reskate.console.print("unloading " .. name) end) + reskate.on("level_loaded", function(name) reskate.console.print("loaded " .. name) end) + local ticks = 0 + reskate.on("tick", function() + ticks += 1 + if ticks % 20 == 0 then reskate.console.print("level " .. reskate.level.current().name) end + end) + )"); + std::vector log; SteamTransport transport; Host host(config, transport, [&](const std::string &line) { log.push_back(line); }); @@ -274,6 +287,18 @@ int run() { log.clear(); 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"); + + // The level, as players' games name it, and its change heard when the server changes it. + const auto *first = find_level(config.map); + const auto other = std::find_if(levels().begin(), levels().end(), [&](const auto &l) { return l.asset != first->asset; }); + check(first && other != levels().end(), "The test needs two retail maps"); + check(has(log, "level " + first->asset), "level.current() is not the server's map: " + first->asset); + host.change_map(other->name); + log.clear(); + run_for(1100000); + check(has(log, "unloading " + first->asset) && has(log, "loaded " + other->asset), "The plugin did not hear the map change"); + check(has(log, "level " + other->asset) && !has(log, "level " + first->asset), "level.current() did not follow the map change"); + check(!has(log, "skaters 2"), "Skaters stayed on the old map's places after the map changed"); if (failures) for (const auto &line : log) std::fprintf(stderr, "log: %s\n", line.c_str()); return failures ? 1 : 0; diff --git a/Server/server_host.cpp b/Server/server_host.cpp index 6ba1a32e..b4cc006b 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -240,6 +240,11 @@ void Host::fetching(Guest &guest) { } std::string Host::invite() const { return format_invite({id_, secret_}); } std::string Host::map_name() const { return map_label(config_.map); } +std::string Host::map_level() const { + const auto destination = map_destination(config_.map); + const auto split = destination.find('|'); + return split == std::string::npos ? destination : destination.substr(split + 1); +} std::string Host::wire_map_label() const { // players without the map's mod still see its name auto label = map_name(); cut_text(label, max_member_name); diff --git a/Server/server_host.h b/Server/server_host.h index 07d2d920..67507f2d 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -47,6 +47,9 @@ class Host { std::string invite() const; std::string map_name() const; + // The level players load for the map, as their games name it: a destination's level, after + // its "|". What the server's plugins see as reskate.level.current(). + std::string map_level() const; // "maps reload": reads Mods again and says what changed. Also after a map was updated. std::string reload_maps(); // The mod folder of the map in play while anyone is on (an update leaves it alone), or diff --git a/Server/server_plugins.cpp b/Server/server_plugins.cpp index b2223bf9..3f4685ed 100644 --- a/Server/server_plugins.cpp +++ b/Server/server_plugins.cpp @@ -210,6 +210,9 @@ void ServerPlugins::tick(std::uint64_t now_us) { } const double dt = s.last_tick ? static_cast(now_us - s.last_tick) / 1000.0 : 0; s.last_tick = now_us; + // The map the server runs, so plugins hear level_unloading and level_loaded when it + // changes, as a game's plugins do (when the server changes it; players load it after). + s.plugins.set_level(s.server.map_level()); s.plugins.tick(dt); for (auto &bytes : std::exchange(s.environment.outbox, {})) s.server.send_plugin_message(wire::to_of(bytes), bytes); } catch (const std::exception &error) {