Claude/fix platform compilation errors hol yw (#19)

* Add native platform multiplayer support via libwebsockets

Add a general-purpose WebSocket transport using libwebsockets (v4.5-stable)
for all non-Emscripten platforms, enabling multiplayer on desktop, mobile,
and any other platform that libwebsockets supports.

New files:
- LwsTransport: implements NetworkTransport using lws client API with
  non-blocking event loop integration via lws_service(ctx, 0) in Receive()
- NativeCallbacks: implements PlatformCallbacks with SDL_Log output

Build integration:
- libwebsockets fetched conditionally only when ISLE_EXTENSIONS=ON and
  not building for Emscripten, with SSL disabled (ws:// only)
- Follows existing 3rdparty dependency patterns (FetchContent/find_package)

https://claude.ai/code/session_01BojPvEEhREJ4xdihJfin3m

* Fix Connect() reconnection guard to check context instead of connected flag

The previous check (m_connected) would fail to clean up a stale lws context
when reconnecting after a connection error, since the error handler already
sets m_connected=false. Check m_context instead, matching the emscripten
version's behavior of cleaning up any existing connection state.

https://claude.ai/code/session_01BojPvEEhREJ4xdihJfin3m

* Fix cross-platform compilation errors for libwebsockets multiplayer

- Add ISLE_USE_WEBSOCKETS cmake option to conditionally build lws transport
  (disabled on Switch, 3DS, Vita, Emscripten which lack standard networking)
- Disable lws internal -Werror (DISABLE_WERROR) to fix GCC/Clang builds
- Override MSVC /WX with /WX- on the websockets target to fix MSVC builds
- Skip precompiled headers for native transport files to avoid miniwin/windows.h
  type conflicts on MinGW
- Guard native transport includes/instantiation with ISLE_USE_WEBSOCKETS define
  so platforms without lws gracefully skip multiplayer networking

https://claude.ai/code/session_01UdLx6KWCXocF6guCQDq8C8

* Fix native WebSocket transport compilation errors and disconnect detection

- Fix undefined variable: relayUrl → s_relayUrl in multiplayer.cpp
- Fix interface mismatch: LwsTransport now implements WasDisconnected()
  matching the base class pure virtual
- Add WasRejected() to NetworkTransport interface for parity between
  emscripten and native backends (rejected = closed before ever connected)
- Fix post-connection disconnect detection: m_disconnected is now set
  unconditionally on any close/error, not only for pre-connection failures
- Guard shared init code with #if defined(__EMSCRIPTEN__) || defined(ISLE_USE_WEBSOCKETS)
  to avoid creating a NetworkManager on platforms without a transport
- Collapse identical CONNECTION_ERROR/CLOSED handlers into fallthrough

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move exit code constants to networktransport.h and eliminate magic numbers

Move EXIT_ROOM_FULL/EXIT_CONNECTION_LOST from NetworkManager (where they
were unused) to the Multiplayer namespace in networktransport.h so both
backends can reference them. Pass them into the emscripten inline JS as
parameters instead of hardcoding 10/11.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Implement missing PlatformCallbacks pure virtuals in NativeCallbacks

NativeCallbacks only overrode OnPlayerCountChanged, leaving three pure
virtuals unimplemented (OnThirdPersonChanged, OnNameBubblesChanged,
OnAllowCustomizeChanged), making it abstract and un-instantiable on MSVC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove WebSocket subprotocol request to match browser behavior

The relay server doesn't negotiate subprotocols, so lws requesting
Sec-WebSocket-Protocol: lws-multiplayer caused the handshake to stall
and freeze the game. The browser WebSocket API doesn't send this header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add debug logging to native WebSocket transport

Temporary diagnostic logs to find where the game freezes on Windows:
- Before/after lws_create_context, lws_client_connect_via_info, lws_service
- In all lws callback events including unhandled reason codes
- Suppress verbose lws internal logging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move lws_service to background thread to prevent game loop blocking

lws_service(ctx, 0) blocks the main thread on Windows despite the 0ms
timeout, freezing the game on a black screen. Move all libwebsockets
event processing to a dedicated LwsServiceThread using MxThread, with
MxCriticalSection-protected queues for thread-safe send/receive and
std::atomic flags for connection state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix service thread lifecycle for reconnect support

MxThread is single-use: m_running is set TRUE in the constructor and
never reset after Terminate(). Allocate LwsServiceThread on the heap
so each connection gets a fresh instance. Also handle Start() failure
to avoid deadlock in Terminate() if SDL thread creation fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix right-click camera turning on Desktop by tracking button state internally

On Windows, SDL3 uses different mouse device IDs for button events (normal
input) vs motion events (raw input during relative mouse mode). This causes
motion.state to not reflect the right button being held, since the button
state was recorded under a different device ID. Track right button state
internally via SDL_EVENT_MOUSE_BUTTON_DOWN/UP instead of relying on
motion.state, which works reliably across all platforms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore cursor position after right-click camera drag on Desktop

On Desktop, SDL_SetWindowRelativeMouseMode accumulates absolute position
internally, so the cursor appears at a different location when released.
Save the cursor position before entering relative mode and warp it back
on release, matching the Emscripten pointer lock behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix default

* Add debug logging for peer ID assignment

Logs the assigned peer ID when MSG_ASSIGN_ID is received to help
diagnose whether ws:// connections are reaching the Durable Object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add missing SDL_log.h include for SDL_Log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add TLS/WSS support to native WebSocket transport via mbedTLS

Cloudflare custom domains require wss:// for proper Worker routing.
Fetch mbedTLS v3.6.3 via FetchContent and enable LWS_WITH_MBEDTLS so
the Desktop client can connect over TLS. The transport now detects
wss:// URLs and sets the appropriate SSL context and connection flags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Disable iniparser test suite during FetchContent build

iniparser's main branch added tests that require Ruby. Set
BUILD_TESTING OFF in the block to skip test configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "Disable iniparser test suite during FetchContent build"

This reverts commit abdd9b37bced18dd54486c1cfb23a53a2d1b3142.

* Fix mbedTLS tarball MD5 hash

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Bump mbedTLS from 3.6.3 to 3.6.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use official mbedTLS release archive instead of GitHub auto-generated tarball

The auto-generated tarball from /archive/refs/tags/ doesn't include the
mbedtls_framework submodule, causing a build failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix mbedTLS download URL to use correct release tag format

Release tags use 'mbedtls-3.6.5' not 'v3.6.5'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix mbedTLS include dirs by setting cache vars inside block() scope

mbedtls_SOURCE_DIR and mbedtls_BINARY_DIR are only available inside
the block() where FetchContent_MakeAvailable runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Disable lws export targets to fix mbedTLS export set conflict

lws install(EXPORT) fails because FetchContent'd mbedTLS targets
aren't in an export set. LWS_WITH_EXPORT_LWSTARGETS OFF disables
this since we don't need install rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove EXCLUDE_FROM_ALL from mbedTLS FetchContent

mbedcrypto depends on internal sub-libraries (everest, p256m) that
don't get built when EXCLUDE_FROM_ALL is set, causing link failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add explicit build dependencies from websockets to mbedTLS internals

The Visual Studio generator doesn't infer the build order for
mbedTLS's internal sub-libraries (everest, p256m) when they are
transitive dependencies through mbedcrypto.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Force mbedTLS to build as static libraries

Without explicit BUILD_SHARED_LIBS OFF, mbedTLS (and its internal
sub-libraries like everest) build as DLLs instead of static libs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove unnecessary add_dependencies for mbedTLS internals

The real issue was BUILD_SHARED_LIBS, not build ordering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Scope BUILD_SHARED_LIBS to mbedTLS block instead of global cache

CACHE BOOL FORCE writes to the global cache, leaking beyond the
block() and preventing lego1.dll from being built as a shared lib.
A regular variable is properly scoped by block().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Pre-set mbedTLS 3.x API check results for lws configure

lws uses check_function_exists for mbedtls_md_setup but the check
fails since mbedTLS targets aren't built at configure time. Without
the result, lws falls back to the removed 2.x mbedtls_md_init_ctx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add debug logging to multiplayer initialization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Enable verbose lws logging to diagnose TLS handshake failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix TLS: use ALLOW_INSECURE instead of SKIP_SERVER_CERT_HOSTNAME_CHECK

SKIP_SERVER_CERT_HOSTNAME_CHECK also skips setting SNI (Server Name
Indication) via mbedtls_ssl_set_hostname. Without SNI, Cloudflare
can't route the connection to the Worker. ALLOW_INSECURE disables
cert verification while still setting the SNI hostname.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Suppress CMake deprecation error for mbedTLS cmake_minimum_required

Newer CMake versions (3.27+) error on old cmake_minimum_required
values. Setting CMAKE_POLICY_VERSION_MINIMUM to 3.10 suppresses
this for the mbedTLS subdirectory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix left-click and cursor warp regression from right-click camera fix

The mouse button handler ran relative-mouse-mode and cursor-warp logic
for ALL button events, not just right-button events. Left clicks would
enter the else branch, disabling relative mode and warping the cursor
to a stale/zero position. Scope the entire block to right-button events
only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move CMAKE_POLICY_VERSION_MINIMUM outside block() scope

The variable may not propagate from block() scope into
FetchContent's add_subdirectory call on some CMake versions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix mbedTLS cmake_minimum_required deprecation error in CI

CI uses -Werror=dev which makes the CMake deprecation warning for
cmake_minimum_required(VERSION < 3.10) a fatal error. Patch mbedTLS's
CMakeLists.txt via PATCH_COMMAND to use VERSION 3.10 instead.

CMAKE_POLICY_VERSION_MINIMUM only works on CMake 4.x, not 3.28.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rename ISLE_USE_WEBSOCKETS to ISLE_HAS_LWS and clean up multiplayer code

Rename the CMake option and compile definition to better reflect the
presence of the libwebsockets library. Remove debug SDL_Log calls and
unused includes, extract magic numbers into named constants, add null
guard for lws_cancel_service, and use inline constexpr for namespace
constants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rename ISLE_HAS_LWS to ISLE_USE_LWS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix uninitialized variable UB in WorldStateSync::HandleEntityMutation

Split GetInfoArray() output parameter from FindEntityIndex() call to
avoid undefined behavior from unspecified function argument evaluation
order. MSVC evaluates right-to-left, reading `count` before
GetInfoArray() initializes it, triggering _RTC_UninitUse and preventing
entity mutation sync in multiplayer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
foxtacles 2026-03-14 15:38:24 -07:00 committed by GitHub
parent 855c5c4210
commit 13f6239808
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 545 additions and 13 deletions

View File

@ -72,3 +72,65 @@ if(DOWNLOAD_DEPENDENCIES)
set_property(TARGET libweaver PROPERTY CXX_STANDARD_REQUIRED ON) set_property(TARGET libweaver PROPERTY CXX_STANDARD_REQUIRED ON)
endif() endif()
if(ISLE_USE_LWS)
if(DOWNLOAD_DEPENDENCIES)
include(FetchContent)
# Fetch mbedTLS for TLS/WSS support in libwebsockets
FetchContent_Declare(
mbedtls
URL https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.5/mbedtls-3.6.5.tar.bz2
URL_MD5 bc79602daf85f1cf35a686b53056de58
# Patch cmake_minimum_required to avoid deprecation error with -Werror=dev
PATCH_COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/patch_mbedtls_cmake.cmake
)
block()
set(ENABLE_TESTING OFF CACHE BOOL "" FORCE)
set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE)
set(MBEDTLS_FATAL_WARNINGS OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF)
FetchContent_MakeAvailable(mbedtls)
# Point lws at the mbedTLS targets so it skips its own find logic
# Must be inside block() to access mbedtls_SOURCE_DIR/mbedtls_BINARY_DIR
set(LWS_MBEDTLS_INCLUDE_DIRS
"${mbedtls_SOURCE_DIR}/include;${mbedtls_BINARY_DIR}/include"
CACHE STRING "" FORCE
)
set(LWS_MBEDTLS_LIBRARIES
"mbedtls;mbedx509;mbedcrypto"
CACHE STRING "" FORCE
)
endblock()
FetchContent_Declare(
libwebsockets
GIT_REPOSITORY "https://github.com/warmcat/libwebsockets.git"
GIT_TAG "v4.5-stable"
EXCLUDE_FROM_ALL
)
block()
set(LWS_WITH_SSL ON CACHE BOOL "" FORCE)
set(LWS_WITH_MBEDTLS ON CACHE BOOL "" FORCE)
set(LWS_WITH_EXPORT_LWSTARGETS OFF CACHE BOOL "" FORCE)
# mbedTLS isn't built yet at configure time, so lws's check_function_exists
# calls fail. Pre-set the results for APIs that exist in mbedTLS 3.x.
set(LWS_HAVE_mbedtls_md_setup 1 CACHE INTERNAL "" FORCE)
set(LWS_HAVE_mbedtls_rsa_complete 1 CACHE INTERNAL "" FORCE)
set(LWS_WITH_SHARED OFF CACHE BOOL "" FORCE)
set(LWS_WITH_STATIC ON CACHE BOOL "" FORCE)
set(LWS_WITHOUT_TESTAPPS ON CACHE BOOL "" FORCE)
set(LWS_WITH_MINIMAL_EXAMPLES OFF CACHE BOOL "" FORCE)
# Disable treating warnings as errors in libwebsockets (GCC/Clang)
set(DISABLE_WERROR ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(libwebsockets)
endblock()
# Disable MSVC /WX (warnings as errors) that lws sets unconditionally
if(TARGET websockets AND MSVC)
target_compile_options(websockets PRIVATE /WX-)
endif()
else()
find_package(Libwebsockets REQUIRED)
endif()
endif()

7
3rdparty/patch_mbedtls_cmake.cmake vendored Normal file
View File

@ -0,0 +1,7 @@
file(READ "CMakeLists.txt" content)
string(REGEX REPLACE
"cmake_minimum_required\\(VERSION [0-9]+\\.[0-9]+[0-9.]*\\)"
"cmake_minimum_required(VERSION 3.10)"
content "${content}"
)
file(WRITE "CMakeLists.txt" "${content}")

View File

@ -56,6 +56,7 @@ option(ISLE_WERROR "Treat warnings as errors" OFF)
cmake_dependent_option(ISLE_USE_DX5 "Build with internal DirectX 5 SDK" "${NOT_MINGW}" "WIN32;CMAKE_SIZEOF_VOID_P EQUAL 4" OFF) cmake_dependent_option(ISLE_USE_DX5 "Build with internal DirectX 5 SDK" "${NOT_MINGW}" "WIN32;CMAKE_SIZEOF_VOID_P EQUAL 4" OFF)
cmake_dependent_option(ISLE_MINIWIN "Use miniwin" ON "NOT ISLE_USE_DX5" OFF) cmake_dependent_option(ISLE_MINIWIN "Use miniwin" ON "NOT ISLE_USE_DX5" OFF)
cmake_dependent_option(ISLE_EXTENSIONS "Use extensions" ON "NOT ISLE_USE_DX5;NOT WINDOWS_STORE" OFF) cmake_dependent_option(ISLE_EXTENSIONS "Use extensions" ON "NOT ISLE_USE_DX5;NOT WINDOWS_STORE" OFF)
cmake_dependent_option(ISLE_USE_LWS "Use libwebsockets for native multiplayer" ON "ISLE_EXTENSIONS;NOT EMSCRIPTEN;NOT NINTENDO_3DS;NOT NINTENDO_SWITCH;NOT VITA" OFF)
cmake_dependent_option(ISLE_BUILD_CONFIG "Build CONFIG.EXE application" ON "MSVC OR ISLE_MINIWIN;NOT NINTENDO_3DS;NOT NINTENDO_SWITCH;NOT WINDOWS_STORE;NOT VITA" OFF) cmake_dependent_option(ISLE_BUILD_CONFIG "Build CONFIG.EXE application" ON "MSVC OR ISLE_MINIWIN;NOT NINTENDO_3DS;NOT NINTENDO_SWITCH;NOT WINDOWS_STORE;NOT VITA" OFF)
cmake_dependent_option(ISLE_COMPILE_SHADERS "Compile shaders" ON "SDL_SHADERCROSS_BIN;TARGET Python3::Interpreter" OFF) cmake_dependent_option(ISLE_COMPILE_SHADERS "Compile shaders" ON "SDL_SHADERCROSS_BIN;TARGET Python3::Interpreter" OFF)
cmake_dependent_option(CMAKE_POSITION_INDEPENDENT_CODE "Build with -fPIC" ON "NOT VITA" OFF) cmake_dependent_option(CMAKE_POSITION_INDEPENDENT_CODE "Build with -fPIC" ON "NOT VITA" OFF)
@ -560,6 +561,19 @@ if (ISLE_EXTENSIONS)
extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp
extensions/src/multiplayer/platforms/emscripten/callbacks.cpp extensions/src/multiplayer/platforms/emscripten/callbacks.cpp
) )
elseif(ISLE_USE_LWS)
target_sources(lego1 PRIVATE
extensions/src/multiplayer/platforms/native/lwstransport.cpp
extensions/src/multiplayer/platforms/native/nativecallbacks.cpp
)
# Skip precompiled headers for native transport files to avoid miniwin/windows.h conflicts
set_source_files_properties(
extensions/src/multiplayer/platforms/native/lwstransport.cpp
extensions/src/multiplayer/platforms/native/nativecallbacks.cpp
PROPERTIES SKIP_PRECOMPILE_HEADERS ON
)
target_compile_definitions(lego1 PRIVATE ISLE_USE_LWS)
target_link_libraries(lego1 PRIVATE websockets)
endif() endif()
endif() endif()

View File

@ -18,6 +18,7 @@ class NetworkTransport {
virtual void Disconnect() = 0; virtual void Disconnect() = 0;
virtual bool IsConnected() const = 0; virtual bool IsConnected() const = 0;
virtual bool WasDisconnected() const = 0; virtual bool WasDisconnected() const = 0;
virtual bool WasRejected() 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

@ -18,6 +18,7 @@ class WebSocketTransport : public NetworkTransport {
void Disconnect() override; void Disconnect() override;
bool IsConnected() const override; bool IsConnected() const override;
bool WasDisconnected() const override; bool WasDisconnected() const override;
bool WasRejected() 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;

View File

@ -0,0 +1,72 @@
#pragma once
#ifndef __EMSCRIPTEN__
#include "extensions/multiplayer/networktransport.h"
#include "mxcriticalsection.h"
#include "mxthread.h"
#include <atomic>
#include <deque>
#include <string>
#include <vector>
struct lws_context;
struct lws;
namespace Multiplayer
{
class LwsTransport;
class LwsServiceThread : public MxThread {
public:
LwsServiceThread() : m_transport(nullptr) {}
MxResult Run() override;
void SetTransport(LwsTransport* p_transport) { m_transport = p_transport; }
private:
LwsTransport* m_transport;
};
class LwsTransport : public NetworkTransport {
friend class LwsServiceThread;
public:
LwsTransport(const std::string& p_relayBaseUrl);
~LwsTransport() override;
void Connect(const char* p_roomId) override;
void Disconnect() override;
bool IsConnected() const override;
bool WasDisconnected() const override;
bool WasRejected() const 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;
// Called from static lws callback trampoline
int HandleLwsEvent(struct lws* p_wsi, int p_reason, void* p_in, size_t p_len);
private:
void ServiceLoop();
std::string m_relayBaseUrl;
struct lws_context* m_context;
std::atomic<struct lws*> m_wsi;
std::atomic<bool> m_connected;
std::atomic<bool> m_disconnected;
std::atomic<bool> m_wasEverConnected;
MxCriticalSection m_sendCS;
MxCriticalSection m_recvCS;
std::deque<std::vector<uint8_t>> m_sendQueue;
std::deque<std::vector<uint8_t>> m_recvQueue;
std::vector<uint8_t> m_fragment;
LwsServiceThread* m_serviceThread;
std::atomic<bool> m_wantWritable;
};
} // namespace Multiplayer
#endif // !__EMSCRIPTEN__

View File

@ -0,0 +1,20 @@
#pragma once
#ifndef __EMSCRIPTEN__
#include "extensions/multiplayer/platformcallbacks.h"
namespace Multiplayer
{
class NativeCallbacks : public PlatformCallbacks {
public:
void OnPlayerCountChanged(int p_count) override;
void OnThirdPersonChanged(bool p_enabled) override;
void OnNameBubblesChanged(bool p_enabled) override;
void OnAllowCustomizeChanged(bool p_enabled) override;
};
} // namespace Multiplayer
#endif // !__EMSCRIPTEN__

View File

@ -42,6 +42,9 @@ class InputHandler {
bool m_wantsAutoDisable; bool m_wantsAutoDisable;
bool m_wantsAutoEnable; bool m_wantsAutoEnable;
bool m_rightButtonHeld;
float m_savedMouseX;
float m_savedMouseY;
}; };
} // namespace ThirdPersonCamera } // namespace ThirdPersonCamera

View File

@ -25,6 +25,9 @@
#include "extensions/multiplayer/platforms/emscripten/websockettransport.h" #include "extensions/multiplayer/platforms/emscripten/websockettransport.h"
#include <emscripten.h> #include <emscripten.h>
#elif defined(ISLE_USE_LWS)
#include "extensions/multiplayer/platforms/native/lwstransport.h"
#include "extensions/multiplayer/platforms/native/nativecallbacks.h"
#endif #endif
using namespace Extensions; using namespace Extensions;
@ -51,7 +54,12 @@ void MultiplayerExt::Initialize()
#ifdef __EMSCRIPTEN__ #ifdef __EMSCRIPTEN__
s_transport = new Multiplayer::WebSocketTransport(s_relayUrl); s_transport = new Multiplayer::WebSocketTransport(s_relayUrl);
s_callbacks = new Multiplayer::EmscriptenCallbacks(); s_callbacks = new Multiplayer::EmscriptenCallbacks();
#elif defined(ISLE_USE_LWS)
s_transport = new Multiplayer::LwsTransport(s_relayUrl);
s_callbacks = new Multiplayer::NativeCallbacks();
#endif
#if defined(__EMSCRIPTEN__) || defined(ISLE_USE_LWS)
s_networkManager = new Multiplayer::NetworkManager(); s_networkManager = new Multiplayer::NetworkManager();
s_networkManager->Initialize(s_transport, s_callbacks); s_networkManager->Initialize(s_transport, s_callbacks);

View File

@ -121,6 +121,11 @@ bool WebSocketTransport::WasDisconnected() const
return m_disconnectedFlag != 0; return m_disconnectedFlag != 0;
} }
bool WebSocketTransport::WasRejected() const
{
return m_disconnectedFlag != 0 && m_wasEverConnected == 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)
{ {
if (m_socketId <= 0 || !m_connectedFlag) { if (m_socketId <= 0 || !m_connectedFlag) {

View File

@ -0,0 +1,289 @@
#ifndef __EMSCRIPTEN__
#include "extensions/multiplayer/platforms/native/lwstransport.h"
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_stdinc.h>
#include <libwebsockets.h>
namespace Multiplayer
{
static int LwsCallback(struct lws* p_wsi, enum lws_callback_reasons p_reason, void* p_user, void* p_in, size_t p_len)
{
LwsTransport* transport = static_cast<LwsTransport*>(lws_get_opaque_user_data(p_wsi));
if (transport) {
return transport->HandleLwsEvent(p_wsi, static_cast<int>(p_reason), p_in, p_len);
}
return 0;
}
static constexpr size_t LWS_RX_BUFFER_SIZE = 8192;
static constexpr int LWS_SERVICE_TIMEOUT_MS = 50;
// clang-format off
static const struct lws_protocols s_protocols[] = {
{"lws-multiplayer", LwsCallback, 0, LWS_RX_BUFFER_SIZE},
LWS_PROTOCOL_LIST_TERM
};
// clang-format on
MxResult LwsServiceThread::Run()
{
while (IsRunning()) {
m_transport->ServiceLoop();
}
return MxThread::Run();
}
LwsTransport::LwsTransport(const std::string& p_relayBaseUrl)
: m_relayBaseUrl(p_relayBaseUrl), m_context(nullptr), m_wsi(nullptr), m_connected(false), m_disconnected(false),
m_wasEverConnected(false), m_serviceThread(nullptr), m_wantWritable(false)
{
}
LwsTransport::~LwsTransport()
{
Disconnect();
}
void LwsTransport::Connect(const char* p_roomId)
{
if (m_context) {
Disconnect();
}
m_disconnected.store(false);
m_wasEverConnected.store(false);
// lws_parse_uri modifies the string in place, so we need a mutable copy
std::string fullUrl = m_relayBaseUrl + "/room/" + p_roomId;
std::vector<char> urlBuf(fullUrl.begin(), fullUrl.end());
urlBuf.push_back('\0');
const char* protocol = nullptr;
const char* address = nullptr;
const char* path = nullptr;
int port = 0;
if (lws_parse_uri(&urlBuf[0], &protocol, &address, &port, &path)) {
SDL_Log("[Multiplayer] Failed to parse relay URL: %s", fullUrl.c_str());
m_disconnected.store(true);
return;
}
bool useSSL = (SDL_strcmp(protocol, "wss") == 0 || SDL_strcmp(protocol, "https") == 0);
SDL_Log("[Multiplayer] Connecting to %s://%s:%d/%s (SSL=%d)", protocol, address, port, path, useSSL);
lws_set_log_level(LLL_ERR | LLL_WARN, nullptr);
struct lws_context_creation_info ctxInfo;
SDL_memset(&ctxInfo, 0, sizeof(ctxInfo));
ctxInfo.port = CONTEXT_PORT_NO_LISTEN;
ctxInfo.protocols = s_protocols;
if (useSSL) {
ctxInfo.options |= LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
}
m_context = lws_create_context(&ctxInfo);
if (!m_context) {
SDL_Log("[Multiplayer] Failed to create lws context");
m_disconnected.store(true);
return;
}
// path from lws_parse_uri does not include the leading '/', so prepend it
std::string fullPath = std::string("/") + path;
struct lws_client_connect_info connInfo;
SDL_memset(&connInfo, 0, sizeof(connInfo));
connInfo.context = m_context;
connInfo.address = address;
connInfo.port = port;
connInfo.path = fullPath.c_str();
connInfo.host = address;
connInfo.origin = address;
connInfo.ssl_connection = useSSL ? (LCCSCF_USE_SSL | LCCSCF_ALLOW_INSECURE) : 0;
connInfo.local_protocol_name = s_protocols[0].name;
connInfo.opaque_user_data = this;
struct lws* wsi = lws_client_connect_via_info(&connInfo);
if (!wsi) {
SDL_Log("[Multiplayer] Failed to initiate WebSocket connection to %s:%d%s", address, port, fullPath.c_str());
lws_context_destroy(m_context);
m_context = nullptr;
m_disconnected.store(true);
return;
}
m_wsi.store(wsi);
m_serviceThread = new LwsServiceThread();
m_serviceThread->SetTransport(this);
if (m_serviceThread->Start(0, 0) != SUCCESS) {
SDL_Log("[Multiplayer] Failed to start WebSocket service thread");
delete m_serviceThread;
m_serviceThread = nullptr;
m_wsi.store(nullptr);
lws_context_destroy(m_context);
m_context = nullptr;
m_disconnected.store(true);
return;
}
}
void LwsTransport::Disconnect()
{
if (m_context) {
lws_cancel_service(m_context);
m_serviceThread->Terminate();
delete m_serviceThread;
m_serviceThread = nullptr;
lws_context_destroy(m_context);
m_context = nullptr;
}
m_wsi.store(nullptr);
m_connected.store(false);
m_wantWritable.store(false);
m_sendCS.Enter();
m_sendQueue.clear();
m_sendCS.Leave();
m_recvCS.Enter();
m_recvQueue.clear();
m_recvCS.Leave();
m_fragment.clear();
}
bool LwsTransport::IsConnected() const
{
return m_connected.load();
}
bool LwsTransport::WasDisconnected() const
{
return m_disconnected.load();
}
bool LwsTransport::WasRejected() const
{
return m_disconnected.load() && !m_wasEverConnected.load();
}
void LwsTransport::Send(const uint8_t* p_data, size_t p_length)
{
if (!m_connected.load() || !m_wsi.load()) {
return;
}
std::vector<uint8_t> buf(LWS_PRE + p_length);
SDL_memcpy(&buf[LWS_PRE], p_data, p_length);
m_sendCS.Enter();
m_sendQueue.push_back(std::move(buf));
m_sendCS.Leave();
m_wantWritable.store(true);
if (m_context) {
lws_cancel_service(m_context);
}
}
size_t LwsTransport::Receive(std::function<void(const uint8_t*, size_t)> p_callback)
{
if (!m_context) {
return 0;
}
std::deque<std::vector<uint8_t>> local;
m_recvCS.Enter();
local.swap(m_recvQueue);
m_recvCS.Leave();
for (const auto& msg : local) {
p_callback(msg.data(), msg.size());
}
return local.size();
}
void LwsTransport::ServiceLoop()
{
if (m_wantWritable.exchange(false)) {
struct lws* wsi = m_wsi.load();
if (wsi) {
lws_callback_on_writable(wsi);
}
}
lws_service(m_context, LWS_SERVICE_TIMEOUT_MS);
}
int LwsTransport::HandleLwsEvent(struct lws* p_wsi, int p_reason, void* p_in, size_t p_len)
{
switch (p_reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED:
SDL_Log("[Multiplayer] WebSocket connection established");
m_connected.store(true);
m_wasEverConnected.store(true);
break;
case LWS_CALLBACK_CLIENT_RECEIVE:
m_fragment.insert(m_fragment.end(), static_cast<uint8_t*>(p_in), static_cast<uint8_t*>(p_in) + p_len);
if (lws_is_final_fragment(p_wsi)) {
m_recvCS.Enter();
m_recvQueue.push_back(std::move(m_fragment));
m_recvCS.Leave();
m_fragment.clear();
}
break;
case LWS_CALLBACK_CLIENT_WRITEABLE: {
m_sendCS.Enter();
if (!m_sendQueue.empty()) {
std::vector<uint8_t> front = std::move(m_sendQueue.front());
m_sendQueue.pop_front();
bool hasMore = !m_sendQueue.empty();
m_sendCS.Leave();
size_t payloadLen = front.size() - LWS_PRE;
lws_write(p_wsi, &front[LWS_PRE], payloadLen, LWS_WRITE_BINARY);
if (hasMore) {
lws_callback_on_writable(p_wsi);
}
}
else {
m_sendCS.Leave();
}
break;
}
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
SDL_Log("[Multiplayer] WebSocket connection error: %s", p_in ? static_cast<const char*>(p_in) : "unknown");
m_disconnected.store(true);
m_connected.store(false);
m_wsi.store(nullptr);
break;
case LWS_CALLBACK_CLIENT_CLOSED:
SDL_Log("[Multiplayer] WebSocket connection closed");
m_disconnected.store(true);
m_connected.store(false);
m_wsi.store(nullptr);
break;
default:
break;
}
return 0;
}
} // namespace Multiplayer
#endif // !__EMSCRIPTEN__

View File

@ -0,0 +1,37 @@
#ifndef __EMSCRIPTEN__
#include "extensions/multiplayer/platforms/native/nativecallbacks.h"
#include <SDL3/SDL_log.h>
namespace Multiplayer
{
void NativeCallbacks::OnPlayerCountChanged(int p_count)
{
if (p_count < 0) {
SDL_Log("[Multiplayer] Left multiplayer world");
}
else {
SDL_Log("[Multiplayer] Player count changed: %d", p_count);
}
}
void NativeCallbacks::OnThirdPersonChanged(bool p_enabled)
{
SDL_Log("[Multiplayer] Third person camera: %s", p_enabled ? "enabled" : "disabled");
}
void NativeCallbacks::OnNameBubblesChanged(bool p_enabled)
{
SDL_Log("[Multiplayer] Name bubbles: %s", p_enabled ? "enabled" : "disabled");
}
void NativeCallbacks::OnAllowCustomizeChanged(bool p_enabled)
{
SDL_Log("[Multiplayer] Allow customization: %s", p_enabled ? "enabled" : "disabled");
}
} // namespace Multiplayer
#endif // !__EMSCRIPTEN__

View File

@ -23,7 +23,7 @@ export class GameRoom implements DurableObject {
private nextPeerId = 1; private nextPeerId = 1;
private hostPeerId = 0; private hostPeerId = 0;
private maxPlayers = 5; private maxPlayers = 5;
private maxActors = 5; private maxActors = 0;
constructor( constructor(
private state: DurableObjectState, private state: DurableObjectState,

View File

@ -182,12 +182,14 @@ MxBool WorldStateSync::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeT
if (p_entity->GetType() == LegoEntity::e_plant) { if (p_entity->GetType() == LegoEntity::e_plant) {
entityType = ENTITY_PLANT; entityType = ENTITY_PLANT;
MxS32 count; MxS32 count;
idx = FindEntityIndex(PlantManager()->GetInfoArray(count), count, p_entity); LegoPlantInfo* info = PlantManager()->GetInfoArray(count);
idx = FindEntityIndex(info, count, p_entity);
} }
else if (p_entity->GetType() == LegoEntity::e_building) { else if (p_entity->GetType() == LegoEntity::e_building) {
entityType = ENTITY_BUILDING; entityType = ENTITY_BUILDING;
MxS32 count; MxS32 count;
idx = FindEntityIndex(BuildingManager()->GetInfoArray(count), count, p_entity); LegoBuildingInfo* info = BuildingManager()->GetInfoArray(count);
idx = FindEntityIndex(info, count, p_entity);
} }
else { else {
return FALSE; return FALSE;

View File

@ -7,7 +7,9 @@
using namespace Extensions::ThirdPersonCamera; using namespace Extensions::ThirdPersonCamera;
InputHandler::InputHandler() : m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false) InputHandler::InputHandler()
: m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false), m_rightButtonHeld(false), m_savedMouseX(0.0f),
m_savedMouseY(0.0f)
{ {
} }
@ -103,7 +105,7 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
if (!p_active) { if (!p_active) {
break; break;
} }
if (p_event->motion.state & SDL_BUTTON_RMASK) { if (m_rightButtonHeld) {
p_orbit.AdjustYaw(-p_event->motion.xrel * 0.005f); p_orbit.AdjustYaw(-p_event->motion.xrel * 0.005f);
p_orbit.AdjustPitch(p_event->motion.yrel * 0.005f); p_orbit.AdjustPitch(p_event->motion.yrel * 0.005f);
p_orbit.ClampPitch(); p_orbit.ClampPitch();
@ -112,19 +114,28 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool
case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP: { case SDL_EVENT_MOUSE_BUTTON_UP: {
if (!p_active) { if (p_event->button.button == SDL_BUTTON_RIGHT) {
break; m_rightButtonHeld = p_event->button.down;
} if (!p_active) {
SDL_Window* window = SDL_GetWindowFromID(p_event->button.windowID); break;
if (window) { }
SDL_SetWindowRelativeMouseMode(window, SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_RMASK); SDL_Window* window = SDL_GetWindowFromID(p_event->button.windowID);
if (window) {
if (m_rightButtonHeld) {
SDL_GetMouseState(&m_savedMouseX, &m_savedMouseY);
SDL_SetWindowRelativeMouseMode(window, true);
}
else {
SDL_SetWindowRelativeMouseMode(window, false);
SDL_WarpMouseInWindow(window, m_savedMouseX, m_savedMouseY);
}
}
} }
break; break;
} }
case SDL_EVENT_FINGER_DOWN: { case SDL_EVENT_FINGER_DOWN: {
if (!IsFingerTracked(p_event->tfinger.fingerID) && m_touch.count < 2 && if (!IsFingerTracked(p_event->tfinger.fingerID) && m_touch.count < 2 && p_event->tfinger.x >= CAMERA_ZONE_X) {
p_event->tfinger.x >= CAMERA_ZONE_X) {
int idx = m_touch.count; int idx = m_touch.count;
m_touch.id[idx] = p_event->tfinger.fingerID; m_touch.id[idx] = p_event->tfinger.fingerID;
m_touch.x[idx] = p_event->tfinger.x; m_touch.x[idx] = p_event->tfinger.x;