Launcher: mod profiles, sharing them by code or file, and Thunderstore dependencies

The Mods page has a third tab, PROFILES. A profile is a saved list of which
mods are on, in what order and at what version; every mod stays installed
once, in the one Mods folder. Loading a profile rewrites mods.json, switches
off what it does not name, and downloads from Thunderstore what it names
that is missing. Profiles live in Mods/profiles.json.

A profile travels the way r2modman's do, so the two managers read each
other's: export.r2x inside a .r2z file, and a code on Thunderstore's
profile service. Config files in an r2modman profile are ignored.

Installing or updating a Thunderstore package now brings the packages it
depends on, needed ones first, leaving out any already installed at a new
enough version. A mod pack is a package that is nothing else.

A kick, by an admin or by a passed vote, used to keep the player out until
the server restarted. "kick_minutes" in the players section of the config
(1-1440; 0, the default, is still until the restart) and the console
command "kick-time <minutes>|restart" now set how long it lasts.

The player is told how long when they are kicked, and how many minutes are
left if they try to join early. A kick keeps the time it was given, so
changing the setting is for later kicks. Bans are unchanged.
This commit is contained in:
AyeZeeBB committed 2026-10-13 00:55:29 -05:00
1 parent 242ac11814
commit 2e186b9839
24 files changed
+1488 -38

No files matched your search

+4 -1
View File
@@ -121,6 +121,7 @@ Layout layout(const ServerConfig &c) {
players.set("allow_parties", c.parties);
players.set("party_size", c.party_size);
players.set("afk_kick_minutes", c.afk_kick);
players.set("kick_minutes", c.kick_minutes);
players.set("word_warnings", c.word_warnings);
players.set("use_global_word_filter", c.global_words);
players.set("allow_voice_chat", c.voice_chat);
@@ -365,6 +366,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
c.parties = get("players", "allow_parties", c.parties, {"parties"});
c.party_size = std::clamp(get("players", "party_size", c.party_size), 2U, 8U);
c.afk_kick = get("players", "afk_kick_minutes", c.afk_kick);
c.kick_minutes = get("players", "kick_minutes", c.kick_minutes);
c.word_warnings = get("players", "word_warnings", c.word_warnings);
c.global_words = get("players", "use_global_word_filter", c.global_words);
c.voice_chat = get("players", "allow_voice_chat", c.voice_chat, {"voice_chat"});
@@ -756,7 +758,7 @@ bool custom_command_name_free(std::string_view name) noexcept {
// The chat's own commands, then the server commands admins type as /<command>.
for (const std::string_view taken :
{"help", "party", "p", "w", "whisper", "tell", "poll", "vote", "yes", "y", "no", "n", "tp",
"activity-log", "admin", "admins", "afk-kick", "role", "roles", "announce", "announce-throwdowns", "announce-to", "announcements",
"activity-log", "admin", "admins", "afk-kick", "kick-time", "role", "roles", "announce", "announce-throwdowns", "announce-to", "announcements",
"ban", "bans", "board-check", "bone-scale", "boosts", "boosts-allow", "chat-color", "chat-colour", "clear-objects", "crowd",
"distances", "effects", "kick", "layer", "layer-sync", "layers", "listed", "map", "map-pool", "maps", "msg",
"msg-admins", "msg-party", "name", "net", "nobail", "nobail-allow", "noclip", "noclip-allow", "object-limit",
@@ -826,6 +828,7 @@ std::string config_error(const ServerConfig &c) {
using namespace multiplayer;
if (!parse_colour(c.chat_color)) return "chat_color must be a colour like #8E5CFF.";
if (c.afk_kick > 1440) return "afk_kick_minutes must be 0 (never) to 1440.";
if (c.kick_minutes > 1440) return "kick_minutes must be 0 (until the server restarts) to 1440.";
if (c.word_warnings > 10) return "word_warnings must be 0 (words are not checked) to 10.";
if (!parse_colour(c.chat_text_color)) return "chat_text_color must be a colour like #D9C8FF.";
if (!valid_server_name(c.name)) return std::string("name must be ") + server_name_rule + ".";