mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 17:27:19 +02:00
Park mods: reload and set up parks while the server runs
- park-mod reload <name> [<name>...] reads a spawned park's file again and swaps its objects in place, keeping its timer; all or none, within park_limits, and players are told the park was updated - park-mod config add <name> | <mod folder> | <map> [| <key>] and park-mod config remove <name> change maps.park_mods without a restart, validated like the startup config and saved like the map pool; removing a spawned park's entry removes the park too - The client's admin "/" menu lists reload, config add and config remove - README.txt and help cover the new commands; server_park_mods tests reload (in place, timer kept, refused when not spawned or broken) and config add/remove (saved, refusals, by an admin) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2G4APEkhHgVs59RDL3XWD
This commit is contained in:
1 parent
c3751b9714
commit
f8b01dc3d5
5 files changed
+196
-16
No files matched your search
@@ -533,6 +533,11 @@ std::vector<MultiplayerChatCommand> chat_commands(const Session &s) {
|
||||
"Admin: spawn park mods on this map, for 1-1440 minutes or until removed"});
|
||||
list.push_back({"/park-mod remove", "/park-mod remove <name> [<name>...]", "Admin: remove spawned park mods"});
|
||||
list.push_back({"/park-mod clear", "/park-mod clear", "Admin: remove every spawned park mod"});
|
||||
list.push_back({"/park-mod reload", "/park-mod reload <name> [<name>...]",
|
||||
"Admin: read a spawned park mod's file again and update it in place"});
|
||||
list.push_back({"/park-mod config add", "/park-mod config add <name> | <mod folder> | <map> [| <key>]",
|
||||
"Admin: set up a park mod from the server's Mods folder, without a restart"});
|
||||
list.push_back({"/park-mod config remove", "/park-mod config remove <name>", "Admin: remove a park mod from the server's setup"});
|
||||
list.erase(std::remove_if(list.begin() + static_cast<std::ptrdiff_t>(before_commands), list.end(), [&](const auto &entry) {
|
||||
// "/poll end" is the polls permission's; the others are named for their command, the
|
||||
// first word ("/park-mod add" is park-mod's).
|
||||
|
||||
@@ -44,6 +44,12 @@ in maps.park_mods with a name and the map it was built on, and spawn it with
|
||||
and when it goes. Without, it stays until removed. Every park goes when the map
|
||||
changes, and none come back after a restart.
|
||||
|
||||
Nothing needs a restart: a mod copied into Mods can be set up with
|
||||
"park-mod config add" (saved to the config like the map pool), and a park whose
|
||||
mod was updated is read again with "park-mod reload <name>". Every park-mod
|
||||
add reads the park's file afresh. Don't edit ReSkateServer.json by hand while
|
||||
the server runs: it writes its settings back whenever an admin changes one.
|
||||
|
||||
A spawned park belongs to the server, not to a player: it counts against nobody's
|
||||
object limit, nobody can move or delete it, and it stays when players leave.
|
||||
Players need no mod for it: its objects are Build Kit items the game has. An
|
||||
@@ -509,6 +515,13 @@ or Travel, and voice, distances, placement and kicks from the Multiplayer menu.
|
||||
park-mod add <name> [<name>...] [minutes] Spawn parks on the current map:
|
||||
for 1-1440 minutes, or until removed. All or none.
|
||||
park-mod remove <name> [<name>...] park-mod clear Remove spawned parks.
|
||||
park-mod reload <name> [<name>...] Read a spawned park's file again (its mod
|
||||
was updated) and swap its objects in place; it keeps
|
||||
its timer.
|
||||
park-mod config add <name> | <mod folder> | <map> [| <key>] Set up a park mod
|
||||
while the server runs, saved to maps.park_mods, e.g.
|
||||
park-mod config add street | popular skate 2 street park | San Vansterdam | bam
|
||||
park-mod config remove <name> Take one out of the setup (removing it if spawned).
|
||||
layer-sync on|off layer <key> default|on|off
|
||||
layers <key>=<mode> ... Several world layers at once, each default, on or off.
|
||||
tod <default|morning|noon|afternoon|evening|night|weatherday|weathernight>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Engine/Game/Build/supported_build.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
@@ -212,7 +213,11 @@ int run(const std::filesystem::path &fixtures) {
|
||||
load_levels(empty); // the retail maps
|
||||
ServerConfig config;
|
||||
config.file = std::filesystem::temp_directory_path() / "reskate_server_park_tests" / "ReSkateServer.json";
|
||||
config.mods = fixtures;
|
||||
// A copy of the fixtures the test may change, as an owner updates a mod while the server runs.
|
||||
const auto mods = std::filesystem::temp_directory_path() / "reskate_server_park_tests_live";
|
||||
std::filesystem::remove_all(mods);
|
||||
std::filesystem::copy(fixtures, mods, std::filesystem::copy_options::recursive);
|
||||
config.mods = mods;
|
||||
config.auto_update = false;
|
||||
config.park_mods = {{"street", "popular skate 2 street park", "San Vansterdam", "bam"},
|
||||
{"street2", "popular skate 2 street park", "San Vansterdam", "bam"},
|
||||
@@ -351,6 +356,64 @@ int run(const std::filesystem::path &fixtures) {
|
||||
check(reply == "[parks] street3 removed (by console)", "clear: " + reply);
|
||||
check(command("park-mod clear") == "No parks are spawned.", "clear with nothing spawned");
|
||||
|
||||
// Live: a spawned park's file read again, in place, keeping its timer.
|
||||
const auto street_file = mods / "popular skate 2 street park" / "parks" / "bam.park.json";
|
||||
command("park-mod add street 10");
|
||||
run_for(1000000);
|
||||
std::ofstream(street_file, std::ios::binary | std::ios::trunc) << R"({"format": "reskate-park", "schema_version": 1, "revision": 1,
|
||||
"maps": {"bam": [{"id": 1, "item": "own_bkpads_generic_padlow_00001", "position": [0, 10, 0], "rotation": [0, 0, 0, 1]},
|
||||
{"id": 2, "item": "own_bkpads_generic_padlow_00001", "position": [4, 10, 0], "rotation": [0, 0, 0, 1]},
|
||||
{"id": 3, "item": "own_bkpads_generic_padlow_00001", "position": [8, 10, 0], "rotation": [0, 0, 0, 1]}]}})";
|
||||
reply = command("park-mod reload street");
|
||||
check(reply == "[parks] street reloaded (3 objects) by console", "reload: " + reply);
|
||||
run_for(1000000);
|
||||
check(!a.refused && a.park.objects().size() == 3, "A reloaded park did not reach the player");
|
||||
check(has(a.chat, "The street park was updated."), "Players were not told of the update");
|
||||
check(command("park-mod list").find("3 objects, 10 min left") != std::string::npos, "A reload lost the park's timer");
|
||||
check(command("park-mod reload street2").find("street2 is not spawned") != std::string::npos, "A park that is not up was reloaded");
|
||||
check(command("park-mod reload nothing").find("no park mod called") != std::string::npos, "An unknown park was reloaded");
|
||||
std::ofstream(street_file, std::ios::binary | std::ios::trunc) << "{ not json";
|
||||
check(command("park-mod reload street").find("[parks] street could not be reloaded: ") == 0, "A broken file was reloaded");
|
||||
run_for(1000000);
|
||||
check(a.park.objects().size() == 3, "A failed reload changed the park");
|
||||
std::filesystem::copy_file(fixtures / "popular skate 2 street park" / "parks" / "bam.park.json", street_file,
|
||||
std::filesystem::copy_options::overwrite_existing);
|
||||
command("park-mod clear");
|
||||
|
||||
// Live: parks set up from the console or an admin, saved to the config, no restart.
|
||||
reply = command("park-mod config add newpark | popular skate 2 street park | san vansterdam | bam");
|
||||
check(reply == "[parks] newpark set up (\"popular skate 2 street park\", bam on San Vansterdam) by console", "config add: " + reply);
|
||||
{
|
||||
const auto saved = load_config(config.file);
|
||||
check(!saved.park_mods.empty() && saved.park_mods.back().name == "newpark" && saved.park_mods.back().map == "San Vansterdam" &&
|
||||
saved.park_mods.back().folder == "popular skate 2 street park" && saved.park_mods.back().key == "bam",
|
||||
"A park set up live was not saved to the config");
|
||||
}
|
||||
check(command("park-mod add newpark") == "[parks] newpark spawned (8 objects, until removed) by console", "A park set up live did not spawn");
|
||||
check(command("park-mod config add newpark | popular skate 2 street park | San Vansterdam").find("already set up") != std::string::npos,
|
||||
"A park was set up twice");
|
||||
check(command("park-mod config add New Park! | x | San Vansterdam").find("could not be set up: park_mods") != std::string::npos,
|
||||
"A bad park name was set up");
|
||||
check(command("park-mod config add elsewhere | x | Nowhere").find("is not a single known map") != std::string::npos, "A park on an unknown map was set up");
|
||||
check(command("park-mod config add later | ../x | San Vansterdam").find("folder must be") != std::string::npos, "A path was set up as a folder");
|
||||
check(command("park-mod config add half | x").find("park-mod config add <name>") == 0, "A park without a map was set up");
|
||||
reply = command("park-mod config add later | copied later | Isle of Grom");
|
||||
check(reply.find("its folder is not in the Mods folder yet") != std::string::npos, "A missing folder was not mentioned: " + reply);
|
||||
a.say("/park-mod config remove later", now);
|
||||
run_for(500000);
|
||||
check(logged("[parks] later no longer set up (by Robin)"), "An admin could not remove a park from the setup");
|
||||
reply = command("park-mod config remove newpark");
|
||||
check(reply == "[parks] newpark removed (by console)\n[parks] newpark no longer set up (by console)", "config remove: " + reply);
|
||||
run_for(1000000);
|
||||
check(a.park.objects().empty(), "A park removed from the setup stayed on the map");
|
||||
{
|
||||
const auto saved = load_config(config.file);
|
||||
check(std::none_of(saved.park_mods.begin(), saved.park_mods.end(),
|
||||
[](const auto &park) { return park.name == "newpark" || park.name == "later"; }),
|
||||
"A park removed from the setup is still in the config");
|
||||
}
|
||||
check(command("park-mod config remove newpark").find("no park mod called") != std::string::npos, "A park was removed from the setup twice");
|
||||
|
||||
// A map change takes the parks with it.
|
||||
command("park-mod add street");
|
||||
run_for(1000000);
|
||||
|
||||
@@ -27,7 +27,8 @@ constexpr std::string_view help_text =
|
||||
"announce <text> | announce-to <player> <text> | announcements [list|add <text>|remove <n>|clear|interval <minutes>|off]\n"
|
||||
"map-pool [add|remove <map>|clear] | rotation [<minutes>|off]\n"
|
||||
"park <lot> <layout> | park random | layer-sync on|off | layer <key> default|on|off | tod <time|default>\n"
|
||||
"park-mod list | park-mod add <name> [<name>...] [minutes] | park-mod remove <name> [<name>...] | park-mod clear\n"
|
||||
"park-mod list | park-mod add <name> [<name>...] [minutes] | park-mod remove <name> [<name>...] | park-mod clear | park-mod reload <name> [<name>...]\n"
|
||||
"park-mod config add <name> | <mod folder> | <map> [| <key>] | park-mod config remove <name>\n"
|
||||
"activity-log on|off | announce-throwdowns on|off | parties [on|off] | party-size <2-8> | afk-kick <minutes>|off | speed-check off|warn|kick | board-check off|warn|kick\n"
|
||||
"score-check [off|warn|kick] | score-allow [<fingerprint>|remove <fingerprint>]\n"
|
||||
"reserved [slots <n> | add|remove <SteamID64>] | roles [reload] | role <player or SteamID64> <role>|none | update | quit";
|
||||
|
||||
+112
-14
@@ -167,9 +167,10 @@ void Host::tick_parks() {
|
||||
publish_parks();
|
||||
}
|
||||
|
||||
// "park-mod list | add <name> [<name>...] [minutes] | remove <name> [<name>...] | clear". What it
|
||||
// did is a "[parks]" line: the console and a vote log the reply, so it is logged here only for an
|
||||
// admin in game. A vote's refusal also goes to chat, where the players who voted can read it.
|
||||
// "park-mod list | add <name> [<name>...] [minutes] | remove <name> [<name>...] | clear |
|
||||
// reload <name> [<name>...] | config add <name> | <folder> | <map> [| <key>] | config remove <name>".
|
||||
// What it did is a "[parks]" line: the console and a vote log the reply, so it is logged here only
|
||||
// for an admin in game. A vote's refusal also goes to chat, where the players who voted can read it.
|
||||
std::string Host::park_mod_command(std::string_view argument, std::uint64_t admin) {
|
||||
const auto [action, rest] = split(argument);
|
||||
const auto verb = lower(action);
|
||||
@@ -196,6 +197,69 @@ std::string Host::park_mod_command(std::string_view argument, std::uint64_t admi
|
||||
for (const auto &park : parks_spawned_) count += park.objects.size();
|
||||
return count;
|
||||
};
|
||||
// What of a park's file players' games would not show, for the log.
|
||||
const auto skipped_line = [](std::string_view name, const std::vector<std::string> &skipped) {
|
||||
std::string items;
|
||||
for (std::size_t j = 0; j < skipped.size() && j < 8; ++j) items += (j ? ", " : "") + skipped[j];
|
||||
return "[parks] " + std::string(name) + ": skipped " + std::to_string(skipped.size()) +
|
||||
" object(s) players' games would not show: " + items + (skipped.size() > 8 ? ", ..." : "");
|
||||
};
|
||||
const auto named_twice = [](const std::vector<std::string> &names) -> std::optional<std::string> {
|
||||
for (std::size_t i = 1; i < names.size(); ++i)
|
||||
if (std::find(names.begin(), names.begin() + static_cast<std::ptrdiff_t>(i), names[i]) != names.begin() + static_cast<std::ptrdiff_t>(i))
|
||||
return names[i];
|
||||
return {};
|
||||
};
|
||||
// Setting parks up while the server runs: saved to ReSkateServer.json like the map pool, so a
|
||||
// park mod copied into Mods needs no restart.
|
||||
if (verb == "config") {
|
||||
const auto [what, fields] = split(rest);
|
||||
const auto sub = lower(what);
|
||||
if (sub == "add") {
|
||||
std::vector<std::string> parts;
|
||||
for (auto left = fields;;) {
|
||||
const auto bar = left.find('|');
|
||||
parts.emplace_back(trim(left.substr(0, bar)));
|
||||
if (bar == std::string_view::npos) break;
|
||||
left = left.substr(bar + 1);
|
||||
}
|
||||
if (parts.size() < 3 || parts.size() > 4 || parts[0].empty())
|
||||
return "park-mod config add <name> | <mod folder> | <map> [| <key>], e.g. park-mod config add street | "
|
||||
"popular skate 2 street park | San Vansterdam | bam";
|
||||
ParkMod park{lower(parts[0]), parts[1], parts[2], parts.size() > 3 ? lower(parts[3]) : std::string{}};
|
||||
const auto refuse = [&](const std::string &why) { return done({"[parks] " + park.name + " could not be set up: " + why}); };
|
||||
if (configured(park.name)) return refuse(park.name + " is already set up (park-mod config remove " + park.name + " first).");
|
||||
if (const auto *level = find_level(park.map)) park.map = level->name;
|
||||
auto next = config_;
|
||||
next.park_mods.push_back(park);
|
||||
if (const auto error = park_mods_error(next); !error.empty()) return refuse(error);
|
||||
config_.park_mods.push_back(park);
|
||||
save();
|
||||
std::error_code missing;
|
||||
const bool there = std::filesystem::is_directory(config_.mods / std::filesystem::path(park.folder), missing);
|
||||
return done({"[parks] " + park.name + " set up (\"" + park.folder + "\"" + (park.key.empty() ? "" : ", " + park.key) + " on " +
|
||||
park.map + ") by " + who + (there ? "" : "; its folder is not in the Mods folder yet")});
|
||||
}
|
||||
if (sub == "remove") {
|
||||
const auto name = lower(trim(fields));
|
||||
if (name.empty()) return "park-mod config remove <name>";
|
||||
const auto found = std::find_if(config_.park_mods.begin(), config_.park_mods.end(), [&](const auto &park) { return park.name == name; });
|
||||
if (found == config_.park_mods.end())
|
||||
return done({"[parks] " + name + " could not be removed from the setup: there is no park mod called \"" + name + "\" (park-mod list)."});
|
||||
std::vector<std::string> lines;
|
||||
if (const auto up = spawned(name); up != parks_spawned_.end()) {
|
||||
parks_spawned_.erase(up);
|
||||
publish_parks();
|
||||
send_chat("The " + name + " park was removed.");
|
||||
lines.push_back("[parks] " + name + " removed (by " + who + ")");
|
||||
}
|
||||
config_.park_mods.erase(found);
|
||||
save();
|
||||
lines.push_back("[parks] " + name + " no longer set up (by " + who + ")");
|
||||
return done(lines);
|
||||
}
|
||||
return "park-mod config add <name> | <mod folder> | <map> [| <key>] | park-mod config remove <name>";
|
||||
}
|
||||
std::vector<std::string> words;
|
||||
for (auto left = rest; !left.empty();) {
|
||||
const auto [word, more] = split(left);
|
||||
@@ -203,7 +267,8 @@ std::string Host::park_mod_command(std::string_view argument, std::uint64_t admi
|
||||
left = more;
|
||||
}
|
||||
if (verb.empty() || verb == "list") {
|
||||
if (config_.park_mods.empty()) return "No park mods are set up (maps.park_mods in ReSkateServer.json).";
|
||||
if (config_.park_mods.empty())
|
||||
return "No park mods are set up (maps.park_mods in ReSkateServer.json, or park-mod config add <name> | <mod folder> | <map>).";
|
||||
std::string text = "Park mods (" + std::to_string(parks_spawned_.size()) + " of " + std::to_string(config_.park_limits.parks) +
|
||||
" spawned, " + std::to_string(objects()) + " of " + std::to_string(config_.park_limits.objects) + " objects):";
|
||||
for (const auto &park : config_.park_mods) {
|
||||
@@ -269,12 +334,7 @@ std::string Host::park_mod_command(std::string_view argument, std::uint64_t admi
|
||||
}
|
||||
lines.push_back("[parks] " + park.name + " spawned (" + std::to_string(park.objects.size()) + " objects, " +
|
||||
(minutes ? std::to_string(*minutes) + " min" : std::string("until removed")) + ") by " + who);
|
||||
if (const auto &skipped = loaded[i].skipped; !skipped.empty()) {
|
||||
std::string items;
|
||||
for (std::size_t j = 0; j < skipped.size() && j < 8; ++j) items += (j ? ", " : "") + skipped[j];
|
||||
lines.push_back("[parks] " + park.name + ": skipped " + std::to_string(skipped.size()) +
|
||||
" object(s) players' games would not show: " + items + (skipped.size() > 8 ? ", ..." : ""));
|
||||
}
|
||||
if (!loaded[i].skipped.empty()) lines.push_back(skipped_line(park.name, loaded[i].skipped));
|
||||
parks_spawned_.push_back(std::move(park));
|
||||
}
|
||||
publish_parks();
|
||||
@@ -289,9 +349,7 @@ std::string Host::park_mod_command(std::string_view argument, std::uint64_t admi
|
||||
if (words.empty()) return "No parks are spawned.";
|
||||
}
|
||||
if (words.empty()) return "park-mod remove <name> [<name>...]";
|
||||
for (std::size_t i = 1; i < words.size(); ++i)
|
||||
if (std::find(words.begin(), words.begin() + static_cast<std::ptrdiff_t>(i), words[i]) != words.begin() + static_cast<std::ptrdiff_t>(i))
|
||||
return done({"[parks] " + words[i] + " could not be removed: it is named twice."});
|
||||
if (const auto twice = named_twice(words)) return done({"[parks] " + *twice + " could not be removed: it is named twice."});
|
||||
for (const auto &name : words)
|
||||
if (spawned(name) == parks_spawned_.end()) {
|
||||
const auto why = configured(name) ? name + " is not spawned." : "there is no park mod called \"" + name + "\" (park-mod list).";
|
||||
@@ -307,6 +365,46 @@ std::string Host::park_mod_command(std::string_view argument, std::uint64_t admi
|
||||
send_chat(words.size() > 1 ? "The " + listed(words) + " parks were removed." : "The " + words.front() + " park was removed.");
|
||||
return done(lines);
|
||||
}
|
||||
return "park-mod list | add <name> [<name>...] [minutes] | remove <name> [<name>...] | clear";
|
||||
// A spawned park's file read again (its mod was updated): its objects are swapped in place, and
|
||||
// it keeps its timer. All of them, or none.
|
||||
if (verb == "reload") {
|
||||
if (words.empty()) return "park-mod reload <name> [<name>...]";
|
||||
const auto names = listed(words);
|
||||
const auto refuse = [&](const std::string &why) {
|
||||
if (by_vote_) send_chat("The " + names + " park" + (words.size() > 1 ? "s" : "") + " could not be reloaded: " + why);
|
||||
return done({"[parks] " + names + " could not be reloaded: " + why});
|
||||
};
|
||||
if (const auto twice = named_twice(words)) return refuse(*twice + " is named twice.");
|
||||
std::vector<LoadedPark> loaded;
|
||||
auto total = objects();
|
||||
for (const auto &name : words) {
|
||||
const auto *park = configured(name);
|
||||
if (!park) return refuse("there is no park mod called \"" + name + "\" (park-mod list).");
|
||||
const auto up = spawned(name);
|
||||
if (up == parks_spawned_.end()) return refuse(name + " is not spawned (park-mod add " + name + ").");
|
||||
try {
|
||||
loaded.push_back(load_park_mod(config_.mods, *park));
|
||||
} catch (const std::exception &e) {
|
||||
return refuse(e.what());
|
||||
}
|
||||
total = total - up->objects.size() + loaded.back().objects.size();
|
||||
}
|
||||
if (total > config_.park_limits.objects)
|
||||
return refuse("that makes " + std::to_string(total) + " park objects, and at most " + std::to_string(config_.park_limits.objects) +
|
||||
" can be spawned at once.");
|
||||
std::vector<std::string> lines;
|
||||
for (std::size_t i = 0; i < words.size(); ++i) {
|
||||
auto &park = *spawned(words[i]);
|
||||
park.objects = std::move(loaded[i].objects);
|
||||
for (auto &object : park.objects) object.id = ++park_object_ids_;
|
||||
lines.push_back("[parks] " + park.name + " reloaded (" + std::to_string(park.objects.size()) + " objects) by " + who);
|
||||
if (!loaded[i].skipped.empty()) lines.push_back(skipped_line(park.name, loaded[i].skipped));
|
||||
}
|
||||
publish_parks();
|
||||
send_chat(words.size() > 1 ? "The " + names + " parks were updated." : "The " + names + " park was updated.");
|
||||
return done(lines);
|
||||
}
|
||||
return "park-mod list | add <name> [<name>...] [minutes] | remove <name> [<name>...] | clear | reload <name> [<name>...]\n"
|
||||
"park-mod config add <name> | <mod folder> | <map> [| <key>] | park-mod config remove <name>";
|
||||
}
|
||||
} // namespace dingosdk::server
|
||||
Reference in new issue
Block a user