mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 00:27:06 +02:00
312 lines
14 KiB
C++
312 lines
14 KiB
C++
#pragma once
|
|
#include "Extension/Multiplayer/Remote/cosmetics.h"
|
|
#include "Extension/Multiplayer/Session/peer_slots.h"
|
|
#include "Extension/Multiplayer/Session/object_state.h"
|
|
#include "Extension/Multiplayer/Remote/audio_state.h"
|
|
#include "Extension/Multiplayer/Voice/voice_state.h"
|
|
#include "Engine/Game/Multiplayer/distance_settings.h"
|
|
#include "Engine/Game/Multiplayer/object_placement.h"
|
|
#include "Engine/Game/Multiplayer/session_model.h"
|
|
#include "Engine/Game/Multiplayer/tick_settings.h"
|
|
#include "Engine/Game/World/park_rotation.h"
|
|
#include "Engine/Game/World/world_layers.h"
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <deque>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace dingosdk::multiplayer {
|
|
constexpr std::size_t max_skater_bones = 512, max_board_bones = 64;
|
|
constexpr std::size_t max_packet = 24576;
|
|
constexpr std::size_t packet_header_size = 64;
|
|
constexpr std::uint16_t protocol_version = 38;
|
|
constexpr std::size_t max_throwdown_message = 4096;
|
|
// Packet::tuning: the host's SkatePhysicsTuning differences (Extension/Skater/physics_tuning.h).
|
|
constexpr std::size_t max_physics_tuning = 16384;
|
|
// Votes a dedicated server runs (Packet::server_votes).
|
|
constexpr std::uint8_t server_vote_map = 1, server_vote_kick = 2, server_vote_time = 4;
|
|
enum class PacketKind : std::uint16_t {
|
|
hello = 1,
|
|
welcome = 2,
|
|
pose = 3,
|
|
away = 4,
|
|
cosmetics = 5,
|
|
audio = 6,
|
|
roster = 7,
|
|
challenge = 11,
|
|
routes = 12,
|
|
peer_hello = 13,
|
|
peer_welcome = 14,
|
|
map_request = 15,
|
|
map_offer = 16,
|
|
world_state = 17,
|
|
world_ready = 18,
|
|
objects = 19,
|
|
voice = 20,
|
|
chat = 21,
|
|
// A dedicated server's admin asking it to change a setting, and the
|
|
// server's answer: one line of text either way (valid_admin_text).
|
|
admin = 22,
|
|
// A dedicated server's ban list, sent to its admins (newest first, capped).
|
|
bans = 23,
|
|
// The levels a dedicated server's admins may switch it to (level assets).
|
|
maps = 24,
|
|
// One throwdown message (Extension/Throwdowns/throwdown_wire.h), opaque here:
|
|
// relayed through the host or dedicated server like chat.
|
|
throwdown = 25,
|
|
// The host or dedicated server sending one player somewhere (tpall, tphere).
|
|
teleport = 26,
|
|
// The host's skate physics tuning, as differences from the game's own (opaque here;
|
|
// empty = the game's). Only the host sends it; a dedicated server never needs to.
|
|
physics_tuning = 27,
|
|
// A party request to a dedicated server, or its notice to one player (PartyAction).
|
|
party = 28,
|
|
// A player telling the host or dedicated server how its mods change trick scoring
|
|
// (Engine/Vfs/mod_scoring.h): sent once known, again whenever it changes.
|
|
scoring = 29
|
|
};
|
|
// Packet::party_action. Requests go from a player to the dedicated server (party_player =
|
|
// the other player involved, 0 for leave/open/close); invited and withdrawn go from the
|
|
// server to the invitee (party_player = the inviter). The server answers everything else
|
|
// with a chat line and the next roster.
|
|
enum class PartyAction : std::uint8_t {
|
|
invite = 1, // invite party_player into the sender's party
|
|
accept = 2, // accept party_player's invite
|
|
decline = 3, // decline party_player's invite
|
|
join = 4, // join party_player's party (open, or invited)
|
|
leave = 5, // leave the sender's party
|
|
kick = 6, // the leader removes party_player
|
|
promote = 7, // the leader hands the lead to party_player
|
|
open = 8, // the leader lets anyone join
|
|
close = 9, // the leader makes the party invite-only
|
|
invited = 10, // server -> invitee: party_player invited you
|
|
withdrawn = 11 // server -> invitee: party_player's invite can no longer be accepted
|
|
};
|
|
bool valid_party_request(PartyAction action, std::uint64_t player) noexcept;
|
|
// Steam accounts in the public universe. Players are individual accounts; a
|
|
// dedicated server signs in anonymously as a game server (type 3 or 4) and gets
|
|
// a new ID each time it starts.
|
|
inline bool individual_steam_id(std::uint64_t id) noexcept {
|
|
return (id >> 56) == 1 && ((id >> 52) & 15) == 1 && (id & 0xffffffffULL);
|
|
}
|
|
inline bool game_server_steam_id(std::uint64_t id) noexcept {
|
|
const auto type = (id >> 52) & 15;
|
|
return (id >> 56) == 1 && (type == 3 || type == 4) && (id & 0xffffffffULL);
|
|
}
|
|
// A player's own name as sent in their hello: at most this many bytes.
|
|
constexpr std::size_t max_member_name = 64;
|
|
constexpr std::size_t max_admin_text = 320;
|
|
constexpr std::size_t max_ban_rows = 256;
|
|
constexpr std::size_t max_server_maps = 128, max_map_asset = 128;
|
|
// A level asset as a server's map list carries it: printable ASCII, no '|'.
|
|
bool valid_map_asset(std::string_view asset) noexcept;
|
|
struct Transform {
|
|
std::array<float, 3> position{};
|
|
std::array<float, 4> rotation{0, 0, 0, 1};
|
|
std::array<float, 3> scale{1, 1, 1};
|
|
bool operator==(const Transform &) const = default;
|
|
};
|
|
struct Pose {
|
|
Transform root;
|
|
// board[0] is the separate board entity's world transform; remaining
|
|
// entries are its rig pose. Empty means the board is currently unavailable.
|
|
std::vector<Transform> skater, board;
|
|
};
|
|
struct Member {
|
|
std::uint64_t id{}, epoch{};
|
|
std::string name;
|
|
bool admin{}; // roster: may change a dedicated server's settings
|
|
// roster: the player's party (0 = none), whether they lead it, and (on the leader) whether
|
|
// anyone may join it. Each party has exactly one leader. A listen host's lobby is one party.
|
|
std::uint32_t party{};
|
|
bool party_leader{}, party_open{};
|
|
// roster: a dedicated server measured the player's game running fast (a speedhack): nobody
|
|
// plays linked throwdowns or coop challenges with them (Server/speed_check.h).
|
|
bool speeding{};
|
|
// roster: the host or dedicated server found the player's mods change trick scoring: the same
|
|
// as speeding, nobody plays linked throwdowns or coop challenges with them.
|
|
bool scoring{};
|
|
bool operator==(const Member &) const = default;
|
|
};
|
|
bool valid_roster(std::span<const Member>, unsigned capacity) noexcept;
|
|
bool valid_routes(std::span<const Member>) noexcept;
|
|
struct Packet {
|
|
PacketKind kind = PacketKind::pose;
|
|
std::uint32_t sequence{};
|
|
std::uint64_t session{}, map{}, epoch{}, time_us{}, source{};
|
|
std::uint64_t world = 1;
|
|
std::array<std::uint8_t, 32> build{}, proof{};
|
|
std::uint64_t challenge{};
|
|
std::string destination;
|
|
bool map_authorized{};
|
|
bool world_ready{};
|
|
Pose pose;
|
|
std::uint32_t pose_interval_us = 50000;
|
|
// pose: the sender has the game's party collision on (the top bit of the pose's rate field)
|
|
bool player_collision{};
|
|
Appearance appearance;
|
|
std::vector<AudioSample> audio;
|
|
VoiceData voice;
|
|
VoicePolicy voice_policy;
|
|
unsigned capacity = max_players;
|
|
std::vector<Member> members;
|
|
MultiplayerDistances distances; // Authoritative host policy carried by its reliable roster.
|
|
unsigned tps = multiplayer_default_tps;
|
|
// Session placement policy, including for players joining later. Clients
|
|
// lock their editor and native tools from it; the host enforces it by
|
|
// freezing guest layouts (see publish_guest_objects).
|
|
ObjectPlacement object_placement = ObjectPlacement::everyone;
|
|
// Bumped each time the host deletes all guest objects. Guests delete their
|
|
// own session objects when it changes after their first roster.
|
|
std::uint32_t object_clears{};
|
|
bool force_world_layers{};
|
|
WorldLayerState layers = WorldLayerState(world_layers().size()); // all "default"
|
|
ParkChoices parks; // One allowlisted selection byte per shared native park slot.
|
|
ObjectChunk objects;
|
|
// chat: one message (valid_chat_text); hello: the sender's name (may be
|
|
// empty); admin: a request or its answer (valid_admin_text).
|
|
std::string text;
|
|
float voice_range = default_voice_range; // roster: how far the host forwards proximity voice
|
|
// roster: whether guests may use noclip (and teleport) / No Bail / the forward and up boosts
|
|
// (the host and a dedicated server's admins always may)
|
|
bool guest_noclip = true, guest_no_bail = true, guest_boosts = true;
|
|
// roster: guests skate with the host's physics tuning (a dedicated server's: the game's own)
|
|
bool enforce_tuning = true;
|
|
// roster: the votes a dedicated server lets players start (server_vote_* bits)
|
|
std::uint8_t server_votes{};
|
|
std::vector<MultiplayerBan> bans; // bans: newest first
|
|
std::uint32_t ban_total{}; // bans: how many the server has in all
|
|
std::vector<std::string> maps; // maps: level assets
|
|
std::vector<std::uint8_t> throwdown; // throwdown: one encoded message (1..max_throwdown_message bytes)
|
|
std::array<float, 3> teleport{}; // teleport: where the receiver goes (world position)
|
|
std::vector<std::uint8_t> tuning; // physics_tuning: 0..max_physics_tuning bytes
|
|
PartyAction party_action = PartyAction::leave; // party: what is asked or told
|
|
std::uint64_t party_player{}; // party: the other player (see PartyAction)
|
|
// scoring: the sender's scoring fingerprint, 0 for the game's own; `text` names the mods
|
|
// that change it (empty, or valid_admin_text).
|
|
std::uint64_t scoring{};
|
|
};
|
|
// Empty means a compatible greeting. The caller supplies its current local
|
|
// map/session and, after the handshake, the already accepted peer epoch.
|
|
std::string_view greeting_error(const Packet &, std::uint64_t session, std::uint64_t map,
|
|
const std::array<std::uint8_t, 32> &build,
|
|
std::uint64_t peer_epoch = 0) noexcept;
|
|
bool valid_transform(const Transform &) noexcept;
|
|
// A chat message: 1..multiplayer_chat_max_bytes of UTF-8 with no control characters.
|
|
bool valid_chat_text(std::string_view) noexcept;
|
|
// A player name in a hello or roster: UTF-8 without control characters.
|
|
bool valid_member_name(std::string_view) noexcept;
|
|
bool valid_admin_text(std::string_view) noexcept;
|
|
// The message a player typed, made valid: control characters and broken UTF-8
|
|
// dropped, surrounding blanks trimmed, cut to the byte limit on a character boundary.
|
|
std::string clean_chat_text(std::string_view);
|
|
bool valid_pose(const Pose &) noexcept;
|
|
std::vector<std::uint8_t> encode(const Packet &, bool compact_pose = false);
|
|
// The same, with a pose encoded at another update interval (a recipient thinned by
|
|
// distance) instead of the packet's own, so the packet need not be copied for it.
|
|
std::vector<std::uint8_t> encode(const Packet &, bool compact_pose, std::uint32_t pose_interval_us);
|
|
std::optional<Packet> decode(std::span<const std::uint8_t>) noexcept;
|
|
bool newer_sequence(std::uint32_t candidate, std::uint32_t previous) noexcept;
|
|
std::uint64_t map_hash(std::string_view) noexcept;
|
|
bool valid_map_destination(std::string_view) noexcept;
|
|
Transform interpolate(const Transform &, const Transform &, float);
|
|
void offset_pose(Pose &, const std::array<float, 3> &);
|
|
std::array<float, 16> to_matrix(const Transform &);
|
|
Transform from_matrix(const std::array<float, 16> &);
|
|
struct Invite {
|
|
std::uint64_t steam_id{}, secret{};
|
|
};
|
|
std::optional<Invite> parse_invite(std::string_view) noexcept;
|
|
std::string format_invite(Invite);
|
|
|
|
class AudioBuffer {
|
|
public:
|
|
bool push(const Packet &, std::uint64_t arrival);
|
|
std::optional<AudioState> sample(std::uint64_t now);
|
|
void clear();
|
|
std::size_t size() const { return frames_.size(); }
|
|
|
|
private:
|
|
struct Frame {
|
|
AudioState state;
|
|
std::uint64_t source{};
|
|
bool event{};
|
|
};
|
|
std::deque<Frame> frames_;
|
|
std::optional<AudioState> current_;
|
|
std::uint64_t epoch_{}, last_arrival_{}, played_source_{}, seen_{};
|
|
std::int64_t clock_offset_{};
|
|
std::uint32_t sequence_{};
|
|
};
|
|
|
|
// Reliable appearance messages have their own receive sequence. An older
|
|
// reliable packet can arrive after a newer unreliable pose without being lost.
|
|
class AppearanceBuffer {
|
|
public:
|
|
bool push(const Packet &p);
|
|
const std::optional<Appearance> &value() const { return value_; }
|
|
void clear() {
|
|
value_.reset();
|
|
epoch_ = 0;
|
|
sequence_ = 0;
|
|
}
|
|
|
|
private:
|
|
std::optional<Appearance> value_;
|
|
std::uint64_t epoch_{};
|
|
std::uint32_t sequence_{};
|
|
};
|
|
|
|
// Arrival times use one local monotonic clock; remote clocks need not agree.
|
|
enum class PosePlaybackMode { unavailable, buffered, predicted, held };
|
|
struct PosePlayback {
|
|
PosePlaybackMode mode = PosePlaybackMode::unavailable;
|
|
std::uint64_t prediction_us{};
|
|
bool correcting{};
|
|
};
|
|
class PoseBuffer {
|
|
public:
|
|
bool push(const Packet &, std::uint64_t arrival_us);
|
|
// For packets returned by decode(), which already performed the complete
|
|
// untrusted transform walk. The rvalue form moves the pose into the buffer.
|
|
bool push_validated(const Packet &, std::uint64_t arrival_us);
|
|
bool push_validated(Packet &&, std::uint64_t arrival_us);
|
|
// Reuses the caller-owned vectors. The optional-returning overloads remain
|
|
// for tests and infrequent callers, while render playback avoids allocating
|
|
// two transform arrays on every client callback.
|
|
bool sample(std::uint64_t now_us, Pose &out) const;
|
|
std::optional<Pose> sample(std::uint64_t now_us) const;
|
|
bool sample_remote(std::uint64_t now_us, Pose &out);
|
|
std::optional<Pose> sample_remote(std::uint64_t now_us);
|
|
PosePlayback playback() const { return playback_; }
|
|
void clear();
|
|
std::size_t size() const { return frames_.size(); }
|
|
|
|
private:
|
|
struct Frame {
|
|
Pose pose;
|
|
std::uint64_t arrival, source;
|
|
};
|
|
std::deque<Frame> frames_;
|
|
std::uint64_t epoch_{};
|
|
std::uint32_t sequence_{};
|
|
bool sender_clock_{};
|
|
std::int64_t clock_offset_{};
|
|
std::uint32_t interpolation_delay_us_ = 100000;
|
|
std::uint64_t playback_at_{}, correction_start_{}, correction_end_{};
|
|
std::array<float, 3> correction_{};
|
|
PosePlayback playback_;
|
|
Pose anchors_; // scratch for arrival continuity, which needs only the anchor transforms
|
|
template <class P> bool push_frame(P &&, std::uint64_t arrival_us);
|
|
std::uint64_t target_time(std::uint64_t now_us) const;
|
|
// anchors_only fills just the transforms prediction and correction move (see offset_pose).
|
|
bool sample_frames(std::uint64_t now_us, Pose &out, bool anchors_only) const;
|
|
bool predict(std::uint64_t now_us, PosePlayback &state, Pose &out, bool anchors_only = false) const;
|
|
void correct(Pose &, std::uint64_t now_us) const;
|
|
};
|
|
} // namespace dingosdk::multiplayer
|