#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 #include #include #include #include #include #include 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 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{}; // The local player made this player's placed objects solid; they are ghosts otherwise. bool objects_solid{}; }; // 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 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 " 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 answers; std::vector 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 lines; std::vector commands; // what "/" offers in this session std::vector 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 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 from{}, to{}; }; std::vector 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 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 server_maps; std::vector 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 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 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 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::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); return model.browse_ready_us > now ? static_cast((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