diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index 1e45f8f6..2eea1c5e 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -206,6 +206,8 @@ std::vector stale_pose_deltas(Session &s, const std::vector streams; for (auto i = messages.size(); i-- > 0;) { const auto &bytes = messages[i].bytes; + // (A dedicated server sends none of these: what it sends that starts the same is sound.) + if (dedicated_host(s) && messages[i].peer == s.host_id) continue; // Sparse pose patches (delta_codec.cpp): "RMS1"/"RMS2", source at 4, kind at 20. if (bytes.size() <= 34 || bytes[0] != 'R' || bytes[1] != 'M' || bytes[2] != 'S' || (bytes[3] != '1' && bytes[3] != '2')) @@ -505,19 +507,23 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { } if (stale[index]) continue; - // The server saying which of this game's own pose messages it read. - if (const auto ack = pose_batch::Ack::read(message.bytes)) { - if (s.mode == Mode::join && !direct_link && dedicated_host(s)) s.pose_upload.ack(*ack); - continue; - } - if (sound_codec::is_sound(message.bytes)) { - if (s.mode == Mode::join && !direct_link && dedicated_host(s)) batch = unpack_sound(s, message.bytes); - if (batch.empty()) continue; - } - if (pose_batch::is_batch(message.bytes)) { - // Only a dedicated server sends these, and only to its players. - if (s.mode == Mode::join && !direct_link && dedicated_host(s)) batch = unpack_poses(s, message.bytes); - if (batch.empty()) continue; + // A dedicated server's own messages (pose_batch.h, sound_codec.h). Only what comes + // from the server is read as one: sound_codec's first four bytes are also those of + // the pose updates games send each other directly (delta_codec's "RMS1"), which + // must go on to be decoded as what they are. + if (s.mode == Mode::join && !direct_link && dedicated_host(s)) { + // The server saying which of this game's own pose messages it read. + if (const auto ack = pose_batch::Ack::read(message.bytes)) { + s.pose_upload.ack(*ack); + continue; + } + if (sound_codec::is_sound(message.bytes)) { + batch = unpack_sound(s, message.bytes); + if (batch.empty()) continue; + } else if (pose_batch::is_batch(message.bytes)) { + batch = unpack_poses(s, message.bytes); + if (batch.empty()) continue; + } } } bool missing_reference{}; diff --git a/Server/README.txt b/Server/README.txt index 1ea22487..4feebf03 100644 --- a/Server/README.txt +++ b/Server/README.txt @@ -72,14 +72,13 @@ activity_log Log what players do (default true): throwdown drops placed, admins SteamID64s (as strings) who may change settings in-game. reserved_players_slots SteamID64s of the players with a reserved slot, e.g. - ["76561198000000000"] (default none). A slot is kept free - for each of them who is not on: with 64 players and 4 - listed, none of them on, anyone can join until 60 are on - and only they can take the last 4. One who is on has taken - theirs. The browser still shows 64, and everyone else is - told the remaining slots are reserved. Admins may take a - kept slot too, but none is kept for them. Must list fewer - players than max_players. + ["76561198000000000"] (default none). They can join when + the server is full, as the admins always can: each of them + adds a slot beyond max_players that only they use. With 32 + players and 2 admins and 2 listed, anyone can join until + 32 are on, and those four can still join after that, up to + 36. The browser then shows 33/32 and so on. Nothing is + held back from everyone else. use_global_bans Turn away players the ReSkate team has banned from multiplayer (default true). The list is read from api.reskate.dev at startup and every ten minutes. false lets them in; the server's own @@ -146,10 +145,12 @@ enforce_tuning Players skate with the game's own Gameplay/SkatePhysicsTuning everyone. bone_scale_limit How far a mod may resize part of a skater for the other players (a "big head" mod and the like): the most a bone may - be scaled, 1 to 8. 1 (default) shows every skater at the - game's own proportions; 0 is no limit. The player with the - mod still sees it on their own screen. Console: - bone-scale <1-8>|off. + be scaled, 1 to 8 (default 2); 0 is no limit. The game's + own skater height is a scale too, so 1 shows every skater + at the same height and build as well as stopping mods; 2 + leaves height alone and still halves the largest heads. + The player with the mod still sees it on their own screen. + Console: bone-scale <1-8>|off. "network" - How players connect and how much they are sent. The defaults suit most servers. use_steam_relay How players reach the server: true (default) or false. @@ -233,7 +234,7 @@ and change voice, distances, placement and kicks from the Multiplayer menu. help A short list of every command. status Name, map, players, code. players Connected players and their SteamID64s. - reserved Who has a reserved slot. + reserved Who has a reserved slot, and how many extra slots there are. reserved add|remove (console only) net [player] How the connections are doing right now: traffic, queues, the server's own loop timing, and the twelve diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index a485a3d6..2a7e331c 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -1,5 +1,6 @@ // A config written by an older server gains the settings added since, keeping its own values. #include "Server/server_config.h" +#include "Extension/Multiplayer/Session/peer_slots.h" #include #include #include @@ -68,7 +69,7 @@ int run() { ServerConfig rate; rate.send_rate = 64; check(config_error(rate).find("send_rate") != std::string::npos, "A send rate too low to play with was accepted"); - check(has("anti_cheat.bone_scale_limit") && config.bone_scale_limit == 1, "bone_scale_limit is not a new setting of 1"); + check(has("anti_cheat.bone_scale_limit") && config.bone_scale_limit == 2, "bone_scale_limit is not a new setting of 2"); ServerConfig scaled; scaled.bone_scale_limit = 0.5f; check(config_error(scaled).find("bone_scale_limit") != std::string::npos, "A bone scale limit under 1 was accepted"); @@ -79,25 +80,29 @@ int run() { crowd.crowd_budget = 50; check(config_error(crowd).find("crowd_budget") != std::string::npos, "A crowd budget too small to play with was accepted"); } - // Reserved slots: none unless set; the last of them only the listed players and admins take. + // Reserved slots: beyond max_players, for the listed players and the admins only. { check(has("access.reserved_players_slots") && config.reserved.empty(), "Reserved slots are not a new setting that reserves nothing"); ServerConfig slots; slots.max_players = 4; const std::uint64_t vip = 76561198000000001ULL, admin = 76561198000000002ULL, anyone = 76561198000000003ULL; - check(may_join(slots, anyone, 3, 0) && !may_join(slots, anyone, 4, 0) && !may_join(slots, vip, 4, 0), "A server with nothing reserved did not fill up plainly"); - // A slot for each reserved player who is not on; one who is on has taken theirs. + check(may_join(slots, anyone, 3) && !may_join(slots, anyone, 4) && !may_join(slots, vip, 4) && extra_slots(slots) == 0, + "A server with nothing reserved did not fill up plainly"); + // Nothing is held back from anyone; the listed players and the admins have a slot each past the last. const std::uint64_t vip2 = 76561198000000004ULL; slots.reserved = {vip, vip2}; - slots.admins = {admin}; - check(may_join(slots, anyone, 1, 0) && !may_join(slots, anyone, 2, 0) && !may_join(slots, anyone, 3, 0), "Reserved slots were given to anyone"); - check(may_join(slots, anyone, 2, 1) && !may_join(slots, anyone, 3, 1) && may_join(slots, anyone, 3, 2), "A slot was kept for a reserved player who is already on"); - check(may_join(slots, vip, 2, 0) && may_join(slots, vip, 3, 1) && may_join(slots, admin, 3, 0), "A reserved player or an admin was kept out of a reserved slot"); - check(!may_join(slots, vip, 4, 1) && !may_join(slots, admin, 4, 2), "A full server let someone in"); + slots.admins = {admin, vip}; // listed twice: one slot + check(extra_slots(slots) == 3, "Extra slots were not one for each reserved player and admin"); + check(may_join(slots, anyone, 3) && !may_join(slots, anyone, 4) && !may_join(slots, anyone, 6), "An extra slot was given to anyone, or a slot was held back"); + check(may_join(slots, vip, 4) && may_join(slots, admin, 5) && may_join(slots, vip2, 6), "A reserved player or an admin was kept out of a full server"); + check(!may_join(slots, vip, 7) && !may_join(slots, admin, 7), "More joined than there are extra slots"); + ServerConfig brim; + brim.max_players = static_cast(dingosdk::multiplayer::max_players) - 2; + brim.reserved = {vip, vip2}; + check(extra_slots(brim) == 1 && may_join(brim, vip, brim.max_players) && !may_join(brim, vip2, brim.max_players + 1), + "Extra slots went past what a session can hold"); const auto refused = [](const ServerConfig &c) { return config_error(c).find("reserved") != std::string::npos; }; check(!refused(slots), "Valid reserved slots were refused"); - slots.reserved = {vip, vip2, 76561198000000005ULL, 76561198000000006ULL}; - check(refused(slots), "A server with every slot reserved was accepted"); slots.reserved = {vip, 42}; check(refused(slots), "A reserved entry that is not a player was accepted"); auto kept = reloaded; diff --git a/Server/server_config.cpp b/Server/server_config.cpp index e0512782..6a514563 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -354,20 +354,26 @@ std::optional parse_scoring(std::string_view text) { if (!value) return std::nullopt; return value; } -bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on, std::size_t reserved_on) noexcept { - if (on >= config.max_players) return false; +std::size_t extra_slots(const ServerConfig &config) noexcept { + // (An admin who is also listed has one slot, not two.) + std::size_t slots = config.reserved.size(); + for (const auto id : config.admins) + if (std::find(config.reserved.begin(), config.reserved.end(), id) == config.reserved.end()) ++slots; + // Never more players than a session can hold, the server itself being one of them. + const std::size_t room = multiplayer::max_players - 1; + return config.max_players >= room ? 0 : std::min(slots, room - config.max_players); +} +bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on) noexcept { + if (on < config.max_players) return true; const auto listed = [&](const std::vector &ids) { return std::find(ids.begin(), ids.end(), id) != ids.end(); }; - if (listed(config.reserved) || listed(config.admins)) return true; - const auto kept = config.reserved.size() - std::min(reserved_on, config.reserved.size()); - return on + kept < config.max_players; + return (listed(config.reserved) || listed(config.admins)) && on < config.max_players + extra_slots(config); } std::string config_error(const ServerConfig &c) { using namespace multiplayer; if (!valid_server_name(c.name)) return std::string("name must be ") + server_name_rule + "."; for (const auto id : c.reserved) if (!individual_steam_id(id)) return "reserved_players_slots must be SteamID64s (17 digits starting 7656119)."; - if (c.reserved.size() >= c.max_players) - return "reserved_players_slots must list fewer players than max_players: a slot is kept for each, so nobody else could join."; + if (c.reserved.size() > 1024) return "reserved_players_slots holds at most 1024 players."; if (c.send_rate < 128 || c.send_rate > 16384) return "send_rate must be 128 to 16384 (KB/s for each player)."; if (c.bone_scale_limit != 0 && !(c.bone_scale_limit >= 1.f && c.bone_scale_limit <= 8.f)) return "bone_scale_limit must be 0 (no limit) or 1 to 8 (1: no resized body parts at all)."; diff --git a/Server/server_config.h b/Server/server_config.h index c6caca25..58d3ce15 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -40,9 +40,9 @@ struct ServerConfig { // The most poses a second one player is sent (crowd_limits); 0: no limit. unsigned crowd_budget = crowd_pose_budget; // The most a mod may resize part of a skater for the other players, as a factor (and its - // inverse the least): 1, the default, shows every skater at the game's own proportions; - // 0 is no limit. - float bone_scale_limit = 1; + // inverse the least). The game's own skater height is a scale as well, so 1 shows every + // skater at one height and build; 2, the default, leaves height alone. 0 is no limit. + float bone_scale_limit = 2; // How players reach the server: true, through Steam's relay network only; false, straight // to `port` (UDP). A direct server still answers through the relays, for a player the port // does not reach, one who has turned direct connections off, or an older game. @@ -58,7 +58,7 @@ struct ServerConfig { unsigned finger_distance = 25; // What the server may send each player, in KB/s (128-16384). unsigned send_rate = 900; - // The players with a reserved slot: one is kept free for each of them who is not on. + // The players with a reserved slot: they can join a full server, as the admins can. std::vector reserved; std::string password; // empty: anyone may join std::string welcome; // sent to each player as they join @@ -117,11 +117,12 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector(config_.distances.half_rate_start); if (distance > p.crowd.half || (config_.distances.valid() && distance > reach * reach)) continue; } - // Frequent gameplay streams skip the server once the receiver confirms a direct route. - if (source && (packet.kind == PacketKind::pose || packet.kind == PacketKind::audio) && - !needs_relay(p.direct_routes, p.route_reported, source->member, now_)) - continue; + // Poses and sound go through the server to everyone, whether or not two games have linked + // to each other: what they send each other directly is the older, larger format, and + // games of 1.1.6 as first released discard most of it (they took its first four bytes + // for sound_codec's). // Sound is sent with everyone else's they hear, once a pass (flush_poses). if (packet.kind == PacketKind::audio) { if (!sound) sound = std::make_shared>(packet.audio); @@ -1398,13 +1398,8 @@ void Host::tick(std::uint64_t now) { continue; } if (!guest) { - const auto reserved_on = static_cast(std::count_if( - config_.reserved.begin(), config_.reserved.end(), [&](std::uint64_t id) { return guests_.contains(id); })); - if (!individual_steam_id(link.id) || !may_join(config_, link.id, guests_.size(), reserved_on)) { - // Full for them: for everyone, or with only the reserved slots left. - transport_.disconnect(link.id, guests_.size() >= config_.max_players || !individual_steam_id(link.id) - ? "The server is full." - : "The server is full: the slots that are left are reserved."); + if (!individual_steam_id(link.id) || !may_join(config_, link.id, guests_.size())) { + transport_.disconnect(link.id, "The server is full."); continue; } auto created = std::make_unique(); @@ -2118,8 +2113,8 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { if (!console) return "Only the server console manages reserved slots."; const auto [sub, who] = split(argument); if (sub.empty()) { - std::string text = std::to_string(config_.reserved.size()) + " of " + std::to_string(config_.max_players) + - " slots are reserved, one for each of these players while they are not on"; + std::string text = std::to_string(extra_slots(config_)) + " extra slots beyond the " + std::to_string(config_.max_players) + + ": the admins and these players can join when the server is full"; for (const auto id : config_.reserved) { const auto *guest = find(id); text += "\n " + std::to_string(id) + (guest ? " " + guest_name(*guest) : std::string{}); @@ -2131,10 +2126,9 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { if (!individual_steam_id(id) || (sub != "add" && sub != "remove")) return "reserved | reserved add|remove "; const bool listed = std::find(config_.reserved.begin(), config_.reserved.end(), id) != config_.reserved.end(); if (sub == "add") { - if (!listed && config_.reserved.size() + 1 >= config_.max_players) - return "Every slot but one is reserved already: a slot is kept for each reserved player."; + if (!listed && config_.reserved.size() >= 1024) return "The reserved list is full."; if (!listed) config_.reserved.push_back(id); - return changed(std::to_string(id) + " has a reserved slot."); + return changed(std::to_string(id) + " has a reserved slot: they can join when the server is full."); } std::erase(config_.reserved, id); return changed(std::to_string(id) + " no longer has a reserved slot."); diff --git a/Server/server_host.h b/Server/server_host.h index f65a9162..a67e885c 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -243,6 +243,9 @@ class Host { Guest *find(std::uint64_t id); Packet packet(PacketKind kind, std::uint64_t now); unsigned capacity() const { return config_.max_players + 1; } + // What the connection layer is opened for: every extra slot there could be as well, since + // the reserved players and admins change while the server runs. Who gets in is may_join's. + unsigned connection_capacity() const { return static_cast(multiplayer::max_players); } std::string guest_name(const Guest &) const; std::string player_name(std::string_view wanted, std::uint64_t id) const; bool is_admin(std::uint64_t id) const;