mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
Let server hosts choose the map pool and rotate through it
This commit is contained in:
1 parent
5bfe4f7bfa
commit
b643f76c2f
8 files changed
+210
-10
No files matched your search
@@ -42,6 +42,14 @@ name Shown in the browser (1-64 characters).
|
||||
map The map everyone skates, named like the game's load command:
|
||||
"San Vansterdam", "Isle of Grom", "Super Ultra Mega Resort",
|
||||
"Stadium 1", or a custom map such as "bbcity" (see Custom maps).
|
||||
map_pool The maps players may vote for and the rotation goes through,
|
||||
in order, e.g. ["San Vansterdam", "Isle of Grom", "bbcity"].
|
||||
Empty (the default) allows every map the server knows.
|
||||
Admins can still change to any map.
|
||||
map_rotation_minutes Minutes on each map before the server moves to the next
|
||||
one in map_pool (default 0: off). Players get a minute's
|
||||
warning; the clock waits while nobody is on, and starts
|
||||
over whenever the map changes (by a vote or an admin too).
|
||||
max_players 1-249.
|
||||
password Empty for anyone; otherwise players type it to join.
|
||||
welcome A chat line sent to each player as they join.
|
||||
@@ -53,6 +61,7 @@ global_bans Turn away players the ReSkate team has banned from multiplaye
|
||||
"bans" apply either way.
|
||||
votes Player votes, each off until turned on:
|
||||
"map": {"enabled": true, "percent": 60} /vote map <map>
|
||||
(a map in map_pool)
|
||||
"kick": {"enabled": true, "percent": 60} /vote kick <player>
|
||||
"time_of_day": {"enabled": true, "percent": 50} /vote tod <time>
|
||||
(needs world_layer_sync)
|
||||
@@ -128,6 +137,9 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
|
||||
ban <player or id> [name] For good. unban <id> bans
|
||||
map <name> e.g. map San Vansterdam, map grom, map bbcity
|
||||
maps The maps this server knows.
|
||||
map-pool [add|remove <map>|clear] The maps players vote between and the
|
||||
rotation uses (see map_pool).
|
||||
rotation [<minutes>|off] Change the map on a timer (see map_rotation_minutes).
|
||||
name <text> password <text|off> welcome <text|off> listed on|off
|
||||
tps 20|30|60|120 voice on|off voice-range <m>
|
||||
distances <full> <half> <half-return> <low>
|
||||
|
||||
@@ -35,7 +35,7 @@ int run() {
|
||||
const auto config = load_config(file, &added);
|
||||
const auto has = [&](std::string_view name) { return std::ranges::find(added, name) != added.end(); };
|
||||
check(has("enforce_tuning") && has("votes.seconds") && has("votes.kick") && has("score_check") && has("score_allow") &&
|
||||
has("global_bans"),
|
||||
has("global_bans") && has("map_pool") && has("map_rotation_minutes"),
|
||||
"New settings not reported");
|
||||
check(!has("name") && !has("tps") && !has("votes.map"), "Settings the file had reported as new");
|
||||
const auto written = text(file);
|
||||
@@ -85,6 +85,38 @@ int run() {
|
||||
const auto edges = load_config(file);
|
||||
check(edges.port == 65535 && edges.query_port == 1, "Ports at the ends of the range refused");
|
||||
|
||||
static_cast<void>(load_levels(folder / "Mods")); // no Mods folder: the retail maps only
|
||||
ServerConfig pool;
|
||||
check(pool_levels(pool).size() == levels().size() && in_map_pool(pool, "Stadium 2"),
|
||||
"An empty pool does not allow every map");
|
||||
pool.map_pool = {"Isle", "Isle of Grom", "San Vansterdam", "Stadium 1"};
|
||||
check(pool_levels(pool).size() == 3, "Pool maps not resolved once each");
|
||||
check(in_map_pool(pool, "San Vansterdam") && in_map_pool(pool, "Levels/Game/DingoLevel_SDM/DingoLevel_SDM_Int_001/DingoLevel_SDM_Int_001") &&
|
||||
!in_map_pool(pool, "Super Ultra Mega Resort") && !in_map_pool(pool, "Nowhere"),
|
||||
"Pool membership wrong");
|
||||
const auto next = [&](std::string_view map) {
|
||||
const auto *level = next_pool_map(pool, map);
|
||||
return level ? level->name : std::string{};
|
||||
};
|
||||
check(next("Isle of Grom") == "San Vansterdam" && next("Stadium 1") == "Isle of Grom" &&
|
||||
next("Super Ultra Mega Resort") == "Isle of Grom",
|
||||
"Rotation does not follow the pool's order");
|
||||
pool.map_pool = {"Isle of Grom"};
|
||||
check(next("Isle of Grom").empty() && next("San Vansterdam") == "Isle of Grom", "A one-map pool rotated to itself");
|
||||
check(config_error(pool).empty(), "A valid pool refused");
|
||||
pool.map_pool = {"Isle of Grom", "Nowhere"};
|
||||
check(!config_error(pool).empty(), "An unknown pool map accepted");
|
||||
pool.map_pool = {"Isle of Grom", "Stadium 1"};
|
||||
pool.map_rotation = 15;
|
||||
pool.file = file;
|
||||
save_config(pool);
|
||||
const auto rotating = load_config(file);
|
||||
check(rotating.map_pool == pool.map_pool && rotating.map_rotation == 15, "Map pool or rotation lost on save");
|
||||
std::ofstream(file, std::ios::binary) << R"({"map_pool": ["Isle of Grom", 7, ""], "map_rotation_minutes": 5000})";
|
||||
const auto odd_pool = load_config(file);
|
||||
check(odd_pool.map_pool == std::vector<std::string>{"Isle of Grom"} && odd_pool.map_rotation == 1440,
|
||||
"Bad pool entries kept, or rotation not capped");
|
||||
|
||||
std::filesystem::remove_all(folder);
|
||||
if (failures) return 1;
|
||||
std::cout << "server config: ok\n";
|
||||
|
||||
+8
-1
@@ -312,10 +312,17 @@ int run(int argc, char **argv, bool skip_update) {
|
||||
for (const auto &problem : load_levels(here / "Mods")) write_log("Mods: skipped " + problem);
|
||||
if (levels().size() > 6) write_log("Mods: " + std::to_string(levels().size() - 6) + " custom map(s).");
|
||||
// Older configs name the map by its full destination; keep the plain name instead.
|
||||
bool renamed{};
|
||||
if (const auto setting = map_setting(config.map); setting != config.map && !setting.empty()) {
|
||||
config.map = setting;
|
||||
try { save_config(config); } catch (...) {}
|
||||
renamed = true;
|
||||
}
|
||||
for (auto &map : config.map_pool) // short pool names ("isle") are saved in full
|
||||
if (const auto *level = find_level(map); level && level->name != map) {
|
||||
map = level->name;
|
||||
renamed = true;
|
||||
}
|
||||
if (renamed) try { save_config(config); } catch (...) {}
|
||||
if (const auto error = config_error(config); !error.empty()) {
|
||||
write_log("Config problem: " + error);
|
||||
return 1;
|
||||
|
||||
@@ -16,6 +16,10 @@ Json to_json(const ServerConfig &c) {
|
||||
auto root = Json::object();
|
||||
root["name"] = c.name;
|
||||
root["map"] = c.map;
|
||||
auto pool = Json::array();
|
||||
for (const auto &map : c.map_pool) pool.push_back(map);
|
||||
root["map_pool"] = std::move(pool);
|
||||
root["map_rotation_minutes"] = c.map_rotation;
|
||||
root["max_players"] = c.max_players;
|
||||
root["password"] = c.password;
|
||||
root["welcome"] = c.welcome;
|
||||
@@ -111,6 +115,10 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
if (!root.is_object()) throw std::runtime_error("The server config must be a JSON object.");
|
||||
c.name = root.value("name", c.name);
|
||||
c.map = root.value("map", c.map);
|
||||
if (root.contains("map_pool") && root.at("map_pool").is_array())
|
||||
for (const auto &map : root.at("map_pool"))
|
||||
if (map.is_string() && !map.string().empty()) c.map_pool.push_back(map.string());
|
||||
c.map_rotation = std::min(root.value("map_rotation_minutes", c.map_rotation), 1440U);
|
||||
c.max_players = root.value("max_players", c.max_players);
|
||||
c.password = root.value("password", c.password);
|
||||
c.welcome = root.value("welcome", c.welcome);
|
||||
@@ -226,6 +234,10 @@ std::string config_error(const ServerConfig &c) {
|
||||
if (c.map.empty() || !valid_map_destination(map_destination(c.map)))
|
||||
return "map \"" + c.map + "\" is not a known map. Use a name like \"San Vansterdam\", or put the map's mod "
|
||||
"folder in Mods next to the server.";
|
||||
for (const auto &map : c.map_pool)
|
||||
if (!find_level(map) || !valid_map_destination(map_destination(map)))
|
||||
return "map_pool: \"" + map + "\" is not a single known map. Use names like \"Isle of Grom\", or put the "
|
||||
"map's mod folder in Mods next to the server.";
|
||||
if (c.max_players < 1 || c.max_players + 1 > max_players)
|
||||
return "max_players must be 1 to " + std::to_string(max_players - 1) + ".";
|
||||
if (c.password.size() > 64) return "password must be at most 64 characters.";
|
||||
@@ -333,4 +345,32 @@ std::string map_label(std::string_view map) {
|
||||
if (const auto *level = find_level(map)) return level->name;
|
||||
return world_level_name(world_destination_asset(map_destination(map)));
|
||||
}
|
||||
std::vector<const ServerLevel *> pool_levels(const ServerConfig &config) {
|
||||
std::vector<const ServerLevel *> pool;
|
||||
const auto add = [&](const ServerLevel *level) {
|
||||
if (level && multiplayer::valid_map_destination(map_destination(level->asset)) &&
|
||||
std::find(pool.begin(), pool.end(), level) == pool.end())
|
||||
pool.push_back(level);
|
||||
};
|
||||
if (config.map_pool.empty())
|
||||
for (const auto &level : level_list()) add(&level);
|
||||
for (const auto &map : config.map_pool) add(find_level(map));
|
||||
return pool;
|
||||
}
|
||||
bool in_map_pool(const ServerConfig &config, std::string_view map) {
|
||||
if (config.map_pool.empty()) return true;
|
||||
const auto pool = pool_levels(config);
|
||||
const auto *level = find_level(map);
|
||||
return level && std::find(pool.begin(), pool.end(), level) != pool.end();
|
||||
}
|
||||
const ServerLevel *next_pool_map(const ServerConfig &config, std::string_view map) {
|
||||
const auto pool = pool_levels(config);
|
||||
const auto *current = find_level(map);
|
||||
const auto at = static_cast<std::size_t>(std::find(pool.begin(), pool.end(), current) - pool.begin());
|
||||
for (std::size_t step = 1; step <= pool.size(); ++step) {
|
||||
const auto *next = at == pool.size() ? pool[step - 1] : pool[(at + step) % pool.size()];
|
||||
if (next != current) return next;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace dingosdk::server
|
||||
@@ -33,6 +33,8 @@ struct ServerConfig {
|
||||
// The map everyone skates, named like the game's `load` command: "San Vansterdam",
|
||||
// "Isle of Grom", or a custom map in Mods\ such as "bbcity".
|
||||
std::string map = "San Vansterdam";
|
||||
std::vector<std::string> map_pool; // maps for votes and the rotation, in order; empty: every map
|
||||
unsigned map_rotation = 0; // minutes per map before the next pool map (0: off)
|
||||
unsigned max_players = 16; // players; the server itself is not one
|
||||
std::string password; // empty: anyone may join
|
||||
std::string welcome; // sent to each player as they join
|
||||
@@ -107,4 +109,7 @@ std::string map_destination(std::string_view map);
|
||||
std::string map_setting(std::string_view map);
|
||||
// A map's name for people: "San Vansterdam".
|
||||
std::string map_label(std::string_view map);
|
||||
std::vector<const ServerLevel *> pool_levels(const ServerConfig &config); // known pool maps once each; all when empty
|
||||
bool in_map_pool(const ServerConfig &config, std::string_view map);
|
||||
const ServerLevel *next_pool_map(const ServerConfig &config, std::string_view map); // after `map`; null if no other
|
||||
} // namespace dingosdk::server
|
||||
+57
-5
@@ -47,6 +47,7 @@ constexpr std::string_view help_text =
|
||||
"tps 20|30|60|120 | voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low>\n"
|
||||
"placement everyone|admins|nobody | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n"
|
||||
"tpall [player] | tphere <player> | votes [map|kick|tod on|off|<percent>] | vote-cancel\n"
|
||||
"map-pool [add|remove <map>|clear] | rotation [<minutes>|off]\n"
|
||||
"park <lot> <layout> | layer-sync on|off | layer <key> default|on|off | tod <time|default>\n"
|
||||
"activity-log on|off | announce-throwdowns on|off | parties [on|off] | party-size <2-8> | speed-check off|warn|kick\n"
|
||||
"score-check [off|warn|kick] | score-allow [<fingerprint>|remove <fingerprint>]\n"
|
||||
@@ -322,12 +323,16 @@ void Host::send_bans(Guest &admin) {
|
||||
}
|
||||
if (send_packet(admin, list, true, false)) admin.bans_sent = bans_revision_;
|
||||
}
|
||||
// The maps an admin may switch to: the retail ones and the server's custom maps.
|
||||
void Host::send_maps(Guest &admin) {
|
||||
void Host::send_maps(Guest &guest) { // admins: every map; players: the pool
|
||||
auto list = packet(PacketKind::maps, now_);
|
||||
for (const auto &level : levels())
|
||||
const auto add = [&](const ServerLevel &level) {
|
||||
if (valid_map_asset(level.asset) && list.maps.size() < max_server_maps) list.maps.push_back(level.asset);
|
||||
if (send_packet(admin, list, true, false)) admin.maps_sent = true;
|
||||
};
|
||||
if (is_admin(guest.member.id))
|
||||
for (const auto &level : levels()) add(level);
|
||||
else
|
||||
for (const auto *level : pool_levels(config_)) add(*level);
|
||||
if (send_packet(guest, list, true, false)) guest.maps_sent = true;
|
||||
}
|
||||
void Host::change_map(std::string_view map) {
|
||||
if (!valid_map_destination(map_destination(map_setting(map))))
|
||||
@@ -361,6 +366,8 @@ void Host::change_map(std::string_view map) {
|
||||
activity_.clear(); // throwdowns end with the world
|
||||
config_.map = map_setting(map);
|
||||
map_ = map_hash(map_destination(config_.map));
|
||||
map_since_ = now_;
|
||||
rotation_warned_ = false;
|
||||
travel_started_ = now_;
|
||||
last_world_state_ = 0;
|
||||
send_world_state();
|
||||
@@ -807,6 +814,7 @@ void Host::tick(std::uint64_t now) {
|
||||
activity_.tick(now_);
|
||||
if (std::exchange(vote_recount_, false)) check_vote(false);
|
||||
if (vote_ && now_ >= vote_->ends) check_vote(true);
|
||||
tick_rotation();
|
||||
std::erase_if(vote_cooldowns_, [&](const auto &entry) { return now_ >= entry.second; });
|
||||
join_backoff_.prune(now_);
|
||||
}
|
||||
@@ -914,9 +922,51 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
}
|
||||
if (name == "maps") {
|
||||
std::string text = std::to_string(levels().size()) + " maps (custom maps come from Mods next to the server)";
|
||||
for (const auto &level : levels()) text += "\n " + level.name + (same_map(level.asset) ? " (now)" : "");
|
||||
for (const auto &level : levels())
|
||||
text += "\n " + level.name + (same_map(level.asset) ? " (now)" : "") +
|
||||
(!config_.map_pool.empty() && in_map_pool(config_, level.asset) ? " (pool)" : "");
|
||||
return text;
|
||||
}
|
||||
if (name == "map-pool") { // map-pool [add|remove <map>|clear]
|
||||
const auto [what_text, map] = split(argument);
|
||||
const auto what = lower(what_text);
|
||||
if (what.empty()) return pool_text();
|
||||
if (what == "clear") {
|
||||
config_.map_pool.clear();
|
||||
resend_maps();
|
||||
return changed("The map pool is cleared: players vote between every map, and the rotation goes through them all.");
|
||||
}
|
||||
if (what != "add" && what != "remove") return "map-pool [add|remove <map>|clear]";
|
||||
const auto *level = find_level(map);
|
||||
if (!level || !valid_map_destination(map_destination(level->asset)))
|
||||
return "No single map is called \"" + std::string(map) + "\". Type maps for the list.";
|
||||
const auto pooled = [&](const std::string &entry) { return find_level(entry) == level; };
|
||||
const bool listed = std::any_of(config_.map_pool.begin(), config_.map_pool.end(), pooled);
|
||||
if (what == "add") {
|
||||
if (listed || config_.map_pool.empty()) return level->name + " is already in the map pool.";
|
||||
config_.map_pool.push_back(level->name);
|
||||
} else {
|
||||
if (config_.map_pool.empty()) // every map: keep all the others
|
||||
for (const auto *other : pool_levels(config_)) config_.map_pool.push_back(other->name);
|
||||
else if (!listed) return level->name + " is not in the map pool.";
|
||||
if (std::all_of(config_.map_pool.begin(), config_.map_pool.end(), pooled))
|
||||
return "The map pool needs at least one map. map-pool clear allows every map again.";
|
||||
std::erase_if(config_.map_pool, pooled);
|
||||
}
|
||||
resend_maps();
|
||||
return changed(level->name + (what == "add" ? " added to" : " removed from") + " the map pool.");
|
||||
}
|
||||
if (name == "rotation") { // rotation [<minutes>|off]
|
||||
if (argument.empty()) return rotation_text();
|
||||
const auto value = lower(argument);
|
||||
const auto minutes = value == "off" ? std::optional<std::uint64_t>(0) : number(value);
|
||||
if (!minutes || *minutes > 1440) return "rotation <1-1440 minutes>|off";
|
||||
config_.map_rotation = static_cast<unsigned>(*minutes);
|
||||
map_since_ = now_;
|
||||
rotation_warned_ = false;
|
||||
resend_maps();
|
||||
return changed(rotation_text());
|
||||
}
|
||||
if (name == "name") {
|
||||
if (argument.empty() || argument.size() > 64 || !valid_member_name(argument)) return "Server names are 1 to 64 characters.";
|
||||
config_.name = argument;
|
||||
@@ -1292,10 +1342,12 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
if (!individual_steam_id(id)) return "admin add|remove <player or SteamID64>";
|
||||
if (sub == "add") {
|
||||
if (!is_admin(id)) config_.admins.push_back(id);
|
||||
resend_maps();
|
||||
return changed(std::to_string(id) + " is an admin.");
|
||||
}
|
||||
if (sub == "remove") {
|
||||
std::erase(config_.admins, id);
|
||||
resend_maps();
|
||||
return changed(std::to_string(id) + " is no longer an admin.");
|
||||
}
|
||||
return "admin add|remove <player or SteamID64>";
|
||||
|
||||
@@ -91,7 +91,7 @@ class Host {
|
||||
bool scoring_flagged{}; // out of linked activities (config score_check)
|
||||
ChatBudget scoring_budget;
|
||||
std::uint64_t bans_sent{}; // the ban list revision this admin has
|
||||
bool maps_sent{}; // this admin has the server's map list
|
||||
bool maps_sent{}; // this player has the server's map list (send_maps)
|
||||
// The owner's own upload, and what the server shares of it.
|
||||
ObjectState objects, shared;
|
||||
std::uint64_t shared_from{};
|
||||
@@ -120,6 +120,8 @@ class Host {
|
||||
std::optional<Vote> vote_;
|
||||
bool vote_recount_{}; // a player left: recount in tick(), never while guests_ is being walked
|
||||
std::map<std::uint64_t, std::uint64_t> vote_cooldowns_;
|
||||
std::uint64_t map_since_{}; // rotation clock start: the last map change, or while nobody is on
|
||||
bool rotation_warned_{}; // players were told the next map is a minute away
|
||||
// Parties (server_party.cpp): the server owns them; each roster carries them to everyone.
|
||||
PartyBook parties_;
|
||||
std::uint64_t party_revision_{};
|
||||
@@ -173,6 +175,10 @@ class Host {
|
||||
std::uint8_t enabled_votes() const;
|
||||
void reply(Guest &, std::string_view text);
|
||||
Guest *match_player(std::string_view text);
|
||||
void tick_rotation();
|
||||
std::string pool_text() const; // the map pool, one map a line
|
||||
std::string rotation_text() const; // the rotation's interval and next map
|
||||
void resend_maps(); // after the pool or the admins change
|
||||
// Parties (server_party.cpp).
|
||||
void party_request(Guest &, PartyAction, std::uint64_t player);
|
||||
void party_command(Guest &, std::string_view line); // "/party ..."
|
||||
|
||||
+48
-2
@@ -5,7 +5,7 @@
|
||||
#include <cmath>
|
||||
|
||||
// Player votes (map, kick, time of day) and the chat commands that run them. Each vote is
|
||||
// switched on and given its pass percentage in ReSkateServer.json ("votes").
|
||||
// switched on and given its pass percentage in ReSkateServer.json ("votes"); also the map pool and rotation.
|
||||
namespace dingosdk::server {
|
||||
namespace {
|
||||
constexpr std::array<std::string_view, 8> times{"default", "morning", "noon", "afternoon",
|
||||
@@ -67,6 +67,7 @@ void Host::chat_command(Guest &guest, std::string_view line) {
|
||||
if (votes & server_vote_kick) text += "/vote kick <player>: start a vote to kick a player\n";
|
||||
if (votes & server_vote_time) text += "/vote tod <time>: vote for a time of day (morning, noon, night...)\n";
|
||||
if (votes) text += "/yes or /no: vote in the running vote\n";
|
||||
if (config_.map_rotation) text += "The map changes every " + std::to_string(config_.map_rotation) + " min.\n";
|
||||
if (config_.parties) text += "/party: your party (invite, accept, leave...; /party help); /p <message>: party chat\n";
|
||||
if (is_admin(guest.member.id)) text += "Admins: any server command as /<command>, e.g. /kick, /map, /tpall, /votes\n";
|
||||
return reply(guest, text.empty() ? "This server has no player votes. Type /tp <player> to teleport." : text);
|
||||
@@ -117,8 +118,10 @@ void Host::start_vote(Guest &guest, VoteKind kind, std::string_view argument) {
|
||||
case VoteKind::map: {
|
||||
if (argument.empty()) return reply(guest, "/vote map <map>, e.g. /vote map grom");
|
||||
// Players vote between the server's own maps (/maps); a raw level path is admins only.
|
||||
if (!find_level(argument) || !valid_map_destination(map_destination(argument)))
|
||||
const auto *level = find_level(argument);
|
||||
if (!level || !valid_map_destination(map_destination(argument)))
|
||||
return reply(guest, "No single map is called \"" + std::string(argument) + "\".");
|
||||
if (!in_map_pool(config_, level->asset)) return reply(guest, level->name + " is not one of this server's maps.\n" + pool_text());
|
||||
if (map_hash(map_destination(argument)) == map_) return reply(guest, "The server is already on that map.");
|
||||
vote.value = std::string(argument);
|
||||
vote.label = "change the map to " + map_label(argument);
|
||||
@@ -219,4 +222,47 @@ void Host::check_vote(bool expired) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string Host::pool_text() const {
|
||||
std::string text = config_.map_pool.empty() ? "Map pool: every map" : "Map pool:";
|
||||
if (!config_.map_pool.empty())
|
||||
for (const auto *level : pool_levels(config_)) text += "\n " + level->name + (same_map(level->asset) ? " (now)" : "");
|
||||
return text;
|
||||
}
|
||||
std::string Host::rotation_text() const {
|
||||
if (!config_.map_rotation) return "Map rotation is off.";
|
||||
const auto every = "The map changes every " + std::to_string(config_.map_rotation) + " min";
|
||||
const auto *next = next_pool_map(config_, config_.map);
|
||||
if (!next) return every + ", but the map pool has no other map.";
|
||||
if (!players()) return every + " while players are on. Next: " + next->name + ".";
|
||||
const auto due = map_since_ + std::uint64_t{config_.map_rotation} * 60000000;
|
||||
const auto left = due > now_ ? (due - now_ + 59999999) / 60000000 : 0;
|
||||
return every + ". Next: " + next->name + " in about " + std::to_string(std::max<std::uint64_t>(left, 1)) + " min.";
|
||||
}
|
||||
void Host::resend_maps() {
|
||||
for (auto &[id, guest] : guests_) guest->maps_sent = false;
|
||||
}
|
||||
void Host::tick_rotation() { // waits while nobody is on, and for a running map vote
|
||||
if (!config_.map_rotation || !players()) {
|
||||
map_since_ = now_;
|
||||
rotation_warned_ = false;
|
||||
return;
|
||||
}
|
||||
const auto due = map_since_ + std::uint64_t{config_.map_rotation} * 60000000;
|
||||
if (now_ + 60000000 < due) return;
|
||||
const auto *next = next_pool_map(config_, config_.map);
|
||||
if (!next) {
|
||||
map_since_ = now_;
|
||||
return;
|
||||
}
|
||||
if (!rotation_warned_ && config_.map_rotation > 1) {
|
||||
rotation_warned_ = true;
|
||||
send_chat("Next map in 1 minute: " + next->name + ".");
|
||||
}
|
||||
if (now_ < due || (vote_ && vote_->kind == VoteKind::map)) return;
|
||||
send_chat("Changing the map to " + next->name + ".");
|
||||
log_("[rotation] Changing the map to " + next->name + ".");
|
||||
change_map(next->name);
|
||||
save();
|
||||
}
|
||||
} // namespace dingosdk::server
|
||||
Reference in new issue
Block a user