--- a/rts/Net/GameServer.cpp +++ b/rts/Net/GameServer.cpp @@ -70,6 +70,7 @@ CONFIG(int, ServerSleepTime).defaultValue(5).description("Number of milliseconds to sleep per tick for the server thread. Lower values have marginally higher CPU load, while high values can introduce additional latency."); CONFIG(int, SpeedControl).defaultValue(1).minimumValue(1).maximumValue(2) .description("Sets how server adjusts speed according to player's load (CPU), 1: use average, 2: use highest"); +CONFIG(bool, WebbarRequireAllPlayers).defaultValue(false).description("Require every configured participant to finish loading before starting a private WebBAR match."); CONFIG(bool, AllowSpectatorJoin).defaultValue(true).dedicatedValue(false).description("allow any unauthenticated clients to join as spectator with any name, name will be prefixed with ~"); CONFIG(bool, WhiteListAdditionalPlayers).defaultValue(true); CONFIG(bool, ServerRecordDemos).defaultValue(false).dedicatedValue(true); @@ -2191,6 +2192,7 @@ void CGameServer::CheckForGameStart(bool forced) { assert(!gameHasStarted); + const bool requireAllPlayers = configHandler->GetBool("WebbarRequireAllPlayers"); bool allReady = true; // anyReady is needed for the case when *nodoby* is connected to the server yet, so in principle // everybody that is, is ready. Without that logic the game will start after XX seconds (see below) @@ -2200,6 +2202,11 @@ for (size_t a = static_cast(myGameSetup->numDemoPlayers); a < players.size(); a++) { if (players[a].isFromDemo) continue; + + if (requireAllPlayers && players[a].myState != GameParticipant::INGAME) { + allReady = false; + break; + } if (players[a].myState == GameParticipant::UNCONNECTED && serverStartTime + spring_secs(30) < spring_gettime()) { // autostart the game when 30 seconds have passed and everyone who managed to connect is ready @@ -2214,6 +2221,14 @@ } else { anyReady = true; } + } + + // Private rooms must not bypass readiness through timeout, force-start, or + // a participant disconnecting during the countdown. The room service owns + // a bounded loading deadline and cancellation. + if (requireAllPlayers && (!allReady || !anyReady)) { + readyTime = spring_notime; + return; } // msecs to wait until the game starts after all players are ready --- a/rts/System/Net/UDPConnection.cpp +++ b/rts/System/Net/UDPConnection.cpp @@ -257,6 +257,16 @@ Init(); } +#ifdef __EMSCRIPTEN__ +UDPConnection::UDPConnection(WebbarDatagram* transport, const ip::udp::endpoint& peerAddress) + : addr(peerAddress) + , sharedSocket(true) +{ + browserTransport = transport; + Init(); +} +#endif + UDPConnection::UDPConnection(CConnection& conn) : sharedSocket(true) { @@ -446,7 +456,7 @@ #ifdef __EMSCRIPTEN__ if (browserTransport) { - while (!closed && browserTransport->Receive(recvBuffer)) { + for (uint32_t n = 0; n < WebbarDatagram::COUNT && !closed && browserTransport->Receive(recvBuffer); ++n) { Packet packet(recvBuffer.data(), recvBuffer.size()); ProcessRawPacket(packet); } --- a/rts/System/Net/UDPConnection.h +++ b/rts/System/Net/UDPConnection.h @@ -85,6 +85,9 @@ public: UDPConnection(std::shared_ptr netSocket, const asio::ip::udp::endpoint& myAddr); UDPConnection(int sourceport, const std::string& address, const unsigned port); +#ifdef __EMSCRIPTEN__ + UDPConnection(WebbarDatagram* transport, const asio::ip::udp::endpoint& peerAddress); +#endif UDPConnection(CConnection& conn); ~UDPConnection(); --- a/rts/System/Net/UDPListener.cpp +++ b/rts/System/Net/UDPListener.cpp @@ -1,6 +1,8 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "UDPListener.h" +#include "WebbarDatagram.h" +#include "System/Misc/SpringTime.h" #ifdef DEBUG #include "System/SpringFormat.h" @@ -25,6 +27,13 @@ UDPListener::UDPListener(int port, const std::string& ip): acceptNewConnections(false) { +#ifdef __EMSCRIPTEN__ + if ((browserListener = WebbarDatagram::Listen()) != nullptr) { + SetAcceptingConnections(true); + LOG("[WebbarPeerHost] ABI 1 listening for peer slot 1"); + return; + } +#endif // resets socket on any exception const std::string err = TryBindSocket(port, socket, ip); @@ -106,6 +115,30 @@ } void UDPListener::Update(int loopSleepTime) { +#ifdef __EMSCRIPTEN__ + if (browserListener) { + if (loopSleepTime > 0) spring_msecs(loopSleepTime).sleep(true); + // An internal address supplies native endpoint identity, never an OS + // socket or an address decoded from untrusted peer data. + const ip::udp::endpoint peerAddress(ip::address_v4(0x7f000002), 8460); + const auto it = connMap.find(peerAddress); + if (it != connMap.end()) { + if (auto connection = it->second.lock()) connection->Update(); + return; // no rejoin or slot reuse within this engine lifetime + } + if (!acceptNewConnections || browserListener->status.load() != 1) return; + for (uint32_t n = 0; n < WebbarDatagram::COUNT && browserListener->Receive(recvBuffer); ++n) { + Packet data(recvBuffer.data(), recvBuffer.size()); + if (data.lastContinuous != -1 || data.nakType != 0 || data.chunks.empty() || data.chunks.front()->chunkNumber != 0) continue; + auto incoming = std::make_shared(browserListener, peerAddress); + waiting.push(incoming); + connMap[peerAddress] = incoming; + incoming->ProcessRawPacket(data); + break; + } + return; + } +#endif if (loopSleepTime == 0) netservice.poll(); else { --- a/rts/System/Net/UDPListener.h +++ b/rts/System/Net/UDPListener.h @@ -1,7 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ -#ifndef _UDP_LISTENER_H -#define _UDP_LISTENER_H +#pragma once #include "System/Misc/NonCopyable.h" #include @@ -13,6 +12,9 @@ namespace netcode { class UDPConnection; +#ifdef __EMSCRIPTEN__ +struct WebbarDatagram; +#endif /** * @brief Class for handling Connections on an UDPSocket @@ -83,6 +85,9 @@ * If true, we will create a new connection, if false, they get dropped. */ bool acceptNewConnections; +#ifdef __EMSCRIPTEN__ + WebbarDatagram* browserListener = nullptr; +#endif /// socket being listened on std::shared_ptr socket; @@ -98,4 +103,3 @@ } -#endif // _UDP_LISTENER_H --- a/rts/System/Net/WebbarDatagram.h +++ b/rts/System/Net/WebbarDatagram.h @@ -35,6 +35,19 @@ return transport; } + // Experimental ABI 1: one remote peer (slot 1), local player is slot 0. + // Storage, like the client bridge, lives until the engine Worker exits. + static WebbarDatagram* Listen() + { + if (std::getenv("WEBBAR_P2P_HOST") == nullptr) return nullptr; + auto* transport = new WebbarDatagram{}; + MAIN_THREAD_EM_ASM({ + if (typeof Module['webbarPeerListen'] !== 'function') throw new Error('Missing peer host ABI 1'); + Module['webbarPeerListen'](1, 1, HEAPU8.buffer, $0, $1, $2, $3, $4); + }, &transport->status, &transport->incoming, &transport->outgoing, COUNT, BYTES); + return transport; + } + bool Receive(std::vector& bytes) { const uint32_t read = incoming.read.load(std::memory_order_relaxed);