The SI extra data format uses colon as key-value separator
(e.g. PTATCAM:HEAD;INFOHAT), matching KeyValueStringParse's
delimiter set. The parser incorrectly required an equals sign,
so PTATCAM directives were never found and the feature was
completely inactive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Include 3dmanager/lego3dmanager.h to provide the full class definition,
which was only forward-declared via legovideomanager.h.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extend the multiplayer animation system to support animations from all
three game worlds (ACT1, ACT2, ACT3) while playing in the Isle world.
Catalog: Parse DTA files directly for all worlds instead of borrowing
from LegoAnimationManager. World-encoded animIndex (top 2 bits = world
slot) provides globally unique IDs without wire protocol changes.
Loader: Support multiple SI files (isle.si, act2main.si, act3.si) with
lazy opening and composite (worldId, objectId) cache keys.
WDB: Load missing model LODs from WORLD.WDB for all worlds during
catalog refresh, using LegoPartPresenter for parts and
LegoModelPresenter for compound models (ray, chptr).
Protocol: AnimCompleteMsg now carries animIndex instead of objectId.
Also fix pre-existing bugs:
- PhonemePlayer UAF when multiple tracks target the same ROI
- ModelDbModel buffer overflow on word-aligned strlcpy reads
- SIReader UBSan violation on uninitialized filetype enum
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The inline `class LegoROI*` in Catalog::GetVehicleState was being
resolved within `Multiplayer::Animation` namespace on some compilers
(e.g. devkitA64 GCC), creating an incomplete type. Move the forward
declaration before the namespace block.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace magic numbers with named constants across third-person camera,
multiplayer, and common utilities. Extract duplicated code into shared
helpers: CancelExternalAnim, StartEmotePhase, DeriveDependentIndices,
ReaddROI, SendFixedMessage. Deduplicate finger-down handling via
TryClaimFinger and tighten Extensions::Enable() dispatch with a
table-driven approach. Fix missing <functional> include in sceneplayer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The build exit animations (e.g. c_ips002ro_RunAnim for helicopter) are
triggered via FUN_10060dc0 in SpawnPlayer, which bypasses the
m_enableCamAnims check. Mark m_playedExitScript = TRUE for all vehicle
build states in EnforceDisableNPCs to prevent them from firing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In large vehicles the third-person camera display ROI is frozen at the
entry position, so the name bubble stayed there. Fall back to the actual
UserActor ROI when the camera controller is inactive due to a large
vehicle, matching how remote players already handle this.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ComputeChildOffsets and ApplyHierarchyTransform had the multiplication
operands swapped. In row-major convention with translation in row 3:
offset = child_world * inverse(parent_world)
child_new = offset * new_parent_world
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract generic SI file reading and audio extraction from
Multiplayer::Animation::Loader into a new Multiplayer::SIReader class.
This eliminates the coupling where NetworkManager reached into the
Animation namespace to extract horn WAV data, and removes the wasteful
intermediate horn cache (LegoCacheSound::Create copies PCM data, so the
cache served no purpose after template creation).
- New Multiplayer::SIReader owns SI file handle, header-only reading,
lazy object loading, and audio track extraction
- New Multiplayer::AudioTrack struct (moved from SceneAnimData::AudioTrack)
- Animation::Loader delegates SI access via SetSIReader() pointer
- NetworkManager owns SIReader, passes it to Loader, uses it directly
for horn sound extraction via ExtractFirstAudio()
- Consolidate duplicate horn vehicle arrays into single g_hornVehicles[]
- Move HornMsg next to EmoteMsg in protocol (both one-shot broadcasts)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stop active ScenePlayer animation in OnActorEnter/OnActorExit before
modifying ride animation state — the ScenePlayer may still hold a
reference to the ride vehicle ROI that ClearRideAnimation frees.
Deactivate() and OnWorldDisabled() already had this guard.
Add alignment padding to MessageHeader (13→14 bytes) so uint16_t fields
in packed protocol structs no longer sit at odd offsets (UBSan violation).
Breaking wire format change — all clients and relay must update together.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GetCharacterVehicleId used hardcoded g_characters[] indices but was
called with g_actorInfoInit[] indices. Since g_actorInfoInit has an
extra "infoman" entry at index 5, all characters after Laura were
off-by-one — bikers got the wrong vehicle and sy (Shiney Doris) fell
off the switch entirely, disabling filtering completely.
Replace with data-driven lookup (actorInfoInit name → g_characters
vehicleId), consolidate duplicate GetVehicleCategory into Catalog,
remove dead characterIndex field, fix stale g_characters comments,
remove temporary debug logging, and DRY local vehicle state computation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add vehicle-based filtering to the multiplayer ScenePlayer so that
animations requiring a specific vehicle (skateboard, bike, motorcycle)
are only offered when the performer is actually riding that vehicle.
- Add vehicleMask to CatalogEntry from AnimInfo::m_unk0x2a
- Three-state vehicle detection: on foot, on own vehicle, on foreign vehicle
- Filter performer animations by vehicle state in eligibility computation
- Spectator-only roles remain visible regardless of vehicle state
- Host validates vehicle state on interest and re-validates during countdown
- Cancel active sessions when local player's vehicle state changes
Includes temporary debug logging tagged TODO(vehicle-filter).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Manually override 6 spectator-only animations (557, 596, 709-711, 754)
as cam anims in the catalog, with 754's location set to Hospital.
For animations referencing world-placed models (BIRD, SHARK, CAT) whose
LOD data isn't independently registered in the ViewLODListManager, add a
scene ROI cloning fallback in SetupROIs. DeepCloneROI recursively clones
the ROI hierarchy sharing LOD geometry via refcount, producing independent
copies safe for concurrent playback. Moved to AnimUtils as a reusable
utility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The engine derives characterIndex from the last 2 chars of the animation
filename, which misses animations with suffixes like "in" (infoman), "p1",
"sl". These were incorrectly categorized as e_otherAnim and hidden from
multiplayer despite having valid named character performers.
Categorize based on whether the animation has at least one named character
in its performer mask (g_actorInfoInit indices 0-47 + 54-57) instead of
requiring characterIndex >= 0. This adds 23 animations to the catalog.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the single ScenePlayer/m_playingAnimIndex with a map of
ScenePlayers keyed by animation index, allowing non-overlapping groups
of players to play different animations simultaneously. Each player can
still only participate in one animation at a time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PhonemePlayer resolved ROIs by name, but multiplayer participants have
unique ROI names (e.g. pepper_mp_8) that don't match the animation actor
name in the SI phoneme track (pepper). Pass actor-name-to-ROI aliases
from SetupROIs through to PhonemePlayer::Init so it can resolve
participant ROIs correctly.
Also lower/raise background audio volume for the duration of scene
animations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Players near multiple location points now see cam animations from all
overlapping locations instead of only the nearest one. Location proximity
radius reduced from 15 to 5 units. NPC animations unchanged (still
proximity-based). JSON output updated from "location" to "locations" array.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Camera animations compute high listener velocities via
CalculateWorldVelocity, and miniaudio's default Doppler factor of 1.0
shifts the pitch/speed of all spatialized sounds accordingly. Disable
Doppler on sounds created by the multiplayer AudioPlayer using a friend
class to access LegoCacheSound's private ma_sound handle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When AttemptReconnect() disconnects a still-CONNECTING socket and creates
a new one, the old socket's onclose fires asynchronously after Connect()
clears m_disconnectedFlag, setting it back to 1. With connectedFlag=1
and disconnectedFlag=1 both stuck, the state machine bounces between
STATE_CONNECTED and STATE_RECONNECTING every frame (30-60x/sec) without
ever reaching the backoff timer or attempt limit.
Guard all WebSocket event handlers to only modify shared flags when
their socket is still the active one in Module._mpSockets.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add RoomRegistry durable object for tracking public rooms with
stale-entry cleanup. GameRoom notifies the registry on connect,
disconnect, and creation. Extract duplicated CORS_HEADERS to a
shared cors.ts module and remove dead OPTIONS handlers from DOs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Add animation completion protocol message for Nick Brick's Memories
Add MSG_ANIM_COMPLETE (15) protocol message broadcast by the host on
natural animation completion. Contains a 64-bit random event ID, the
SI object ID, and per-participant data (charIndex, displayName).
- BroadcastAnimComplete: gathers participants from session slots,
resolves spectator characters from display actors, generates event ID
- HandleAnimComplete: filters observers (only participants get callback),
builds JSON with eventId/objectId/participants for frontend
- OnAnimationCompleted callback in PlatformCallbacks, implemented for
Emscripten (CustomEvent dispatch) and Native (SDL_Log)
- GetDisplayName() getter on RemotePlayer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix participant ordering in animation completion message
Emit the local player first in the JSON participants array so the
frontend can rely on participants[0] being self when reporting to the
server. Extract participant JSON-building into a lambda to avoid
duplication. Retain null-termination safety for displayName.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Disable workers.dev and preview URLs for relay server
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix path actor assertion failure / freeze on repeated vehicle enter/exit
When the third-person camera is active, LMB triggers both forward movement
and vehicle interaction. This leaves the previous actor with m_worldSpeed > 0
when entering a vehicle, causing it to wander on the path system in non-user-nav
mode. On exit, SetBoundary() overwrites m_boundary without updating m_destEdge,
creating a boundary/edge mismatch. On the next vehicle enter, the stale spline
finishes and SwitchBoundary asserts (debug) or loops infinitely (release).
Stop the previous actor from wandering by zeroing its world speed on enter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add hold threshold to disambiguate LMB click from hold-to-walk
A 300ms time threshold prevents brief clicks (for interacting with
world objects) from also triggering forward movement in third-person
camera mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add missing SDL_timer.h include for SDL_GetTicks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove maxActors setting from multiplayer — NPCs are always disabled
The maxActors room setting added unnecessary complexity for a feature
that should always be off in multiplayer. Remove it from the relay
server protocol, room configuration API, C++ client, and simplify
NPC enforcement to be unconditional.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
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
* WIP: NPC animation local playback
Add NpcAnimCatalog and NpcAnimPlayer for playing NPC interaction
animations directly on player ROIs in multiplayer, bypassing the
singleplayer streaming pipeline.
- NpcAnimCatalog: reads animation entries from LegoAnimationManager
with eligibility filtering per actor
- NpcAnimPlayer: minimal SI file reader (header + offset table only,
then single MxSt read per object), extracts animation/audio/phoneme
data from ISLE.SI composite objects
- Skeletal animation with position rebasing (absolute world coords
converted to player-relative deltas)
- Audio via LegoCacheSound with proper WaveFormat parsing from SI
chunks, wall-clock sync via SDL_GetTicks
- Phoneme lip sync with FLC decode, palette update, and proper
texture restore on cleanup
- Movement lock via Controller::m_npcAnimPlaying flag
- Test trigger: emote 0 plays first eligible NPC animation
Still WIP: debug logging present, network sync not implemented,
needs testing with more animations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Fix NPC animation playback, props, crash safety, and movement lock
- Fix SI file reading: use declared offset count, handle RIFF word-alignment
for odd-sized MxCh chunks, parse WaveFormat struct directly (not RIFF WAV)
- Fix animation type matching: use presenter name instead of MxOb::Type enum
(skeletal anim is type Object=0x0B, phoneme is type Video=0x03)
- Fix animation positioning: compute full rigid-body rebase transform
(savedTransform * inverse(animPose0)) so all motion, rotation, and extra
actor positions are preserved relative to the player's current pose
- Add extra character support: use CharacterCloner::Clone for root-level
characters (RHODA, RD, BD, PG), extend AssignROIIndices to match
non-*-prefixed root-level nodes against extra ROIs
- Fix phoneme: update palette via SetEntries after FLC decode, restore
original texture by passing saved pointer (not NULL), initialize
filetype_/volume_ to avoid UBSan errors
- Fix sync: use SDL_GetTicks (wall-clock) instead of Timer()->GetTime()
(game timer stalls during freezes), defer clock start to first Tick
- Fix crash on camera transition: add NPC anim stop callback in
Controller::Deactivate and OnWorldDisabled (fires before ROI destruction)
- Block camera toggle and scroll/zoom disable during NPC animation
- Block player movement and camera-relative input during NPC animation
- Add StopNpcAnimation() public API on NetworkManager
- Add EnsureROIMapVisibility in Tick for prop visibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Fix rebase for nested camera nodes, revert test to eligible[0]
Accumulate parent transforms when computing the player's animation-
space world pose at time 0. Fixes position offset for animations with
nested '-' nodes (e.g. -SBA001BU -> -TILT -> BU) where the local
transform alone didn't account for parent TILT offset.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Catalog filtering, prop LOD trimming, click anim blocking
- Split NpcAnimCatalog into NPC (location==-1) and cam (location>=0) buckets
- Filter by display actor's character index (not actorId)
- Eligibility: require all 5 main actor bits set (no counterpart for now)
- DisplayActorToCharacterIndex maps display actor -> g_characters index
- Trim trailing digits/underscores from prop LOD names matching original
game's e_managedInvisibleRoiTrimmed logic (LETR12 -> letr)
- Handle *-prefixed non-actor root siblings as props via CreateAutoROI
- Block click animations during NPC animation playback (both local and
remote player paths)
- Remove verbose per-entry catalog logging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Actor metadata ROI creation, vehicle reuse, AssignROIIndices fix
- Replace scanForCharacters heuristic with LegoAnimActorEntry metadata
loop (GetNumActors/GetActorType/GetActorName) matching original game
- Player identified by animation name suffix, not tree position
- Handle all actor types: managed actors (2), trimmed props (3),
exact props (4), scene ROIs (5/6), and scene actors (0/1)
- Vehicle ROI reuse: borrow existing ride vehicle ROI for type 0/1
actors when CreateAutoROI fails, restore name on Stop
- Fix AssignROIIndices: check extras before claiming root to handle
tree ordering (BIKESY before SY)
- Use GetRefCount(ROI*) for cleanup instead of name-based Exists()
- Block click animations during NPC anim playback
- Vehicle animation not yet fully working (known issue)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Fix vehicle animation, skip Controller tick during NPC anim
The ride animation and SyncTransformFromNative in Controller::Tick
were overwriting ROI transforms set by NpcAnimPlayer every frame.
Skip the entire ride animation path and non-vehicle character
animation path when m_npcAnimPlaying is true, so only NpcAnimPlayer
controls ROI positioning during NPC animations.
Also add comprehensive ROI map and tree assignment debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move SI parsing from NpcAnimPlayer into libweaver
Replace ~300 lines of custom RIFF/MxOb/MxCh parsing in NpcAnimPlayer
with libweaver's new HeaderOnly read mode and slot-based ReadObject API.
This reuses ReadChunk's existing logic for the full MxSt->MxOb->MxCh
chain, eliminating code duplication.
Extract HD/CD path resolution into a shared ResolveGamePath utility
(extensions/common/pathutils), replacing the duplicated pattern in
NpcAnimPlayer, SiLoader, and TextureLoader.
Update libweaver to 17c7736 (HeaderOnly + ReadObject support).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor animation playback into Multiplayer::Animation namespace
Split the monolithic NpcAnimPlayer and NpcAnimCatalog into five focused
components under extensions/multiplayer/animation/:
- Catalog: AnimInfo index with category enum, stores all animations
- Loader: SI file I/O, parsing, AnimData cache
- Controller: Play/Tick/Stop orchestrator, ROI creation, rebase matrix
- AudioPlayer: LegoCacheSound timed playback
- PhonemePlayer: FLC decoding, texture swap, lip sync
Also removes ~50 SDL_Log debug calls, renames all NpcAnim* references
to match the new structure, simplifies the camera animation callback
API, and documents AnimUtils divergences from the original game.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* DRY: Extract TrimLODSuffix helper and AnimData::ReleaseTracks
- controller.cpp: Extract repeated digit/underscore trimming loop into
static TrimLODSuffix() (was duplicated 3 times in CreateExtraROIs)
- loader.cpp: Extract duplicated track cleanup loops into
AnimData::ReleaseTracks() (was in both destructor and move-assignment)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Updates
* Clarify animation system separation with renames and DRY extractions
Rename Controller → ScenePlayer to distinguish multi-actor scene
animations from simple character poses. Rename AnimData → SceneAnimData
to avoid confusion with the character lookup tables. Rename
animdata.h/cpp → charactertables.h/cpp to reflect their actual content
(walk/idle/emote/vehicle tables).
Extract ApplyTree, TrimLODSuffix, and ResolvePropLODName into AnimUtils
to reduce duplication across CharacterAnimator and ScenePlayer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove dta.py accidentally committed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix CleanupProps corrupting global NPC actor state
ReleaseActor looks up g_actorInfo[] by ROI name, which for renamed
clones (e.g. "ma") matches the real global NPC and deletes its actor
entity. Since all props are independent clones (not obtained via
GetActorROI), use ReleaseAutoROI unconditionally — it performs
identical map/ROI cleanup without touching g_actorInfo[].
Also removes the redundant explicit Remove() call since ReleaseAutoROI
already calls RemoveROI() internally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Add animation catalog rework, location proximity, and coordinator
Prerequisite systems for cooperative animation reenactment feature:
- Catalog: expanded CatalogEntry with performerMask, spectatorMask,
location index. Exact character matching (replaces engine's lossy
2-char prefix). New CanTrigger() checks collective player eligibility
(spectator + all performers, mutually exclusive roles).
- LocationProximity: 2D XZ distance to nearest g_locations entry,
integrated into Tickle with OnNearestLocationChanged callback.
Remote player locations derived from ROI positions.
- Coordinator: state machine (idle/interested/countdown/playing/completed),
ComputeEligibility for frontend consumption (pre-filtered to local
player's participable animations). Networking hooks are stubs.
- NetworkManager: location proximity + coordinator integration, atomic
request pattern for anim interest/cancel, state resets on all
transition paths (world disable, disconnect, reconnect, stop).
- Bridge: OnNearestLocationChanged callback (Emscripten + Native),
mp_set_anim_interest / mp_cancel_anim_interest WASM exports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Add push-based animation state bridge and slot fill computation
Add OnAnimationsAvailable callback that pushes full animation eligibility
state (location, coordinator state, per-animation slot fill status) to
the frontend whenever relevant state changes. Uses a dirty flag system
with 250ms cooldown (bypassed for interest changes) to batch updates.
- Add CanTriggerDetailed to Catalog (refactor CanTrigger as wrapper)
- Enrich EligibilityInfo with SlotInfo vector and CatalogEntry pointer
- Add Coordinator::OnLocationChanged for auto-clearing stale interest
- Add dirty flag triggers at all 7 state change points in NetworkManager
- Build JSON payload with unified slots (performers + spectator)
- Remove OnNearestLocationChanged (superseded by push-based approach)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Ghastly
* WIP
* Fix animation session sync, camera-cancel, and state management bugs
- Add m_cancelPending to coordinator to prevent stale session re-enrollment
- Allow ClearInterest during countdown/playing for camera-cancel support
- Camera toggle now cancels animation in any active state instead of blocking
- Safety net: cancel animation when camera is disabled by any source
- Push idle JSON when camera unavailable so frontend clears countdown UI
- HandleCancel includes playing sessions and erases them (explicit cancel stops all)
- HandleAnimCancel/HandleAnimUpdate detect playing→idle to stop local scenes
- SendAnimUpdateToPlayer for targeted session sync to newly joined players
- HandleHostAssign: skip ResetAnimationState on initial assignment to avoid race
- IsPeerNearby helper for shared proximity checks
- HandleAnimInterest: evict far-away participants when session is full
- PushAnimationState: suppress session display when no participant is nearby
- World filter in UpdateRemotePlayers and PushAnimationState
- Set m_animStateDirty on camera change and remote player world change
- Use anyInIsle instead of anyNearby for continuous proximity-based push
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Clean up animation code: extract DRY helpers, remove debug logging
- Extract BuildAnimUpdateMsg() and ExtractSlotPeerIds() to deduplicate
message-building logic in BroadcastAnimUpdate/SendAnimUpdateToPlayer
- Replace manual tree iteration in controller.cpp with AnimUtils::ApplyTree
- Remove all [Anim]/[SessionHost] SDL_Log debug calls and unused includes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: ScenePlayer multi-participant support and cam_anim playback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* WIP: Vehicle ROI support, alias-based ROI mapping, audio fix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor animation infrastructure: remove dead code, DRY, tighten data
Remove all debug logging (~20 SDL_Log calls), dead fields (m_savedVehicleName,
m_debugFirstTickLogged, boundingRadius, centerPoint), dead methods
(RestoreVehicleROI, HasActiveSounds), and unused parameters (Tick's deltaTime).
Tighten data structures: replace raw m_propROIs array with vector, derive
isSpectator from charIndex via IsSpectator() method (SessionSlot,
ParticipantROI), group action transform fields into sub-struct, replace
vehicle category magic numbers with VehicleCategory enum.
Extract DRY helpers: addAlias/createProp lambdas in SetupROIs,
StopScenePlayback() in NetworkManager (5 call sites), combine dual slot
iteration into single loop in HandleAnimStartLocally. Simplify Play()
signature by removing redundant p_localROI/p_vehicleROI params.
Fix bug: HandleAnimCancel now unlocks remote player ROIs when stopping
during playback (was skipping unlock, leaving remotes permanently locked).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Async SI asset preloading during animation countdown
Preload animation data from isle.si on a background thread when the
countdown starts (4s window), so ScenePlayer::Play() finds the data
already cached and avoids a 500ms-1s main-thread stall.
Adds Loader::PreloadAsync() with a one-shot MxThread subclass that
opens its own si::File/Interleaf, parses the object, and inserts into
the cache under MxCriticalSection. EnsureCached() joins any in-progress
preload before falling back to synchronous load.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Refactor animation system: fix dangling pointer, DRY extractions, correctness
- Fix dangling .c_str() in ScenePlayer::SetupROIs by using std::deque
for aliasNames (vector reallocation invalidated stored pointers)
- Push idle JSON fallback when userActor is null in PushAnimationState
instead of silently returning with stale frontend state
- Extract GetPerformerIndices() to eliminate 3 duplicate bit-iteration
loops across coordinator.cpp and sessionhost.cpp
- Promote CheckSpectatorMask to public Catalog method, replacing
inlined duplicate in sessionhost.cpp
- Extract Loader::OpenSIHeaderOnly() to consolidate duplicated SI file
open/parse between OpenSI() and PreloadThread::Run()
- Extract IDLE_ANIM_STATE_JSON constant to avoid string duplication
- Clarify proximity radius distinction with comment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Observer mode: uninvolved players see animations play out locally
Non-participant players in the same world now see scene animations
(cam_anim, npc_anim) play out on the performing players' ROIs as an
ambient background scene. The observer's camera, movement, and vehicle
are completely unaffected.
Key changes:
- HandleAnimStartLocally always runs (not just for session participants)
- ScenePlayer gains observer mode: skips camera control, spectator
hiding, and vehicle hiding
- Preload during countdown for all clients, not just participants
- Fix 1st person camera: skip display ROI check for observers (the
display clone is destroyed in 1st person mode)
- Track m_playingAnimIndex for observer early-stop detection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GameRoom keeps all state in memory and never uses this.state.storage,
so new_classes suffices over new_sqlite_classes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When LMB is held in 3rd person mode, the character walks forward in
the camera's facing direction. Works simultaneously with RMB camera
rotation. Transitions between 1st and 3rd person are seamless while
holding LMB. Also fixes RMB mouse cursor not reappearing when
releasing after a 3rd-to-1st person transition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Automatically reconnect when the WebSocket connection is lost (e.g.
phone sleep, alt-tab, network blip) instead of exiting the game.
- Add reconnection state machine (CONNECTED → RECONNECTING → CONNECTED
or DISCONNECTED) with exponential backoff (1s→30s cap, 10 max attempts)
- Add OnConnectionStatusChanged callback (connected/reconnecting/failed/
rejected) to PlatformCallbacks, with Emscripten CustomEvent dispatch
and native SDL_Log implementations
- Add WorldStateSync::ResetForReconnect() to clear session state
- Only exit the game on room-full rejection (WasRejected); connection
loss is handled internally by the state machine
- Rename WasDisconnected→WasRejected, CheckDisconnected→CheckRejected,
IsMultiplayerDisconnected→IsMultiplayerRejected through the full call
chain for accurate naming
- Remove Module._exitCode from JS onclose; use C++ callback +
sessionStorage for room-full signaling instead
- Clean up EXIT_CONNECTION_LOST constant (obsolete)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The transparent code path skipped texture sampling entirely, producing
a solid colored rectangle instead of the textured shape. Move texture
sampling before the opaque/transparent branch so both paths get texel
color modulation, and use texel alpha for per-pixel transparency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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