From d540b8864006997e22453cde2ebe9dbf05d7d9e4 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Wed, 7 Oct 2026 15:27:05 -0500 Subject: [PATCH] Dedicated servers run at 20 TPS; still players at the low rate; message commands in the "/" menu Server - Dedicated servers run at 20 network updates a second, fixed for now: whatever tps says in ReSkateServer.json is read as 20, and the tps command no longer changes it. A busy server's traffic, in and out, is its players' poses, and 20 a second is a third less than 30. Lobbies keep their own choice - A player who has not moved or turned for three seconds is sent on at five poses a second; the full rate is back with their first movement - "net" shows what a pose costs as sent, and what it would cost against a reference renewed every second, every quarter second, and against the pose before, measured on every fourth pose that arrives. It only measures: a nearer reference packs smaller, and this says by how much before the server is made to send them that way Game - The "/" menu on a dedicated server lists /w for everyone, and /msg, /msg-party and /msg-admins for admins. The server already answered them; the menu is the game's own list and never had them --- .../Multiplayer/Session/session_view.cpp | 6 ++ Server/README.txt | 7 ++- Server/Test/server_config_tests.cpp | 2 +- Server/server_config.cpp | 4 +- Server/server_config.h | 6 +- Server/server_host.cpp | 58 +++++++++++++++++-- Server/server_host.h | 15 +++++ 7 files changed, 85 insertions(+), 13 deletions(-) diff --git a/Extension/Multiplayer/Session/session_view.cpp b/Extension/Multiplayer/Session/session_view.cpp index 7644e761..7eff34d6 100644 --- a/Extension/Multiplayer/Session/session_view.cpp +++ b/Extension/Multiplayer/Session/session_view.cpp @@ -457,6 +457,9 @@ std::vector chat_commands(const Session &s) { list.push_back({"/party", "/party", "Who is in your party"}); } if (dedicated_host(s)) { + // The server's own commands (server_votes.cpp, server_host.cpp): it answers them, and + // this list is only what the "/" menu offers, so one left out here still works unseen. + list.push_back({"/w", "/w ", "Send a player a private message", "player"}); if (s.server_votes & server_vote_map) list.push_back({"/vote map", "/vote map ", "Start a vote to change the map", "map"}); if (s.server_votes & server_vote_kick) list.push_back({"/vote kick", "/vote kick ", "Start a vote to kick a player", "player"}); @@ -467,6 +470,9 @@ std::vector chat_commands(const Session &s) { list.push_back({"/no", "/no", "Vote no in the running vote"}); } if (s.server_admin) { + list.push_back({"/msg", "/msg ", "Admin: message a player privately", "player"}); + list.push_back({"/msg-party", "/msg-party ", "Admin: message everyone in a player's party", "player"}); + list.push_back({"/msg-admins", "/msg-admins ", "Admin: message the admins who are on"}); list.push_back({"/kick", "/kick ", "Admin: kick a player until the server restarts", "player"}); list.push_back({"/ban", "/ban ", "Admin: ban a player", "player"}); list.push_back({"/map", "/map ", "Admin: change the server's map", "map"}); diff --git a/Server/README.txt b/Server/README.txt index 170323ac..dd5a177f 100644 --- a/Server/README.txt +++ b/Server/README.txt @@ -63,7 +63,7 @@ send_rate The most the server sends one player, in KB/s (default 900, crowd_budget The most position updates a second one player is sent (default 600, 0 for no limit). Players near each other are sent at the full rate (tps); this only matters once more are - in one place than budget / tps, about 20 at 30 TPS. Then the + in one place than budget / tps, about 30 at 20 TPS. Then the nearest stay at the full rate and the farthest of the crowd drop to 10 and 5 a second, instead of everyone's connection filling up. About 1000 is what reaches a player through @@ -135,7 +135,8 @@ activity_log Log what players do (default true): throwdown drops placed, joins, starts, turns and results; objects placed or removed; how long players take to load. port, query_port Steam game server ports (default 27015, 27016). -tps Network updates per second: 20, 30, 60 or 120. +tps Network updates per second. Fixed at 20 on dedicated servers + for now: whatever is set here is read as 20. voice_chat Allow voice chat. voice_range How far proximity voice reaches, 50-1000 m. distances When far-away players update less often (metres). @@ -193,7 +194,7 @@ and change voice, distances, placement and kicks from the Multiplayer menu. rotation uses (see map_pool). rotation [|off] Change the map on a timer (see map_rotation_minutes). name password welcome listed on|off - tps 20|30|60|120 voice on|off voice-range + voice on|off voice-range distances placement everyone|admins|nobody clear-objects objects |off How many objects each player may have placed. diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index f518de44..2312f0f2 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -42,7 +42,7 @@ int run() { const auto written = text(file); check(written.find("\"enforce_tuning\"") != std::string::npos && written.find("\"seconds\"") != std::string::npos, "New settings not written into the file"); - check(config.name == "Old Server" && config.tps == 60 && !config.boosts && config.votes.map.enabled && + check(config.name == "Old Server" && config.tps == dedicated_tps /* fixed: the file's 60 is not kept */ && !config.boosts && config.votes.map.enabled && config.votes.map.percent == 60 && config.enforce_tuning && config.global_bans, "The file's own values or the new defaults were lost"); const auto reloaded = load_config(file); diff --git a/Server/server_config.cpp b/Server/server_config.cpp index adcbc36e..3c64b41d 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -163,7 +163,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector 64) return "password must be at most 64 characters."; if (!c.welcome.empty() && !valid_chat_text(c.welcome)) return "welcome must be one chat line (at most 200 bytes)."; - if (!valid_multiplayer_tps(c.tps)) return "tps must be 20, 30, 60 or 120."; + if (c.tps != dedicated_tps) return "tps is " + std::to_string(dedicated_tps) + " on dedicated servers."; if (!valid_voice_range(c.voice_range)) return "voice_range must be 50 to 1000."; if (!valid_object_limit(c.object_limit)) return "object_limit must be 0 (no limit) to " + std::to_string(max_object_limit) + "."; if (!c.distances.valid()) return "distances must be ordered: full_rate_return < half_rate_start <= half_rate_return < low_rate_start <= 10000."; diff --git a/Server/server_config.h b/Server/server_config.h index a257a963..3bb75025 100644 --- a/Server/server_config.h +++ b/Server/server_config.h @@ -27,6 +27,7 @@ struct VoteSettings { }; // ReSkateServer.json. Every setting an admin or the console changes is saved // back, so a restart keeps it. +inline constexpr unsigned dedicated_tps = 20; struct ServerConfig { std::filesystem::path file; std::string name = "ReSkate server"; @@ -70,7 +71,10 @@ struct ServerConfig { // everyone installs). Each player's fingerprint is in the console when they are flagged. std::vector score_allow; std::uint16_t port = 27015, query_port = 27016; - unsigned tps = multiplayer_default_tps; + // Fixed for dedicated servers for now (dedicated_tps): a busy one's traffic, in and out, + // is its players' poses, and at 20 a second that is a third less than at 30. Whatever + // the file says is read as this; lobbies keep their own choice. + unsigned tps = dedicated_tps; bool voice_chat = true; float voice_range = default_voice_range; MultiplayerDistances distances; diff --git a/Server/server_host.cpp b/Server/server_host.cpp index dee1ffa2..3dd4bf09 100644 --- a/Server/server_host.cpp +++ b/Server/server_host.cpp @@ -1,6 +1,8 @@ #include "server_host.h" #include "server_text.h" #include "Extension/Multiplayer/Net/wire_codec.h" +#include "Extension/Multiplayer/Net/block_codec.h" +#include "Extension/Multiplayer/Net/pose_delta.h" #include "Extension/Multiplayer/developer_identity.h" #include "Extension/Multiplayer/Session/monotonic_clock.h" #include "Engine/Core/Text/word_filter.h" @@ -44,7 +46,7 @@ std::uint64_t nonce() { constexpr std::string_view help_text = "status | net [player] | players | say | msg | msg-party | msg-admins | kick | ban [name] | unban | bans\n" "map | maps | name | password | welcome | listed on|off\n" - "tps 20|30|60|120 | voice on|off | voice-range <50-1000> | distances | crowd |off | rate \n" + "voice on|off | voice-range <50-1000> | distances | crowd |off | rate \n" "placement everyone|admins|nobody | objects |off | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n" "tpall [player] | tphere | votes [map|kick|tod on|off|] | vote-cancel\n" "map-pool [add|remove |clear] | rotation [|off]\n" @@ -203,7 +205,32 @@ void Host::send_required(Guest &g, const std::vector &bytes) { if (!p || !send_packet(g, *p, true, false)) transport_.disconnect(g.member.id, "Cannot deliver required session data. Join again."); } +void Host::measure_pose(Guest &from, const Packet &packet) { + auto raw = encode(packet, true); + // As DeltaSender would send it: the patch packed, behind its 34 byte header. + const auto cost = [&](const Guest::Earlier &reference) -> std::uint64_t { + if (reference.raw.empty()) return 0; + const auto patch = pose_delta::encode(raw, reference.raw); + return patch.empty() ? 0 : 34 + compress_block(patch).bytes.size(); + }; + if (packet.sequence % 4 == 0) { + const auto last = cost(from.pose_last), quarter = cost(from.pose_quarter), second = cost(from.pose_second); + if (last && quarter && second) { + ++pose_sizes_.samples; + pose_sizes_.whole += encode_wire_bytes(raw).size() + 4; + pose_sizes_.last += last; + pose_sizes_.quarter += quarter; + pose_sizes_.second += second; + } + } + // Each kept reference is replaced once it is its age old, as a whole state sent that often would be. + for (auto [kept, age] : {std::pair{&from.pose_quarter, 250000ULL}, std::pair{&from.pose_second, 1000000ULL}}) + if (kept->raw.empty() || packet.time_us < kept->time || packet.time_us - kept->time >= age) *kept = {raw, packet.time_us}; + from.pose_last = {std::move(raw), packet.time_us}; +} void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint64_t except) { + if (packet.kind == PacketKind::pose) + if (auto *from = find(packet.source)) measure_pose(*from, packet); const auto *source = find(packet.source); // Recipients holding the same delta reference share one patch and compression // (DeltaCache): every update is built first, then all are sent and recorded. @@ -272,6 +299,9 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 } else { delivery->by_distance = 0; } + // Standing still for a few seconds: five poses a second carry it to anyone. The + // rate is back at once when they move (the change of rate sends the next pose). + if (source && source->moved_at && now_ - source->moved_at > 3000000) interval = std::max(interval, 200000); if (delivery->interval_us != interval) delivery->next_source_time = 0; delivery->interval_us = interval; if (interval > multiplayer_pose_interval(config_.tps) && packet.time_us < delivery->next_source_time) continue; @@ -299,6 +329,10 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6 counted->total.out[traffic_kind(sending.kind)] += out.update.bytes.size(); counted->total.snapshots += whole; } + if (sending.kind == PacketKind::pose && !whole) { + ++pose_sizes_.sent; + pose_sizes_.sent_bytes += out.update.bytes.size(); + } out.guest->sender.sent(sending, std::move(out.update)); } } @@ -544,6 +578,16 @@ bool Host::accept_data(Guest &source, const Packet &p) { if (p.kind == PacketKind::pose) { source.pose_arrival = now_; source.latest_root = p.pose.root; + { + const auto &root = p.pose.root, &was = source.still_at; + float moved{}, facing{}; + for (unsigned i = 0; i < 3; ++i) moved += (root.position[i] - was.position[i]) * (root.position[i] - was.position[i]); + for (unsigned i = 0; i < 4; ++i) facing += root.rotation[i] * was.rotation[i]; + if (!source.moved_at || moved > 0.05f * 0.05f || std::abs(facing) < 0.9995f) { + source.still_at = root; + source.moved_at = now_; + } + } return check_speed(source, p.time_us); // last: a speed-check kick frees `source` } return true; @@ -942,6 +986,12 @@ std::string Host::network_report(std::string_view player, bool console) { if (!console) return text; text += "\nloop: " + loop_report(); text += "\n" + by_kind(traffic_); + if (pose_sizes_.samples && pose_sizes_.sent) { + const auto each = [&](std::uint64_t sum) { return std::to_string(sum / pose_sizes_.samples) + " B"; }; + text += "\npose size: " + std::to_string(pose_sizes_.sent_bytes / pose_sizes_.sent) + " B as sent (whole " + each(pose_sizes_.whole) + + "); it would be " + each(pose_sizes_.second) + " against a reference renewed every second, " + each(pose_sizes_.quarter) + + " every quarter second, " + each(pose_sizes_.last) + " against the pose before (" + std::to_string(pose_sizes_.samples) + " measured)"; + } { // Who uploads most, and what most of it is: one player's outfit or objects can cost // every other player's connection. @@ -1507,11 +1557,7 @@ std::string Host::command(std::string_view line, std::uint64_t admin) { return changed(*value ? "The server is listed in the server browser." : "The server is hidden; players need the code."); } if (name == "tps") { - const auto value = number(argument); - if (!value || !valid_multiplayer_tps(static_cast(*value))) return "tps 20|30|60|120"; - config_.tps = static_cast(*value); - for (auto &[id, guest] : guests_) guest->pose_delivery = {}; - return changed("Network updates set to " + std::to_string(config_.tps) + " TPS."); + return "Dedicated servers run at " + std::to_string(dedicated_tps) + " TPS for now; it cannot be changed."; } if (name == "voice") { const auto value = on_off(argument); diff --git a/Server/server_host.h b/Server/server_host.h index a354c9be..4d35af4b 100644 --- a/Server/server_host.h +++ b/Server/server_host.h @@ -79,6 +79,14 @@ class Host { struct PendingCosmetics { Packet packet; std::uint64_t received{}; }; std::vector pending_cosmetics; std::optional latest_root; + // Where they last stood and faced, and when they last moved from it: a player standing + // still (in a menu, away from the keyboard, watching) is sent on at the low rate. + // Their earlier poses, as encoded: the one before, and ones kept a quarter of a second + // and a second (measure_pose). + struct Earlier { std::vector raw; std::uint64_t time{}; }; + Earlier pose_last, pose_quarter, pose_second; + Transform still_at; + std::uint64_t moved_at{}; std::uint64_t pose_arrival{}; std::array pose_delivery; CrowdLimits crowd; // how far the full and half rates reach for them in a crowd @@ -165,6 +173,13 @@ class Host { } loop_, last_loop_, worst_loop_; std::uint64_t pass_started_{}; Counted traffic_; + // What a pose costs to send as a difference from references of different ages, measured on + // every fourth pose that arrives (net). A nearer reference changes less, so it packs + // smaller: this says by how much, before the server is made to send them that way. + struct PoseSizes { + std::uint64_t samples{}, whole{}, last{}, quarter{}, second{}, sent{}, sent_bytes{}; + } pose_sizes_; + void measure_pose(Guest &from, const Packet &packet); std::uint64_t traffic_mark_{}, traffic_window_us_{}; // when `mark` was taken, and how long `last` covers void meet_later(Guest &guest); void introduce();