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:
AyeZeeBB committed 2026-10-07 15:27:05 -05:00
1 parent 737c1611af
commit d540b88640
7 files changed
+85 -13

No files matched your search

+52 -6
View File
@@ -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);