mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 23:17:06 +02:00
251 lines
11 KiB
C++
251 lines
11 KiB
C++
#include "Extension/Multiplayer/Steam/steam_lobbies.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <stdexcept>
|
|
#include <set>
|
|
|
|
using namespace dingosdk;
|
|
using namespace dingosdk::multiplayer;
|
|
namespace {
|
|
void check(bool condition, const char *why) {
|
|
if (!condition)
|
|
throw std::runtime_error(why);
|
|
}
|
|
constexpr std::uint64_t host_id = 76561198000000123ULL, guest_id = 76561198000000456ULL;
|
|
const std::string map_name = "levels/Game/Beach/Beach";
|
|
struct Fake final : LobbyApi {
|
|
std::uint64_t next{}, create_call{}, list_call{}, join_call{};
|
|
int at_calls{};
|
|
bool fail_write{};
|
|
std::map<std::uint64_t, LobbyResult> results;
|
|
std::map<std::uint64_t, std::map<std::string, std::string>> metadata;
|
|
std::map<std::uint64_t, std::uint64_t> owners;
|
|
std::vector<std::uint64_t> ids, left;
|
|
std::set<std::uint64_t> exposed;
|
|
void open() override {}
|
|
std::uint64_t create(unsigned) override { return create_call = ++next; }
|
|
std::uint64_t search() override { return list_call = ++next; }
|
|
std::uint64_t join(std::uint64_t) override { return join_call = ++next; }
|
|
std::optional<LobbyResult> poll(std::uint64_t id, LobbyCall) override {
|
|
const auto found = results.find(id);
|
|
if (found == results.end())
|
|
return {};
|
|
const auto value = found->second;
|
|
results.erase(found);
|
|
return value;
|
|
}
|
|
void leave(std::uint64_t id) override {
|
|
left.push_back(id);
|
|
exposed.erase(id);
|
|
}
|
|
std::uint64_t at(int i) override {
|
|
++at_calls;
|
|
return ids.at(i % ids.size());
|
|
}
|
|
std::uint64_t owner(std::uint64_t id) override { return owners[id]; }
|
|
std::string data(std::uint64_t id, const char *key) override { return metadata[id][key]; }
|
|
bool data(std::uint64_t id, const char *key, const std::string &v) override {
|
|
if (fail_write)
|
|
return false;
|
|
metadata[id][key] = v;
|
|
return true;
|
|
}
|
|
bool joinable(std::uint64_t id, bool value) override {
|
|
if (value) {
|
|
check(read_lobby(id, owner(id), [&](const char *k) { return data(id, k); }).has_value(),
|
|
"A public lobby was exposed before all metadata was valid");
|
|
exposed.insert(id);
|
|
} else
|
|
exposed.erase(id);
|
|
return true;
|
|
}
|
|
bool visibility(std::uint64_t, bool) override { return true; }
|
|
std::string name() override { return "Test host"; }
|
|
void record(std::uint64_t id = 9001) {
|
|
owners[id] = host_id;
|
|
metadata[id] = {{"rs_kind", "reskate-co-skate"},
|
|
{"rs_protocol", std::to_string(protocol_version)},
|
|
{"rs_build", std::string(supported_build::game_sha256)},
|
|
{"rs_name", "Test host"},
|
|
{"rs_code", format_invite({host_id, 123})},
|
|
{"rs_map", map_name},
|
|
{"rs_players", "1"},
|
|
{"rs_capacity", "8"},
|
|
{"rs_password", "0"},
|
|
{"rs_open", "1"}};
|
|
}
|
|
};
|
|
struct Fixture {
|
|
Fake *api = new Fake;
|
|
SteamLobbies lobbies{std::unique_ptr<LobbyApi>(api)};
|
|
void list() {
|
|
api->record();
|
|
api->ids = {9001};
|
|
lobbies.refresh(1000000);
|
|
api->results[api->list_call] = {true, 0, 1, 1};
|
|
lobbies.tick(1100000);
|
|
check(lobbies.status().rows.size() == 1, "Valid public lobby not listed");
|
|
}
|
|
void host() {
|
|
lobbies.host(format_invite({host_id, 123}));
|
|
lobbies.update_host(true, 1, map_name, 1000000);
|
|
api->owners[9001] = host_id;
|
|
api->results[api->create_call] = {true, 9001, 0, 1};
|
|
lobbies.tick(1100000);
|
|
check(!lobbies.status().listed, "Lobby published before readiness update");
|
|
lobbies.update_host(true, 1, map_name, 1100000);
|
|
}
|
|
};
|
|
void metadata_tests() {
|
|
Fake f;
|
|
f.record();
|
|
auto get = [&](const char *key) { return f.data(9001, key); };
|
|
check(read_lobby(9001, host_id, get).has_value(), "Valid lobby rejected");
|
|
check(read_lobby(9001, 0, get).has_value(), "Non-member browser discarded advertised host");
|
|
check(!read_lobby(9001, guest_id, get), "Changed owner accepted");
|
|
for (const auto &[key, value] : std::map<std::string, std::string>{{"rs_kind", "another-game"},
|
|
{"rs_protocol", "999"},
|
|
{"rs_build", "other-build"},
|
|
{"rs_code", "malformed"},
|
|
{"rs_players", "8"},
|
|
{"rs_open", "0"},
|
|
{"rs_name", "bad\nname"},
|
|
{"rs_map", std::string(257, 'x')}}) {
|
|
f.record();
|
|
f.metadata[9001][key] = value;
|
|
check(!read_lobby(9001, host_id, get), "Incompatible or malformed metadata accepted");
|
|
}
|
|
}
|
|
void host_tests() {
|
|
Fixture f;
|
|
f.host();
|
|
check(f.lobbies.status().listed && f.api->exposed.contains(9001), "Host not publicly listed");
|
|
f.lobbies.update_host(true, multiplayer_lobby_player_limit, map_name, 1200000);
|
|
check(!f.lobbies.status().listed && f.api->metadata[9001]["rs_players"] == std::to_string(multiplayer_lobby_player_limit),
|
|
"Occupied host stayed open");
|
|
f.lobbies.update_host(true, 1, map_name, 1300000);
|
|
check(f.lobbies.status().listed, "Guest departure did not reopen listing");
|
|
f.lobbies.update_host(false, 2, "", 1400000);
|
|
check(!f.lobbies.status().listed && f.api->metadata[9001]["rs_open"] == "0" && f.api->left.empty(),
|
|
"Host travel left a stale public listing or destroyed the lobby");
|
|
const auto old_code = f.api->metadata[9001]["rs_code"];
|
|
f.lobbies.update_host(true, 2, "Levels/Root|Levels/NewMap", 1500000);
|
|
check(f.lobbies.status().listed && f.api->metadata[9001]["rs_map"] == "Levels/Root|Levels/NewMap" &&
|
|
f.api->metadata[9001]["rs_code"] == old_code, "Host travel did not reopen the same lobby on the new map");
|
|
f.lobbies.stop();
|
|
check(!f.lobbies.status().listed && f.api->left == std::vector<std::uint64_t>{9001},
|
|
"Host stop leaked lobby");
|
|
Fixture late;
|
|
late.lobbies.host(format_invite({host_id, 123}));
|
|
late.lobbies.update_host(true, 1, map_name, 1000000);
|
|
late.lobbies.stop();
|
|
late.api->results[late.api->create_call] = {true, 9002, 0, 1};
|
|
late.lobbies.tick(2000000);
|
|
check(late.api->left == std::vector<std::uint64_t>{9002} && !late.lobbies.status().listed,
|
|
"Late create after cancellation leaked a lobby");
|
|
Fixture failure;
|
|
failure.api->fail_write = true;
|
|
failure.host();
|
|
check(!failure.lobbies.status().listed && failure.api->left.size() == 1,
|
|
"Failed publication left a partial public lobby");
|
|
}
|
|
void join_tests() {
|
|
Fixture f;
|
|
f.list();
|
|
bool rejected{};
|
|
try {
|
|
f.lobbies.join(9001, host_id, 1200000);
|
|
} catch (...) {
|
|
rejected = true;
|
|
}
|
|
check(rejected && !f.api->join_call, "Self join reached Steam");
|
|
// Discovery no longer needs a locally loaded map; the host supplies it over P2P.
|
|
f.lobbies.join(9001, guest_id, 1300000);
|
|
f.api->results[f.api->join_call] = {true, 9001, 0, 1};
|
|
f.lobbies.tick(1400000);
|
|
const auto joined = f.lobbies.take_join();
|
|
check(joined && joined->owner == host_id && f.api->left.size() == 1,
|
|
"Browser join failed or retained directory membership");
|
|
check(!f.lobbies.take_join(), "Browser join was delivered twice");
|
|
Fixture stale;
|
|
stale.list();
|
|
stale.lobbies.join(9001, guest_id, 1300000);
|
|
stale.api->metadata[9001]["rs_open"] = "0";
|
|
stale.api->results[stale.api->join_call] = {true, 9001, 0, 1};
|
|
stale.lobbies.tick(1400000);
|
|
check(!stale.lobbies.take_join() && stale.api->left.size() == 1, "Stale browser record reached P2P");
|
|
Fixture cancelled;
|
|
cancelled.list();
|
|
cancelled.lobbies.join(9001, guest_id, 1300000);
|
|
cancelled.lobbies.stop();
|
|
cancelled.api->results[cancelled.api->join_call] = {true, 9001, 0, 1};
|
|
cancelled.lobbies.tick(1400000);
|
|
check(!cancelled.lobbies.take_join() && cancelled.api->left.size() == 1,
|
|
"Cancelled join was delivered or leaked");
|
|
Fixture timeout;
|
|
timeout.list();
|
|
timeout.lobbies.join(9001, guest_id, 1300000);
|
|
timeout.lobbies.tick(30000000);
|
|
check(!timeout.lobbies.status().joining, "Timed-out join remained busy");
|
|
timeout.api->results[timeout.api->join_call] = {true, 9001, 0, 1};
|
|
timeout.lobbies.tick(31000000);
|
|
check(!timeout.lobbies.take_join() && timeout.api->left.size() == 1,
|
|
"Late timed-out join leaked membership");
|
|
Fixture bounded;
|
|
bounded.api->record();
|
|
bounded.api->ids = {9001};
|
|
bounded.lobbies.refresh(1000000);
|
|
bounded.api->results[bounded.api->list_call] = {true, 0, 1000000, 1};
|
|
bounded.lobbies.tick(1100000);
|
|
check(bounded.api->at_calls == 50 && bounded.lobbies.status().rows.size() == 1,
|
|
"Search result bounds or duplicate handling failed");
|
|
}
|
|
void friend_join_tests() {
|
|
Fixture f;
|
|
f.api->record(); // Friend joining does not require opening the browser first.
|
|
f.lobbies.join_friend(9001, guest_id, 1000000);
|
|
f.api->results[f.api->join_call] = {true, 9001, 0, 1};
|
|
f.lobbies.tick(1100000);
|
|
const auto joined = f.lobbies.take_join();
|
|
check(joined && joined->owner == host_id && f.api->left.size() == 1,
|
|
"Friend join must verify the host and release temporary Steam membership");
|
|
for (const auto& [key, value] : std::map<std::string, std::string>{
|
|
{"rs_password", "1"}, {"rs_open", "0"}, {"rs_build", "other-build"},
|
|
{"rs_protocol", "999"}, {"rs_players", "8"}, {"rs_code", "malformed"}}) {
|
|
Fixture rejected;
|
|
rejected.api->record(); rejected.api->metadata[9001][key] = value;
|
|
rejected.lobbies.join_friend(9001, guest_id, 1000000);
|
|
rejected.api->results[rejected.api->join_call] = {true, 9001, 0, 1};
|
|
rejected.lobbies.tick(1100000);
|
|
check(!rejected.lobbies.take_join() && rejected.api->left.size() == 1,
|
|
"Incompatible, closed, full or password-protected friend lobby must not start P2P");
|
|
}
|
|
Fixture wrong_owner;
|
|
wrong_owner.api->record(); wrong_owner.api->owners[9001] = guest_id + 1;
|
|
wrong_owner.lobbies.join_friend(9001, guest_id, 1000000);
|
|
wrong_owner.api->results[wrong_owner.api->join_call] = {true, 9001, 0, 1};
|
|
wrong_owner.lobbies.tick(1100000);
|
|
check(!wrong_owner.lobbies.take_join(), "Friend join cannot use a forged advertised host");
|
|
Fixture cancelled;
|
|
cancelled.api->record(); cancelled.lobbies.join_friend(9001, guest_id, 1000000);
|
|
cancelled.lobbies.stop();
|
|
cancelled.api->results[cancelled.api->join_call] = {true, 9001, 0, 1};
|
|
cancelled.lobbies.tick(1100000);
|
|
check(!cancelled.lobbies.take_join() && cancelled.api->left.size() == 1,
|
|
"Cancelled friend joins must release late membership");
|
|
}
|
|
} // namespace
|
|
int main() {
|
|
try {
|
|
metadata_tests();
|
|
host_tests();
|
|
join_tests();
|
|
friend_join_tests();
|
|
std::cout << "Steam lobby lifecycle regressions passed.\n";
|
|
} catch (const std::exception &e) {
|
|
std::cerr << e.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|