mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-19 00:55:22 +02:00
Follow-ups to the trainer (PR 37), and one multiplayer change.
Off until asked for (Extension/Trainer)
- The jump read-out and the LB+RB controller shortcuts start off. Jumps
are logged only while the read-out or telemetry recording is on, and
trick launches only when the trainer scaled them. A saved trainer.json
keeps what the player chose.
Memory search on demand (trainer.cpp, trainer_classes.cpp)
- The search for the game's tuning classes reads all writable memory.
It ran for every player after a level loaded, and up to seven times
when it found nothing. It now runs only when a class value or the flip
speed is not the game's own (the player's, or a host's they skate
with) or the EVERYTHING list is open, and at most three times a level.
- For those players it runs again on each level, which brings fresh
copies; a search from an earlier level used to count as done.
Guests under enforced physics
- A guest is locked by the session's enforce setting, from the moment
of joining (Engine/Game/Multiplayer/session_physics.h), not by whether
the enforcement status text was set yet.
- A guest's class values no longer come back after a map change.
The host's whole setup reaches its guests (protocol 39)
- Only Gameplay/SkatePhysicsTuning travelled, so a host could push,
sprint or jump further than the guests it held to the game's own.
- New packet physics_extras (30): the host's class values, trick
multipliers and auto push (trainer_session.h), sent when they change
and to players who join later, taken from the host only. A dedicated
server's are the game's own and it ignores the packet.
- Under enforcement a guest's own trick multipliers stand down for the
host's, whatever the boosts switch says. With enforcement off nothing
changes.
- A guest refuses bytes that are not an encoding, holds values to the
trainer's own ranges, and leaves out class values written for another
game build's table.
- protocol_version 38 -> 39: released versions disconnect from a peer
that sends a packet they cannot decode.
Out-of-view players are posed every frame again (Session/session.cpp)
- Players outside the camera's view and past 8 m were sampled at 20 Hz
(b763631). The game's replays record each skater as it stood every
frame and their camera looks anywhere, so those players moved at
20 Hz in replays. Removed; the distance rule (20 Hz past 200 m, 10 Hz
past 350 m) stays. This gives back the client-tick time it saved on
busy servers.
343 lines
16 KiB
C++
343 lines
16 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/session_physics.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 <cmath>
|
|
#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 = 39;
|
|
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 whoever hosts, 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,
|
|
// The host's physics that its tuning does not carry: the trainer's tuning-class values and
|
|
// trick multipliers (Engine/Game/Multiplayer/session_physics.h; opaque here, empty = the
|
|
// game's own). Like physics_tuning, only the host sends it and a dedicated server never does.
|
|
physics_extras = 30
|
|
};
|
|
// Packet::party_action. Requests go from a player to whoever hosts, a dedicated server or a
|
|
// lobby's host (party_player = the other player involved, 0 for leave/open/close); invited
|
|
// and withdrawn go from the host to the invitee (party_player = the inviter). The host
|
|
// 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, // host -> invitee: party_player invited you
|
|
withdrawn = 11 // host -> 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.
|
|
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
|
|
std::vector<std::uint8_t> extras; // physics_extras: 0..max_physics_extras 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);
|
|
// A player's name as a roster carries it (valid_roster): the same cleaning, at most 128 bytes.
|
|
std::string clean_roster_name(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;
|
|
// Normalized lerp between two transforms. Runs for every joint of every remote player each
|
|
// frame, so it is inline and plain arithmetic (std::lerp's edge-case branches and a divide
|
|
// per component were ~1% of a multiplayer client frame, profiled 2026-10-02).
|
|
inline Transform interpolate(const Transform &a, const Transform &b, float t) {
|
|
Transform out;
|
|
t = t < 0.0f ? 0.0f : t > 1.0f ? 1.0f : t;
|
|
for (unsigned i = 0; i < 3; ++i) {
|
|
out.position[i] = a.position[i] + (b.position[i] - a.position[i]) * t;
|
|
out.scale[i] = a.scale[i] + (b.scale[i] - a.scale[i]) * t;
|
|
}
|
|
const float dot = a.rotation[0] * b.rotation[0] + a.rotation[1] * b.rotation[1] +
|
|
a.rotation[2] * b.rotation[2] + a.rotation[3] * b.rotation[3];
|
|
const float sign = dot < 0 ? -1.0f : 1.0f;
|
|
float norm{};
|
|
for (unsigned i = 0; i < 4; ++i) {
|
|
out.rotation[i] = a.rotation[i] + (sign * b.rotation[i] - a.rotation[i]) * t;
|
|
norm += out.rotation[i] * out.rotation[i];
|
|
}
|
|
const float inverse = 1.0f / std::sqrt(norm);
|
|
for (float &v : out.rotation)
|
|
v *= inverse;
|
|
return out;
|
|
}
|
|
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
|