Commit Graph

48 Commits

Author SHA1 Message Date
Christian Semmler
0c986aff2a
Add multi-world animation support (ACT2/ACT3)
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>
2026-03-28 18:07:37 -07:00
Christian Semmler
f7947e1720
Refactor extensions: DRY, named constants, and cleanup
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>
2026-03-28 15:08:23 -07:00
Christian Semmler
af80f161d6
Suppress build exit camera animations in multiplayer
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>
2026-03-28 14:44:33 -07:00
Christian Semmler
8413647463
Fix local player name bubble not following large vehicles
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>
2026-03-28 14:30:18 -07:00
Christian Semmler
b566ddaea8
Decouple SI extraction from Animation::Loader into reusable SIReader
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>
2026-03-28 14:19:24 -07:00
Christian Semmler
05716eb94f
WIP 2026-03-28 13:26:13 -07:00
Christian Semmler
e57164d345
Fix use-after-free crash and misaligned protocol access
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>
2026-03-28 11:04:06 -07:00
Christian Semmler
3396d42db8
Fix vehicle filter bug and refactor: wrong index space in GetCharacterVehicleId
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>
2026-03-28 10:33:07 -07:00
Christian Semmler
aa9df3370b
WIP: Filter animations by vehicle eligibility
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>
2026-03-28 10:05:55 -07:00
Christian Semmler
2bdbb594da
Fix catalog actor index 2026-03-27 16:42:44 -07:00
Christian Semmler
f92967735f
Support concurrent animation playback by independent player groups
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>
2026-03-27 15:57:12 -07:00
Christian Semmler
32fd90d804
Support multiple overlapping locations for scene animation eligibility
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>
2026-03-27 10:42:14 -07:00
foxtacles
99c871ab16
Nick bricks memories (#21)
* 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>
2026-03-23 23:46:16 +01:00
foxtacles
647bd28a07
Claude/npc animations local playback (#20)
* 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>
2026-03-21 22:40:15 +01:00
Christian Semmler
982957ee5e
Add WebSocket reconnection with exponential backoff
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>
2026-03-16 13:34:45 -07:00
Christian Semmler
09ed6edb3e
Fix player count not updating when exiting Isle overlay areas
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>
2026-03-15 13:54:48 -07:00
Christian Semmler
aa48001eb3
Disable third person camera and hide remote players in Isle overlay areas
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>
2026-03-14 16:19:16 -07:00
Christian Semmler
680c7c28fe
Distinguish "room full" from "connection lost" in WebSocket error handling
Fix race condition where onerror clearing m_connectedFlag before onclose
caused any network drop to be misidentified as a room-full rejection.
Add m_wasEverConnected flag set once in onopen, use it in onclose to
assign exit code 10 (room full) vs 11 (connection lost). Rename
rejected API surface to disconnected to reflect the generalized meaning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 09:00:07 -07:00
Christian Semmler
74271aa189
Fix mobile camera zoom/transition and name bubble issues
- 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>
2026-03-13 19:10:49 -07:00
Christian Semmler
a588e3bb67
Disable all NPCs when maxActors=0
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>
2026-03-13 16:05:55 -07:00
foxtacles
569c8b467b
Separate extensions (#18)
* 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>
2026-03-13 23:12:07 +01:00
foxtacles
0ad5361e6a
Claude/auto switch camera zoom dcg go (#17)
* 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>
2026-03-13 18:09:06 +01:00
foxtacles
004e3b3bbf
Fix bugs (#16)
* 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>
2026-03-13 04:52:58 +01:00
Christian Semmler
9145a23ffe
Fix: stop previous click animation before starting a new one
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>
2026-03-10 19:45:20 -07:00
foxtacles
1a8c6c70ea
Add disassemble/assemble emote (#13)
* 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>
2026-03-10 02:18:37 +01:00
foxtacles
fe5ef4f9a5
Fix camera flip bugs, refactor camera (#11)
* 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
2026-03-10 01:15:22 +01:00
Christian Semmler
37f33a91df
Replace SetMaxAllowedExtras setter with friend class 2026-03-09 13:53:37 -07:00
Christian Semmler
a5b2ea0ce9
Extract CharacterAnimator component, EncodeUsername utility, replace C stdlib with SDL
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.
2026-03-08 10:48:26 -07:00
Christian Semmler
9e8ecd6d44
Move routing info into message header, make relay type-agnostic 2026-03-08 10:21:36 -07:00
Christian Semmler
a8c3ec7b2f
Always derive display actor from actorId when no INI actor
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.
2026-03-08 10:07:22 -07:00
Christian Semmler
1fe1b732e0
Show name bubble on local player in 3rd person camera 2026-03-08 09:53:24 -07:00
Christian Semmler
dd56e6c686
Fix player count showing 0 after returning from a race
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)).
2026-03-07 21:57:20 -08:00
foxtacles
eb6d2b8728
Sync sky light (#9)
* 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

---------
2026-03-08 05:55:00 +01:00
Christian Semmler
853e8981fa
Control NPC count per room 2026-03-07 14:58:27 -08:00
foxtacles
a9747dec11
Character customization (#8)
* 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
2026-03-07 23:20:55 +01:00
foxtacles
c943d2455d
Add player name bubbles above remote players' heads in multiplayer (#7)
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)
2026-03-07 19:38:33 +01:00
foxtacles
ed4e248be4
Implement display actors (#6)
* 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
2026-03-07 18:02:53 +01:00
foxtacles
c8c3b7276e
Fix 3rd/1st person camera switch direction bugs (#4)
- Fix broadcast direction: use IsActive() instead of IsROITurnedAround()
  so the negate in BroadcastLocalState only fires when movement inversion
  is active, not based on a default-true flag

- Fix vehicle ROI direction for 3rd-person camera: undo Enter()'s
  TurnAround on small vehicles so the backward-z convention is preserved.
  Vehicles are placed with ROI z opposite to visual forward, and Enter()'s
  TurnAround breaks this for 3rd-person rendering. Applied in both
  OnActorEnter (entering while 3rd-person enabled) and ReinitForCharacter
  (enabling 3rd-person while already on a vehicle)

- Fix vehicle direction on exit: apply extra TurnAround in OnActorExit
  when 3rd-person is active, since Exit()'s TurnAround assumes Enter()'s
  TurnAround is still in effect

- Add WrappedUpdateWorldData() after manual direction flips in Disable()
  and ReinitForCharacter() to keep bounding volumes consistent and prevent
  stale world data from causing momentary camera/direction glitches

- Remove unused IsROITurnedAround() method

- Fix data race between WASM exports and game thread
2026-03-07 05:49:39 +01:00
foxtacles
37b328a595
3rd person camera (#3)
* Add feasibility plan for reusing multiplayer animation system for third-person camera

Evaluates reusing the multiplayer extension's RemotePlayer animation system
(BuildROIMap, AssignROIIndices, ApplyAnimationTransformation) for the local
player to enable a third-person camera mode. Conclusion: feasible with only
3 single-line extension hooks added to core game code.

https://claude.ai/code/session_01NC3zdQZ4nqEcYjyvStqcdD

* WIP: Third-person camera with animation reuse and movement fix

* Fix third-person camera bugs: vehicles, remote facing, emote distortion (#2)

- Fix spawn pose and building re-entry by applying idle frame 0 and
  reinitializing on world enable
- Handle vehicle transitions: ride animations for small vehicles,
  first-person fallback for large vehicles and helicopter
- Keep vehicle dashboards visible for exit controls
- Disable third-person camera for large vehicles, fix ROI cleanup
- Move HandleActorExit hook to end of Exit() for immediate reinit
- Fix remote player facing 180 degrees wrong by negating direction
  in BroadcastLocalState when third-person camera is active
- Fix Hat Tip emote distortion from compounding transform scale by
  saving clean parent transform at emote start and restoring after
  each frame's animation application

* DRY cleanup for third-person camera branch

- Extract shared DetectVehicleType() to protocol.h/cpp (was duplicated
  in ThirdPersonCamera and NetworkManager)
- Remove no-op HandlePostApplyTransform hook chain (called every frame
  for every LegoPathActor but did nothing)
- Add ThirdPersonCamera::ClearAnimCaches() helper (pattern repeated 5x)
- Add AnimUtils::EnsureROIMapVisibility() inline helper (loop repeated
  5x across ThirdPersonCamera and RemotePlayer)
- Remove redundant static_cast in multiplayer.cpp (UserActor() already
  returns LegoPathActor*)
- Delete THIRD_PERSON_CAMERA_ANIMATION_REUSE_PLAN.md development artifact
2026-03-07 01:38:45 +01:00
Christian Semmler
0997610bad
Extract PlatformCallbacks interface and consolidate Emscripten files
Move all Emscripten-specific multiplayer code under platforms/emscripten/
and introduce an abstract PlatformCallbacks interface for outbound
notifications, mirroring the existing NetworkTransport pattern. This
removes all #ifdef __EMSCRIPTEN__ blocks from networkmanager.cpp.
2026-03-02 15:46:37 -08:00
Christian Semmler
e47d2dab67
Refactor WASM exports and add push-based player count events
Move WASM exports (mp_set_walk_animation, mp_set_idle_animation,
mp_trigger_emote) from multiplayer.cpp into a dedicated
wasm_exports.cpp, added to the isle target (guarded by both
EMSCRIPTEN and ISLE_EXTENSIONS) so the linker keeps the symbols.

Replace the polling mp_get_player_count export with push-based
playerCountChanged CustomEvents dispatched from NetworkManager.
The count only reflects players visible in the Isle world: null
when the local player is outside Isle, filtered by remote player
worldId when inside. Remove the now-unused GetPlayerCount() method.
2026-03-02 15:26:27 -08:00
Claude
3e85941cbc
Add animation protocol: walk/idle selection, emote triggers, WASM exports
Implement the animation system from the Phase 1 plan:

Protocol: Add walkAnimId/idleAnimId fields to PlayerStateMsg (2 extra bytes
per 15Hz tick), add MSG_EMOTE (type 9) with EmoteMsg struct, and define
shared animation lookup tables (walk: 6 anims, idle: 3, emote: 2).

NetworkManager: Store local walk/idle animation indices, include them in
every state broadcast, handle incoming MSG_EMOTE by dispatching to the
target remote player's TriggerEmote(). Add SetWalkAnimation(),
SetIdleAnimation(), SendEmote(), GetPlayerCount() public API.

RemotePlayer: Replace per-animation raw pointers with AnimCache struct
and lazy m_animCacheMap (name -> ROI map, built on first use, cleared on
Despawn). UpdateFromNetwork() detects walk/idle ID changes and swaps the
active animation cache. UpdateAnimation() now has three states: moving
(configurable walk anim), emote (one-shot with duration tracking,
interrupted by movement), and idle (configurable idle anim after 2.5s
timeout). Add TriggerEmote() for one-shot emote playback.

WASM exports: mp_set_walk_animation(), mp_set_idle_animation(),
mp_trigger_emote(), mp_get_player_count() with EMSCRIPTEN_KEEPALIVE.
CMakeLists.txt adds EXPORTED_FUNCTIONS and EXPORTED_RUNTIME_METHODS
for Svelte ccall/cwrap access.

https://claude.ai/code/session_01BEYdu8gXr1QmYwzRRgaEA6
2026-03-02 03:06:48 +00:00
Christian Semmler
5b56db3c33
Add room management, relay capacity, and rejection handling
- Remove hardcoded multiplayer config from emscripten config.cpp
- Add relay HTTP endpoints for room preview (GET) and creation (POST)
  with capacity check, CORS headers, and configurable max players
- Add WebSocket rejection detection (room full/503) via onclose flag
- Add CheckRejected extension call in IsleApp::Tick for clean shutdown
  through SDL_APP_SUCCESS path instead of calling exit()
- Set Module._exitCode in JS for sessionStorage-based toast after reload
2026-03-01 14:37:08 -08:00
Christian Semmler
c760db50a9
Extract WorldStateSync from NetworkManager
Move world state synchronization logic (snapshots, events, entity
mutation routing) into a dedicated WorldStateSync class, reducing
NetworkManager from ~790 to ~420 lines.
2026-03-01 10:14:47 -08:00
Christian Semmler
a0629c45a1
Minimize LEGO1 changes: revert globals, move CreateCharacterClone to extension
Revert plant/building manager globals back to private class statics
(matching master) and use friend declarations for extension access.
Move CreateCharacterClone out of LegoCharacterManager into a new
CharacterCloner class in the multiplayer extension.
2026-03-01 10:04:13 -08:00
Christian Semmler
12a63c105c
Implement multiplayer world state sync for plants and buildings
Add serialization framework using C++ templates and table-driven lookup
to sync plant and building state between players. Includes world snapshot
routing to requesting peer, relay server Docker support, and fixes for
building color sync, ride vehicle visibility, and ARM compilation.
2026-03-01 09:48:03 -08:00
Christian Semmler
fb1d596704
Clean up multiplayer extension: use vec.h macros, remove debug logging
- Replace manual vector math in UpdateTransform with CalcLocalTransform and vec.h macros (LERP3, SET3, MV3, DISTSQRD3, ZEROVEC3)
- Remove all SDL_Log debug logging from multiplayer code
- Strip extraneous comments that restate the code
- Extract CreateAndSpawnPlayer helper to consolidate repeated spawn pattern
- Simplify UpdateVehicleState control flow
- Remove unused includes (SDL_log.h, mxgeometry/mxmatrix.h)
2026-02-28 12:11:19 -08:00
Christian Semmler
5c8a2ffd3b
Implement multiplayer MVP: WebSocket networking, remote player rendering, vehicle support
- WebSocket relay server (Cloudflare Worker + Durable Object)
- Remote player character cloning with walk/idle/ride animations
- Vehicle support for remote players
- INI config for relay URL
- Extension hook for world transition ROI management
2026-02-28 12:00:01 -08:00