mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-19 07:45:26 +02:00
Crash dumps now go to the project's own Sentry server. The launcher's out-of-process helper is unchanged; only where it posts and what it expects back are: - The default endpoint is the Sentry project's minidump URL. Report attributes travel as Sentry tags, with the release as reskate@<version>. - An upload counts as accepted when the server answers with an event ID. - "backtrace" is "crash_report" throughout: the namespace, files, CMake targets, DINGOSDK_CRASH_REPORT_URL, DINGOSDK_BUILD_CRASH_REPORT_TESTS and the RESKATE_CRASH_REPORT_URL override. Performance timings are new, and off unless "Send performance timings" is ticked in the launcher's settings (RESKATE_TRACING=1). A small tracer (Engine/Core/Debug/trace.h) posts each finished step to Sentry as a transaction from a background thread; the launcher and the game it starts share one trace. Timed: the update check, the Steam download, Thunderstore installs, the mod merge, starting the game, the launch up to the first playable level, each level load, joining a session, fetching the identity lists, and the time spent in each level with its average frame rate and long frames. Turning crash reports off turns timings off too.
316 lines
16 KiB
C++
316 lines
16 KiB
C++
#pragma once
|
|
#include "distance_settings.h"
|
|
#include "object_placement.h"
|
|
#include "role_commands.h"
|
|
#include "session_limits.h"
|
|
#include "tick_settings.h"
|
|
#include "voice_settings.h"
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace dingosdk {
|
|
constexpr unsigned max_map_rotation = 1440; // minutes a server's rotation keeps one map, at most
|
|
// A dedicated server's name: 1 to 64 letters, digits, spaces and - _ / [ ] ( ), with a
|
|
// letter or digit among them and no space at either end. A server refuses any other
|
|
// name, and the server browser does not show one.
|
|
inline constexpr char server_name_rule[] = "1 to 64 letters, numbers, spaces and - _ / [ ] ( )";
|
|
[[nodiscard]] constexpr bool valid_server_name(std::string_view name) noexcept {
|
|
if (name.empty() || name.size() > 64 || name.front() == ' ' || name.back() == ' ') return false;
|
|
bool named{};
|
|
for (const auto c : name) {
|
|
const bool word = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
|
|
if (!word && std::string_view(" -_/[]()").find(c) == std::string_view::npos) return false;
|
|
named |= word;
|
|
}
|
|
return named;
|
|
}
|
|
struct MultiplayerLobby {
|
|
std::uint64_t id{}, owner{};
|
|
std::string name, map, code;
|
|
bool password_required{};
|
|
int players = 1, capacity = multiplayer_player_limit;
|
|
// A dedicated server rather than a player's lobby: joined by its code.
|
|
bool dedicated{};
|
|
// A dedicated server the ReSkate team runs (developer_identity.h): first in the browser.
|
|
bool official{};
|
|
int ping = -1; // ms, when the server answered directly
|
|
// A dedicated server that takes direct connections: its address (host byte order) and UDP
|
|
// port, 0 for none.
|
|
std::uint32_t direct_ip{};
|
|
int direct_port{};
|
|
// Steam friends skating there now, by name (steam_social.h).
|
|
std::vector<std::string> friends;
|
|
};
|
|
// "Ana, Ben and 2 more": the friends in a server, as a browser row has room for.
|
|
inline std::string lobby_friends_text(const MultiplayerLobby &lobby, std::size_t named = 2) {
|
|
std::string text;
|
|
for (std::size_t i = 0; i < lobby.friends.size() && i < named; ++i) text += (i ? ", " : "") + lobby.friends[i];
|
|
if (lobby.friends.size() > named) text += " and " + std::to_string(lobby.friends.size() - named) + " more";
|
|
return text;
|
|
}
|
|
struct MultiplayerPlayer {
|
|
std::uint64_t id{};
|
|
std::string name;
|
|
bool connected{}, visible{};
|
|
std::string native_status, cosmetic_status, audio_status, ui_status;
|
|
std::string route;
|
|
float pose_hz{};
|
|
unsigned pose_target_tps = 20;
|
|
float distance_m = -1;
|
|
std::uint64_t pose_age_ms{};
|
|
float native_pose_hz{}, native_board_hz{};
|
|
float native_animation_hz{}, native_animation_ms{}, pose_apply_ms{};
|
|
std::uint64_t native_animation_skipped{};
|
|
std::string playback;
|
|
unsigned prediction_ms{};
|
|
bool correcting{};
|
|
std::uint64_t epoch{};
|
|
// The player's party (0 = none), whether they lead it, and whether it is the local player's.
|
|
std::uint32_t party{};
|
|
bool party_leader{}, party_member{};
|
|
};
|
|
// A party invite waiting for an answer (dedicated servers).
|
|
struct MultiplayerPartyInvite {
|
|
std::uint64_t from{};
|
|
std::string name;
|
|
};
|
|
struct MultiplayerClientTiming {
|
|
float callback_hz{}, gap_max_ms{}, work_ms{}, work_max_ms{};
|
|
// Capture/setup, receive/routing, encode/send, remote playback, diagnostics.
|
|
std::array<float, 5> mean_ms{}, peak_ms{};
|
|
};
|
|
// The host settings last used, saved in the local profile, so the host pages
|
|
// open with them. The password itself is never saved.
|
|
struct MultiplayerHostPreferences {
|
|
bool loaded{}, public_lobby{true}, password_required{};
|
|
int capacity = multiplayer_lobby_player_limit;
|
|
unsigned tps = multiplayer_default_tps;
|
|
std::string lobby_name;
|
|
};
|
|
// The least time between two searches of the lobby and server lists, whoever asks: the
|
|
// browser's refresh, a page opening, the console. The browser counts it down.
|
|
inline constexpr std::uint64_t multiplayer_browse_interval_us = 10000000;
|
|
// Session text chat. A message is UTF-8 without control characters, at most
|
|
// this many bytes; the log keeps the most recent lines of the session.
|
|
inline constexpr std::size_t multiplayer_chat_max_bytes = 200;
|
|
inline constexpr std::size_t multiplayer_chat_history = 50;
|
|
struct MultiplayerChatLine {
|
|
std::uint64_t sequence{}; // local arrival order, increasing within the process
|
|
std::uint64_t sender{}; // Steam id; 0 for a notice from ReSkate itself
|
|
std::uint64_t received{}; // local monotonic time (microseconds) the line arrived
|
|
std::string name, text;
|
|
bool local{}; // sent by this player
|
|
// The sender's role, as their nametag shows it: its colour (IM_COL32 layout, 0 = none)
|
|
// and a tag shown in a box before the name ("Dev", "ReSkate Admin", "Content Creator", "Centrix", "Homie", "Admin", "Host", "Friend", "Server" or empty).
|
|
std::uint32_t color{};
|
|
std::string tag;
|
|
// With the chat filter on, `text` is masked and this is the line as sent (same length), so
|
|
// the overlay can keep emote names the filter caught; empty when nothing was masked.
|
|
std::string unmasked;
|
|
bool server{}; // said by the dedicated server itself: its console, welcome or answers
|
|
// The colour of the line's own text when it is not the usual one (IM_COL32 layout, 0 = usual):
|
|
// a dedicated server's lines, in the colour its owner chose.
|
|
std::uint32_t text_color{};
|
|
};
|
|
// A command typed into chat with a leading "/" (shown as the player types "/").
|
|
struct MultiplayerChatCommand {
|
|
std::string name; // "/vote map"
|
|
std::string usage; // "/vote map <map>"
|
|
std::string description;
|
|
std::string argument; // what Tab completes after it: "player", "map", "time" or ""
|
|
};
|
|
// What the chat overlay reads each frame: cheap to copy, unlike the full model.
|
|
// The vote a dedicated server is running or has just finished, for the card above chat.
|
|
struct MultiplayerVote {
|
|
std::uint32_t id{}; // 0: none
|
|
std::string label; // "change the map to ..."
|
|
unsigned yes{}, no{}, needed{}, seconds{}; // seconds: left of a running one
|
|
std::uint8_t outcome{}; // 0 running, 1 passed, 2 failed, 3 cancelled
|
|
std::uint8_t mine{}; // this player's answer: 0 none yet, 1 yes, 2 no; in a poll, 1 + the answer's index
|
|
bool may_vote{}; // not the player a kick vote is about
|
|
std::uint32_t yes_bind{}, no_bind{}; // the player's binds for Yes and No (controller_bindings.h), 0: none
|
|
// A poll: a question (`label`) with answers and a count for each, instead of yes and no.
|
|
bool poll{};
|
|
std::vector<std::string> answers;
|
|
std::vector<unsigned> counts;
|
|
// The poll is the rotation's vote for the next map: its answers are maps (and "Random map",
|
|
// "Stay here"), and it is shown as the maps to pick from.
|
|
bool next_map{};
|
|
};
|
|
// A dedicated server's announcement, for its card. `id` 0: none showing.
|
|
struct MultiplayerAnnouncement {
|
|
std::uint32_t id{};
|
|
std::string text;
|
|
unsigned seconds{}; // how long it still shows
|
|
};
|
|
struct MultiplayerChat {
|
|
bool available{}; // in a session that can carry chat
|
|
std::uint64_t latest{}; // sequence of the newest line, 0 when empty
|
|
std::vector<MultiplayerChatLine> lines;
|
|
std::vector<MultiplayerChatCommand> commands; // what "/" offers in this session
|
|
std::vector<std::string> players, maps; // what their arguments complete to
|
|
MultiplayerVote vote;
|
|
MultiplayerAnnouncement announcement;
|
|
};
|
|
// A player this PC's lobbies never admit, kept in the local profile.
|
|
struct MultiplayerBan {
|
|
std::uint64_t id{}; // SteamID64
|
|
std::string name; // as they were known when banned; may be empty
|
|
std::int64_t added{}; // Unix time
|
|
};
|
|
inline constexpr float player_distance_least = 50.f, player_distance_unlimited = 1000.f;
|
|
struct MultiplayerModel {
|
|
VoiceModel voice;
|
|
std::vector<MultiplayerBan> bans;
|
|
unsigned tps = multiplayer_default_tps;
|
|
ObjectPlacement object_placement = ObjectPlacement::everyone;
|
|
// Objects each player may have placed in this session (0: no limit), the limit this
|
|
// player is held to (0 for the host and a server's admins), and how many they have placed.
|
|
unsigned object_limit{}, object_limit_own{}, objects_placed{};
|
|
// This player may resize the objects they place (a dedicated server can turn it off for its
|
|
// players; its admins always may).
|
|
bool object_scaling{true};
|
|
// Host setting: whether guests may use noclip / No Bail (the host and server admins always may).
|
|
bool guest_noclip{true}, guest_no_bail{true}, guest_boosts{true};
|
|
// Host setting: guests skate with the host's physics tuning (on a dedicated server: the
|
|
// game's own). Guest: what that does here, while enforced.
|
|
bool enforce_tuning{true};
|
|
std::string tuning_status;
|
|
// Local display preference: the floating name label above each peer.
|
|
bool nametags{true};
|
|
// Local: whether session text chat shows at all (and T opens it).
|
|
bool chat_visible{true};
|
|
// Local: bad words in chat names and messages show as **** (on by default).
|
|
bool chat_filter{true};
|
|
// Local, for ReSkate's nametags: `nametag_distance` is how far away a player's name still
|
|
// shows (past it they are a dot), `nametag_dots` whether those dots and the ones at the
|
|
// screen's edge for off-screen players show at all, `nametags_friends` names only the
|
|
// player's Steam friends.
|
|
// Local: how far away another player still gets a skater built for them, in metres. Past
|
|
// it they keep their nametag or dot. Each skater costs memory and frame time, in view or
|
|
// not. `player_distance_unlimited` and above: every player, however far.
|
|
float player_distance{120.f};
|
|
// Local: connect straight to dedicated servers that offer it (their "connection": "direct")
|
|
// instead of through Steam's relays. Off: always the relays, and no server sees this PC's address.
|
|
bool prefer_direct{true};
|
|
float nametag_distance{120.f};
|
|
bool nametag_dots{true};
|
|
bool nametags_friends{};
|
|
// Local: chat bubbles above each skater's head. `chat_bubbles_distance` is how far away
|
|
// a player may be and still show one, `chat_bubbles_duration` how many seconds a line
|
|
// stays before it fades, `chat_bubbles_history` how many recent lines stack up.
|
|
// `chat_bubbles_own` also shows this player's own lines.
|
|
bool chat_bubbles{true};
|
|
bool chat_bubbles_own{};
|
|
float chat_bubbles_distance{40.f};
|
|
float chat_bubbles_duration{5.f};
|
|
int chat_bubbles_history{3};
|
|
// Local: the tag the ReSkate backend gives this player ("Dev", "ReSkate Admin", "Content Creator", "Centrix" or "Homie"; empty
|
|
// for most players) and its role colour, and whether they show it, and the animated items
|
|
// that come with it, to everyone.
|
|
// The local player is on the ReSkate team's developer, staff or homie list: the player list
|
|
// gives each player's Steam ID.
|
|
bool steam_ids_shown{};
|
|
std::string identity_tag;
|
|
std::uint32_t identity_tag_colour{};
|
|
bool identity_tag_shown{true}, identity_items_shown{true};
|
|
// Local: how each of this player's marked cosmetics is coloured, for the Special page: what
|
|
// they wear in each slot ("Top", "Shoes", ...), then the parts of their board. mode: 0 what
|
|
// their list gives (`identity_animation`: "RAINBOW", "GREEN", "RED", "BLUE" or "GOLD"), 1 off, 2 a gradient
|
|
// between the two colours they picked, 3 the first of them alone, 4 the rainbow (offered
|
|
// when `identity_rainbow`: to the staff). speed: 0 normal, 1 slow, 2 fast.
|
|
struct IdentityStyle {
|
|
std::string name;
|
|
int mode{}, speed{};
|
|
std::array<float, 3> from{}, to{};
|
|
};
|
|
std::vector<IdentityStyle> identity_styles;
|
|
bool identity_rainbow{};
|
|
std::string identity_animation;
|
|
float voice_range = default_voice_range; // how far the host (or server) forwards proximity voice
|
|
// In a dedicated server's session: the server is the host but not a player.
|
|
// Admins it lists may change its settings, and its map through Levels.
|
|
// server_admin: the player's role there has at least one of the server's commands.
|
|
bool dedicated{}, server_admin{};
|
|
// Their role on it (empty: none), the tag it wears, whether it has every command or the
|
|
// ones listed, and what it gives beyond commands (the protocol's role_* bits).
|
|
std::string server_role, server_role_tag;
|
|
bool server_all_commands{};
|
|
std::vector<std::string> server_commands;
|
|
std::uint32_t server_permissions{};
|
|
// `bans` is the dedicated server's list (for its admins) rather than this PC's.
|
|
bool server_bans{};
|
|
unsigned server_ban_total{};
|
|
// For its admins: the levels (assets) the dedicated server can switch to.
|
|
std::vector<std::string> server_maps;
|
|
std::vector<std::string> server_map_pool; // in rotation order; empty: every map
|
|
unsigned server_map_rotation{}; // minutes per map (0: off)
|
|
bool server_map_votes{}; // players may vote for a map
|
|
MultiplayerHostPreferences saved_host;
|
|
bool force_world_layers{};
|
|
std::uint64_t host_id{};
|
|
std::string local_name;
|
|
std::string lobby_name;
|
|
std::string object_status;
|
|
MultiplayerClientTiming client_timing;
|
|
MultiplayerDistances distances;
|
|
bool password_required{};
|
|
int players = 1, capacity = multiplayer_player_limit;
|
|
std::vector<MultiplayerPlayer> roster;
|
|
// The local player's party. Players form their own, in a lobby as on a dedicated server
|
|
// (`parties`: invite, leave, kick... are available in any session).
|
|
std::uint32_t party{};
|
|
bool party_leader{}, party_open{}, parties{};
|
|
std::vector<MultiplayerPartyInvite> party_invites; // newest last
|
|
bool active{}, hosting{}, connected{}, echo{}, remote_visible{}, local_ready{};
|
|
// The session is connected but waiting on its map, which this PC does not have and is
|
|
// being asked about or downloaded: not yet a session the player is in.
|
|
bool map_fetching{};
|
|
std::uint64_t local_id{}, peer_id{}, sent{}, received{}, dropped{}, pose_updates{}, board_pose_updates{};
|
|
std::size_t skater_bones{}, board_bones{};
|
|
std::string invite, map;
|
|
// The code of the session this player is in, as host or guest (`invite` is a host's own).
|
|
std::string join_code;
|
|
std::string status = "Multiplayer is off.";
|
|
std::string native_status;
|
|
std::string cosmetic_status;
|
|
std::string audio_status;
|
|
std::uint64_t audio_captured{}, audio_played{};
|
|
std::string peer_name, player_ui_status;
|
|
std::uint64_t player_map_updates{};
|
|
bool public_host{}, lobby_listed{}, lobby_searching{}, lobby_joining{}, lobby_searched{};
|
|
std::uint64_t public_lobby{}; // Verified public listing, never a private invitation.
|
|
std::string lobby_status, browser_status = "Refresh to find public ReSkate lobbies.";
|
|
// When the lists may be searched again, on the steady clock in microseconds (browse_wait).
|
|
std::uint64_t browse_ready_us{};
|
|
std::vector<MultiplayerLobby> lobbies;
|
|
bool network_telemetry{};
|
|
unsigned prioritized_connections{};
|
|
std::uint64_t cosmetic_queue_us{};
|
|
unsigned direct_connections{}, fallback_streams{}, direct_upload_limit{};
|
|
int ping_ms{}, send_rate{}, pending_bytes{};
|
|
float outgoing_bps{}, incoming_bps{}, delivery_local = -1, delivery_remote = -1;
|
|
std::uint64_t queue_us{}, skipped_updates{}, send_failures{}, invalid_messages{}, sent_bytes{},
|
|
raw_sent_bytes{};
|
|
};
|
|
// Whether the player's role on the dedicated server they are on has one of its commands.
|
|
// Whole seconds until the lobby and server lists may be searched again; 0 when they may be now.
|
|
inline int browse_wait(const MultiplayerModel& model) noexcept {
|
|
const auto now = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
return model.browse_ready_us > now ? static_cast<int>((model.browse_ready_us - now + 999999) / 1000000) : 0;
|
|
}
|
|
inline bool server_can(const MultiplayerModel& model, std::string_view command) noexcept {
|
|
return model.dedicated && role_allows(model.server_all_commands, model.server_commands, command);
|
|
}
|
|
inline bool multiplayer_controls_level(const MultiplayerModel& model) noexcept {
|
|
// The level choice of a player who may change the server's map is sent to the server, which moves everyone.
|
|
return model.lobby_joining || (model.active && !model.hosting && !model.echo && !server_can(model, "map"));
|
|
}
|
|
} // namespace dingosdk
|