diff --git a/Extension/Multiplayer/Session/session_commands.cpp b/Extension/Multiplayer/Session/session_commands.cpp index f78f70e..b69df1a 100644 --- a/Extension/Multiplayer/Session/session_commands.cpp +++ b/Extension/Multiplayer/Session/session_commands.cpp @@ -936,6 +936,7 @@ std::string command(std::string_view action, std::string_view argument, std::str }; // Joining is for the host or the server to refuse (a server may opt out of the bans). if (action == "host" && reskate_banned(s.transport.status().local_id)) return refuse(std::string(banned_notice)); + if (action == "host" && banned_host(s.transport.status().local_id)) return refuse(std::string(banned_host_notice)); if (action == "host") { const auto space = argument.find(' '); if (space != std::string_view::npos) { @@ -975,6 +976,7 @@ std::string command(std::string_view action, std::string_view argument, std::str if (!invitation) return refuse("Invalid join code. Paste the complete SteamID-session code from the host."); if (blocked_server(invitation->steam_id)) return refuse(std::string(blocked_server_notice)); + if (banned_host(invitation->steam_id)) return refuse(std::string(banned_host_lobby_notice)); } stop(s, "Starting multiplayer..."); s.chat.clear(); // A new session starts with an empty chat. diff --git a/Extension/Multiplayer/Session/session_internal.h b/Extension/Multiplayer/Session/session_internal.h index db48698..1a42f23 100644 --- a/Extension/Multiplayer/Session/session_internal.h +++ b/Extension/Multiplayer/Session/session_internal.h @@ -215,7 +215,7 @@ struct Session { std::mutex request_mutex; std::deque> requests; SteamTransport transport; - SteamLobbies lobbies{make_steam_lobby_api()}; + SteamLobbies lobbies{make_steam_lobby_api(), [](std::uint64_t host) { return banned_host(host) || reskate_banned(host); }}; SteamServerBrowser servers; // Guest of a dedicated server: whether the roster lists us as an admin, // and the server's voice range from it. diff --git a/Extension/Multiplayer/Session/session_receive.cpp b/Extension/Multiplayer/Session/session_receive.cpp index 1e233a4..4912096 100644 --- a/Extension/Multiplayer/Session/session_receive.cpp +++ b/Extension/Multiplayer/Session/session_receive.cpp @@ -454,6 +454,15 @@ void networking(Session &s, const NativeFrame &local, std::uint64_t now) { stop(s, "This host is banned from ReSkate multiplayer."); return; } + // The same for a player who may not host (banned_host): theirs stops, and nobody stays in it. + if (s.mode == Mode::host && banned_host(s.transport.status().local_id)) { + stop(s, std::string(banned_host_notice)); + return; + } + if (s.mode == Mode::join && banned_host(s.host_id)) { + stop(s, std::string(banned_host_lobby_notice)); + return; + } for (const auto &link : links) { if (s.mode == Mode::host && s.banned.contains(link.id)) { s.transport.disconnect(link.id, "You were kicked from this session."); diff --git a/Extension/Multiplayer/Steam/steam_lobbies.cpp b/Extension/Multiplayer/Steam/steam_lobbies.cpp index 8be1147..fbf65b0 100644 --- a/Extension/Multiplayer/Steam/steam_lobbies.cpp +++ b/Extension/Multiplayer/Steam/steam_lobbies.cpp @@ -254,7 +254,7 @@ void SteamLobbies::tick(std::uint64_t now) { for (std::uint32_t i = 0; i < std::min(result.count, 50U); ++i) { const auto id = api_->at(static_cast(i)); const auto row = read_lobby(id, 0, [&](const char *key) { return api_->data(id, key); }); - if (row && !text::contains_bad_words(row->name) && + if (row && !text::contains_bad_words(row->name) && !(hidden_host_ && hidden_host_(row->owner)) && std::none_of(state_.rows.begin(), state_.rows.end(), [id](const auto &v) { return v.id == id; })) state_.rows.push_back(*row); diff --git a/Extension/Multiplayer/Steam/steam_lobbies.h b/Extension/Multiplayer/Steam/steam_lobbies.h index 19378d8..bb6d043 100644 --- a/Extension/Multiplayer/Steam/steam_lobbies.h +++ b/Extension/Multiplayer/Steam/steam_lobbies.h @@ -38,7 +38,9 @@ struct LobbyStatus { }; class SteamLobbies { public: - explicit SteamLobbies(std::unique_ptr api) : api_(std::move(api)) {} + // `hidden_host`: whether a player's lobbies are left out of the browser (the team's bans). + explicit SteamLobbies(std::unique_ptr api, std::function hidden_host = {}) + : api_(std::move(api)), hidden_host_(std::move(hidden_host)) {} void host(const std::string &code, unsigned capacity = multiplayer_lobby_player_limit, bool password_required = false, std::string_view name = {}); void update_host(bool ready, unsigned players, std::string_view map, std::uint64_t now); @@ -57,6 +59,7 @@ class SteamLobbies { bool cancelled{}; }; std::unique_ptr api_; + std::function hidden_host_; LobbyStatus state_; Pending creating_, searching_, joining_; bool host_wanted_{}, failed_{}, ready_{}, password_required_{}; diff --git a/Extension/Multiplayer/Steam/steam_transport.cpp b/Extension/Multiplayer/Steam/steam_transport.cpp index 386ba40..d7ba572 100644 --- a/Extension/Multiplayer/Steam/steam_transport.cpp +++ b/Extension/Multiplayer/Steam/steam_transport.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -139,6 +140,9 @@ struct SteamTransport::Impl { note_direct("Direct connection to the server did not come up (" + why + "); using Steam's relays."); } std::string direct_note; // how the last direct attempt went + // When each player's direct connection was last noted, and how often they asked since. + struct DirectAsked { std::uint64_t since{}; unsigned again{}; }; + std::unordered_map direct_asked; std::vector direct_notes; // take_direct_notes void note_direct(std::string text) { if (direct_notes.size() < 64) direct_notes.push_back(std::move(text)); @@ -705,7 +709,19 @@ void SteamTransport::poll() { if (existing != p.links.end() && existing->second.handle == event.m_hConn) continue; const bool direct = p.direct_listener && event.m_info.m_hListenSocket == p.direct_listener; - if (direct) p.note_direct("Direct connection asked for by " + std::to_string(id) + "."); + // Once a minute a player: one waiting for a place on a full server asks again every + // second or so, and each ask used to be a line. + if (direct) { + if (p.direct_asked.size() > 512) std::erase_if(p.direct_asked, [&](const auto &asked) { return now - asked.second.since >= 60000; }); + auto &asked = p.direct_asked[id]; + if (!asked.since || now - asked.since >= 60000) { + p.note_direct("Direct connection asked for by " + std::to_string(id) + + (asked.again ? " (and " + std::to_string(asked.again) + " more times in the last minute)." : ".")); + asked = {now, 0}; + } else { + ++asked.again; + } + } // (A connection by address names a Steam ID only when Steam has vouched for it.) if ((event.m_info.m_hListenSocket != p.listener && !direct) || !p.listener || !id || id == p.state.local_id || (!p.state.hosting && !p.allowed.contains(id)) || p.links.size() >= p.capacity - 1 || diff --git a/Extension/Multiplayer/Test/developer_identity_tests.cpp b/Extension/Multiplayer/Test/developer_identity_tests.cpp index f8f2b27..e3cfa53 100644 --- a/Extension/Multiplayer/Test/developer_identity_tests.cpp +++ b/Extension/Multiplayer/Test/developer_identity_tests.cpp @@ -62,6 +62,13 @@ void parsing() { blocks[static_cast(L::official_server)].empty() && servers[static_cast(L::blocked_server)].empty(), "The blocked server list was not read"); check(rejected(R"({"categories":{},"blocked_servers":["76561198000000001"]})"), "A player was accepted as a blocked server"); + // Players who may not host: players' IDs, their own list, and none when the answer has none. + const auto no_hosts = parse_identity_lists(R"({"categories":{},"banned":["76561198000000001"],"banned_hosts":["76561198000000007","76561198000000003"]})"); + check(no_hosts[static_cast(L::banned_host)] == std::vector{76561198000000003ULL, 76561198000000007ULL} && + no_hosts[static_cast(L::banned)] == std::vector{76561198000000001ULL} && + blocks[static_cast(L::banned_host)].empty(), + "The list of players banned from hosting was not read"); + check(rejected(R"({"categories":{},"banned_hosts":["85568392924040001"]})"), "A server was accepted as a banned host"); check(parse_server_tokens_required(R"({"categories":{},"server_tokens_required":true})") && !parse_server_tokens_required(R"({"categories":{},"server_tokens_required":false})") && !parse_server_tokens_required(R"({"categories":{}})") && !parse_server_tokens_required(R"({"server_tokens_required":"yes"})"), diff --git a/Extension/Multiplayer/Test/mesh_tests.cpp b/Extension/Multiplayer/Test/mesh_tests.cpp index 8835178..ba01961 100644 --- a/Extension/Multiplayer/Test/mesh_tests.cpp +++ b/Extension/Multiplayer/Test/mesh_tests.cpp @@ -43,6 +43,7 @@ bool clear_lobby_guest_objects() { ++simulated_guest_wipes; return true; } } namespace dingosdk { bool teleport_local_skater(const std::array&) { return true; } +ControllerBindingsModel local_profile_controller_bindings() { return {}; } void update_board_lock(std::uintptr_t, std::uintptr_t, bool) noexcept {} void update_developer_hoodie(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperHoodieState &, const multiplayer::MarkStyles &) noexcept {} diff --git a/Extension/Multiplayer/developer_identity.cpp b/Extension/Multiplayer/developer_identity.cpp index d4373cb..4921ba7 100644 --- a/Extension/Multiplayer/developer_identity.cpp +++ b/Extension/Multiplayer/developer_identity.cpp @@ -18,6 +18,7 @@ constexpr std::array, 5> categories{{ constexpr auto banned = static_cast(IdentityList::banned); constexpr auto servers = static_cast(IdentityList::official_server); constexpr auto blocked = static_cast(IdentityList::blocked_server); +constexpr auto hosts = static_cast(IdentityList::banned_host); std::atomic> current; @@ -65,6 +66,7 @@ IdentityLists parse_identity_lists(std::string_view json) { if (answer.contains("banned")) read(answer.at("banned"), lists[banned]); if (answer.contains("official_servers")) read(answer.at("official_servers"), lists[servers], server_id); if (answer.contains("blocked_servers")) read(answer.at("blocked_servers"), lists[blocked], any_server_id); + if (answer.contains("banned_hosts")) read(answer.at("banned_hosts"), lists[hosts]); return lists; } bool parse_server_tokens_required(std::string_view json) { diff --git a/Extension/Multiplayer/developer_identity.h b/Extension/Multiplayer/developer_identity.h index 851f530..1d456c9 100644 --- a/Extension/Multiplayer/developer_identity.h +++ b/Extension/Multiplayer/developer_identity.h @@ -16,8 +16,8 @@ namespace dingosdk::multiplayer { // The last list is not of players: the dedicated servers the ReSkate team runs, which the // server browser puts first. A server is on it by the Steam ID its login token gives it. // And one of servers the team has blocked: the browser does not show them and the game does -// not join them. -enum class IdentityList : std::uint8_t { developer, homie, content_creator, centrix, banned, official_server, staff, blocked_server, count }; +// not join them. And one of players who may not host a lobby (banned_host, below). +enum class IdentityList : std::uint8_t { developer, homie, content_creator, centrix, banned, official_server, staff, blocked_server, banned_host, count }; using IdentityLists = std::array, static_cast(IdentityList::count)>; inline constexpr std::string_view identity_lists_url = "https://api.reskate.dev/api/v1/steam-ids"; @@ -57,6 +57,13 @@ inline bool reskate_banned(std::uint64_t id) noexcept { return identity_listed(i inline bool official_server(std::uint64_t id) noexcept { return identity_listed(id, IdentityList::official_server); } inline bool blocked_server(std::uint64_t id) noexcept { return identity_listed(id, IdentityList::blocked_server); } inline constexpr std::string_view blocked_server_notice = "This server has been blocked by the ReSkate team."; +// A player the team has stopped from hosting lobbies ("banned_hosts"); they still play on +// servers and in other players' lobbies. Their own game does not host, and since that is theirs +// to change, everyone else's does the rest: the lobby browser leaves their lobbies out and the +// game does not join one or stay in one. +inline bool banned_host(std::uint64_t id) noexcept { return identity_listed(id, IdentityList::banned_host); } +inline constexpr std::string_view banned_host_notice = "You are banned from hosting ReSkate lobbies. You can still join servers and other players' lobbies."; +inline constexpr std::string_view banned_host_lobby_notice = "This player has been banned from hosting lobbies by the ReSkate team."; // Whether the server browser shows only dedicated servers signed in with a Steam login token // (protocol.h: persistent_server_steam_id), which the backend switches on and off beside the // lists ("server_tokens_required"). Such a server keeps its Steam ID, so a block holds on it. @@ -70,7 +77,7 @@ bool parse_server_tokens_required(std::string_view json); inline constexpr std::string_view banned_notice = "You are banned from ReSkate multiplayer."; // The API's answer, {"categories":{"dev":["7656119..."],"homie":[],...},"banned":[], -// "official_servers":["8556839..."],"blocked_servers":[]}, as sorted lists. A category this build does not know +// "official_servers":["8556839..."],"blocked_servers":[],"banned_hosts":[]}, as sorted lists. A category this build does not know // is ignored and a list the answer leaves out is empty; an entry that is not a player's // SteamID64 (for the servers: a token server's) throws. IdentityLists parse_identity_lists(std::string_view json);