Files
ReSkate/Extension/Multiplayer/Net/delta_codec.h
T
AyeZeeBB c98c3f3207 Keep busy dedicated servers playable; hold stale skaters instead of rebuilding them
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
2026-10-07 14:43:45 -05:00

81 lines
3.5 KiB
C++

#pragma once
#include "wire_codec.h"
#include <map>
#include <tuple>
namespace dingosdk::multiplayer {
using StreamKey = std::pair<std::uint64_t, PacketKind>;
struct WireUpdate {
std::vector<std::uint8_t> bytes, baseline;
bool establishes_baseline() const { return !baseline.empty(); }
};
// Reference-encoded updates one broadcast already built for one packet encoding. A delta
// depends only on that encoding and on the reference snapshot (its sequence and exact
// bytes), so a later recipient holding the same reference gets the same bytes without
// another patch and compression. References point into the senders: keep a cache only
// while no sender records a send (DeltaSender::sent).
struct DeltaCache {
struct Entry {
std::uint32_t sequence{};
const std::vector<std::uint8_t> *reference{};
std::vector<std::uint8_t> bytes;
};
std::vector<Entry> entries;
};
// Every delta references a reliable full snapshot, never the previous delta.
// Failed/skipped sends do not advance the sender's reference state.
class DeltaSender {
struct Base {
std::vector<std::uint8_t> raw;
std::uint64_t epoch{}, world{}, time{}, touched{};
std::uint32_t sequence{};
};
std::map<StreamKey, Base> bases_;
std::uint64_t clock_{};
std::uint64_t refresh_us_ = 2000000;
bool aligned_{};
WireUpdate build(const Packet &, std::span<const std::uint8_t> raw, std::span<const std::uint8_t> wire,
DeltaCache *cache) const;
public:
WireUpdate prepare(const Packet &) const;
WireUpdate prepare(const Packet &, std::span<const std::uint8_t> raw,
std::span<const std::uint8_t> wire) const;
// The same result, reusing (and adding to) deltas built for other recipients of
// exactly this raw/wire encoding.
WireUpdate prepare(const Packet &, std::span<const std::uint8_t> raw, std::span<const std::uint8_t> wire,
DeltaCache &cache) const;
void sent(const Packet &, WireUpdate &&);
// How old a pose or sound reference may get before a whole state replaces it. Each one
// is sent reliably, so Steam resends it until it arrives: a dedicated server, with a
// stream for every pair of players, keeps them rarer than the two seconds a game does.
// `aligned`: on the source's own clock, at each multiple of the time, instead of that
// long after this recipient's last one. Every recipient of a source then takes the same
// packet as its reference, and one patch serves them all (DeltaCache).
void set_refresh(std::uint64_t microseconds, bool aligned = false) {
refresh_us_ = microseconds;
aligned_ = aligned;
}
};
class DeltaReceiver {
struct Base {
std::vector<std::uint8_t> raw;
std::uint64_t epoch{}, world{}, session{}, map{};
std::uint32_t sequence{};
};
struct Stream {
std::deque<Base> bases;
std::uint64_t touched{};
};
std::map<StreamKey, Stream> streams_;
std::uint64_t clock_{};
public:
// Missing references are a recoverable dropped frame, not protocol failure.
std::optional<Packet> receive(std::span<const std::uint8_t>, bool &missing_reference,
std::uint64_t accepted_world = 0) noexcept;
// Whether a delta of this stream against baseline `sequence` of `epoch` could be decoded now.
bool holds(std::uint64_t source, PacketKind kind, std::uint64_t epoch, std::uint32_t sequence) const noexcept;
};
} // namespace dingosdk::multiplayer