mirror of
https://github.com/Swordpdf/ProsperoEden.git
synced 2026-10-19 04:09:47 -05:00
* Validate Eden room packets and add offline interoperability checks * Bound proxy decompression and receive queues * Add asynchronous room controller and cancellable connections * Wire room settings and lifecycle into the launcher * Bound outgoing room queues and test stalled peers * Own room receive callbacks per proxy socket * Support proxy polling and release sockets between games * Retain guest sockets across descriptor changes * Record offline qualification and freeze fresh candidates * Record room server and read-only console preflight evidence * Show room members and reported games in a scrollable list * Document Tailscale capabilities and verify room rejection recovery * Record remaining hardware validation prerequisites * Record successful room transport over Tailscale * Record verified console deployment for operator testing * Use a supported keyboard type for masked password entry * Remember room passwords and report connection failures * Report ENet handshake errors and packet counts * Parse numeric room addresses before invoking the resolver * Record operator-confirmed PS5 room connection * Use the room virtual interface for guest wireless networking * Compose room interfaces with the native getifaddrs adaptation * Exercise LDN discovery and peer connection through a real room * Record PS5 Mario Kart wireless lobby validation * Record live LDN traffic, recovery and Tailscale component validation * Prepare official desktop reference and correct room interface guidance * Record public room connection and native DNS failure * Show multiplayer connection status on home and settings * Make external Tailscale qualification optional * Invalidate network compiler cache for generated room headers * Use the pinned Boost version in multiplayer host checks * Show room server ping in the game overlay * Record verified ping overlay deployment to PS5 * Respect detailed logging settings for multiplayer diagnostics * Make the room socket non-blocking on the console so the member loop can send the join request * Add public room browser beside private multiplayer connection * Keep multiplayer destination choice selectable while connected * Record local public browser validation and deferred console test * Explain multiplayer connection failures in a persistent UI panel * Define idle-only multiplayer validation on console .40 * Record console .40 public directory and private room validation * Update public room notes for PR publication * Keep the saved private room on public joins, save the private form as typed, and harden the public room list * Translate the multiplayer and public room texts in all 29 languages
697 lines
34 KiB
C++
697 lines
34 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Exercise the real upstream room server and the derived client over loopback.
|
|
#include <atomic>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <thread>
|
|
#include "common/logging.h"
|
|
#include "multiplayer_logging.h"
|
|
#include "common/assert.h"
|
|
#include "common/settings.h"
|
|
#include "core/hle/service/sockets/sockets_translate.h"
|
|
#include "network/network.h"
|
|
#include "core/internal_network/socket_proxy.h"
|
|
#include "multiplayer_proxy.h"
|
|
#include "multiplayer.h"
|
|
#include "multiplayer_session.h"
|
|
#include "multiplayer_descriptors.h"
|
|
#include "enet/enet.h"
|
|
#include "network/packet.h"
|
|
#include "core/internal_network/network_interface.h"
|
|
#include "core/hle/service/ldn/lan_discovery.h"
|
|
|
|
static std::atomic<bool> reject_host_lookup{};
|
|
extern "C" int __real_enet_address_set_host(ENetAddress*, const char*);
|
|
extern "C" int __wrap_enet_address_set_host(ENetAddress* address, const char* name) {
|
|
if (reject_host_lookup.load()) return -1;
|
|
return __real_enet_address_set_host(address, name);
|
|
}
|
|
|
|
// Compile production BSD operations without the unrelated IPC framework.
|
|
namespace Service::Sockets {
|
|
class BSD_USA {
|
|
public:
|
|
using FileDescriptor = Eden::Multiplayer::SocketDescriptor;
|
|
static constexpr auto MAX_FD = Eden::Multiplayer::SocketDescriptors::Capacity;
|
|
static inline std::atomic<unsigned> live_instances{};
|
|
static inline Eden::Multiplayer::SocketDescriptors file_descriptors;
|
|
bool is_user = true;
|
|
BSD_USA() { ++live_instances; }
|
|
~BSD_USA();
|
|
std::pair<s32, Errno> SocketImpl(Domain, Type, Protocol);
|
|
std::pair<s32, Errno> PollImpl(std::vector<u8>&, std::span<const u8>, s32, s32);
|
|
std::pair<s32, Errno> AcceptImpl(s32, std::vector<u8>&);
|
|
Errno CloseImpl(s32);
|
|
std::variant<s32, Errno> DuplicateSocketImpl(s32);
|
|
};
|
|
#include "bsd_helpers.cpp"
|
|
#include "bsd_lifetime.cpp"
|
|
#include "multiplayer_bsd.inc"
|
|
}
|
|
|
|
void CheckBsdService() {
|
|
using namespace Service::Sockets;
|
|
BSD_USA bsd;
|
|
Eden::Multiplayer::guest_socket_mode = 1;
|
|
const auto [fd, error] = bsd.SocketImpl(Domain::INET, Type::DGRAM, Protocol::UDP);
|
|
assert(fd == 0 && error == Errno::SUCCESS);
|
|
const auto duplicate = std::get<s32>(bsd.DuplicateSocketImpl(fd));
|
|
assert(duplicate == 1);
|
|
assert(bsd.CloseImpl(duplicate) == Errno::SUCCESS);
|
|
assert(bsd.file_descriptors.Get(fd)->socket->IsOpened());
|
|
std::array<PollFD, 3> input{{{fd, PollEvents::Out, {}}, {128, PollEvents::In, {}}, {-1, PollEvents::In, {}}}};
|
|
std::vector<u8> read(sizeof(input)), written(sizeof(input));
|
|
std::memcpy(read.data(), input.data(), read.size());
|
|
assert(bsd.PollImpl(written, read, 3, 0).first == 2);
|
|
std::memcpy(input.data(), written.data(), written.size());
|
|
assert(input[0].revents == PollEvents::Out && input[1].revents == PollEvents::Nval &&
|
|
input[2].revents == PollEvents{});
|
|
assert(bsd.PollImpl(written, {}, 1, 0).second == Errno::INVAL);
|
|
assert(bsd.PollImpl(written, read, 129, 0).second == Errno::INVAL);
|
|
assert(bsd.PollImpl(written, read, 1, -2).second == Errno::INVAL);
|
|
std::vector<u8> empty;
|
|
assert(bsd.PollImpl(empty, read, 1, 0).second == Errno::INVAL);
|
|
assert(bsd.AcceptImpl(fd, written).second == Errno::NOTCONN);
|
|
std::weak_ptr<Network::SocketBase> released = bsd.file_descriptors.Get(fd)->socket;
|
|
input[0] = {fd, PollEvents::In, {}};
|
|
std::memcpy(read.data(), input.data(), read.size());
|
|
std::jthread close([&] {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
assert(bsd.CloseImpl(fd) == Errno::SUCCESS);
|
|
});
|
|
assert(bsd.PollImpl(written, read, 1, -1).first == 1);
|
|
close.join();
|
|
std::memcpy(input.data(), written.data(), sizeof(PollFD));
|
|
assert(input[0].revents == PollEvents::Nval && released.expired());
|
|
assert(bsd.CloseImpl(fd) == Errno::BADF);
|
|
assert(std::get<Errno>(bsd.DuplicateSocketImpl(128)) == Errno::BADF);
|
|
Eden::Multiplayer::guest_socket_mode = -1;
|
|
}
|
|
|
|
void CheckBsdLifetime() {
|
|
using Service::Sockets::BSD_USA;
|
|
auto first = std::make_unique<BSD_USA>();
|
|
auto second = std::make_unique<BSD_USA>();
|
|
auto last = std::make_unique<BSD_USA>();
|
|
auto socket = std::make_shared<Network::ProxySocket>();
|
|
socket->Initialize(Network::Domain::INET, Network::Type::DGRAM, Network::Protocol::UDP);
|
|
assert(BSD_USA::file_descriptors.Insert(socket, false) == 0);
|
|
assert(BSD_USA::file_descriptors.Duplicate(0).first == 1);
|
|
first.reset();
|
|
second.reset();
|
|
assert(socket->IsOpened() && BSD_USA::file_descriptors.Get(0));
|
|
last.reset();
|
|
assert(!socket->IsOpened() && BSD_USA::live_instances == 0);
|
|
for (int fd = 0; fd < 128; ++fd) assert(!BSD_USA::file_descriptors.Get(fd));
|
|
std::weak_ptr<Network::ProxySocket> released = socket;
|
|
socket.reset();
|
|
assert(released.expired());
|
|
BSD_USA next_game;
|
|
for (int fd = 0; fd < 128; ++fd) assert(!BSD_USA::file_descriptors.Get(fd));
|
|
}
|
|
|
|
void CheckDescriptors() {
|
|
using namespace Network;
|
|
Eden::Multiplayer::SocketDescriptors table;
|
|
const auto make = [] {
|
|
auto socket = std::make_shared<ProxySocket>();
|
|
socket->Initialize(Domain::INET, Type::DGRAM, Protocol::UDP);
|
|
return socket;
|
|
};
|
|
assert(!table.Get(-1) && !table.Get(128));
|
|
assert(table.Close(128) == Errno::BADF && table.Duplicate(-1).second == Errno::BADF);
|
|
assert(table.Insert(nullptr, false) == -1);
|
|
auto original = make();
|
|
assert(table.Insert(original, false) == 0);
|
|
auto retained = table.Get(0);
|
|
assert(table.Duplicate(0).first == 1);
|
|
retained->flags = FLAG_O_NONBLOCK;
|
|
assert(table.Get(1)->flags == FLAG_O_NONBLOCK);
|
|
assert(table.Close(0) == Errno::SUCCESS && original->IsOpened());
|
|
auto replacement = make();
|
|
assert(table.Insert(replacement, false) == 0);
|
|
assert(table.Close(1) == Errno::SUCCESS && !original->IsOpened());
|
|
assert(retained->socket == original && table.Get(0)->socket == replacement);
|
|
table.Clear();
|
|
assert(!replacement->IsOpened());
|
|
for (int fd = 0; fd < 128; ++fd) assert(table.Insert(make(), false) == fd);
|
|
assert(table.Insert(make(), false) == -1 && table.Duplicate(0).second == Errno::MFILE);
|
|
table.Clear();
|
|
std::vector<std::jthread> workers;
|
|
for (int thread = 0; thread < 4; ++thread) workers.emplace_back([&] {
|
|
for (int i = 0; i < 100; ++i) {
|
|
auto socket = make();
|
|
const auto fd = table.Insert(socket, false);
|
|
assert(fd >= 0);
|
|
auto snapshot = table.Get(fd);
|
|
const auto [duplicate, error] = table.Duplicate(fd);
|
|
assert(error == Errno::SUCCESS && snapshot->socket == socket);
|
|
assert(table.Close(fd) == Errno::SUCCESS && socket->IsOpened());
|
|
assert(table.Close(duplicate) == Errno::SUCCESS && !socket->IsOpened());
|
|
assert(snapshot->socket == socket);
|
|
}
|
|
});
|
|
workers.clear();
|
|
for (int fd = 0; fd < 128; ++fd) assert(!table.Get(fd));
|
|
}
|
|
|
|
template <typename Predicate>
|
|
void Wait(Predicate predicate) {
|
|
const auto end = std::chrono::steady_clock::now() + std::chrono::seconds{8};
|
|
while (!predicate() && std::chrono::steady_clock::now() < end)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{5});
|
|
assert(predicate());
|
|
}
|
|
|
|
void CheckProxySocket() {
|
|
Network::ProxySocket socket;
|
|
assert(socket.Initialize(Network::Domain::INET, Network::Type::DGRAM,
|
|
Network::Protocol::UDP) == Network::Errno::SUCCESS);
|
|
assert(socket.Bind({Network::Domain::INET, {192, 168, 0, 2}, 1234}) == Network::Errno::SUCCESS);
|
|
socket.SetNonBlock(true);
|
|
const std::vector<u8> oversized(Eden::Multiplayer::MaxProxyPayloadBytes + 1);
|
|
assert(socket.SendTo(0, oversized, nullptr).second == Network::Errno::MSGSIZE);
|
|
Network::ProxyPacket packet{};
|
|
packet.local_endpoint = {Network::Domain::INET, {192, 168, 0, 1}, 4321};
|
|
packet.remote_endpoint = {Network::Domain::INET, {192, 168, 0, 2}, 1234};
|
|
packet.protocol = Network::Protocol::UDP;
|
|
const auto payload = [&](const std::vector<u8>& bytes) {
|
|
packet.data.resize(ZSTD_compressBound(bytes.size()));
|
|
const auto size = ZSTD_compress(packet.data.data(), packet.data.size(),
|
|
bytes.data(), bytes.size(), 1);
|
|
assert(!ZSTD_isError(size));
|
|
packet.data.resize(size);
|
|
};
|
|
payload({4, 5, 6});
|
|
socket.HandleProxyPacket(packet);
|
|
std::vector<u8> bytes(Eden::Multiplayer::MaxProxyPayloadBytes);
|
|
Network::SockAddrIn sender{};
|
|
auto result = socket.RecvFrom(0, bytes, &sender);
|
|
assert(result.first == 3 && bytes[0] == 4 && bytes[2] == 6 && sender.portno == 4321);
|
|
packet.data = {1, 2, 3};
|
|
socket.HandleProxyPacket(packet);
|
|
assert(socket.RecvFrom(0, bytes, nullptr).second == Network::Errno::AGAIN);
|
|
|
|
// Empty datagrams still occupy a queue slot, so both count and byte limits matter.
|
|
payload({});
|
|
for (std::size_t i = 0; i < Eden::Multiplayer::MaxProxyQueuePackets + 1; ++i)
|
|
socket.HandleProxyPacket(packet);
|
|
for (std::size_t i = 0; i < Eden::Multiplayer::MaxProxyQueuePackets; ++i)
|
|
assert(socket.RecvFrom(0, bytes, nullptr).first == 0);
|
|
assert(socket.RecvFrom(0, bytes, nullptr).second == Network::Errno::AGAIN);
|
|
payload(bytes);
|
|
const auto capacity = Eden::Multiplayer::MaxProxyQueueBytes / bytes.size();
|
|
for (std::size_t i = 0; i <= capacity; ++i) socket.HandleProxyPacket(packet);
|
|
for (std::size_t i = 0; i < capacity; ++i)
|
|
assert(socket.RecvFrom(0, bytes, nullptr).first == static_cast<s32>(bytes.size()));
|
|
assert(socket.RecvFrom(0, bytes, nullptr).second == Network::Errno::AGAIN);
|
|
payload({7});
|
|
socket.HandleProxyPacket(packet);
|
|
assert(socket.RecvFrom(0, bytes, nullptr).first == 1 && bytes[0] == 7);
|
|
|
|
payload({1, 2, 3});
|
|
socket.HandleProxyPacket(packet);
|
|
assert(socket.RecvFrom(0, std::span{bytes}.first(1), nullptr).second == Network::Errno::MSGSIZE);
|
|
assert(socket.RecvFrom(0, bytes, nullptr).second == Network::Errno::AGAIN);
|
|
|
|
Network::ProxySocket stream;
|
|
assert(stream.Initialize(Network::Domain::INET, Network::Type::STREAM,
|
|
Network::Protocol::TCP) == Network::Errno::SUCCESS);
|
|
assert(stream.Bind(packet.remote_endpoint) == Network::Errno::SUCCESS);
|
|
stream.SetNonBlock(true);
|
|
packet.protocol = Network::Protocol::TCP;
|
|
stream.HandleProxyPacket(packet);
|
|
// ReceivePacket is called under the socket lock in production; this test is single-threaded.
|
|
assert(stream.ReceivePacket(Network::FLAG_MSG_PEEK, bytes, nullptr, 1).first == 1);
|
|
assert(stream.RecvFrom(0, std::span{bytes}.first(1), nullptr).first == 1 && bytes[0] == 1);
|
|
assert(stream.RecvFrom(0, bytes, nullptr).first == 2 && bytes[0] == 2 && bytes[1] == 3);
|
|
assert(stream.RecvFrom(0, bytes, nullptr).second == Network::Errno::AGAIN);
|
|
// A full-size packet must still fit after partial reads and a truncated datagram.
|
|
packet.protocol = Network::Protocol::UDP;
|
|
payload(bytes);
|
|
for (std::size_t i = 0; i < capacity; ++i) socket.HandleProxyPacket(packet);
|
|
for (std::size_t i = 0; i < capacity; ++i)
|
|
assert(socket.RecvFrom(0, bytes, nullptr).first == static_cast<s32>(bytes.size()));
|
|
socket.SetNonBlock(false);
|
|
Network::Errno closed_result{};
|
|
const auto close_start = std::chrono::steady_clock::now();
|
|
std::jthread receive([&] { closed_result = socket.RecvFrom(0, bytes, nullptr).second; });
|
|
socket.Close();
|
|
receive.join();
|
|
assert(closed_result == Network::Errno::BADF);
|
|
assert(std::chrono::steady_clock::now() - close_start < std::chrono::seconds{2});
|
|
}
|
|
|
|
void CheckProxyPoll() {
|
|
using namespace Network;
|
|
ProxySocket proxy;
|
|
proxy.Initialize(Domain::INET, Type::DGRAM, Protocol::UDP);
|
|
const SockAddrIn endpoint{Domain::INET, {127, 0, 0, 1}, 1234};
|
|
proxy.Bind(endpoint);
|
|
std::vector<PollFD> fds{{&proxy, PollEvents::In, {}}};
|
|
assert(Poll(fds, 0).first == 0 && fds[0].revents == PollEvents{});
|
|
fds[0].events = PollEvents::Out;
|
|
assert(Poll(fds, 0).first == 1 && fds[0].revents == PollEvents::Out);
|
|
fds[0].events = PollEvents::In;
|
|
const auto start = std::chrono::steady_clock::now();
|
|
assert(Poll(fds, 20).first == 0);
|
|
assert(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds{20});
|
|
assert(std::chrono::steady_clock::now() - start < std::chrono::seconds{2});
|
|
|
|
ProxyPacket packet{};
|
|
packet.remote_endpoint = endpoint;
|
|
packet.protocol = Protocol::UDP;
|
|
const std::array<u8, 1> bytes{42};
|
|
packet.data.resize(ZSTD_compressBound(bytes.size()));
|
|
packet.data.resize(ZSTD_compress(packet.data.data(), packet.data.size(), bytes.data(), bytes.size(), 1));
|
|
std::jthread arrival([&] {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
proxy.HandleProxyPacket(packet);
|
|
});
|
|
assert(Poll(fds, -1).first == 1 && fds[0].revents == PollEvents::In);
|
|
arrival.join();
|
|
assert(Poll(fds, 0).first == 1); // Poll never consumes the packet.
|
|
std::array<u8, 1> received{};
|
|
assert(proxy.RecvFrom(0, received, nullptr).first == 1 && received == bytes);
|
|
assert(Poll(fds, 0).first == 0);
|
|
|
|
Socket native, sender;
|
|
assert(native.Initialize(Domain::INET, Type::DGRAM, Protocol::UDP) == Errno::SUCCESS);
|
|
assert(sender.Initialize(Domain::INET, Type::DGRAM, Protocol::UDP) == Errno::SUCCESS);
|
|
assert(native.Bind({Domain::INET, {127, 0, 0, 1}, 0}) == Errno::SUCCESS);
|
|
const auto [address, address_error] = native.GetSockName();
|
|
assert(address_error == Errno::SUCCESS);
|
|
fds.push_back({&native, PollEvents::In, {}});
|
|
assert(sender.SendTo(0, bytes, &address).first == 1);
|
|
assert(Poll(fds, 1000).first == 1);
|
|
assert(fds[0].revents == PollEvents{} && fds[1].revents == PollEvents::In);
|
|
assert(native.RecvFrom(0, received, nullptr).first == 1);
|
|
proxy.HandleProxyPacket(packet);
|
|
assert(sender.SendTo(0, bytes, &address).first == 1);
|
|
assert(Poll(fds, 1000).first == 2);
|
|
assert(proxy.RecvFrom(0, received, nullptr).first == 1);
|
|
assert(native.RecvFrom(0, received, nullptr).first == 1);
|
|
assert(Poll(fds, 0).first == 0); // No stale readiness from the previous native poll.
|
|
std::vector<PollFD> native_only{{&native, PollEvents::In, {}}};
|
|
assert(Poll(native_only, 0).first == 0);
|
|
|
|
std::jthread interrupt([] {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
CancelPendingSocketOperations();
|
|
});
|
|
assert(Poll(fds, -1).first == 0);
|
|
interrupt.join();
|
|
RestartSocketOperations();
|
|
std::jthread close([&] {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
proxy.Close();
|
|
});
|
|
assert(Poll(fds, -1).first == 1 && fds[0].revents == PollEvents::Nval);
|
|
close.join();
|
|
}
|
|
|
|
void CheckController(u16 port) {
|
|
using namespace Eden::Multiplayer;
|
|
RoomClient client;
|
|
Network::Room room;
|
|
assert(room.Create("Controller test", "", "127.0.0.1", port, "secret", 2,
|
|
"", {}, std::make_unique<Network::VerifyUser::NullBackend>()));
|
|
Connection connection{"localhost", port, "PlayerOne", "wrong"};
|
|
auto invalid = connection;
|
|
invalid.nickname = "x";
|
|
assert(!client.Connect(invalid));
|
|
assert(client.Connect(connection));
|
|
assert(!client.BeginGame());
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Failed; });
|
|
assert(client.GetSnapshot().error == "Incorrect password. Enter the room password again; it is case-sensitive.");
|
|
connection.password = "secret";
|
|
assert(client.Connect(connection));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
const auto snapshot = client.GetSnapshot();
|
|
assert(snapshot.room == "Controller test" && snapshot.members == std::vector<std::string>{"PlayerOne"});
|
|
auto member = Network::GetRoomMember().lock();
|
|
assert(member);
|
|
Wait([&] { return member->GetPingMilliseconds() >= 0; });
|
|
assert(member->GetPingMilliseconds() <= 9999);
|
|
const auto previous_interface = Settings::values.network_interface.GetValue();
|
|
Settings::values.network_interface.SetValue("missing-host-adapter-for-room-test");
|
|
const auto room_interface = Network::GetSelectedNetworkInterface();
|
|
assert(room_interface && room_interface->kind == Network::HostAdapterKind::Wifi);
|
|
assert(Network::TranslateIPv4(room_interface->ip_address) == member->GetFakeIpAddress());
|
|
assert((Network::TranslateIPv4(room_interface->subnet_mask) == Network::IPv4Address{255, 255, 0, 0}));
|
|
{
|
|
Service::LDN::LANDiscovery ldn;
|
|
assert(ldn.Initialize([] {}).IsSuccess());
|
|
assert(ldn.OpenAccessPoint().IsSuccess());
|
|
Service::LDN::NetworkConfig config{};
|
|
config.node_count_max = 8;
|
|
assert(ldn.CreateNetwork({}, {}, config).IsSuccess());
|
|
Service::LDN::NetworkInfo info{};
|
|
assert(ldn.GetNetworkInfo(info).IsSuccess());
|
|
auto ip = member->GetFakeIpAddress();
|
|
std::reverse(ip.begin(), ip.end());
|
|
assert(info.ldn.nodes[0].ipv4_address == ip);
|
|
assert(info.ldn.node_count == 1);
|
|
const auto receive = member->BindOnLdnPacketReceived(
|
|
[&](const auto& packet) { ldn.ReceivePacket(packet); });
|
|
Network::RoomMember peer;
|
|
std::atomic<bool> discovered{}, connected{}, destroyed{};
|
|
const auto peer_receive = peer.BindOnLdnPacketReceived([&](const auto& packet) {
|
|
if (packet.type == Network::LDNPacketType::DestroyNetwork) destroyed = true;
|
|
if (packet.type != Network::LDNPacketType::ScanResp &&
|
|
packet.type != Network::LDNPacketType::SyncNetwork) return;
|
|
assert(packet.data.size() == sizeof(Service::LDN::NetworkInfo));
|
|
Service::LDN::NetworkInfo received{};
|
|
std::memcpy(&received, packet.data.data(), sizeof(received));
|
|
assert(received.ldn.nodes[0].ipv4_address == ip);
|
|
if (packet.type == Network::LDNPacketType::ScanResp) discovered = true;
|
|
if (packet.type == Network::LDNPacketType::SyncNetwork && received.ldn.node_count == 2)
|
|
connected = true;
|
|
});
|
|
peer.Join("LdnPeer", "127.0.0.1", port, 0, Network::NoPreferredIP, "secret");
|
|
Wait([&] { return peer.GetState() == Network::RoomMember::State::Joined; });
|
|
peer.SendLdnPacket({Network::LDNPacketType::Scan, peer.GetFakeIpAddress(), {}, true, {}});
|
|
Wait([&] { return discovered.load(); });
|
|
Service::LDN::NodeInfo node{};
|
|
node.ipv4_address = peer.GetFakeIpAddress();
|
|
std::reverse(node.ipv4_address.begin(), node.ipv4_address.end());
|
|
node.mac_address.raw = {2, 0, 1, 2, 3, 4};
|
|
Network::LDNPacket connect{Network::LDNPacketType::Connect, peer.GetFakeIpAddress(),
|
|
member->GetFakeIpAddress(), false, {}};
|
|
connect.data.resize(sizeof(node));
|
|
std::memcpy(connect.data.data(), &node, sizeof(node));
|
|
peer.SendLdnPacket(connect);
|
|
Wait([&] { return connected.load(); });
|
|
assert(ldn.Finalize().IsSuccess());
|
|
Wait([&] { return destroyed.load(); });
|
|
member->Unbind(receive);
|
|
peer.Unbind(peer_receive);
|
|
peer.Leave();
|
|
Wait([&] { return client.GetSnapshot().members == std::vector<std::string>{"PlayerOne"}; });
|
|
}
|
|
member->SendGameInfo({"Example game", 1, "1.2.3"});
|
|
Wait([&] { return client.GetSnapshot().members ==
|
|
std::vector<std::string>{"PlayerOne - Example game (1.2.3)"}; });
|
|
member->SendGameInfo({});
|
|
Wait([&] { return client.GetSnapshot().members == std::vector<std::string>{"PlayerOne"}; });
|
|
assert(!client.Connect(connection));
|
|
assert(client.BeginGame());
|
|
assert(guest_socket_mode == 1 && !client.GameConnectionLost());
|
|
client.Leave();
|
|
assert(client.GetSnapshot().phase == Phase::Connected);
|
|
client.EndGame();
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
|
|
assert(!Network::GetSelectedNetworkInterface());
|
|
assert(member->GetPingMilliseconds() == -1);
|
|
Settings::values.network_interface.SetValue(previous_interface);
|
|
assert(client.GetSnapshot().members.empty());
|
|
assert(client.BeginGame());
|
|
assert(guest_socket_mode == 0 && !client.Connect(connection));
|
|
client.EndGame();
|
|
assert(client.Connect(connection));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
assert(client.BeginGame());
|
|
room.Destroy();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Failed; });
|
|
assert(client.GetSnapshot().error == "Connection lost. Check your internet connection, then rejoin the room before restarting Local Wireless in the game.");
|
|
assert(client.GameConnectionLost() && guest_socket_mode == 1);
|
|
assert(member->GetPingMilliseconds() == -1);
|
|
assert(!client.Connect(connection));
|
|
client.EndGame();
|
|
connection.host = "127.0.0.1";
|
|
assert(client.Connect(connection));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{100});
|
|
const auto start = std::chrono::steady_clock::now();
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
|
|
assert(std::chrono::steady_clock::now() - start < std::chrono::seconds{2});
|
|
assert(client.Connect(connection));
|
|
// Destruction must also cancel a join without waiting for its five-second timeout.
|
|
}
|
|
|
|
void CheckSocketDelivery(u16 port) {
|
|
using namespace Eden::Multiplayer;
|
|
RoomClient client;
|
|
Network::Room room;
|
|
assert(room.Create("Socket delivery", "", "127.0.0.1", port, "", 2,
|
|
"", {}, std::make_unique<Network::VerifyUser::NullBackend>()));
|
|
assert(client.Connect({"127.0.0.1", port, "PlayerOne", ""}));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
Network::RoomMember peer;
|
|
peer.Join("PlayerTwo", "127.0.0.1", port);
|
|
Wait([&] { return peer.GetState() == Network::RoomMember::State::Joined; });
|
|
Network::ProxyPacket packet{};
|
|
packet.local_endpoint = {Network::Domain::INET, peer.GetFakeIpAddress(), 4321};
|
|
packet.remote_endpoint = {Network::Domain::INET,
|
|
Network::GetRoomMember().lock()->GetFakeIpAddress(), 1234};
|
|
packet.protocol = Network::Protocol::UDP;
|
|
const u8 data[]{4, 5, 6};
|
|
packet.data.resize(ZSTD_compressBound(sizeof(data)));
|
|
packet.data.resize(ZSTD_compress(packet.data.data(), packet.data.size(), data, sizeof(data), 1));
|
|
const auto make_socket = [&] {
|
|
auto socket = std::make_shared<Network::ProxySocket>();
|
|
socket->Initialize(Network::Domain::INET, Network::Type::DGRAM, Network::Protocol::UDP);
|
|
socket->Bind(packet.remote_endpoint);
|
|
socket->SetNonBlock(true);
|
|
return socket;
|
|
};
|
|
auto socket = make_socket();
|
|
auto duplicate = socket;
|
|
assert(socket->IsOpened());
|
|
peer.SendProxyPacket(packet);
|
|
std::array<u8, 3> bytes{};
|
|
bool received = false;
|
|
Wait([&] {
|
|
if (!received) received = socket->RecvFrom(0, bytes, nullptr).first == 3;
|
|
return received;
|
|
});
|
|
assert(bytes == (std::array<u8, 3>{4, 5, 6}));
|
|
assert(duplicate->RecvFrom(0, bytes, nullptr).second == Network::Errno::AGAIN);
|
|
socket->Close();
|
|
assert(!duplicate->IsOpened());
|
|
assert(duplicate->RecvFrom(0, bytes, nullptr).second == Network::Errno::BADF);
|
|
duplicate.reset();
|
|
socket.reset();
|
|
// Destruction must unbind safely while the room thread is delivering packets.
|
|
std::jthread traffic([&](std::stop_token stop) {
|
|
while (!stop.stop_requested()) {
|
|
peer.SendProxyPacket(packet);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{1});
|
|
}
|
|
});
|
|
for (int i = 0; i < 50; ++i) {
|
|
auto transient = make_socket();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{2});
|
|
transient->Close();
|
|
}
|
|
traffic.request_stop();
|
|
traffic.join();
|
|
peer.Leave();
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
|
|
room.Destroy();
|
|
}
|
|
|
|
void CheckRejectedRoom(u16 port) {
|
|
using namespace Eden::Multiplayer;
|
|
RoomClient client;
|
|
Network::Room room;
|
|
assert(room.Create("Rejection test", "", "127.0.0.1", port, "", 2,
|
|
"", {}, std::make_unique<Network::VerifyUser::NullBackend>()));
|
|
Network::RoomMember occupied, second;
|
|
std::atomic<std::size_t> members{};
|
|
const auto info = occupied.BindOnRoomInformationChanged([&](const auto&) {
|
|
members = occupied.GetMemberInformation().size();
|
|
});
|
|
occupied.Join("Occupied", "127.0.0.1", port, 0, Network::NoPreferredIP, "");
|
|
Wait([&] { return occupied.GetState() == Network::RoomMember::State::Joined; });
|
|
assert(client.Connect({"127.0.0.1", port, "Occupied", ""}));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Failed; });
|
|
assert(client.GetSnapshot().error == "Nickname already in use. Enter a different nickname and try again.");
|
|
assert(client.Connect({"127.0.0.1", port, "Available", ""}));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle && members == 1; });
|
|
second.Join("SecondPlayer", "127.0.0.1", port, 0, Network::NoPreferredIP, "");
|
|
Wait([&] { return members == 2; });
|
|
assert(client.Connect({"127.0.0.1", port, "Available", ""}));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Failed; });
|
|
assert(client.GetSnapshot().error == "Room full. Wait for a free place or choose another room.");
|
|
second.Leave();
|
|
Wait([&] { return members == 1; });
|
|
assert(client.Connect({"127.0.0.1", port, "Available", ""}));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
|
|
occupied.Leave();
|
|
occupied.Unbind(info);
|
|
room.Destroy();
|
|
}
|
|
|
|
enum class TestRoom { OutOfOrder, Stalled, VersionMismatch, JoinTimeout };
|
|
|
|
void CheckTestRoom(u16 port, TestRoom mode) {
|
|
using namespace Eden::Multiplayer;
|
|
const bool stall = mode == TestRoom::Stalled;
|
|
RoomClient client;
|
|
ENetAddress address{};
|
|
assert(enet_address_set_host_ip(&address, "127.0.0.1") == 0);
|
|
address.port = port;
|
|
auto* server = enet_host_create(&address, 1, Network::NumChannels, 0, 0);
|
|
assert(server);
|
|
std::atomic<bool> paused{};
|
|
std::jthread replies([&](std::stop_token stop) {
|
|
while (!stop.stop_requested()) {
|
|
if (paused) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{10});
|
|
continue;
|
|
}
|
|
ENetEvent event{};
|
|
if (enet_host_service(server, &event, 10) <= 0) continue;
|
|
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
|
|
const bool join = event.packet->dataLength && event.packet->data[0] == Network::IdJoinRequest;
|
|
enet_packet_destroy(event.packet);
|
|
if (!join) continue;
|
|
// Keep ENet responsive, but never accept the player's room join.
|
|
if (mode == TestRoom::JoinTimeout) continue;
|
|
if (mode == TestRoom::VersionMismatch) {
|
|
Network::Packet reply;
|
|
reply.Write(u8{Network::IdVersionMismatch}).Write(u32{Network::network_version + 1});
|
|
auto* packet = enet_packet_create(reply.GetData(), reply.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
|
|
assert(enet_peer_send(event.peer, 0, packet) == 0);
|
|
enet_host_flush(server);
|
|
continue;
|
|
}
|
|
if (stall) {
|
|
Network::Packet info;
|
|
info.Write(u8{Network::IdRoomInformation}).Write(std::string{"Test room"})
|
|
.Write(std::string{}).Write(u32{2}).Write(port)
|
|
.Write(std::string{}).Write(std::string{}).Write(u32{1})
|
|
.Write(std::string{"PlayerOne"}).Write(Network::IPv4Address{192, 168, 0, 1})
|
|
.Write(std::string{}).Write(u64{0}).Write(std::string{})
|
|
.Write(std::string{}).Write(std::string{}).Write(std::string{});
|
|
auto* packet = enet_packet_create(info.GetData(), info.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
|
|
assert(enet_peer_send(event.peer, 0, packet) == 0);
|
|
}
|
|
// Without room information, the otherwise valid success must be rejected.
|
|
const u8 reply[]{Network::IdJoinSuccess, 192, 168, 0, 1};
|
|
auto* packet = enet_packet_create(reply, sizeof(reply), ENET_PACKET_FLAG_RELIABLE);
|
|
assert(enet_peer_send(event.peer, 0, packet) == 0);
|
|
enet_host_flush(server);
|
|
if (stall) paused = true;
|
|
}
|
|
}
|
|
});
|
|
assert(client.Connect({"127.0.0.1", port, "PlayerOne", ""}));
|
|
if (stall) {
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
auto member = Network::GetRoomMember().lock();
|
|
Network::ProxyPacket packet{};
|
|
packet.protocol = Network::Protocol::UDP;
|
|
packet.data.resize(Eden::Multiplayer::MaxProxyPayloadBytes / 2, 7);
|
|
// The peer has stopped processing ENet packets, including acknowledgements.
|
|
for (unsigned i = 0; i < 20 && client.GetSnapshot().phase == Phase::Connected; ++i) {
|
|
member->SendProxyPacket(packet);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{20});
|
|
}
|
|
}
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Failed; });
|
|
assert(client.GetSnapshot().error == (stall ? "Connection lost. Check your internet connection, then rejoin the room before restarting Local Wireless in the game." :
|
|
mode == TestRoom::JoinTimeout ? "Room join timed out. The server connection opened, but room acceptance did not finish. Retry; if it repeats, ask the host to check the server log." :
|
|
mode == TestRoom::VersionMismatch ? "Incompatible room version. Update ProsperoEden or choose a compatible room; the host may also need to update the server." : "Connection failed. Try again. If it repeats, enable detailed logging in Settings and share the log with support."));
|
|
paused = false;
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
|
|
replies.request_stop();
|
|
replies.join();
|
|
enet_host_destroy(server);
|
|
if (mode == TestRoom::VersionMismatch) {
|
|
Network::Room compatible;
|
|
assert(compatible.Create("Compatible retry", "", "127.0.0.1", port, "", 2,
|
|
"", {}, std::make_unique<Network::VerifyUser::NullBackend>()));
|
|
assert(client.Connect({"127.0.0.1", port, "PlayerOne", ""}));
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Connected; });
|
|
client.Leave();
|
|
Wait([&] { return client.GetSnapshot().phase == Phase::Idle; });
|
|
compatible.Destroy();
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
assert(argc == 2 || argc == 3);
|
|
Eden::Multiplayer::SetDetailedLogging(argc == 3);
|
|
const auto port = static_cast<u16>(std::strtoul(argv[1], nullptr, 10));
|
|
Common::Log::Initialize();
|
|
Network::NetworkInstance socket_network;
|
|
assert(Network::Init());
|
|
CheckProxySocket();
|
|
CheckBsdLifetime();
|
|
CheckDescriptors();
|
|
CheckBsdService();
|
|
CheckProxyPoll();
|
|
{
|
|
Network::Room room;
|
|
assert(room.Create("Offline test", "", "127.0.0.1", port, "secret", 2,
|
|
"", {}, std::make_unique<Network::VerifyUser::NullBackend>()));
|
|
Network::RoomMember first, second;
|
|
// Reproduce the console resolver failure even for a numeric address.
|
|
reject_host_lookup = true;
|
|
std::atomic<bool> rejected{}, scanned{}, proxied{}, lost{};
|
|
const auto errors = first.BindOnError([&](const auto& error) {
|
|
if (error == Network::RoomMember::Error::WrongPassword) rejected = true;
|
|
if (error == Network::RoomMember::Error::LostConnection) lost = true;
|
|
});
|
|
first.Join("PlayerOne", "127.0.0.1", port, 0, Network::NoPreferredIP, "wrong");
|
|
Wait([&] { return rejected.load(); });
|
|
first.Leave();
|
|
first.Join("PlayerOne", "127.0.0.1", port, 0, Network::NoPreferredIP, "secret");
|
|
second.Join("PlayerTwo", "127.0.0.1", port, 0, Network::NoPreferredIP, "secret");
|
|
const auto joined = [](const auto& member) {
|
|
return member.GetState() == Network::RoomMember::State::Joined;
|
|
};
|
|
Wait([&] { return joined(first) && joined(second); });
|
|
reject_host_lookup = false;
|
|
assert(first.GetFakeIpAddress() != second.GetFakeIpAddress());
|
|
const auto ldn_callback = second.BindOnLdnPacketReceived([&](const auto& packet) {
|
|
scanned = packet.type == Network::LDNPacketType::Scan && packet.broadcast;
|
|
});
|
|
const auto proxy_callback = second.BindOnProxyPacketReceived([&](const auto& packet) {
|
|
proxied = packet.data == std::vector<u8>{1, 2, 3} &&
|
|
packet.remote_endpoint.portno == 1234;
|
|
});
|
|
first.SendLdnPacket({Network::LDNPacketType::Scan, first.GetFakeIpAddress(), {}, true, {}});
|
|
Network::ProxyPacket packet{};
|
|
packet.local_endpoint = {Network::Domain::INET, first.GetFakeIpAddress(), 1234};
|
|
packet.remote_endpoint = {Network::Domain::INET, second.GetFakeIpAddress(), 1234};
|
|
packet.protocol = Network::Protocol::UDP;
|
|
packet.data = {1, 2, 3};
|
|
first.SendProxyPacket(packet);
|
|
Wait([&] { return scanned.load() && proxied.load(); });
|
|
second.Leave();
|
|
second.Unbind(ldn_callback);
|
|
second.Unbind(proxy_callback);
|
|
room.Destroy();
|
|
Wait([&] { return lost.load(); });
|
|
first.Leave();
|
|
first.Unbind(errors);
|
|
}
|
|
Network::Shutdown();
|
|
CheckController(port);
|
|
CheckSocketDelivery(port);
|
|
CheckRejectedRoom(port);
|
|
CheckTestRoom(port, TestRoom::OutOfOrder);
|
|
CheckTestRoom(port, TestRoom::Stalled);
|
|
CheckTestRoom(port, TestRoom::VersionMismatch);
|
|
CheckTestRoom(port, TestRoom::JoinTimeout);
|
|
const auto start = std::chrono::steady_clock::now();
|
|
{
|
|
Eden::Multiplayer::RoomClient client;
|
|
assert(client.Connect({"127.0.0.1", port, "PlayerOne", ""}));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds{100});
|
|
}
|
|
assert(std::chrono::steady_clock::now() - start < std::chrono::seconds{2});
|
|
}
|