diff --git a/CMakeLists.txt b/CMakeLists.txt index 798d0960..b357227f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,7 +151,7 @@ if(DINGOSDK_BUILD_MULTIPLAYER_TESTS) dingosdk_lz4 dingosdk_zstd dingosdk_miniz dingosdk_word_filter OpenSSL::Crypto dingosdk_plugins dl pthread) endif() add_test(NAME server_locks COMMAND dingosdk_server_lock_tests) - # What the server's plugins see of the server: its players' skaters. + # What the server's plugins see of the server and do there: its players' skaters, its map, its park mods. add_executable(dingosdk_server_plugin_tests Server/Test/server_plugin_tests.cpp Server/server_host.cpp Server/server_party.cpp Extension/Multiplayer/Session/party_book.cpp Server/server_config.cpp Server/server_update.cpp Server/server_release.cpp $<$:Launcher/updater.cpp> Server/global_bans.cpp Server/server_words.cpp Extension/Multiplayer/developer_identity.cpp @@ -169,7 +169,7 @@ if(DINGOSDK_BUILD_MULTIPLAYER_TESTS) target_link_libraries(dingosdk_server_plugin_tests PRIVATE dingosdk_launcher_support dingosdk_world_layer_scan dingosdk_json dingosdk_lz4 dingosdk_zstd dingosdk_miniz dingosdk_word_filter OpenSSL::Crypto dingosdk_plugins dl pthread) endif() - add_test(NAME server_plugins COMMAND dingosdk_server_plugin_tests) + add_test(NAME server_plugins COMMAND dingosdk_server_plugin_tests "${PROJECT_SOURCE_DIR}/Server/Test/fixtures/park_mods") add_executable(dingosdk_server_release_tests Server/Test/server_release_tests.cpp Server/server_release.cpp) target_include_directories(dingosdk_server_release_tests PRIVATE "${PROJECT_SOURCE_DIR}") target_link_libraries(dingosdk_server_release_tests PRIVATE dingosdk_json) diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp index 7fc6c282..203b3e76 100644 --- a/Extension/Plugins/Test/plugin_tests.cpp +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -510,6 +510,15 @@ struct FakeGame final : Game { calls.push_back(std::format("announce {} {}", plugin, text)); return {}; } + std::vector 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)); + return name == "plaza" ? "plaza belongs on Isle of Grom, and the server is on San Vansterdam." : std::string{}; + } + std::string remove_park(std::string_view plugin, std::string_view name) override { + calls.push_back(std::format("park remove {} {}", plugin, name)); + return {}; + } std::optional camera_view() override { return CameraView{{1, 2, 3}, {0, 0, 1}, 60}; } std::string set_camera(std::string_view plugin, const std::array& pose, float fov) override { calls.push_back(std::format("camera {} {:.1f} {:.1f} {:.1f} back {:.2f} {:.2f} {:.2f} fov {:.0f}", plugin, pose[12], pose[13], pose[14], @@ -1954,6 +1963,46 @@ void plugins_server_copies_are_the_authority() { running(player_host, "mode-p", player); } +// A dedicated server's park mods, for a server copy with server.parks: listed, spawned and removed +// as the server's park-mod does. Not for a copy without the permission, nor for a player's copy. +void plugins_server_copies_spawn_park_mods() { + TestEnvironment server, player, blind; + const std::string main = R"( + reskate.console.command("parks", "x", function() + local list = reskate.server.parks() + assert(#list == 2 and list[1].name == "street" and list[1].here and list[1].spawned and list[1].objects == 12 and list[1].minutes_left == 5) + assert(list[2].map == "Isle of Grom" and not list[2].here and not list[2].spawned) + assert(reskate.server.park_spawn("street", 30)) + assert(reskate.server.park_spawn("street")) + local ok, why = reskate.server.park_spawn("plaza") + assert(not ok and why:find("belongs on Isle of Grom"), why) + assert(reskate.server.park_remove("street")) + assert(not pcall(reskate.server.park_spawn, "Street"), "a park mod's name, lower case") + assert(not pcall(reskate.server.park_spawn, "street 5; map grom"), "one name and nothing else") + assert(not pcall(reskate.server.park_spawn, "street", 0), "minutes 1 to 1440") + assert(not pcall(reskate.server.park_spawn, "street", 1441), "minutes 1 to 1440") + reskate.log("parks done") + end) + )"; + const auto parks = [&](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({parks("parks-s", R"("console", "server.parks")", "server")}); + player_host.apply({parks("parks-p", R"("console", "server.parks")", "client")}); + blind_host.apply({parks("parks-b", R"("console", "server")", "server")}); + for (auto* host : {&server_host, &player_host, &blind_host}) host->run_console("parks"); + check(server.logged("[parks-s] parks done"), "A server copy could not use the park mods\n" + server.all()); + check(server.fake.called("park spawn parks-s street 30") && server.fake.called("park spawn parks-s street 0") && + server.fake.called("park spawn parks-s plaza 0") && server.fake.called("park remove parks-s street"), + "The spawns and removal did not reach the server, by the plugin's id"); + check(!server.fake.called("park spawn parks-s Street") && !server.fake.called("park spawn parks-s street 1441"), "A refused call reached the server"); + check(!player.logged("parks done") && player.all().find("server copy") != std::string::npos, + "A player's copy used the server's park mods\n" + player.all()); + check(!blind.logged("parks done") && blind.all().find("server.parks") != std::string::npos, + "A plugin without server.parks used the park mods\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; @@ -3623,6 +3672,7 @@ int main() { run(plugins_edit_clips); run(animator_studio_records_and_saves); run(plugins_server_copies_are_the_authority); + run(plugins_server_copies_spawn_park_mods); run(plugins_animate_the_skater); run(plugins_play_their_own_sounds); run(plugins_have_menu_panels); diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index bfa4e26d..7773a4ad 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -1752,9 +1752,9 @@ struct WorldApi { return 1; } // ---- server: what only the session's authority does ---------------------------------------------- - static Plugin& authority(lua_State* L, const char* what) { + static Plugin& authority(lua_State* L, const char* what, const char* permission = "server") { auto& plugin = plugin_of(L); - need(L, plugin, "server", what); + need(L, plugin, permission, what); if (!plugin.services_.authority()) luaL_error(L, "%s is for the plugin's server copy (reskate.side is \"server\": \"side\" \"server\" or \"both\" in reskate-plugin.json)", what); if (!within(plugin.services_.now_ms(), plugin.server_second_, plugin.server_calls_, 20)) luaL_error(L, "more than 20 server calls in a second"); @@ -1795,6 +1795,51 @@ struct WorldApi { auto& plugin = authority(L, "reskate.server.unban"); return result(L, game(plugin).unban(player_of(L, 1))); } + // ---- server.parks: a dedicated server's park mods ----------------------------------------------- + // parks(): {name, map, here (fits the map running now), spawned, objects, minutes_left}[]; + // park_spawn(name, minutes?): for that long (1-1440), or until removed; park_remove(name). + static std::string park_name(lua_State* L, int index) { + const std::string name = luaL_checkstring(L, index); + const bool ok = !name.empty() && name.size() <= 16 && std::all_of(name.begin(), name.end(), [](char c) { + return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_'; + }); + if (!ok) luaL_argerror(L, index, "a park mod's name: 1 to 16 of a-z 0-9 - _"); + return name; + } + static int server_parks(lua_State* L) { + auto& plugin = authority(L, "reskate.server.parks", "server.parks"); + const auto parks = game(plugin).server_parks(); + lua_createtable(L, static_cast(parks.size()), 0); + for (std::size_t i = 0; i < parks.size(); ++i) { + const auto& p = parks[i]; + lua_createtable(L, 0, 6); + lua_pushlstring(L, p.name.data(), p.name.size()); + lua_setfield(L, -2, "name"); + lua_pushlstring(L, p.map.data(), p.map.size()); + lua_setfield(L, -2, "map"); + lua_pushboolean(L, p.here); + lua_setfield(L, -2, "here"); + lua_pushboolean(L, p.spawned); + lua_setfield(L, -2, "spawned"); + lua_pushinteger(L, static_cast(p.objects)); + lua_setfield(L, -2, "objects"); + lua_pushinteger(L, static_cast(p.minutes_left)); + lua_setfield(L, -2, "minutes_left"); + lua_rawseti(L, -2, static_cast(i + 1)); + } + return 1; + } + static int server_park_spawn(lua_State* L) { + auto& plugin = authority(L, "reskate.server.park_spawn", "server.parks"); + const auto name = park_name(L, 1); + const auto minutes = lua_isnoneornil(L, 2) ? 0 : luaL_checkinteger(L, 2); + if (!lua_isnoneornil(L, 2) && (minutes < 1 || minutes > 1440)) luaL_argerror(L, 2, "minutes, 1 to 1440 (none: until removed)"); + return result(L, game(plugin).spawn_park(plugin.id(), name, static_cast(minutes))); + } + static int server_park_remove(lua_State* L) { + auto& plugin = authority(L, "reskate.server.park_remove", "server.parks"); + return result(L, game(plugin).remove_park(plugin.id(), park_name(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, 6> permission_fields{{ @@ -2483,7 +2528,8 @@ void WorldApi::install(Plugin& plugin, lua_State* L) { 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}}); + {"ban", server_ban}, {"unban", server_unban}, {"parks", server_parks}, {"park_spawn", server_park_spawn}, + {"park_remove", server_park_remove}}); 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}, diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index 1388066a..cb712075 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -583,6 +583,18 @@ public: }; virtual std::optional permissions(std::uint64_t) { return std::nullopt; } virtual std::string apply_permissions(std::string_view /*plugin*/, std::uint64_t, const Permissions&) { return unavailable; } + // 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. + struct ServerPark { + std::string name, map; + bool here{}, spawned{}; + std::size_t objects{}; + unsigned minutes_left{}; + }; + virtual std::vector server_parks() { return {}; } + virtual std::string spawn_park(std::string_view /*plugin*/, std::string_view /*name*/, unsigned /*minutes*/) { return unavailable; } + virtual std::string remove_park(std::string_view /*plugin*/, std::string_view /*name*/) { return unavailable; } // ---- skater.animation: clips on the local skater's body (Extension/Skater/skater_animation.h) ------ // One plugin plays at a time; none while a throwdown runs. A replacement plays when the game does // `trick` ("kickflip") with that pop ("" any, "low", "mid", "high"); a null clip takes it away. diff --git a/Extension/Plugins/plugin_manifest.cpp b/Extension/Plugins/plugin_manifest.cpp index 914fb012..58376927 100644 --- a/Extension/Plugins/plugin_manifest.cpp +++ b/Extension/Plugins/plugin_manifest.cpp @@ -12,15 +12,16 @@ namespace { // "camera" the free and pad-flown cameras and the Bone Cam (reskate.skater.bone_cam); // "session.read" spectating and the session's world (reskate.session.world); "events.tricks" the // trick events, the trick list's feed and flick diagrams (reskate.tricks); "throwdowns" the -// throwdown kit (modes, documents, the native flag); "variants" choosing which of its own mod's +// throwdown kit (modes, documents, the native flag); "server.parks" a dedicated server's park mods +// (reskate.server.parks, park_spawn, park_remove); "variants" choosing which of its own mod's // variants is in the game (reskate.package.choose). A manifest's "resident" list and "variants" are // the merge's (Engine/Vfs/mod_list.h ResidentAssets, Variants), not the plugin host's. -constexpr std::array permissions{ +constexpr std::array permissions{ "console", "storage", "overlay", "menu", "ui.screen", "input", "hud", "events.tricks", "events.skater", "skater.read", "skater.teleport", "skater.control", "skater.animation", "trainer", "camera", "world.raycast", "world.objects", "world.entities", "world.environment", "world.ai", "travel", "session.read", "session.skaters", "session.nametags", "net.session", "net.http", "voice.control", "chat.read", - "chat.send", "chat.local", "server", "throwdowns", "replay", "music", "progression.read", "settings.read", "audio.ui", "audio", "outfit", "board", "mods.read", + "chat.send", "chat.local", "server", "server.parks", "throwdowns", "replay", "music", "progression.read", "settings.read", "audio.ui", "audio", "outfit", "board", "mods.read", "events.physics", "game.speed", "files", "variants", "settings.write", "room", "capture"}; } diff --git a/Server/Test/server_plugin_tests.cpp b/Server/Test/server_plugin_tests.cpp index 39fc2059..4a682735 100644 --- a/Server/Test/server_plugin_tests.cpp +++ b/Server/Test/server_plugin_tests.cpp @@ -1,7 +1,7 @@ // 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. // -// dingosdk_server_plugin_tests +// dingosdk_server_plugin_tests #include "Server/server_host.h" #include "Extension/Multiplayer/Net/pose_batch.h" #include "Extension/Multiplayer/Net/pose_codec.h" @@ -164,10 +164,12 @@ struct Player { } }; -int run() { +int run(const std::filesystem::path &fixtures) { const auto root = std::filesystem::temp_directory_path() / "reskate_server_plugin_tests"; std::filesystem::remove_all(root); std::filesystem::create_directories(root / "Mods"); + for (const auto *folder : {"popular skate 2 street park", "mixed park"}) + std::filesystem::copy(fixtures / folder, root / "Mods" / folder, std::filesystem::copy_options::recursive); load_levels(root / "Mods"); // the retail maps ServerConfig config; config.file = root / "server" / "ReSkateServer.json"; @@ -175,6 +177,7 @@ int run() { config.auto_update = false; config.activity_log = false; config.roles = default_roles(); + 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)); // A plugin that prints the skaters it sees every 20th tick (a second here), and one without @@ -204,6 +207,10 @@ int run() { local ok, why = pcall(reskate.session.skaters) reskate.console.print(ok and "blind saw them" or "blind refused: " .. tostring(why)) end) + reskate.console.command("blind parks", "park mods without server.parks", function() + local ok, why = pcall(reskate.server.parks) + reskate.console.print(ok and "blind listed parks" or "blind parks refused: " .. tostring(why)) + end) )"); // One that says what the level is, and when it changes. @@ -219,6 +226,31 @@ int run() { end) )"); + // One that lists, spawns and removes park mods when the console asks it to. + write(root / "Plugins" / "parker" / "reskate-plugin.json", + R"({"api": 1, "entry": "plugin/main.lua", "side": "server", "permissions": ["console", "server.parks"]})"); + write(root / "Plugins" / "parker" / "plugin" / "main.lua", R"( + reskate.console.command("parker", "park mods, for the test", function(args) + local verb, name = args[1], args[2] + if verb == "list" then + for _, p in ipairs(reskate.server.parks()) do + reskate.console.print(string.format("park %s on %s, here %s, spawned %s, %d objects, %d min left", p.name, p.map, + tostring(p.here), tostring(p.spawned), p.objects, p.minutes_left)) + end + elseif verb == "spawn" then + local ok, why = reskate.server.park_spawn(name, tonumber(args[3])) + reskate.console.print("spawn " .. name .. ": " .. (ok and "done" or why)) + elseif verb == "remove" then + local ok, why = reskate.server.park_remove(name) + reskate.console.print("remove " .. name .. ": " .. (ok and "done" or why)) + elseif verb == "bad" then + local named = pcall(reskate.server.park_spawn, "Street; map grom") + local timed = pcall(reskate.server.park_spawn, "street", 5000) + reskate.console.print("bad " .. (named and "name taken" or "name refused") .. ", " .. (timed and "minutes taken" or "minutes refused")) + end + end) + )"); + std::vector log; SteamTransport transport; Host host(config, transport, [&](const std::string &line) { log.push_back(line); }); @@ -288,10 +320,42 @@ int run() { 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. + 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("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"); + plugin_command("parker spawn street 5"); + check(has(log, "spawn street: done"), "The plugin could not spawn street"); + check(has(log, "[parks] street spawned (") && has(log, "5 min) by plugin parker"), "The spawn was not logged as the plugin's"); + plugin_command("parker list"); + check(has(log, "park street on San Vansterdam, here true, spawned true, ") && has(log, " objects, 5 min left"), + "street is not listed as spawned for 5 min"); + plugin_command("parker spawn street"); + check(has(log, "spawn street: street is already spawned."), "A spawned park was spawned again"); + plugin_command("parker spawn plaza"); + check(has(log, "spawn plaza: plaza belongs on Isle of Grom"), "A park for another map was spawned"); + plugin_command("parker spawn nothing"); + check(has(log, "spawn nothing: there is no park mod called \"nothing\""), "A park that is not set up was spawned"); + plugin_command("parker bad"); + check(has(log, "bad name refused, minutes refused"), "A bad park name or time was taken"); + plugin_command("parker remove street"); + check(has(log, "remove street: done") && has(log, "[parks] street removed (by plugin parker)"), "The plugin could not remove street"); + plugin_command("parker remove street"); + check(has(log, "remove street: street is not spawned."), "A park not spawned was removed"); + plugin_command("blind parks"); + check(has(log, "blind parks refused: ") && has(log, "server.parks"), "A plugin without server.parks listed the parks"); + // 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"); + log.clear(); + run_for(1100000); check(has(log, "level " + first->asset), "level.current() is not the server's map: " + first->asset); host.change_map(other->name); log.clear(); @@ -305,8 +369,12 @@ int run() { } } // namespace -int main() { - const int result = run(); +int main(int argc, char **argv) { + if (argc < 2) { + std::fprintf(stderr, "Usage: dingosdk_server_plugin_tests \n"); + return 2; + } + const int result = run(argv[1]); if (result == 0) std::printf("server plugin tests passed\n"); return result; } diff --git a/Server/server_host.h b/Server/server_host.h index 67507f2d..2ad1843c 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -95,6 +95,19 @@ class Host { std::vector skater_poses() 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 + // The park mods set up (maps.park_mods), for the server's plugins: which fit the map running + // now, and which are spawned, with how many objects and minutes left (0: until removed). + struct ParkStatus { + std::string name, map; + bool here{}, spawned{}; + std::size_t objects{}; + unsigned minutes_left{}; + }; + std::vector park_status() const; + // One spawned (for `minutes`, 0 until removed) or removed for a server plugin, as "park-mod add" + // and "remove" do, with their checks and limits and their "[parks]" lines logged "by plugin + // ": "" when it is done, else why not. + std::string plugin_park(std::string_view plugin, bool spawn, std::string_view name, unsigned minutes); // A plugin message from the server's plugins (server_plugins.h) to one player, or everyone (0). void send_plugin_message(std::uint64_t to, const std::vector &bytes); @@ -538,6 +551,7 @@ class Host { ObjectState park_objects_; // never reset: its revisions only grow, so no game holds a newer one std::uint64_t park_object_ids_{}; bool by_vote_{}; // a passed custom vote's command is running (as the console) + std::string by_plugin_; // a server plugin's park-mod is running (plugin_park): "plugin " std::string park_mod_command(std::string_view argument, std::uint64_t admin); void publish_parks(); void remove_parks(std::string_view reason); // all of them, with the reason for the log diff --git a/Server/server_parks.cpp b/Server/server_parks.cpp index 6c73390d..75f03943 100644 --- a/Server/server_parks.cpp +++ b/Server/server_parks.cpp @@ -167,6 +167,42 @@ void Host::tick_parks() { publish_parks(); } +std::vector Host::park_status() const { + std::vector list; + for (const auto &park : config_.park_mods) { + ParkStatus status{park.name, park.map, same_map(park.map)}; + const auto up = std::find_if(parks_spawned_.begin(), parks_spawned_.end(), [&](const auto &s) { return s.name == park.name; }); + if (up != parks_spawned_.end()) { + status.spawned = true; + status.objects = up->objects.size(); + if (up->expires) status.minutes_left = static_cast((up->expires - std::min(up->expires, now_) + 59999999) / 60000000); + } + list.push_back(std::move(status)); + } + return list; +} + +std::string Host::plugin_park(std::string_view plugin, bool spawn, std::string_view name, unsigned minutes) { + const auto up = [&] { + return std::any_of(parks_spawned_.begin(), parks_spawned_.end(), [&](const auto &park) { return park.name == name; }); + }; + const bool was = up(); + by_plugin_ = "plugin " + std::string(plugin); + struct Reset { std::string &by; ~Reset() { by.clear(); } } reset{by_plugin_}; + const auto answer = park_mod_command(std::string(spawn ? "add " : "remove ") + std::string(name) + + (spawn && minutes ? " " + std::to_string(minutes) : std::string{}), 0); + // The console logs what park-mod answers; for a plugin it is logged here. + for (std::size_t at = 0; at < answer.size();) { + const auto end = std::min(answer.find('\n', at), answer.size()); + log_(answer.substr(at, end - at)); + at = end + 1; + } + if (was != spawn && up() == spawn) return {}; + // "[parks] could not be spawned: why", or a usage line + const auto colon = answer.find(": "); + return answer.starts_with("[parks] ") && colon != std::string::npos ? answer.substr(colon + 2) : answer; +} + // "park-mod list | add [...] [minutes] | remove [...] | clear | // reload [...] | config add | | [| ] | config remove ". // What it did is a "[parks]" line: the console and a vote log the reply, so it is logged here only @@ -175,7 +211,7 @@ std::string Host::park_mod_command(std::string_view argument, std::uint64_t admi const auto [action, rest] = split(argument); const auto verb = lower(action); const auto *by = admin ? find(admin) : nullptr; - const std::string who = by_vote_ ? "a vote" : by ? guest_name(*by) : admin ? std::to_string(admin) : "console"; + const std::string who = !by_plugin_.empty() ? by_plugin_ : by_vote_ ? "a vote" : by ? guest_name(*by) : admin ? std::to_string(admin) : "console"; const auto done = [&](const std::vector &lines) { std::string text; for (const auto &line : lines) { diff --git a/Server/server_plugins.cpp b/Server/server_plugins.cpp index 3f4685ed..5eeb453e 100644 --- a/Server/server_plugins.cpp +++ b/Server/server_plugins.cpp @@ -80,6 +80,15 @@ class ServerGame final : public Game { host_.set_locks(player, locks, "plugin " + std::string(plugin)); return {}; } + std::vector server_parks() override { + std::vector 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}); + return result; + } + std::string spawn_park(std::string_view plugin, std::string_view name, unsigned minutes) override { + return host_.plugin_park(plugin, true, name, minutes); + } + std::string remove_park(std::string_view plugin, std::string_view name) override { return host_.plugin_park(plugin, false, name, 0); } // The skaters of the players in the world, as their latest poses put them. The server has // no skater of its own, so this is every player's. std::vector peer_skaters() override {