mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 17:27:19 +02:00
No protocol change: a server built from this still takes 1.1.4 players. Server: what each player is sent - A crowd limit: each player is sent at most crowd_budget poses a second (default 600, 0 for none; "crowd <n>|off"). Everyone at 5 a second at least, the nearest at the full rate, the next nearest at 10. Before, everyone within full-rate distance was sent at the full rate, which a crowd of 40 turned into more than a connection carries - A skater's sound goes only to players within the full-rate distance, and in a crowd only from the nearest; it went to everyone - A player who joins, or arrives after a map change, is shown the others about seven a second, nearest first, and none while what they were already sent is still on its way. Before, every outfit and every stream went at once (and to everyone at once after a map change) - Whole pose and sound states every 8 seconds instead of 2, on the source's own clock, and slower pose rates sent on that clock too, so recipients share one patch and compression (DeltaCache) instead of one each. The settings survive a map change - Object updates wait for a connection that is behind Server: Steam connections - send_rate (default 900 KB/s, "rate <KB/s>") is set as both Steam's minimum and maximum, for open connections and each new one. With only the maximum raised a connection stayed at Steam's 256 KB/s; set high, what was lost was resent until the connection was full of resends (about 1.1 MB/s reaches a player through the relays) - A 4 MB send buffer, and a refused must-arrive message no longer ends the player's session at once: 15 seconds of refusals do (30 for object updates). A full queue used to drop everyone who was behind together Server: seeing what is going on - "net [player]": every connection's ping, delivery, queue and rate, the traffic by kind (poses, sound, voice, outfits, objects), who uploads most, and how busy the server's loop is - A [network] line in the log every minute - A leave or failed join says how it ended: closed by the player's game with Steam's reason, connection lost, or closed by the server and why - The queue time no longer prints Steam's "unknown" as a number Server: other - Reserved slots: reserved_slots and reserved in ReSkateServer.json, and the "reserved" command; admins always have one - The speed check flags a game at about 1.5x and over (was 1.06x), and tells only that player; for the admins it is in the log Game - A remote skater with no pose for a second stays where it was for up to eight seconds instead of being destroyed and rebuilt three seconds later. On a busy server one slow frame did that to many players at once, and the rebuilding made the next frames slower still - Every waiting message is read each frame (at most 128 were) - On an official server the role tags (Dev, Staff, Homie, Creator) show for every player, not only the few the game is connected to directly; community servers keep the old rule - The server browser searches Steam at most every 15 seconds (45 after an empty answer) and keeps the last list for 15 minutes when Steam returns nothing, up from 2 - The game's own connections send at up to 900 KB/s
208 lines
9.6 KiB
C++
208 lines
9.6 KiB
C++
#include "delta_codec.h"
|
|
#include "pose_delta.h"
|
|
#include "block_codec.h"
|
|
#include <algorithm>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
namespace {
|
|
bool state_kind(PacketKind kind) {
|
|
return kind == PacketKind::pose || kind == PacketKind::audio || kind == PacketKind::cosmetics;
|
|
}
|
|
bool magic(std::span<const std::uint8_t> b, const char *m) {
|
|
return b.size() >= 4 && std::equal(b.begin(), b.begin() + 4, m);
|
|
}
|
|
void put(std::vector<std::uint8_t> &b, std::uint64_t value, unsigned n) {
|
|
for (unsigned i = 0; i < n; ++i)
|
|
b.push_back(static_cast<std::uint8_t>(value >> (8 * i)));
|
|
}
|
|
std::uint64_t get(std::span<const std::uint8_t> b, std::size_t at, unsigned n) {
|
|
std::uint64_t value{};
|
|
for (unsigned i = 0; i < n; ++i)
|
|
value |= std::uint64_t{b[at + i]} << (8 * i);
|
|
return value;
|
|
}
|
|
} // namespace
|
|
WireUpdate DeltaSender::prepare(const Packet &p) const {
|
|
const auto raw = encode(p, true);
|
|
return prepare(p, raw, encode_wire_bytes(raw));
|
|
}
|
|
WireUpdate DeltaSender::prepare(const Packet &p, std::span<const std::uint8_t> raw,
|
|
std::span<const std::uint8_t> wire) const {
|
|
return build(p, raw, wire, nullptr);
|
|
}
|
|
WireUpdate DeltaSender::prepare(const Packet &p, std::span<const std::uint8_t> raw, std::span<const std::uint8_t> wire,
|
|
DeltaCache &cache) const {
|
|
return build(p, raw, wire, &cache);
|
|
}
|
|
WireUpdate DeltaSender::build(const Packet &p, std::span<const std::uint8_t> raw, std::span<const std::uint8_t> wire,
|
|
DeltaCache *cache) const {
|
|
WireUpdate out;
|
|
if (!state_kind(p.kind)) {
|
|
out.bytes.assign(wire.begin(), wire.end());
|
|
return out;
|
|
}
|
|
const auto found = bases_.find({p.source, p.kind});
|
|
if (found == bases_.end() || found->second.epoch != p.epoch || found->second.world != p.world || p.time_us < found->second.time ||
|
|
(p.kind != PacketKind::cosmetics &&
|
|
(aligned_ ? p.time_us / refresh_us_ != found->second.time / refresh_us_ : p.time_us - found->second.time >= refresh_us_))) {
|
|
if (wire.size() + 4 > max_packet)
|
|
return out;
|
|
out.bytes.assign(wire.begin(), wire.end());
|
|
out.bytes.insert(out.bytes.begin(), {'R', 'M', 'B', '1'});
|
|
out.baseline.assign(raw.begin(), raw.end());
|
|
return out;
|
|
}
|
|
const auto &base = found->second;
|
|
if (cache)
|
|
for (const auto &entry : cache->entries)
|
|
if (entry.sequence == base.sequence && *entry.reference == base.raw) {
|
|
out.bytes = entry.bytes;
|
|
return out;
|
|
}
|
|
std::size_t best = wire.size();
|
|
if (p.kind == PacketKind::pose) {
|
|
// Pose patches encode changed fields directly. Avoid also building and
|
|
// compressing a full XOR candidate for every destination peer.
|
|
const auto patch = pose_delta::encode(raw, base.raw);
|
|
if (!patch.empty()) {
|
|
std::vector<std::uint8_t> sparse{'R', 'M', 'S', '1'};
|
|
put(sparse, p.source, 8);
|
|
put(sparse, p.epoch, 8);
|
|
put(sparse, static_cast<unsigned>(p.kind), 2);
|
|
put(sparse, base.sequence, 4);
|
|
put(sparse, raw.size(), 4);
|
|
put(sparse, patch.size(), 4);
|
|
auto compressed = compress_block(patch);
|
|
if (34 + compressed.bytes.size() < best) {
|
|
if (compressed.codec == BlockCodec::zstd) sparse[3] = '2';
|
|
sparse.insert(sparse.end(), compressed.bytes.begin(), compressed.bytes.end());
|
|
out.bytes = std::move(sparse);
|
|
}
|
|
}
|
|
} else {
|
|
std::vector<std::uint8_t> difference(raw.begin(), raw.end());
|
|
for (std::size_t i = 0; i < std::min(raw.size(), base.raw.size()); ++i)
|
|
difference[i] ^= base.raw[i];
|
|
std::vector<std::uint8_t> delta{'R', 'M', 'D', '1'};
|
|
put(delta, p.source, 8);
|
|
put(delta, p.epoch, 8);
|
|
put(delta, static_cast<unsigned>(p.kind), 2);
|
|
put(delta, base.sequence, 4);
|
|
put(delta, raw.size(), 4);
|
|
auto block = compress_block(difference);
|
|
if (30 + block.bytes.size() < best) {
|
|
if (block.codec == BlockCodec::zstd) delta[3] = '2';
|
|
delta.insert(delta.end(), block.bytes.begin(), block.bytes.end());
|
|
out.bytes = std::move(delta);
|
|
}
|
|
}
|
|
if (out.bytes.empty())
|
|
out.bytes.assign(wire.begin(), wire.end());
|
|
if (cache)
|
|
cache->entries.push_back({base.sequence, &base.raw, out.bytes});
|
|
return out;
|
|
}
|
|
void DeltaSender::sent(const Packet &p, WireUpdate &&update) {
|
|
const StreamKey key{p.source, p.kind};
|
|
if (!update.establishes_baseline()) {
|
|
if (auto it = bases_.find(key); it != bases_.end())
|
|
it->second.touched = ++clock_;
|
|
return;
|
|
}
|
|
// A connection has at most seven active remote sources plus its sender.
|
|
// Evict old departed streams without allowing unbounded retained cosmetics.
|
|
if (!bases_.contains(key) && bases_.size() >= max_players * 3) {
|
|
const auto oldest = std::min_element(bases_.begin(), bases_.end(), [](const auto &a, const auto &b) {
|
|
return a.second.touched < b.second.touched;
|
|
});
|
|
bases_.erase(oldest);
|
|
}
|
|
bases_[key] = {std::move(update.baseline), p.epoch, p.world, p.time_us, ++clock_, p.sequence};
|
|
}
|
|
bool DeltaReceiver::holds(std::uint64_t source, PacketKind kind, std::uint64_t epoch,
|
|
std::uint32_t sequence) const noexcept {
|
|
const auto stream = streams_.find({source, kind});
|
|
return stream != streams_.end() &&
|
|
std::any_of(stream->second.bases.begin(), stream->second.bases.end(),
|
|
[&](const auto &b) { return b.epoch == epoch && b.sequence == sequence; });
|
|
}
|
|
std::optional<Packet> DeltaReceiver::receive(std::span<const std::uint8_t> bytes, bool &missing,
|
|
std::uint64_t accepted_world) noexcept {
|
|
missing = false;
|
|
try {
|
|
if (bytes.size() > max_packet)
|
|
return {};
|
|
if (magic(bytes, "RMB1")) {
|
|
const auto raw = decode_wire_bytes(bytes.subspan(4));
|
|
auto p = raw ? decode(*raw) : std::nullopt;
|
|
if (!p || !state_kind(p->kind))
|
|
return {};
|
|
// Late snapshots must not evict references for the current world.
|
|
if (accepted_world && p->world != accepted_world)
|
|
return p;
|
|
const StreamKey key{p->source, p->kind};
|
|
if (!streams_.contains(key) && streams_.size() >= max_players * 3) {
|
|
const auto oldest =
|
|
std::min_element(streams_.begin(), streams_.end(), [](const auto &a, const auto &b) {
|
|
return a.second.touched < b.second.touched;
|
|
});
|
|
streams_.erase(oldest);
|
|
}
|
|
auto &stream = streams_[key];
|
|
stream.touched = ++clock_;
|
|
// Re-encoding a decoded quaternion can change its last quantized bit.
|
|
// Cache the exact packed bytes, never regenerated native transforms.
|
|
if (stream.bases.empty() || stream.bases.back().epoch != p->epoch)
|
|
stream.bases.clear();
|
|
if (std::none_of(stream.bases.begin(), stream.bases.end(),
|
|
[&](const auto &b) { return b.sequence == p->sequence; }))
|
|
stream.bases.push_back({*raw, p->epoch, p->world, p->session, p->map, p->sequence});
|
|
while (stream.bases.size() > 3)
|
|
stream.bases.pop_front();
|
|
return p;
|
|
}
|
|
const bool sparse = magic(bytes, "RMS1") || magic(bytes, "RMS2");
|
|
if (!sparse && !magic(bytes, "RMD1") && !magic(bytes, "RMD2"))
|
|
return decode_wire(bytes);
|
|
if (bytes.size() <= (sparse ? 34U : 30U))
|
|
return {};
|
|
const StreamKey key{get(bytes, 4, 8), static_cast<PacketKind>(get(bytes, 20, 2))};
|
|
const auto epoch = get(bytes, 12, 8), sequence = get(bytes, 22, 4), size = get(bytes, 26, 4);
|
|
if (!state_kind(key.second) || !epoch || size < packet_header_size || size > max_packet || bytes.size() >= size)
|
|
return {};
|
|
const auto stream = streams_.find(key);
|
|
if (stream == streams_.end()) {
|
|
missing = true;
|
|
return {};
|
|
}
|
|
const auto base =
|
|
std::find_if(stream->second.bases.begin(), stream->second.bases.end(),
|
|
[&](const auto &b) { return b.epoch == epoch && b.sequence == sequence; });
|
|
if (base == stream->second.bases.end()) {
|
|
missing = true;
|
|
return {};
|
|
}
|
|
const auto header = sparse ? 34U : 30U;
|
|
const auto unpacked = sparse ? get(bytes, 30, 4) : size;
|
|
if (sparse && (key.second != PacketKind::pose || unpacked < packet_header_size + 6 ||
|
|
unpacked > max_packet + 1024)) return {};
|
|
std::vector<std::uint8_t> raw(static_cast<std::size_t>(unpacked));
|
|
if (!decompress_block(bytes.subspan(header), raw,
|
|
bytes[3] == '2' ? BlockCodec::zstd : BlockCodec::lz4)) return {};
|
|
if (sparse)
|
|
raw = pose_delta::decode(raw, base->raw, static_cast<std::size_t>(size));
|
|
else
|
|
for (std::size_t i = 0; i < std::min(raw.size(), base->raw.size()); ++i)
|
|
raw[i] ^= base->raw[i];
|
|
auto p = decode(raw);
|
|
if (!p || p->source != key.first || p->kind != key.second || p->epoch != epoch ||
|
|
p->world != base->world || p->session != base->session || p->map != base->map)
|
|
return {};
|
|
stream->second.touched = ++clock_;
|
|
return p;
|
|
} catch (...) {
|
|
return {};
|
|
}
|
|
}
|
|
} // namespace dingosdk::multiplayer
|