Merge branch 'multiplayer' into claude/adjust-camera-switch-distance-duaQ4

This commit is contained in:
Christian Semmler 2026-03-14 09:00:45 -07:00
commit 37587beda4
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
8 changed files with 33 additions and 28 deletions

View File

@ -1300,7 +1300,7 @@ inline bool IsleApp::Tick()
} }
#ifdef EXTENSIONS #ifdef EXTENSIONS
if (Extensions::IsMultiplayerRejected()) { if (Extensions::IsMultiplayerDisconnected()) {
g_closed = TRUE; g_closed = TRUE;
return true; return true;
} }

View File

@ -45,7 +45,7 @@ class MultiplayerExt {
static MxBool IsClonedCharacter(const char* p_name); static MxBool IsClonedCharacter(const char* p_name);
static void HandleBeforeSaveLoad(); static void HandleBeforeSaveLoad();
static void HandleSaveLoaded(); static void HandleSaveLoaded();
static MxBool CheckRejected(); static MxBool CheckDisconnected();
static Multiplayer::NetworkManager* GetNetworkManager(); static Multiplayer::NetworkManager* GetNetworkManager();
@ -58,7 +58,7 @@ class MultiplayerExt {
}; };
#ifdef EXTENSIONS #ifdef EXTENSIONS
LEGO1_EXPORT bool IsMultiplayerRejected(); LEGO1_EXPORT bool IsMultiplayerDisconnected();
#endif #endif
namespace MP namespace MP
@ -72,7 +72,7 @@ constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick;
constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter; constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter;
constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad; constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad;
constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded; constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded;
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected; constexpr auto CheckDisconnected = &MultiplayerExt::CheckDisconnected;
#else #else
constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr; constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr;
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = 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::IsClonedCharacter) IsClonedCharacter = nullptr;
constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr; constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr;
constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr; constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr;
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr; constexpr decltype(&MultiplayerExt::CheckDisconnected) CheckDisconnected = nullptr;
#endif #endif
} // namespace MP } // namespace MP

View File

@ -49,7 +49,7 @@ class NetworkManager : public MxCore {
void Connect(const char* p_roomId); void Connect(const char* p_roomId);
void Disconnect(); void Disconnect();
bool IsConnected() const; bool IsConnected() const;
bool WasRejected() const; bool WasDisconnected() const;
void SetWalkAnimation(uint8_t p_walkAnimId); void SetWalkAnimation(uint8_t p_walkAnimId);
void SetIdleAnimation(uint8_t p_idleAnimId); 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 BROADCAST_INTERVAL_MS = 66; // ~15Hz
static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout
static const int EXIT_ROOM_FULL = 10; static const int EXIT_ROOM_FULL = 10;
static const int EXIT_CONNECTION_LOST = 11;
}; };
} // namespace Multiplayer } // namespace Multiplayer

View File

@ -14,7 +14,7 @@ class NetworkTransport {
virtual void Connect(const char* p_roomId) = 0; virtual void Connect(const char* p_roomId) = 0;
virtual void Disconnect() = 0; virtual void Disconnect() = 0;
virtual bool IsConnected() const = 0; virtual bool IsConnected() const = 0;
virtual bool WasRejected() const = 0; virtual bool WasDisconnected() const = 0;
// Send binary data to all peers via relay // Send binary data to all peers via relay
virtual void Send(const uint8_t* p_data, size_t p_length) = 0; virtual void Send(const uint8_t* p_data, size_t p_length) = 0;

View File

@ -17,15 +17,16 @@ class WebSocketTransport : public NetworkTransport {
void Connect(const char* p_roomId) override; void Connect(const char* p_roomId) override;
void Disconnect() override; void Disconnect() override;
bool IsConnected() const 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; void Send(const uint8_t* p_data, size_t p_length) override;
size_t Receive(std::function<void(const uint8_t*, size_t)> p_callback) override; size_t Receive(std::function<void(const uint8_t*, size_t)> p_callback) override;
private: private:
std::string m_relayBaseUrl; std::string m_relayBaseUrl;
int m_socketId; int m_socketId;
volatile int32_t m_connectedFlag; // Shared with JS main thread via Atomics 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_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]; uint8_t m_recvBuf[8192];
}; };

View File

@ -243,9 +243,9 @@ MxBool MultiplayerExt::IsClonedCharacter(const char* p_name)
return s_networkManager->IsClonedCharacter(p_name) ? TRUE : FALSE; 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; return TRUE;
} }
@ -257,7 +257,7 @@ Multiplayer::NetworkManager* MultiplayerExt::GetNetworkManager()
return s_networkManager; return s_networkManager;
} }
bool Extensions::IsMultiplayerRejected() bool Extensions::IsMultiplayerDisconnected()
{ {
return Extension<MultiplayerExt>::Call(MP::CheckRejected).value_or(FALSE); return Extension<MultiplayerExt>::Call(MP::CheckDisconnected).value_or(FALSE);
} }

View File

@ -185,9 +185,9 @@ bool NetworkManager::IsConnected() const
return m_transport && m_transport->IsConnected(); 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) void NetworkManager::OnWorldEnabled(LegoWorld* p_world)

View File

@ -9,7 +9,7 @@ namespace Multiplayer
{ {
WebSocketTransport::WebSocketTransport(const std::string& p_relayBaseUrl) 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 // clang-format off
MAIN_THREAD_EM_ASM({ MAIN_THREAD_EM_ASM({
@ -35,13 +35,15 @@ void WebSocketTransport::Connect(const char* p_roomId)
std::string url = m_relayBaseUrl + "/room/" + p_roomId; std::string url = m_relayBaseUrl + "/room/" + p_roomId;
m_rejectedFlag = 0; m_disconnectedFlag = 0;
m_wasEverConnected = 0;
// clang-format off // clang-format off
m_socketId = MAIN_THREAD_EM_ASM_INT({ m_socketId = MAIN_THREAD_EM_ASM_INT({
var url = UTF8ToString($0); var url = UTF8ToString($0);
var connPtr = $1; var connPtr = $1;
var rejPtr = $2; var discPtr = $2;
var everConnPtr = $3;
var socketId = Module._mpNextSocketId++; var socketId = Module._mpNextSocketId++;
Module._mpMessageQueues[socketId] = []; Module._mpMessageQueues[socketId] = [];
@ -51,6 +53,7 @@ void WebSocketTransport::Connect(const char* p_roomId)
ws.onopen = function() { ws.onopen = function() {
Atomics.store(HEAP32, connPtr >> 2, 1); Atomics.store(HEAP32, connPtr >> 2, 1);
Atomics.store(HEAP32, everConnPtr >> 2, 1);
}; };
ws.onmessage = function(event) { ws.onmessage = function(event) {
@ -61,13 +64,12 @@ void WebSocketTransport::Connect(const char* p_roomId)
}; };
ws.onclose = function() { ws.onclose = function() {
var wasConnected = Atomics.load(HEAP32, connPtr >> 2); var wasEverConnected = Atomics.load(HEAP32, everConnPtr >> 2);
Atomics.store(HEAP32, connPtr >> 2, 0); Atomics.store(HEAP32, connPtr >> 2, 0);
if (!wasConnected) { Atomics.store(HEAP32, discPtr >> 2, 1);
// Never connected — server rejected (room full / 503) // 10 = room full / server rejected before connecting
Atomics.store(HEAP32, rejPtr >> 2, 1); // 11 = connection lost after successful connect
Module._exitCode = 10; Module._exitCode = wasEverConnected ? 11 : 10;
}
}; };
ws.onerror = function() { ws.onerror = function() {
@ -81,7 +83,7 @@ void WebSocketTransport::Connect(const char* p_roomId)
} }
return socketId; return socketId;
}, url.c_str(), &m_connectedFlag, &m_rejectedFlag); }, url.c_str(), &m_connectedFlag, &m_disconnectedFlag, &m_wasEverConnected);
// clang-format on // clang-format on
if (m_socketId <= 0) { if (m_socketId <= 0) {
@ -105,6 +107,7 @@ void WebSocketTransport::Disconnect()
m_socketId = -1; m_socketId = -1;
m_connectedFlag = 0; m_connectedFlag = 0;
m_wasEverConnected = 0;
} }
} }
@ -113,9 +116,9 @@ bool WebSocketTransport::IsConnected() const
return m_socketId > 0 && m_connectedFlag != 0; 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) void WebSocketTransport::Send(const uint8_t* p_data, size_t p_length)