From 680c7c28fed16e5daedc81b3e0f0b373238b4560 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sat, 14 Mar 2026 09:00:07 -0700 Subject: [PATCH] Distinguish "room full" from "connection lost" in WebSocket error handling Fix race condition where onerror clearing m_connectedFlag before onclose caused any network drop to be misidentified as a room-full rejection. Add m_wasEverConnected flag set once in onopen, use it in onclose to assign exit code 10 (room full) vs 11 (connection lost). Rename rejected API surface to disconnected to reflect the generalized meaning. Co-Authored-By: Claude Opus 4.6 (1M context) --- ISLE/isleapp.cpp | 2 +- extensions/include/extensions/multiplayer.h | 8 +++--- .../extensions/multiplayer/networkmanager.h | 3 ++- .../extensions/multiplayer/networktransport.h | 2 +- .../platforms/emscripten/websockettransport.h | 7 ++--- extensions/src/multiplayer.cpp | 8 +++--- extensions/src/multiplayer/networkmanager.cpp | 4 +-- .../emscripten/websockettransport.cpp | 27 ++++++++++--------- 8 files changed, 33 insertions(+), 28 deletions(-) diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index 9440fa8e..bfc3df23 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -1300,7 +1300,7 @@ inline bool IsleApp::Tick() } #ifdef EXTENSIONS - if (Extensions::IsMultiplayerRejected()) { + if (Extensions::IsMultiplayerDisconnected()) { g_closed = TRUE; return true; } diff --git a/extensions/include/extensions/multiplayer.h b/extensions/include/extensions/multiplayer.h index 0d3078f5..cd087c3f 100644 --- a/extensions/include/extensions/multiplayer.h +++ b/extensions/include/extensions/multiplayer.h @@ -45,7 +45,7 @@ class MultiplayerExt { static MxBool IsClonedCharacter(const char* p_name); static void HandleBeforeSaveLoad(); static void HandleSaveLoaded(); - static MxBool CheckRejected(); + static MxBool CheckDisconnected(); static Multiplayer::NetworkManager* GetNetworkManager(); @@ -58,7 +58,7 @@ class MultiplayerExt { }; #ifdef EXTENSIONS -LEGO1_EXPORT bool IsMultiplayerRejected(); +LEGO1_EXPORT bool IsMultiplayerDisconnected(); #endif namespace MP @@ -72,7 +72,7 @@ constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick; constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter; constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad; constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded; -constexpr auto CheckRejected = &MultiplayerExt::CheckRejected; +constexpr auto CheckDisconnected = &MultiplayerExt::CheckDisconnected; #else constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr; constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr; @@ -82,7 +82,7 @@ constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr; constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr; constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr; constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr; -constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr; +constexpr decltype(&MultiplayerExt::CheckDisconnected) CheckDisconnected = nullptr; #endif } // namespace MP diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index de82f72e..cab005be 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -49,7 +49,7 @@ class NetworkManager : public MxCore { void Connect(const char* p_roomId); void Disconnect(); bool IsConnected() const; - bool WasRejected() const; + bool WasDisconnected() const; void SetWalkAnimation(uint8_t p_walkAnimId); void SetIdleAnimation(uint8_t p_idleAnimId); @@ -151,6 +151,7 @@ class NetworkManager : public MxCore { static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout static const int EXIT_ROOM_FULL = 10; + static const int EXIT_CONNECTION_LOST = 11; }; } // namespace Multiplayer diff --git a/extensions/include/extensions/multiplayer/networktransport.h b/extensions/include/extensions/multiplayer/networktransport.h index 5fd05db6..ea033377 100644 --- a/extensions/include/extensions/multiplayer/networktransport.h +++ b/extensions/include/extensions/multiplayer/networktransport.h @@ -14,7 +14,7 @@ class NetworkTransport { virtual void Connect(const char* p_roomId) = 0; virtual void Disconnect() = 0; virtual bool IsConnected() const = 0; - virtual bool WasRejected() const = 0; + virtual bool WasDisconnected() const = 0; // Send binary data to all peers via relay virtual void Send(const uint8_t* p_data, size_t p_length) = 0; diff --git a/extensions/include/extensions/multiplayer/platforms/emscripten/websockettransport.h b/extensions/include/extensions/multiplayer/platforms/emscripten/websockettransport.h index bd895922..533447d2 100644 --- a/extensions/include/extensions/multiplayer/platforms/emscripten/websockettransport.h +++ b/extensions/include/extensions/multiplayer/platforms/emscripten/websockettransport.h @@ -17,15 +17,16 @@ class WebSocketTransport : public NetworkTransport { void Connect(const char* p_roomId) override; void Disconnect() override; bool IsConnected() const override; - bool WasRejected() const override; + bool WasDisconnected() const override; void Send(const uint8_t* p_data, size_t p_length) override; size_t Receive(std::function p_callback) override; private: std::string m_relayBaseUrl; int m_socketId; - volatile int32_t m_connectedFlag; // Shared with JS main thread via Atomics - volatile int32_t m_rejectedFlag; // Set by JS when connection is rejected (e.g. room full) + volatile int32_t m_connectedFlag; // Shared with JS main thread via Atomics + volatile int32_t m_disconnectedFlag; // Set by JS when connection closes (room full or lost) + volatile int32_t m_wasEverConnected; // Set once in onopen, never cleared by error/close uint8_t m_recvBuf[8192]; }; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index 18c49ec0..ae537381 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -243,9 +243,9 @@ MxBool MultiplayerExt::IsClonedCharacter(const char* p_name) return s_networkManager->IsClonedCharacter(p_name) ? TRUE : FALSE; } -MxBool MultiplayerExt::CheckRejected() +MxBool MultiplayerExt::CheckDisconnected() { - if (s_networkManager && s_networkManager->WasRejected()) { + if (s_networkManager && s_networkManager->WasDisconnected()) { return TRUE; } @@ -257,7 +257,7 @@ Multiplayer::NetworkManager* MultiplayerExt::GetNetworkManager() return s_networkManager; } -bool Extensions::IsMultiplayerRejected() +bool Extensions::IsMultiplayerDisconnected() { - return Extension::Call(MP::CheckRejected).value_or(FALSE); + return Extension::Call(MP::CheckDisconnected).value_or(FALSE); } diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index bf9e45ad..d806cf0a 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -185,9 +185,9 @@ bool NetworkManager::IsConnected() const return m_transport && m_transport->IsConnected(); } -bool NetworkManager::WasRejected() const +bool NetworkManager::WasDisconnected() const { - return m_transport && m_transport->WasRejected(); + return m_transport && m_transport->WasDisconnected(); } void NetworkManager::OnWorldEnabled(LegoWorld* p_world) diff --git a/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp b/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp index 318dc5ed..fdc14c44 100644 --- a/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp +++ b/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp @@ -9,7 +9,7 @@ namespace Multiplayer { WebSocketTransport::WebSocketTransport(const std::string& p_relayBaseUrl) - : m_relayBaseUrl(p_relayBaseUrl), m_socketId(-1), m_connectedFlag(0), m_rejectedFlag(0) + : m_relayBaseUrl(p_relayBaseUrl), m_socketId(-1), m_connectedFlag(0), m_disconnectedFlag(0), m_wasEverConnected(0) { // clang-format off MAIN_THREAD_EM_ASM({ @@ -35,13 +35,15 @@ void WebSocketTransport::Connect(const char* p_roomId) std::string url = m_relayBaseUrl + "/room/" + p_roomId; - m_rejectedFlag = 0; + m_disconnectedFlag = 0; + m_wasEverConnected = 0; // clang-format off m_socketId = MAIN_THREAD_EM_ASM_INT({ var url = UTF8ToString($0); var connPtr = $1; - var rejPtr = $2; + var discPtr = $2; + var everConnPtr = $3; var socketId = Module._mpNextSocketId++; Module._mpMessageQueues[socketId] = []; @@ -51,6 +53,7 @@ void WebSocketTransport::Connect(const char* p_roomId) ws.onopen = function() { Atomics.store(HEAP32, connPtr >> 2, 1); + Atomics.store(HEAP32, everConnPtr >> 2, 1); }; ws.onmessage = function(event) { @@ -61,13 +64,12 @@ void WebSocketTransport::Connect(const char* p_roomId) }; ws.onclose = function() { - var wasConnected = Atomics.load(HEAP32, connPtr >> 2); + var wasEverConnected = Atomics.load(HEAP32, everConnPtr >> 2); Atomics.store(HEAP32, connPtr >> 2, 0); - if (!wasConnected) { - // Never connected — server rejected (room full / 503) - Atomics.store(HEAP32, rejPtr >> 2, 1); - Module._exitCode = 10; - } + Atomics.store(HEAP32, discPtr >> 2, 1); + // 10 = room full / server rejected before connecting + // 11 = connection lost after successful connect + Module._exitCode = wasEverConnected ? 11 : 10; }; ws.onerror = function() { @@ -81,7 +83,7 @@ void WebSocketTransport::Connect(const char* p_roomId) } return socketId; - }, url.c_str(), &m_connectedFlag, &m_rejectedFlag); + }, url.c_str(), &m_connectedFlag, &m_disconnectedFlag, &m_wasEverConnected); // clang-format on if (m_socketId <= 0) { @@ -105,6 +107,7 @@ void WebSocketTransport::Disconnect() m_socketId = -1; m_connectedFlag = 0; + m_wasEverConnected = 0; } } @@ -113,9 +116,9 @@ bool WebSocketTransport::IsConnected() const return m_socketId > 0 && m_connectedFlag != 0; } -bool WebSocketTransport::WasRejected() const +bool WebSocketTransport::WasDisconnected() const { - return m_rejectedFlag != 0; + return m_disconnectedFlag != 0; } void WebSocketTransport::Send(const uint8_t* p_data, size_t p_length)