When transitioning from a restricted area (elevator, observatory, etc.)
back to the Isle world, the player count stayed at 0 because nothing
triggered a recount — the Isle world was already enabled so
OnWorldEnabled didn't fire. Track restricted-area status changes in
BroadcastLocalState and call NotifyPlayerCountChanged on transitions.
Also sync the flag in OnWorldEnabled/OnWorldDisabled to prevent
spurious recounts across world transitions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GameState() can return null during shutdown when camera re-init is
triggered. Add a null check before accessing m_currentArea.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI / ${{ matrix.name }} (false, -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0.26100.0, false, false, Visual Studio 17 2022, true, Xbox One, windows-latest, amd64, false, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/3DS.cmake, false, devkitpro/devkitarm:latest, false, Ninja, true, Nintendo 3DS, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake, false, devkitpro/devkita64:latest, false, Ninja, Nintendo Switch, true, ubuntu-latest, true) (push) Has been cancelled
Overlay areas (elevator rides, observatory, gas station/police doorways)
stay within the Isle world but use fixed cameras with no free-roaming
movement. The third person camera and player display were incorrectly
staying active in these areas, and remote players remained visible at
their last open-world position.
Query GameState::m_currentArea at runtime to detect restricted areas,
avoiding new state variables or hooks into decompiled code. Extract
shared IsRestrictedArea() into a common header used by both the camera
controller and multiplayer networking. On the multiplayer side, broadcast
WORLD_NOT_VISIBLE as the worldId so remote clients hide the player via
existing visibility logic, and exclude the local player from the player
count when in a restricted area.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 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>
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>
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) <noreply@anthropic.com>
Separate the switch-to-first-person threshold from the default starting
distance so the camera can zoom in closer before transitioning. Both
mouse and touch inputs use the same new SWITCH_TO_FIRST_PERSON_DISTANCE
constant (0.5f), while MIN_DISTANCE (1.5f) remains as the default orbit
distance when activating 3rd person mode.
https://claude.ai/code/session_01KJ8KHH4XWx7F5VxhSNGo7M
- Reduce pinch zoom sensitivity (15x → 6x multiplier)
- Add cumulative deadzone threshold for 1st/3rd person transitions
to prevent accidental mode switches from slight finger movement
- Preserve camera touch tracking through 3rd→1st transition so the
same fingers can pinch back without lifting (seamless round-trip)
- On 1st→3rd transition, selectively clear only camera-owned fingers
from LegoInputManager's touch scheme state, preserving any active
left-side movement finger
- Suppress camera gesture processing until finger positions re-sync
after transition to prevent camera jumps from stale coordinates
- Hide local player name bubble when transitioning to 1st person,
restore on transition back to 3rd person
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Emote animations like Toss (CNs013Pa) reference prop nodes (POPMUG,
*POPMUG01) that don't exist in the player's ROI hierarchy. This change
dynamically detects unmatched animation tree nodes and creates prop ROIs
for them, making pizza props visible during the Toss emote.
- Add shared PropGroup struct for ride and emote prop lifecycle
- Add CollectUnmatchedNodes to scan animation trees for missing ROIs
- Extend BuildROIMap/AssignROIIndices to accept an array of extra ROIs
- Add *-prefix fallback: subsequent *-nodes search extra ROIs
- Add ResolvePropLODName mapping for node-to-LOD name differences
- Refactor ride system to use PropGroup (no behavior change)
- Clean up emote props on completion, movement interrupt, and world transition
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 3 idle styles (Wobbly, Peppy, Brickster) and 3 emotes
(Look Around, Headless, Toss). Fix AssignROIIndices to only
let the first root-mapped node claim the root ROI, preventing
prop nodes (e.g. *POPMUG01) from hiding the player via morph keys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow the game room server to accept maxActors=0 (previously floored
at 5). When received, the client disables extra actor spawning, camera
animations, and continuously purges all extras including the ambient
NPCs (mama, papa, brickster) that PurgeExtra(TRUE) deliberately skips.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove unused `using namespace Extensions` from isleapp.cpp and replace
per-header forward declaration blocks in 7 LEGO1 headers with a single
shared forward declarations header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* WIP
* WIP
* Make camera the single source of truth for broadcast state
Remove redundant local copies of walkAnimId, idleAnimId, and
displayActorIndex from NetworkManager. BroadcastLocalState now reads
these from the camera's Controller, eliminating dual-copy sync issues.
Additional cleanup:
- Early-return on null cam in SendEmote/HandleCustomize for clarity
- Only consume camera-dependent pending requests when cam is available
- Move local name bubble creation from BroadcastLocalState to Tickle
- Remove dead NetworkManager::SetDisplayActorIndex method
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix clang format
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Auto-switch camera between 1st and 3rd person based on zoom
When in 3rd person and zooming in past minimum distance (mouse wheel or
pinch), automatically switch to 1st person. When in 1st person and
zooming out (mouse wheel or pinch), automatically switch to 3rd person
starting at minimum distance for a seamless transition.
Adds thirdPersonChanged CustomEvent on canvas to notify the UI toggle
of auto-switch state changes, following the existing PlatformCallbacks
pattern used by OnPlayerCountChanged.
https://claude.ai/code/session_01PuMFBB8Gjd5pyUVUh5QTzz
* Add callback feedback for multiplayer toggle settings
Toggle UI state is now driven by C++ callbacks instead of optimistic
local updates, preventing desync when the game thread hasn't processed
the request yet. Adds OnNameBubblesChanged and OnAllowCustomizeChanged
to PlatformCallbacks, and fires OnThirdPersonChanged for manual toggle
(previously only fired for auto-switch). Includes touch pinch fixes
and ResetTouchState for third-person camera auto-enable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix: restrict 3rd person camera to ISLE world only
The camera was activating in the Infocenter (and other non-ISLE worlds)
because OnWorldEnabled/Disabled forwarded to ThirdPersonCamera
unconditionally. Zoom/pan/auto-switch SDL events were also processed
outside the ISLE world. Gate both on the e_act1 world ID check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix emote interruption when switching to 1st person camera
Allow movement to interrupt multi-part emote phase 1 (not just
non-multi-part emotes). On the remote player side, only suppress
movement during frozen state rather than all multi-part emote phases,
so the remote animator correctly cancels the emote when the local
player switches cameras and moves.
Also track and stop ROI-bound sounds before the ROI is destroyed
to prevent use-after-free in the sound system's 3D position update.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* DRY: extract DispatchBoolEvent helper for emscripten callbacks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix camera drifting during multi-part emotes
Block translational movement in HandleCameraRelativeMovement() when
a multi-part emote is active. Previously, movement input was processed
unconditionally, causing the native ROI (and camera) to move forward
while the display ROI stayed pinned at the emote position. Now the
emote check forces hasInput=false and zeros m_smoothedSpeed so neither
position updates nor coasting occur during any phase of a multi-part
emote. Camera orbit controls (rotation/pan/zoom) remain unaffected.
https://claude.ai/code/session_01QMcZSa3ysdyACVea66QA5K
* Fix use-after-free crash in NameBubbleRenderer::Update
Lego3DSound::FUN_10011a60 (used by LegoCacheSound::Play) did not reset
m_isActor and m_enabled when reassigning the ROI reference. When a
cached sound was reused — first for a known actor name (setting
m_isActor=TRUE), then for a multiplayer clone found via FindROI — the
stale m_isActor flag caused Reset() to call ReleaseActor on the clone's
ROI, freeing it while ThirdPersonCamera still held a pointer. The next
Tick then dereferenced the dangling pointer in NameBubbleRenderer::Update.
Reset the ownership flags at the top of the reassignment path so they
match the clean-state semantics of Lego3DSound::Create.
Also guard multi-part emotes behind an active 3rd-person camera check
and remove a leftover debug log in RemotePlayer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Decouple 3rd person camera from player movement
Arrow keys now move the player relative to the camera direction instead
of using tank controls. The camera stays static unless explicitly
controlled via right-click drag, mouse wheel, or touch gestures.
Adds a single hook in CalculateNewPosDir that lets the extension take
over movement computation. The extension manages its own velocity
smoothing, computes camera-relative directions from an absolute yaw,
and writes nav controller velocities via friend class access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Clean up ThirdPersonCamera: remove dead m_orbitYaw, extract helpers
Remove m_orbitYaw (always DEFAULT_ORBIT_YAW) and pass yaw directly to
ComputeOrbitVectors, eliminating save/restore blocks at all call sites.
Extract GetLocalYaw() and InitAbsoluteYaw() helpers to deduplicate
repeated atan2 patterns. Simplify SetupCamera by passing DEFAULT_ORBIT_YAW
directly. Move TURN_RATE constant to file scope.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Hide cursor while orbiting 3rd person camera with right mouse button
Move SDL event handling from LegoInputManager::UpdateLastInputMethod to
an exported function called at the end of SDL_AppEvent, so cursor state
changes aren't overridden by isleapp's SDL_ShowCursor(). Use
SDL_SetWindowRelativeMouseMode to hide the cursor and freeze its position
while the right mouse button is held in 3rd person camera mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Rapid customize clicks would orphan running click animations because
SetClickAnimObjectId overwrote the tracked ID without stopping the old
animation first. When movement later called StopClickAnimation, only
the last animation was stopped, leaving earlier ones driving the ROI
transform and causing the player model to stay behind while the camera
moved forward.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add optional sound effects to emotes
Replace the raw 2D string emote table with a struct-based EmoteEntry/EmotePhase
table so each emote phase can declare an optional 3D spatialized sound effect.
The sound plays via LegoCacheSoundManager when the emote phase starts, positioned
at the character's ROI. No extra network sync needed since sounds trigger locally
when TriggerEmote is called on each client.
Adds "crash5" sound to the BNsDis01 (disassemble) emote phase.
https://claude.ai/code/session_013XkwJJZ4RbNcBMFESuPTfJ
* Extract animation/asset tables from protocol.h into animdata.h
Move EmotePhase, EmoteEntry structs, animation table externs
(walk/idle/emote/vehicle), and helpers (IsMultiPartEmote,
IsLargeVehicle, DetectVehicleType) into a dedicated animdata.h/cpp
pair. This keeps protocol.h focused on wire-format concerns (message
structs, serialization, enums) while animdata.h owns game asset data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add stateful multi-part emote system with disassemble/reassemble
Introduces a generalized multi-part emote framework where emotes can have
two phases. The first trigger plays phase 1 and freezes the character at its
last frame; the second trigger plays phase 2 to restore normal state.
Movement is blocked for the entire duration of a multi-part emote (from
phase 1 start through frozen state to phase 2 completion). The frozen
state is synced to all peers via customizeFlags bits in PlayerStateMsg,
so new joiners see disassembled players correctly.
The emote table is now a 2D array (g_emoteAnims[][2]) where [1] is the
phase-2 animation name (nullptr for one-shot emotes). Adding future
multi-part emotes only requires a new row in the table.
https://claude.ai/code/session_01L5FiuVFUqASR93iJcaXfEi
* Fix emote movement blocking and frozen state sync
Move movement blocking from CalculateTransform hook (which broke the
camera by skipping p_transform output) to ThirdPersonCamera::Tick where
it zeroes speed/velocity directly. Remove ShouldBlockMovement and
ShouldInvertMovement hooks entirely.
Rebuild frozen emote animation cache in InitAnimCaches when the frozen
state was set before the ROI was available (state message arrived before
world was ready).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Clean up emote branch: remove unused include, extract ClearFrozenState helper
- Remove unused multiplayer.h include and using-directive from legopathactor.cpp
- Extract ClearFrozenState() to DRY up 4 identical frozen state reset blocks
- Clarify bit-encoding comment with mask value and emote ID limit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Suppress right-click game interactions during free camera orbit
Rename IsTouchInputSuppressed to IsThirdPersonCameraActive and simplify
to return TRUE whenever the 3rd person camera is active. Use this in
ProcessOneEvent to skip right-click-only events, preventing accidental
game object interactions while orbiting the free camera.
https://claude.ai/code/session_01KuQtP2prfbVho2myKZ2pxE
* Move touch suppression logic into HandleTouchInput extension hook
Instead of checking IsThirdPersonCameraActive (which suppressed all
touch input whenever the camera was active), add a dedicated
HandleTouchInput hook that only suppresses touch input during active
multi-touch camera gestures. Move g_finger static into LegoInputManager
as m_touchFinger and grant friend access to MultiplayerExt so the
extension can manipulate touch state directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix 180-degree camera flip when exiting sub-worlds without cam anim
When returning from sub-worlds (jukebox, hospital exterior, etc.) with
3rd person camera active, the camera/player flipped 180 degrees. This
happened because SpawnPlayer calls Enter() → TurnAround() before
PlaceActor(), and PlaceActor overwrites the ROI direction with the
path's standard convention (z = forward). For spawn points with cam
anims, OnCamAnimEnd corrected this, but spawn points with m_location=0
(like jukebox exterior) had no correction.
Replace m_roiUnflipped with m_needsDirectionFlip flag that tracks world
transitions. OnWorldEnabled sets the flag, and the first Tick after
PlaceActor completes flips the ROI to backward-z and re-setups the
camera. ReinitForCharacter now always flips the ROI direction, handling
both Disable→Enable toggles and enabling 3rd person after a 1st-person
spawn.
https://claude.ai/code/session_01NQ9vy9Qr3aH6LNsRNLEEtY
* Fix camera flip regressions for vehicle exit and world transitions
The unconditional ROI flip in ReinitForCharacter caused a 180-degree
flip when exiting vehicles (Enter's TurnAround follows and double-flips).
Restore conditional flip using m_roiUnflipped, but now also set it in
OnWorldEnabled (even when disabled) so cold-enabling 3rd person after a
world transition correctly flips from PlaceActor's forward-z.
Key changes:
- Remove m_roiUnflipped clearing from OnActorEnter: Enter() is always
followed by PlaceActor which overwrites the ROI, so clearing the flag
prematurely caused the cold-enable case to miss the needed flip.
- Add orbit camera override in OnActorEnter during world transitions
(m_needsDirectionFlip && m_active) to suppress the 1st-person camera
flash from Enter's TransformPointOfView.
- Clear m_roiUnflipped in OnCamAnimEnd alongside m_needsDirectionFlip,
since the cam anim's PlaceActor + flip handles the correction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Defer orbit camera setup during world transition loading freeze
Remove the premature SetupCamera call from OnActorEnter's world
transition path. The stale orbit camera view (computed before
PlaceActor runs) would freeze on screen during the ~500ms world
load, appearing as a wrong-direction flash. The Tick correction
after PlaceActor now handles the initial orbit camera setup at
the correct position.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Skip orbit camera while cam anim is running to prevent actor glitch
When a cam anim plays with 3rd-person camera active, ApplyOrbitCamera
was fighting the cam anim each frame. If the cam anim was interrupted
(space bar), its end handler read the ViewROI position — which was set
by our orbit camera (elevated, behind player) — and placed the actor
at that position, causing it to glitch into the air.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add documentation for ROI direction conventions and camera corrections
Documents the forward-z vs backward-z conventions, all code paths that
require direction correction, and the flags that coordinate them.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refactor 3rd-person camera from backward-z to forward-z convention
Switch the orbit camera to use forward-z (matching PlaceActor's native
output), eliminating all FlipROIDirection/TurnAround corrections. The
display clone flips to backward-z when syncing from the native ROI so
character meshes face correctly. Use actor state (c_disabled) instead of
m_animRunning to guard against cam anim conflicts, allowing the orbit
camera to resume as soon as the player is released.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Reset orbit camera position on world re-entry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove ShouldInvertMovement and update documentation
* Remove header
Extract ~420 lines of duplicated character animation logic from RemotePlayer
and ThirdPersonCamera into a shared CharacterAnimator component. Both classes
now compose a CharacterAnimator member that handles walk/idle/emote animation
playback, vehicle ride animations, click animation tracking, name bubbles,
and animation cache management.
Behavioral differences between consumers (emote transform save/restore) are
handled via CharacterAnimatorConfig.
Also extract duplicated username encoding (letter indices to ASCII) from
NetworkManager::BroadcastLocalState and ThirdPersonCamera::CreateNameBubble
into EncodeUsername() in protocol.cpp.
Replace C standard library usage across the multiplayer extension with SDL
equivalents: sprintf->SDL_snprintf, sscanf->SDL_sscanf, atoi->SDL_atoi,
strcmp->SDL_strcmp, fabsf->SDL_fabsf, floorf->SDL_floorf, and remove
unnecessary <cmath>, <cstdio>, <cstdlib> headers.
Add m_displayActorFrozen flag to distinguish INI-configured display
actors from auto-derived ones. Derive displayActorIndex (actorId - 1)
at the top of every Tickle(), ensuring it is valid before the 3rd
person camera toggle or any broadcast. This eliminates the native ROI
fallback path in ThirdPersonCamera which was buggy (remote player ROIs
not appearing, customization not propagating, 3rd person camera not
working without INI config).
Remove all dead branches that checked IsValidDisplayActorIndex before
deciding between clone and native ROI paths, since the display actor
index is now always valid. Simplify ResolveActorInfoIndex to a single
parameter and remove the actorId fallback.
When exiting a race, LegoRace::Create stashes the UserActor and sets
it to NULL. The destructor restores it, but runs later than
OnWorldEnabled, so NotifyPlayerCountChanged sees a NULL UserActor and
doesn't count the local player. Fall back to GameState::GetActorId()
which is restored earlier (in LegoRace::Enable(FALSE)).
* Add free camera orbit controls to multiplayer third-person camera
Replace the fixed camera offset with dynamic orbit parameters (yaw,
pitch, distance) computed from spherical coordinates. Mouse wheel
controls zoom, right-click drag controls orbit, and two-finger
touch gestures support pinch-zoom and orbit on touchscreens.
The controls are always active when third-person mode is enabled.
A single SDL event forwarding hook is added to the main event loop;
all other changes are contained within the multiplayer extension.
https://claude.ai/code/session_013FyPCrJSaHxiJwdfGBVnYP
* Fix linker error and refactor orbit camera controls
Move HandleSDLEvent extension call from ISLE (no EXTENSIONS define) into
LegoInputManager::UpdateLastInputMethod in LEGO1 to fix unresolved
symbol errors. DRY up orbit camera code with ClampPitch/ClampDistance/
ResetOrbitState/ApplyOrbitCamera helpers. Simplify direction vector
computation. Fix mouse orbit yaw direction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fixes
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Sync sky color and light position in multiplayer
Add ENTITY_SKY and ENTITY_LIGHT to the WorldEvent system so the host
controls sky color (hue/saturation via observatory sun/moon/palette
buttons) and light position (globe arrows) with the same
host-authoritative pattern used for plants and buildings. Non-host
players send requests to the host who applies and broadcasts. Sky/light
state is appended to the world snapshot so joining players get the
current values.
https://claude.ai/code/session_01X2cPVQEo7c92wpWA7QPPMG
* Clean up sky/light sync: remove debug logging, DRY apply logic, fix host routing
- Extract ApplySkyLightState helper to deduplicate sky/light apply code
between RestoreSkyLightState and HandleWorldSnapshot
- Remove all SDL_Log debug calls and SDL_log.h includes
- Remove dead OnWorldEnabled method from WorldStateSync (replaced by
OnHostChanged in OnSaveLoaded)
- Fix HandleSkyLightMutation host path: return FALSE to let local
switch case proceed, instead of duplicating via ApplyWorldEvent
- Simplify isle.cpp HandleControl: split observatory cases into
individual switch arms with single early-return multiplayer hook
- Add save load hooks to sync world state with multiplayer peers
- Fix player count to exclude local player without valid actor
- Support broadcast snapshots (targetPeerId=0) in relay server
---------
* WIP: Add character customization to multiplayer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Refine character customization: fix message buffering, DRY up code, request-based model
- Register NetworkManager with TickleManager via HandleCreate hook in
LegoOmni::Create(), so packets are processed continuously instead of
buffering between Connect() and OnWorldEnabled()
- Spawn unspawned remote players in OnWorldEnabled() (created before
world was ready)
- Switch to request-based customization: HandleROIClick sends
MSG_CUSTOMIZE to server, server echoes to all peers, HandleCustomize
applies state and plays effects
- DRY up SwitchVariant to delegate LOD cloning to ApplyHatVariant
- Add ApplyChange helper consolidating the switch-on-changeType pattern
- Fix InitFromActorInfo to derive dependent color parts from independent
parts (matching Unpack rules)
- Remove dead code: m_hasBeenTicked, ApplyCustomizeChange on
RemotePlayer, m_localCustomizeState on NetworkManager
- Add null ROI checks in HandleCustomize for unspawned players
- Move MSG_CUSTOMIZE constant to shared protocol.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix
* Fix character customization bugs from code review
- BUG-1: Add bounds check in SwitchColor to prevent OOB access from
unvalidated network input (p_partIndex could exceed array bounds)
- BUG-2: Enforce allowRemoteCustomize on receiver side in HandleCustomize
(was only checked on sender side, byppassable by malicious client)
- BUG-3: Document stale-state sound asymmetry for remote targets
- OBS-1: Remove unused bodyVariantIndex from CustomizeState (never
modified, wasted 3 bits per state message)
- NAME-1: Fix p_ prefix convention on ApplyCustomizeChange parameters
Renders a billboard text bubble showing each remote player's display
name. Includes a WASM export to toggle visibility from the frontend.
- Bitmap font renderer generates paletted textures for name labels
- Billboard quad faces the camera each frame via orientation matrix
- Bubble visibility managed globally by NetworkManager toggle
- Fix miniwin D3DRMIMAGE constructor code style (static_cast, const)
* Implement display actor override for multiplayer extension
Add displayActorIndex to the multiplayer protocol, allowing players to
choose any of the 66 character models from g_actorInfoInit via the
multiplayer:actor INI setting. The visual display is decoupled from the
gameplay actor ID while maintaining backward compatibility.
- Protocol: Add displayActorIndex field to PlayerStateMsg and validation helpers
- RemotePlayer: Use display actor name for cloning instead of actorId
- NetworkManager: Broadcast/handle displayActorIndex, respawn on display change
- ThirdPersonCamera: Create/manage display clone ROI for local player override
- INI: Read multiplayer:actor setting and resolve to g_actorInfoInit index
* Use array syntax for INI option access in display actor setup
Consistent with how relayUrl and room are read from options.
* Fix display actor ROI handling in 3rd person camera
- Fix direction flip targeting display clone instead of native ROI in
Disable(), ReinitForCharacter(), and OnCamAnimEnd(). The native ROI is
the source of truth for TransformPointOfView and Tick() sync.
- Fix use-after-free: DestroyDisplayClone() now nulls m_playerROI when
it points to the destroyed clone, preventing dangling pointer access
in BuildRideAnimation after a 3rd→1st→3rd person toggle on a vehicle.
- Recreate display clone in ReinitForCharacter() vehicle branch.
- Extract EnsureDisplayROI() helper to deduplicate clone setup pattern.
- Move IsValidDisplayActorIndex() to charactercloner.h, replacing magic
number 66 with sizeOfArray(g_actorInfoInit).
* Remove display actors plan document
* Fix 3rd person camera 180-degree flip after cam anim ends
When a cam anim (NPC interaction cutscene) ends, FUN_1004b6d0 places the
player actor at the camera's final position using the camera's direction,
which uses standard convention (z = visual forward). The 3rd person camera
relies on backward-z convention (z = visual backward, from TurnAround).
This mismatch caused the camera to face 180 degrees wrong permanently
after any cam anim.
Add a HandleCamAnimEnd extension hook in FUN_1004b6d0 that, when the 3rd
person camera is active, flips the ROI direction back to backward-z and
re-establishes the correct camera position.