mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 13:57:05 +02:00
Server: remove players who have been away too long
Server: remove players who have been away too long A dedicated server can remove players who are away from their game. - "afk_kick_minutes" in the config's "players" section: 1 to 1440, or 0 (the default) for never. "afk-kick <minutes>|off" from the console or an admin; a change is saved and applies at once. - Away is doing nothing a player at their game does: not moving or turning, not speaking, not sending chat, and not placing, moving or removing an object. - A minute before the limit the player is told in chat; at the limit they are removed with a message saying why and that they can join again. The log has an "[afk]" line. - Admins are never removed for it. The time starts once a player's game has loaded the map, and starts over after a map change and when the setting is changed.
This commit is contained in:
1 parent
9ab985e778
commit
46cf65df0a
7 files changed
+80
-2
No files matched your search
@@ -115,6 +115,11 @@ allow_parties Let players form parties (default true): invite each other
|
||||
members join each other's coop challenges, see each other on
|
||||
the map and talk with /p <message>.
|
||||
party_size Most players in one party, 2-8 (default 8).
|
||||
afk_kick_minutes Remove a player who has been away this many minutes, 1-1440
|
||||
(default 0: never). Away is not moving, speaking, typing
|
||||
in chat or changing their objects. They are warned in chat
|
||||
a minute before and can join again at once. Admins are
|
||||
never removed for it. Console: afk-kick <minutes>|off.
|
||||
allow_voice_chat Allow voice chat.
|
||||
voice_range How far proximity voice reaches, 50-1000 m.
|
||||
object_placement everyone, admins (only admins can build), or nobody.
|
||||
@@ -289,6 +294,7 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
|
||||
announce-throwdowns on|off Chat message when a throwdown is placed.
|
||||
parties [on|off] List the parties, or allow them (off ends them all).
|
||||
party-size <2-8> Most players in one party.
|
||||
afk-kick <minutes>|off Remove players who have been away that long.
|
||||
speed-check off|warn|kick What happens to players whose game runs fast.
|
||||
score-check [off|warn|kick] What happens to players whose mods change scoring
|
||||
or physics; with no argument, every player's result.
|
||||
|
||||
@@ -192,6 +192,18 @@ int run() {
|
||||
"A config in sections did not read back as it was written");
|
||||
std::filesystem::remove_all(folder / "data");
|
||||
}
|
||||
// The away timer: off unless set, and no more than a day.
|
||||
{
|
||||
ServerConfig away;
|
||||
check(away.afk_kick == 0 && config_error(away).find("afk_kick_minutes") == std::string::npos, "The away timer is not off by default");
|
||||
away.file = folder / "away.json";
|
||||
away.afk_kick = 15;
|
||||
save_config(away);
|
||||
check(load_config(away.file).afk_kick == 15, "The away timer was not kept");
|
||||
away.afk_kick = 1441;
|
||||
check(config_error(away).find("afk_kick_minutes") != std::string::npos, "An away timer over a day was accepted");
|
||||
std::filesystem::remove_all(folder / "data");
|
||||
}
|
||||
// Object scaling: allowed unless turned off, and kept in the file.
|
||||
{
|
||||
ServerConfig sized;
|
||||
|
||||
@@ -20,7 +20,7 @@ constexpr std::string_view help_text =
|
||||
"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> | park random | 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"
|
||||
"activity-log on|off | announce-throwdowns on|off | parties [on|off] | party-size <2-8> | afk-kick <minutes>|off | speed-check off|warn|kick\n"
|
||||
"score-check [off|warn|kick] | score-allow [<fingerprint>|remove <fingerprint>]\n"
|
||||
"reserved [slots <n> | add|remove <SteamID64>] | admin add|remove <SteamID64> | admins | update | quit";
|
||||
} // namespace
|
||||
@@ -363,6 +363,17 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
std::to_string(*value / config_.tps) + " players near them at the full rate."
|
||||
: std::string("No crowd limit: every player near is sent at the full rate."));
|
||||
}
|
||||
if (name == "afk-kick") {
|
||||
const auto value = lower(argument) == "off" ? std::optional<std::uint64_t>(0) : number(argument);
|
||||
if (!value || *value > 1440)
|
||||
return "afk-kick <minutes 1-1440>|off (now " + (config_.afk_kick ? std::to_string(config_.afk_kick) + " min" : std::string("off")) + ")";
|
||||
config_.afk_kick = static_cast<unsigned>(*value);
|
||||
// Nobody is removed for time away before the rule was set.
|
||||
for (auto &[id, guest] : guests_)
|
||||
if (guest->active_at) active(*guest);
|
||||
return changed(config_.afk_kick ? "Players away for " + std::to_string(config_.afk_kick) + " min are removed. Admins are not."
|
||||
: std::string("Players are no longer removed for being away."));
|
||||
}
|
||||
if (name == "party-size") {
|
||||
const auto value = number(argument);
|
||||
if (!value || *value < 2 || *value > 8) return "party-size <2-8> (now " + std::to_string(config_.party_size) + ")";
|
||||
|
||||
@@ -99,6 +99,7 @@ Layout layout(const ServerConfig &c) {
|
||||
players.set("allow_noclip", c.noclip);
|
||||
players.set("allow_parties", c.parties);
|
||||
players.set("party_size", c.party_size);
|
||||
players.set("afk_kick_minutes", c.afk_kick);
|
||||
players.set("allow_voice_chat", c.voice_chat);
|
||||
players.set("voice_range", static_cast<double>(c.voice_range));
|
||||
players.set("object_placement", placement_text(c.object_placement));
|
||||
@@ -250,6 +251,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
c.noclip = get("players", "allow_noclip", c.noclip, {"noclip"});
|
||||
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.voice_chat = get("players", "allow_voice_chat", c.voice_chat, {"voice_chat"});
|
||||
c.voice_range = get("players", "voice_range", c.voice_range);
|
||||
const auto placement = get("players", "object_placement", placement_text(c.object_placement));
|
||||
@@ -389,6 +391,7 @@ std::optional<std::uint32_t> parse_colour(std::string_view text) noexcept {
|
||||
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 (!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 + ".";
|
||||
for (const auto id : c.reserved)
|
||||
|
||||
@@ -78,6 +78,9 @@ struct ServerConfig {
|
||||
// panel has eight rows). Off, nobody can be in one.
|
||||
bool parties = true;
|
||||
unsigned party_size = 8;
|
||||
// Minutes a player may be away (not moving, talking, typing or building) before the server
|
||||
// removes them, 1 to 1440; 0: never. Admins are never removed for it.
|
||||
unsigned afk_kick = 0;
|
||||
// Players whose game runs fast (a speedhack; Server/speed_check.h): "warn" takes them out of
|
||||
// throwdowns and coop challenges and tells the admins, "kick" also removes them, "off" does not check.
|
||||
std::string speed_check = "warn";
|
||||
|
||||
+34
-1
@@ -763,6 +763,7 @@ bool Host::accept_data(Guest &source, const Packet &p) {
|
||||
if (!source.moved_at || moved > 0.05f * 0.05f || std::abs(facing) < 0.9995f) {
|
||||
source.still_at = root;
|
||||
source.moved_at = now_;
|
||||
active(source);
|
||||
}
|
||||
}
|
||||
return check_speed(source, p.time_us); // last: a speed-check kick frees `source`
|
||||
@@ -890,7 +891,10 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
|
||||
log_("[map] " + guest_name(*link) + " finished loading (" + std::to_string((now_ - since) / 1000000) + " s)");
|
||||
link->world_ready = p.world_ready;
|
||||
if (p.world_ready) link->travel_since = link->loading_since = 0;
|
||||
if (arrived) meet_later(*link);
|
||||
if (arrived) {
|
||||
meet_later(*link);
|
||||
active(*link); // the time away starts once they are in
|
||||
}
|
||||
return;
|
||||
}
|
||||
case PacketKind::map_request: {
|
||||
@@ -1007,6 +1011,7 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
|
||||
// A listen host's; the server's physics are the game's own.
|
||||
if (p.kind == PacketKind::physics_tuning || p.kind == PacketKind::physics_extras) return;
|
||||
if (p.kind == PacketKind::chat) {
|
||||
active(*link);
|
||||
if (!routed_source(p, link->member, peer, true, id_) ||
|
||||
link->chat_rate.accept(now_, p.text, 1) != ChatRate::Verdict::accepted)
|
||||
return;
|
||||
@@ -1062,8 +1067,10 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
|
||||
if (!routed_source(p, link->member, peer, true, id_)) return;
|
||||
if (p.kind == PacketKind::away) return drop(peer, "A player ended their session.");
|
||||
if (p.kind == PacketKind::objects) {
|
||||
const auto before = link->objects.revision();
|
||||
if (link->objects.receive(p.objects) == ObjectState::Result::invalid)
|
||||
return drop(peer, "Invalid shared object revision or layout.");
|
||||
if (link->objects.revision() != before) active(*link);
|
||||
link->last_packet = now_;
|
||||
return;
|
||||
}
|
||||
@@ -1074,6 +1081,7 @@ void Host::receive(std::uint64_t peer, std::span<const std::uint8_t> bytes, std:
|
||||
link->received_voice = true;
|
||||
link->voice_sequence = p.sequence;
|
||||
link->last_packet = now_;
|
||||
active(*link);
|
||||
broadcast(p, false, true, p.source);
|
||||
return;
|
||||
}
|
||||
@@ -1100,6 +1108,30 @@ void Host::receive_cosmetics() {
|
||||
}
|
||||
}
|
||||
|
||||
// Players who have been away longer than the server allows ("afk_kick_minutes") are removed,
|
||||
// with a warning a minute before. Away is doing nothing a player at their game does: not
|
||||
// moving, speaking, typing in chat or changing their objects. Admins stay, and so does anyone
|
||||
// whose game is still loading the map.
|
||||
void Host::remove_away() {
|
||||
if (!config_.afk_kick) return;
|
||||
const auto limit = static_cast<std::uint64_t>(config_.afk_kick) * 60000000;
|
||||
std::vector<std::uint64_t> away;
|
||||
for (auto &[id, guest] : guests_) {
|
||||
auto &g = *guest;
|
||||
if (!g.handshaken || !g.world_ready || !g.active_at || is_admin(id)) continue;
|
||||
const auto idle = now_ - g.active_at;
|
||||
if (idle >= limit) away.push_back(id);
|
||||
else if (!g.away_warned && limit > 60000000 && idle >= limit - 60000000) {
|
||||
g.away_warned = true;
|
||||
reply(g, "You have been away a while: move or say something within a minute to stay on the server.");
|
||||
}
|
||||
}
|
||||
for (const auto id : away) {
|
||||
if (const auto *g = find(id)) log_("[afk] " + guest_name(*g) + " was removed after " + std::to_string(config_.afk_kick) + " min away.");
|
||||
drop(id, "You were removed from the server for being away too long. You can join again.");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Objects ---------------------------------------------------------------------------------
|
||||
void Host::sync_objects() {
|
||||
if (now_ < next_object_update_) return;
|
||||
@@ -1519,6 +1551,7 @@ void Host::tick(std::uint64_t now) {
|
||||
roster_dirty_ = true;
|
||||
}
|
||||
tick_rotation();
|
||||
remove_away();
|
||||
std::erase_if(vote_cooldowns_, [&](const auto &entry) { return now_ >= entry.second; });
|
||||
join_backoff_.prune(now_);
|
||||
}
|
||||
|
||||
@@ -128,6 +128,11 @@ class Host {
|
||||
std::unordered_map<std::uint16_t, sound_codec::In> sound_in;
|
||||
Transform still_at;
|
||||
std::uint64_t moved_at{};
|
||||
// When they last did something a player at their game does: moved, spoke, typed in chat
|
||||
// or changed their objects (0 until their game has loaded the map). And whether they have
|
||||
// been told they are about to be removed for being away.
|
||||
std::uint64_t active_at{};
|
||||
bool away_warned{};
|
||||
std::uint64_t pose_arrival{};
|
||||
std::array<PoseDelivery, max_players> pose_delivery;
|
||||
CrowdLimits crowd; // how far the full and half rates reach for them in a crowd
|
||||
@@ -192,6 +197,11 @@ class Host {
|
||||
std::uint64_t vote_shown_until_{};
|
||||
std::uint32_t vote_ids_{};
|
||||
void show_vote(const Vote &vote, std::uint8_t outcome, unsigned yes, unsigned no, unsigned needed);
|
||||
void active(Guest &guest) {
|
||||
guest.active_at = now_;
|
||||
guest.away_warned = false;
|
||||
}
|
||||
void remove_away();
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user