mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 16:45:25 +02:00
Dedicated servers: roles, with their tags, commands and permissions A dedicated server had one list of admins who could do everything, and everyone else could do nothing. It now has roles, kept in data/roles.json beside the bans: each a name, the tag its players wear in the game, the server commands it may use, what it may do besides commands, and a rank. A server starts with five: player (everyone given no role: chat, votes, parties, private messages), vip (a reserved slot), moderator (kick, ban, messages, giving out lower roles), admin (everything) and muted. The default role is whoever was given none; a role has what the default has as well as its own unless it says it does not inherit. A reserved slot is a permission, so a role's players have one without being listed. Nobody kicks, bans or changes the role of a player whose rank is theirs or higher, or gives out a role that is; the console is above every role. "*" is every command but role and reserved, which a role has only when it lists them. New commands: role <player or SteamID64> <role>|none, roles, and roles reload. "admin add|remove" and "admins" are gone: the admins an older config lists are moved into the admin role the first time the server starts, and leave the config. In the game, a player's role tag and colour show in chat and on their nametag, and each control in the Multiplayer menu, the map, time of day, world layers and parks follows the command it needs rather than one admin flag, as do the commands chat offers. Protocol 48: the roster carries each player's tag, and a new permissions packet tells a player what their role gives them. Older games and servers do not connect to newer ones. Also fixes a name clash that kept the server config tests from compiling.
301 lines
15 KiB
C++
301 lines
15 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 <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;
|
|
};
|
|
// 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", "Staff", "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;
|
|
};
|
|
// 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", "Staff", "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.";
|
|
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.
|
|
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
|