mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 14:15:53 +02:00
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
This commit is contained in:
1 parent
737c1611af
commit
d540b88640
7 files changed
+85
-13
No files matched your search
@@ -457,6 +457,9 @@ std::vector<MultiplayerChatCommand> 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 <player> <message>", "Send a player a private message", "player"});
|
||||
if (s.server_votes & server_vote_map) list.push_back({"/vote map", "/vote map <map>", "Start a vote to change the map", "map"});
|
||||
if (s.server_votes & server_vote_kick)
|
||||
list.push_back({"/vote kick", "/vote kick <player>", "Start a vote to kick a player", "player"});
|
||||
@@ -467,6 +470,9 @@ std::vector<MultiplayerChatCommand> 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 <player> <message>", "Admin: message a player privately", "player"});
|
||||
list.push_back({"/msg-party", "/msg-party <player> <message>", "Admin: message everyone in a player's party", "player"});
|
||||
list.push_back({"/msg-admins", "/msg-admins <message>", "Admin: message the admins who are on"});
|
||||
list.push_back({"/kick", "/kick <player>", "Admin: kick a player until the server restarts", "player"});
|
||||
list.push_back({"/ban", "/ban <player>", "Admin: ban a player", "player"});
|
||||
list.push_back({"/map", "/map <map>", "Admin: change the server's map", "map"});
|
||||
|
||||
+4
-3
@@ -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 [<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>
|
||||
voice on|off voice-range <m>
|
||||
distances <full> <half> <half-return> <low>
|
||||
placement everyone|admins|nobody clear-objects
|
||||
objects <number>|off How many objects each player may have placed.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -163,7 +163,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
};
|
||||
c.port = read_port("port", c.port);
|
||||
c.query_port = read_port("query_port", c.query_port);
|
||||
c.tps = root.value("tps", c.tps);
|
||||
c.tps = dedicated_tps; // not the file's (see ServerConfig::tps)
|
||||
c.voice_chat = root.value("voice_chat", c.voice_chat);
|
||||
c.voice_range = root.value("voice_range", c.voice_range);
|
||||
if (root.contains("distances")) {
|
||||
@@ -274,7 +274,7 @@ std::string config_error(const ServerConfig &c) {
|
||||
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.";
|
||||
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.";
|
||||
|
||||
@@ -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<std::uint64_t> 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;
|
||||
|
||||
+52
-6
@@ -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 <text> | msg <player> <text> | msg-party <player> <text> | msg-admins <text> | kick <player> | ban <player or SteamID64> [name] | unban <SteamID64> | bans\n"
|
||||
"map <name, e.g. San Vansterdam> | maps | name <text> | password <text|off> | welcome <text|off> | listed on|off\n"
|
||||
"tps 20|30|60|120 | voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low> | crowd <n>|off | rate <KB/s>\n"
|
||||
"voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low> | crowd <n>|off | rate <KB/s>\n"
|
||||
"placement everyone|admins|nobody | objects <number>|off | 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"
|
||||
@@ -203,7 +205,32 @@ void Host::send_required(Guest &g, const std::vector<std::uint8_t> &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<std::uint32_t>(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<unsigned>(*value))) return "tps 20|30|60|120";
|
||||
config_.tps = static_cast<unsigned>(*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);
|
||||
|
||||
@@ -79,6 +79,14 @@ class Host {
|
||||
struct PendingCosmetics { Packet packet; std::uint64_t received{}; };
|
||||
std::vector<PendingCosmetics> pending_cosmetics;
|
||||
std::optional<Transform> 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<std::uint8_t> 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<PoseDelivery, max_players> 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();
|
||||
|
||||
Reference in new issue
Block a user