From c8c3b7276e6ac5fc14b5420bde17819f56db7601 Mon Sep 17 00:00:00 2001 From: foxtacles Date: Fri, 6 Mar 2026 20:49:39 -0800 Subject: [PATCH] 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 --- .../extensions/multiplayer/networkmanager.h | 14 ++ .../multiplayer/thirdpersoncamera.h | 1 + extensions/src/multiplayer/networkmanager.cpp | 46 ++++- .../platforms/emscripten/wasm_exports.cpp | 14 +- .../emscripten/websockettransport.cpp | 3 +- extensions/src/multiplayer/remoteplayer.cpp | 7 +- .../src/multiplayer/thirdpersoncamera.cpp | 173 ++++++++++++------ extensions/src/multiplayer/worldstatesync.cpp | 13 +- 8 files changed, 188 insertions(+), 83 deletions(-) diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index df96d8a8..68222e7e 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -9,6 +9,7 @@ #include "mxcore.h" #include "mxtypes.h" +#include #include #include #include @@ -47,6 +48,13 @@ class NetworkManager : public MxCore { void SetIdleAnimation(uint8_t p_index); void SendEmote(uint8_t p_emoteId); + // Thread-safe request methods for cross-thread callers (e.g. WASM exports + // running on the browser main thread). Deferred to the game thread in Tickle(). + void RequestToggleThirdPerson() { m_pendingToggleThirdPerson.store(true, std::memory_order_relaxed); } + void RequestSetWalkAnimation(uint8_t p_index) { m_pendingWalkAnim.store(p_index, std::memory_order_relaxed); } + void RequestSetIdleAnimation(uint8_t p_index) { m_pendingIdleAnim.store(p_index, std::memory_order_relaxed); } + void RequestSendEmote(uint8_t p_emoteId) { m_pendingEmote.store(p_emoteId, std::memory_order_relaxed); } + void OnWorldEnabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world); @@ -71,6 +79,7 @@ class NetworkManager : public MxCore { void HandleHostAssign(const HostAssignMsg& p_msg); void HandleEmote(const EmoteMsg& p_msg); + void ProcessPendingRequests(); void RemoveRemotePlayer(uint32_t p_peerId); void RemoveAllRemotePlayers(); @@ -96,6 +105,11 @@ class NetworkManager : public MxCore { bool m_inIsleWorld; bool m_registered; + std::atomic m_pendingToggleThirdPerson; + std::atomic m_pendingWalkAnim; + std::atomic m_pendingIdleAnim; + std::atomic m_pendingEmote; + static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout static const int EXIT_ROOM_FULL = 10; diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h index 6b5e5277..f4bc30d5 100644 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ b/extensions/include/extensions/multiplayer/thirdpersoncamera.h @@ -55,6 +55,7 @@ class ThirdPersonCamera { bool m_enabled; bool m_active; + bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies LegoROI* m_playerROI; // Borrowed, not owned // Walk/idle state (same pattern as RemotePlayer) diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 2a8dc59a..e3f54cc9 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -31,7 +31,8 @@ void NetworkManager::SendMessage(const T& p_msg) NetworkManager::NetworkManager() : m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), m_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), m_inIsleWorld(false), - m_registered(false) + m_registered(false), m_pendingToggleThirdPerson(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), + m_pendingEmote(-1) { } @@ -42,6 +43,7 @@ NetworkManager::~NetworkManager() MxResult NetworkManager::Tickle() { + ProcessPendingRequests(); m_thirdPersonCamera.Tick(0.016f); if (!m_transport) { @@ -50,9 +52,8 @@ MxResult NetworkManager::Tickle() uint32_t now = SDL_GetTicks(); - // Broadcast BEFORE receiving: the Send proxy call gives the main thread a - // chance to process incoming WebSocket onmessage events before we drain - // the queue with Receive. + // Broadcast before receiving so the Send proxy lets the main thread + // process WebSocket events before we drain the queue. if (m_transport->IsConnected() && (now - m_lastBroadcastTime) >= BROADCAST_INTERVAL_MS) { BroadcastLocalState(); m_lastBroadcastTime = now; @@ -61,8 +62,7 @@ MxResult NetworkManager::Tickle() ProcessIncomingPackets(); UpdateRemotePlayers(0.016f); - // Re-read time because ProcessIncomingPackets updates player timestamps - // via SDL_GetTicks(), which may be newer than the 'now' captured above. + // Re-read time; ProcessIncomingPackets may have advanced SDL_GetTicks. uint32_t timeoutNow = SDL_GetTicks(); std::vector timedOut; for (auto& [peerId, player] : m_remotePlayers) { @@ -180,6 +180,33 @@ MxBool NetworkManager::HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeT return m_worldSync.HandleEntityMutation(p_entity, p_changeType); } +void NetworkManager::ProcessPendingRequests() +{ + if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) { + if (m_thirdPersonCamera.IsEnabled()) { + m_thirdPersonCamera.Disable(); + } + else { + m_thirdPersonCamera.Enable(); + } + } + + int walkAnim = m_pendingWalkAnim.exchange(-1, std::memory_order_relaxed); + if (walkAnim >= 0) { + SetWalkAnimation(static_cast(walkAnim)); + } + + int idleAnim = m_pendingIdleAnim.exchange(-1, std::memory_order_relaxed); + if (idleAnim >= 0) { + SetIdleAnimation(static_cast(idleAnim)); + } + + int emote = m_pendingEmote.exchange(-1, std::memory_order_relaxed); + if (emote >= 0) { + SendEmote(static_cast(emote)); + } +} + void NetworkManager::BroadcastLocalState() { if (!m_transport) { @@ -223,9 +250,10 @@ void NetworkManager::BroadcastLocalState() SDL_memcpy(msg.position, pos, sizeof(msg.position)); SDL_memcpy(msg.direction, dir, sizeof(msg.direction)); - // Third-person camera: ROI direction is opposite to actual movement direction - // (ShouldInvertMovement preserves TurnAround convention). Negate so remote - // players receive the true movement-facing direction. + // When 3rd-person camera is active, ShouldInvertMovement causes movement + // inversion, and CalculateTransform re-inverts to keep ROI z backward. + // Negate to send the visual-forward direction that remote players expect. + // RemotePlayer::UpdateTransform negates again to restore backward-z. if (m_thirdPersonCamera.IsActive()) { msg.direction[0] = -msg.direction[0]; msg.direction[1] = -msg.direction[1]; diff --git a/extensions/src/multiplayer/platforms/emscripten/wasm_exports.cpp b/extensions/src/multiplayer/platforms/emscripten/wasm_exports.cpp index c183049e..153c4d94 100644 --- a/extensions/src/multiplayer/platforms/emscripten/wasm_exports.cpp +++ b/extensions/src/multiplayer/platforms/emscripten/wasm_exports.cpp @@ -14,7 +14,7 @@ extern "C" { Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); if (mgr) { - mgr->SetWalkAnimation(static_cast(index)); + mgr->RequestSetWalkAnimation(static_cast(index)); } } @@ -22,7 +22,7 @@ extern "C" { Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); if (mgr) { - mgr->SetIdleAnimation(static_cast(index)); + mgr->RequestSetIdleAnimation(static_cast(index)); } } @@ -30,7 +30,7 @@ extern "C" { Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); if (mgr) { - mgr->SendEmote(static_cast(index)); + mgr->RequestSendEmote(static_cast(index)); } } @@ -38,13 +38,7 @@ extern "C" { Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager(); if (mgr) { - Multiplayer::ThirdPersonCamera& cam = mgr->GetThirdPersonCamera(); - if (cam.IsEnabled()) { - cam.Disable(); - } - else { - cam.Enable(); - } + mgr->RequestToggleThirdPerson(); } } diff --git a/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp b/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp index e81228c4..318dc5ed 100644 --- a/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp +++ b/extensions/src/multiplayer/platforms/emscripten/websockettransport.cpp @@ -146,8 +146,7 @@ size_t WebSocketTransport::Receive(std::function p return 0; } - // Drain all queued messages in a single proxy call to avoid starving the main thread event loop. - // Each message is concatenated as [4-byte LE length][payload...]. + // Drain queued messages in one proxy call: [4-byte LE length][payload...] each. // clang-format off int totalBytes = MAIN_THREAD_EM_ASM_INT({ var socketId = $0; diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 3fc0d623..0a82a31e 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -96,7 +96,7 @@ void RemotePlayer::Despawn() m_roi = nullptr; } - // Clear all cached animation ROI maps (anim pointers are world-owned, not ours) + // Clear cached animation ROI maps (anim pointers are world-owned). m_animCacheMap.clear(); m_walkAnimCache = nullptr; m_idleAnimCache = nullptr; @@ -230,7 +230,10 @@ void RemotePlayer::UpdateTransform(float p_deltaTime) LERP3(m_currentDirection, m_currentDirection, m_targetDirection, 0.2f); LERP3(m_currentUp, m_currentUp, m_targetUp, 0.2f); - // Character clones need negated direction + // Negate the received direction to restore the backward-z ROI convention. + // BroadcastLocalState sends visual-forward; negating here gives ROI z = + // backward, so mesh faces -z = forward (matching the sender's visual). + // See also: BroadcastLocalState in networkmanager.cpp. Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]); Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]); Mx3DPointFloat up(m_currentUp[0], m_currentUp[1], m_currentUp[2]); diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp index 1ec2dd3e..de32de18 100644 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ b/extensions/src/multiplayer/thirdpersoncamera.cpp @@ -19,8 +19,21 @@ using namespace Multiplayer; +// Flip the ROI's z-axis direction in place (same operation as IslePathActor::TurnAround). +static void FlipROIDirection(LegoROI* p_roi) +{ + MxMatrix transform(p_roi->GetLocal2World()); + Vector3 right(transform[0]); + Vector3 up(transform[1]); + Vector3 direction(transform[2]); + direction *= -1.0f; + right.EqualsCross(up, direction); + p_roi->SetLocal2World(transform); + p_roi->WrappedUpdateWorldData(); +} + ThirdPersonCamera::ThirdPersonCamera() - : m_enabled(false), m_active(false), m_playerROI(nullptr), m_walkAnimId(0), m_idleAnimId(0), + : m_enabled(false), m_active(false), m_roiUnflipped(false), m_playerROI(nullptr), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), @@ -31,13 +44,46 @@ ThirdPersonCamera::ThirdPersonCamera() void ThirdPersonCamera::Enable() { m_enabled = true; + ReinitForCharacter(); } void ThirdPersonCamera::Disable() { m_enabled = false; + + if (m_active && m_playerROI) { + LegoPathActor* userActor = UserActor(); + LegoWorld* world = CurrentWorld(); + + // Undo TurnAround so the ROI z-axis points in the visual forward + // direction. This keeps the 1st-person camera facing the same way + // as the 3rd-person camera, and ensures the network direction stays + // consistent (no 180-degree flip for others). + // For walking characters the target is m_playerROI; for vehicles it + // is the vehicle actor's ROI (UserActor() returns the vehicle). + LegoROI* turnAroundROI = + (m_currentVehicleType == VEHICLE_NONE) ? m_playerROI : (userActor ? userActor->GetROI() : nullptr); + + if (turnAroundROI) { + FlipROIDirection(turnAroundROI); + m_roiUnflipped = true; + } + + m_playerROI->SetVisibility(FALSE); + VideoManager()->Get3DManager()->Remove(*m_playerROI); + + // Restore vanilla 1st-person camera (eye-height offset, same as ResetWorldTransform). + if (userActor && world && world->GetCameraController()) { + world->GetCameraController()->SetWorldTransform( + Mx3DPointFloat(0.0F, 1.25F, 0.0F), + Mx3DPointFloat(0.0F, 0.0F, 1.0F), + Mx3DPointFloat(0.0F, 1.0F, 0.0F) + ); + userActor->TransformPointOfView(); + } + } + m_active = false; - m_playerROI = nullptr; ClearRideAnimation(); m_animCacheMap.clear(); ClearAnimCaches(); @@ -45,12 +91,19 @@ void ThirdPersonCamera::Disable() void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) { - if (!m_enabled) { + LegoPathActor* userActor = UserActor(); + if (static_cast(p_actor) != userActor) { return; } - LegoPathActor* userActor = UserActor(); - if (static_cast(p_actor) != userActor) { + // Always track vehicle type so OnActorExit can handle exits + // even if Enable() was called after entering the vehicle. + m_currentVehicleType = DetectVehicleType(userActor); + + // Enter() calls TurnAround(), so any previous undo is superseded. + m_roiUnflipped = false; + + if (!m_enabled) { return; } @@ -59,21 +112,14 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) return; } - // Detect if we're entering a vehicle - int8_t vehicleType = DetectVehicleType(userActor); - - if (vehicleType != VEHICLE_NONE) { - // Large vehicles and helicopter: stay first-person with dashboard. - // Track the vehicle type so OnActorExit can trigger reinit on exit. - if (IsLargeVehicle(vehicleType) || vehicleType == VEHICLE_HELICOPTER) { - // Hide the walking character ROI that we made visible earlier. - // Enter() doesn't call Exit() on the previous actor, so our - // OnActorExit never fires for the walking character. + if (m_currentVehicleType != VEHICLE_NONE) { + // Large vehicles and helicopter: stay first-person. + if (IsLargeVehicle(m_currentVehicleType) || m_currentVehicleType == VEHICLE_HELICOPTER) { + // Hide walking character ROI (Enter doesn't call Exit on it). if (m_playerROI) { m_playerROI->SetVisibility(FALSE); VideoManager()->Get3DManager()->Remove(*m_playerROI); } - m_currentVehicleType = vehicleType; m_active = false; return; } @@ -83,24 +129,26 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) return; } - m_currentVehicleType = vehicleType; - m_active = true; + // Undo Enter()'s TurnAround. Vehicles are placed with ROI z opposite + // to their visual forward (mesh faces -z = forward). Enter()'s + // TurnAround flips ROI z to match the visual forward, which breaks + // the backward-z convention the 3rd-person camera relies on. + p_actor->TurnAround(); + m_active = true; SetupCamera(userActor); - BuildRideAnimation(vehicleType); + BuildRideAnimation(m_currentVehicleType); return; } - // Non-vehicle (walking character) entry + // Non-vehicle (walking character) entry — Enter() already called TurnAround. m_playerROI = newROI; - m_currentVehicleType = VEHICLE_NONE; + m_roiUnflipped = false; m_active = true; - // Make the player model visible (Enter() hid it for first-person) m_playerROI->SetVisibility(TRUE); - // SpawnPlayer() removes the ROI from the 3D manager before calling Enter(). - // Re-add it so the character is actually rendered in third-person mode. + // Re-add ROI so it renders in third-person (SpawnPlayer removes it). VideoManager()->Get3DManager()->Remove(*m_playerROI); VideoManager()->Get3DManager()->Add(*m_playerROI); @@ -126,20 +174,27 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor) return; } - // The hook fires at the end of Exit(), after UserActor() has been restored - // to the walking character. For vehicle exit, p_actor is the vehicle (not - // UserActor), so we check m_currentVehicleType instead of comparing actors. + // For vehicle exit, p_actor is the vehicle, not UserActor — + // check m_currentVehicleType instead. if (m_currentVehicleType != VEHICLE_NONE) { - // Exiting a vehicle: reinitialize immediately for the walking character. + // When 3rd-person camera is active, movement inversion causes the + // vehicle to physically drive opposite to vanilla. CalculateTransform + // re-inverts to keep the ROI z backward. Exit()'s TurnAround restores + // the vanilla convention, but that's wrong for the visual driving + // direction. Flip once more so the parked vehicle faces the way it + // was visually driven. + if (m_active) { + p_actor->TurnAround(); + } + + // Exiting a vehicle: reinitialize for the walking character. ClearRideAnimation(); ClearAnimCaches(); m_animCacheMap.clear(); ReinitForCharacter(); } else if (m_active && static_cast(p_actor) == UserActor()) { - // Exiting on foot (e.g., world transition): full teardown. - // Hide the player ROI and remove it from the 3D manager (we added it - // in OnActorEnter so the character would render in third-person). + // Exiting on foot: full teardown. if (m_playerROI) { m_playerROI->SetVisibility(FALSE); VideoManager()->Get3DManager()->Remove(*m_playerROI); @@ -179,10 +234,10 @@ void ThirdPersonCamera::Tick(float p_deltaTime) m_animTime += p_deltaTime * 2000.0f; } - // Use vehicle actor's transform as base (character ROI may be at old position) + // Use vehicle actor's transform as base. MxMatrix transform(actor->GetROI()->GetLocal2World()); - // Position character ROI at the vehicle so bones render at the right place + // Position character ROI at the vehicle for bone rendering. m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform); m_playerROI->SetVisibility(TRUE); @@ -268,8 +323,7 @@ void ThirdPersonCamera::Tick(float p_deltaTime) m_idleAnimTime = 0.0f; } else { - // Use the saved clean parent transform to prevent scale - // accumulation (see TriggerEmote for details). + // Use saved clean transform to prevent scale accumulation. MxMatrix transform(m_emoteParentTransform); LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot(); @@ -282,8 +336,7 @@ void ThirdPersonCamera::Tick(float p_deltaTime) ); } - // Restore the player ROI's transform — the animation's root - // node (ACTOR_01) wrote a scaled value into it. + // Restore player ROI transform (animation root overwrote it). m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(m_emoteParentTransform); } } @@ -367,13 +420,8 @@ void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId) m_emoteDuration = (float) cache->anim->GetDuration(); m_emoteActive = true; - // Save the clean parent transform before the emote starts. - // The emote animation's root node (ACTOR_01) maps to the player ROI, - // so ApplyAnimationTransformation writes a scaled transform into - // m_playerROI->m_local2world each frame. When the character is - // stationary the engine's CalculateTransform does not run, so the ROI - // is never reset — causing the scale to compound across frames. - // Using the saved clean transform as parent prevents this feedback. + // Save clean transform to prevent scale accumulation during emote + // (the animation root writes scaled values into the ROI each frame). m_emoteParentTransform = m_playerROI->GetLocal2World(); } @@ -383,7 +431,7 @@ void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world) return; } - // Clear stale caches (animation presenters may have been recreated) + // Animation presenters may have been recreated. m_animCacheMap.clear(); ClearAnimCaches(); @@ -397,6 +445,7 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world) } m_active = false; + m_roiUnflipped = false; m_playerROI = nullptr; ClearRideAnimation(); m_animCacheMap.clear(); @@ -423,10 +472,8 @@ void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor) return; } - // After Enter()'s TurnAround, the ROI direction is negated. - // The mesh faces -z (local) = +path_forward (correct visual facing). - // +z in ROI-local is the negated direction, i.e. behind the visual model. - // Movement inversion is handled by ShouldInvertMovement in CalculateTransform. + // Camera behind the character; +z in ROI-local is behind the model + // after TurnAround. Movement inversion in CalculateTransform corrects controls. Mx3DPointFloat at(0.0f, 2.5f, 3.0f); Mx3DPointFloat dir(0.0f, -0.3f, -1.0f); Mx3DPointFloat up(0.0f, 1.0f, 0.0f); @@ -462,7 +509,7 @@ void ThirdPersonCamera::BuildRideAnimation(int8_t p_vehicleType) return; } - // Create variant ROI from base vehicle name, rename for anim tree matching + // Create variant ROI, rename to match animation tree. const char* baseName = g_vehicleROINames[p_vehicleType]; m_rideVehicleROI = CharacterManager()->CreateAutoROI("tp_vehicle", baseName, FALSE); if (m_rideVehicleROI) { @@ -531,6 +578,22 @@ void ThirdPersonCamera::ReinitForCharacter() m_active = false; return; } + + // Undo TurnAround on the vehicle ROI so the backward-z convention + // is restored. This handles both entering from 1st-person (Enter's + // TurnAround still in effect) and the Disable→Enable cycle (Disable + // re-applied TurnAround). In both cases ROI z currently matches + // the visual forward and needs to be flipped back. + { + LegoROI* vehicleROI = userActor->GetROI(); + if (vehicleROI) { + FlipROIDirection(vehicleROI); + } + m_roiUnflipped = false; + } + + VideoManager()->Get3DManager()->Remove(*m_playerROI); + VideoManager()->Get3DManager()->Add(*m_playerROI); m_active = true; SetupCamera(userActor); BuildRideAnimation(vehicleType); @@ -539,9 +602,17 @@ void ThirdPersonCamera::ReinitForCharacter() // Reinitializing for walking character m_playerROI = roi; + + // Re-apply TurnAround if we undid it in Disable(). + // Only set the local matrix here; the subsequent Add() will propagate world data. + if (m_roiUnflipped) { + FlipROIDirection(m_playerROI); + m_roiUnflipped = false; + } + m_playerROI->SetVisibility(TRUE); - // Ensure the ROI is in the 3D manager so it gets rendered + // Ensure the ROI is in the 3D manager. VideoManager()->Get3DManager()->Remove(*m_playerROI); VideoManager()->Get3DManager()->Add(*m_playerROI); diff --git a/extensions/src/multiplayer/worldstatesync.cpp b/extensions/src/multiplayer/worldstatesync.cpp index 238749f1..7fce46be 100644 --- a/extensions/src/multiplayer/worldstatesync.cpp +++ b/extensions/src/multiplayer/worldstatesync.cpp @@ -68,15 +68,13 @@ void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length) const uint8_t* snapshotData = p_data + sizeof(WorldSnapshotMsg); - // Apply the snapshot using LegoMemory with the existing Read() methods + // Apply the snapshot via LegoMemory. LegoMemory memory((void*) snapshotData, header.dataLength); PlantManager()->Read(&memory); BuildingManager()->Read(&memory); - // If we're in the Isle world, update entity visuals after applying the snapshot. - // Read() calls AdjustHeight() which updates data arrays, but doesn't update - // entity positions. We need to reload world info to refresh visuals. + // Read() updates data arrays but not entity positions; reload to refresh. if (m_inIsleWorld) { LegoWorld* world = CurrentWorld(); if (world && world->GetWorldId() == LegoOmni::e_act1) { @@ -87,7 +85,7 @@ void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length) } } - // Apply any world events that were queued between snapshot request and response + // Replay events queued while snapshot was in flight. for (const auto& evt : m_pendingWorldEvents) { ApplyWorldEvent(evt.entityType, evt.changeType, evt.entityIndex); } @@ -183,10 +181,7 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId) return; } - // Serialize plant + building state into a buffer using existing Write() methods - // Max sizes: 81 plants * (1+4+4+1+1+1) = 81*12 = 972 bytes - // 16 buildings * (4+4+1+1) = 16*10 = 160 bytes + 1 byte nextVariant - // Total ~1133 bytes. Use 4096 for safety. + // Serialize plant + building state (~1133 bytes max, use 4096 for safety). uint8_t stateBuffer[4096]; LegoMemory memory(stateBuffer, sizeof(stateBuffer));