From f13995dfe858b911241b46424dff4443c56b45fe Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Thu, 8 Oct 2026 17:12:41 -0500 Subject: [PATCH] Fix players freezing on dedicated servers; reserved slots are extra slots; bone scale limit 2 Fix players freezing on dedicated servers; reserved slots are extra slots; bone scale limit 2 Players stopping and jumping (1.1.6): - The new sound messages begin with the same four bytes ("RMS1") as the pose updates games send each other over their direct links. The game took those pose updates for sound and discarded them, so a directly linked player moved only when a whole state arrived. - Game: only messages from the dedicated server are read as its sound, pose or acknowledgement messages; anything from another player is decoded as what it is. - Server: poses and sound go through the server to every player, whether or not two games have linked to each other. This fixes games still on 1.1.6 without an update, at the cost of more outbound on small servers. Reserved slots: - "reserved_players_slots" no longer holds slots back. Anyone joins until max_players are on; the listed players and the admins can still join after that, each adding one slot beyond the limit (33/32 and so on). - An admin who is also listed counts once, and the total never exceeds what a session can hold. - The list holds up to 1024 players again. "reserved" in the console shows how many extra slots there are. Server: - "bone_scale_limit" is 2 by default. The game's own skater height is a bone scale, so 1 showed everyone at one height. A config that already has 1 written in it keeps it until changed. --- .../Multiplayer/Session/session_receive.cpp | 32 +++++++++++-------- Server/README.txt | 27 ++++++++-------- Server/Test/server_config_tests.cpp | 27 +++++++++------- Server/server_config.cpp | 20 ++++++++---- Server/server_config.h | 19 +++++------ Server/server_host.cpp | 28 +++++++--------- Server/server_host.h | 3 ++ 7 files changed, 86 insertions(+), 70 deletions(-) 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;