From a8c3ec7b2fdd62b894913b03835956f3e631f6ce Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sun, 8 Mar 2026 10:07:22 -0700 Subject: [PATCH] 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. --- .../multiplayer/charactercustomizer.h | 2 +- .../extensions/multiplayer/networkmanager.h | 2 + extensions/src/multiplayer.cpp | 11 ++-- .../src/multiplayer/charactercustomizer.cpp | 12 +---- extensions/src/multiplayer/networkmanager.cpp | 44 ++++++++++----- extensions/src/multiplayer/remoteplayer.cpp | 10 ++-- .../src/multiplayer/thirdpersoncamera.cpp | 53 +++++++------------ 7 files changed, 62 insertions(+), 72 deletions(-) diff --git a/extensions/include/extensions/multiplayer/charactercustomizer.h b/extensions/include/extensions/multiplayer/charactercustomizer.h index ca5e7d57..ac4e0eb4 100644 --- a/extensions/include/extensions/multiplayer/charactercustomizer.h +++ b/extensions/include/extensions/multiplayer/charactercustomizer.h @@ -13,7 +13,7 @@ struct CustomizeState; class CharacterCustomizer { public: - static uint8_t ResolveActorInfoIndex(uint8_t p_displayActorIndex, uint8_t p_actorId); + static uint8_t ResolveActorInfoIndex(uint8_t p_displayActorIndex); static bool SwitchColor(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, CustomizeState& p_state, int p_partIndex); diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 2d68a575..ee08a90f 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -97,6 +97,7 @@ class NetworkManager : public MxCore { void HandleEmote(const EmoteMsg& p_msg); void HandleCustomize(const CustomizeMsg& p_msg); + void DeriveDisplayActorIndex(uint8_t p_actorId); void ProcessPendingRequests(); void RemoveRemotePlayer(uint32_t p_peerId); void RemoveAllRemotePlayers(); @@ -122,6 +123,7 @@ class NetworkManager : public MxCore { uint8_t m_localWalkAnimId; uint8_t m_localIdleAnimId; uint8_t m_localDisplayActorIndex; + bool m_displayActorFrozen; bool m_localAllowRemoteCustomize; bool m_inIsleWorld; bool m_registered; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index 7d0022c0..d7fdc5e7 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -108,8 +108,9 @@ MxBool MultiplayerExt::HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationP Multiplayer::RemotePlayer* remote = mgr->FindPlayerByROI(p_rootROI); // Check if it's our own 3rd-person display actor override - bool isSelf = (mgr->GetThirdPersonCamera().GetDisplayROI() != nullptr && - mgr->GetThirdPersonCamera().GetDisplayROI() == p_rootROI); + bool isSelf = + (mgr->GetThirdPersonCamera().GetDisplayROI() != nullptr && + mgr->GetThirdPersonCamera().GetDisplayROI() == p_rootROI); if (!remote && !isSelf) { return FALSE; @@ -160,11 +161,7 @@ MxBool MultiplayerExt::HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationP // For remote targets this avoids flip-flop from stale state messages; for self targets // it keeps the code path uniform. uint32_t targetPeerId = remote ? remote->GetPeerId() : mgr->GetLocalPeerId(); - mgr->SendCustomize( - targetPeerId, - changeType, - static_cast(partIndex >= 0 ? partIndex : 0xFF) - ); + mgr->SendCustomize(targetPeerId, changeType, static_cast(partIndex >= 0 ? partIndex : 0xFF)); return TRUE; } diff --git a/extensions/src/multiplayer/charactercustomizer.cpp b/extensions/src/multiplayer/charactercustomizer.cpp index 94b3e435..8183acac 100644 --- a/extensions/src/multiplayer/charactercustomizer.cpp +++ b/extensions/src/multiplayer/charactercustomizer.cpp @@ -50,17 +50,9 @@ LegoROI* CharacterCustomizer::FindChildROI(LegoROI* p_rootROI, const char* p_nam // MARK: Public API -uint8_t CharacterCustomizer::ResolveActorInfoIndex(uint8_t p_displayActorIndex, uint8_t p_actorId) +uint8_t CharacterCustomizer::ResolveActorInfoIndex(uint8_t p_displayActorIndex) { - if (IsValidDisplayActorIndex(p_displayActorIndex)) { - return p_displayActorIndex; - } - - if (p_actorId >= 1 && p_actorId <= 5) { - return p_actorId - 1; - } - - return 0; + return p_displayActorIndex; } bool CharacterCustomizer::SwitchColor( diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 049333e0..b8ad1729 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -35,10 +35,10 @@ 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_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_localAllowRemoteCustomize(true), m_inIsleWorld(false), - m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false), - m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), - m_pendingToggleAllowCustomize(false), m_showNameBubbles(true) + m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_displayActorFrozen(false), m_localAllowRemoteCustomize(true), + m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false), + m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false), + m_showNameBubbles(true) { } @@ -49,6 +49,15 @@ NetworkManager::~NetworkManager() MxResult NetworkManager::Tickle() { + // Derive display actor early so it is valid before ProcessPendingRequests + // may toggle the 3rd-person camera (which needs a valid display actor index). + { + LegoPathActor* userActor = UserActor(); + if (userActor) { + DeriveDisplayActorIndex(static_cast(userActor)->GetActorId()); + } + } + ProcessPendingRequests(); m_thirdPersonCamera.Tick(0.016f); @@ -345,11 +354,7 @@ void NetworkManager::BroadcastLocalState() } } - uint8_t displayIndex = m_localDisplayActorIndex; - if (displayIndex == DISPLAY_ACTOR_NONE) { - displayIndex = actorId - 1; // actorId already validated above - } - msg.displayActorIndex = displayIndex; + msg.displayActorIndex = m_localDisplayActorIndex; m_thirdPersonCamera.GetCustomizeState().Pack(msg.customizeData); msg.customizeFlags = m_localAllowRemoteCustomize ? 0x01 : 0x00; @@ -597,9 +602,22 @@ void NetworkManager::SendEmote(uint8_t p_emoteId) void NetworkManager::SetDisplayActorIndex(uint8_t p_displayActorIndex) { m_localDisplayActorIndex = p_displayActorIndex; + m_displayActorFrozen = true; m_thirdPersonCamera.SetDisplayActorIndex(p_displayActorIndex); } +void NetworkManager::DeriveDisplayActorIndex(uint8_t p_actorId) +{ + if (m_displayActorFrozen || !IsValidActorId(p_actorId)) { + return; + } + uint8_t derived = p_actorId - 1; + if (derived != m_localDisplayActorIndex) { + m_localDisplayActorIndex = derived; + m_thirdPersonCamera.SetDisplayActorIndex(derived); + } +} + void NetworkManager::HandleEmote(const EmoteMsg& p_msg) { uint32_t peerId = p_msg.header.peerId; @@ -747,17 +765,19 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg) if (effectROI) { CharacterCustomizer::PlayClickSound( - effectROI, m_thirdPersonCamera.GetCustomizeState(), p_msg.changeType == CHANGE_MOOD + effectROI, + m_thirdPersonCamera.GetCustomizeState(), + p_msg.changeType == CHANGE_MOOD ); // Only play click animation in 3rd person (not visible in 1st person) if (m_thirdPersonCamera.GetDisplayROI() && !m_thirdPersonCamera.IsInVehicle()) { MxU32 clickAnimId = CharacterCustomizer::PlayClickAnimation( - m_thirdPersonCamera.GetDisplayROI(), m_thirdPersonCamera.GetCustomizeState() + m_thirdPersonCamera.GetDisplayROI(), + m_thirdPersonCamera.GetCustomizeState() ); m_thirdPersonCamera.SetClickAnimObjectId(clickAnimId); } } } } - diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index a0e092ac..0a7f515a 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -6,7 +6,6 @@ #include "3dmanager/lego3dmanager.h" #include "anim/legoanim.h" #include "extensions/multiplayer/charactercloner.h" -#include "legoactor.h" #include "legoanimpresenter.h" #include "legocharactermanager.h" #include "legovideomanager.h" @@ -86,7 +85,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) m_visible = false; // Initialize customize state from the display actor's info - uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex, m_actorId); + uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); m_customizeState.InitFromActorInfo(actorInfoIndex); // Build initial walk and idle animation caches @@ -128,10 +127,7 @@ void RemotePlayer::Despawn() const char* RemotePlayer::GetDisplayActorName() const { - if (IsValidDisplayActorIndex(m_displayActorIndex)) { - return CharacterManager()->GetActorName(m_displayActorIndex); - } - return LegoActor::GetActorName(m_actorId); + return CharacterManager()->GetActorName(m_displayActorIndex); } void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) @@ -180,7 +176,7 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) newState.Unpack(p_msg.customizeData); if (newState != m_customizeState) { - uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex, m_actorId); + uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); m_customizeState = newState; if (m_spawned && m_roi) { CharacterCustomizer::ApplyFullState(m_roi, actorInfoIndex, m_customizeState); diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp index 4a08f066..d797042a 100644 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ b/extensions/src/multiplayer/thirdpersoncamera.cpp @@ -68,13 +68,9 @@ void ThirdPersonCamera::Disable() // 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). - // When a display actor override is active, flip the native ROI (not the - // display clone) since TransformPointOfView uses it for the 1st-person camera. - LegoROI* turnAroundROI = (m_currentVehicleType == VEHICLE_NONE && !HasDisplayOverride()) - ? m_playerROI - : (userActor ? userActor->GetROI() : nullptr); + // Flip the native ROI (not the display clone) since TransformPointOfView + // uses it for the 1st-person camera. + LegoROI* turnAroundROI = userActor ? userActor->GetROI() : nullptr; if (turnAroundROI) { FlipROIDirection(turnAroundROI); @@ -160,14 +156,9 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) } // Non-vehicle (walking character) entry — Enter() already called TurnAround. - if (IsValidDisplayActorIndex(m_displayActorIndex)) { - newROI->SetVisibility(FALSE); - if (!EnsureDisplayROI()) { - return; - } - } - else { - m_playerROI = newROI; + newROI->SetVisibility(FALSE); + if (!EnsureDisplayROI()) { + return; } m_roiUnflipped = false; m_active = true; @@ -244,9 +235,9 @@ void ThirdPersonCamera::OnCamAnimEnd(LegoPathActor* p_actor) // FUN_1004b6d0's PlaceActor set the ROI with standard direction // (z = visual forward). The 3rd person camera needs backward-z. // Flip the ROI direction, then re-setup the camera. - // When a display actor override is active, flip the native ROI (not the - // display clone) since Tick() syncs the clone's transform from it. - LegoROI* roi = (m_currentVehicleType == VEHICLE_NONE && !HasDisplayOverride()) ? m_playerROI : p_actor->GetROI(); + // Flip the native ROI (not the display clone) since Tick() syncs the + // clone's transform from it. + LegoROI* roi = p_actor->GetROI(); if (roi) { FlipROIDirection(roi); } @@ -497,10 +488,7 @@ void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId) void ThirdPersonCamera::ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex) { - uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex( - m_displayActorIndex, - GameState() ? GameState()->GetActorId() : 0 - ); + uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); CharacterCustomizer::ApplyChange(m_displayROI, actorInfoIndex, m_customizeState, p_changeType, p_partIndex); } @@ -936,7 +924,7 @@ void ThirdPersonCamera::ReinitForCharacter() m_currentVehicleType = vehicleType; if (vehicleType != VEHICLE_NONE) { - if (IsValidDisplayActorIndex(m_displayActorIndex) && !EnsureDisplayROI()) { + if (!EnsureDisplayROI()) { m_active = false; return; } @@ -969,23 +957,18 @@ void ThirdPersonCamera::ReinitForCharacter() } // Reinitializing for walking character - if (IsValidDisplayActorIndex(m_displayActorIndex)) { - roi->SetVisibility(FALSE); - if (!EnsureDisplayROI()) { - m_active = false; - return; - } - } - else { - m_playerROI = roi; + roi->SetVisibility(FALSE); + if (!EnsureDisplayROI()) { + m_active = false; + return; } // Re-apply TurnAround if we undid it in Disable(). // Only set the local matrix here; the subsequent Add() will propagate world data. - // When a display actor override is active, flip the native ROI (not the - // display clone) since Tick() syncs the clone's transform from it. + // Flip the native ROI (not the display clone) since Tick() syncs the + // clone's transform from it. if (m_roiUnflipped) { - FlipROIDirection(HasDisplayOverride() ? roi : m_playerROI); + FlipROIDirection(roi); m_roiUnflipped = false; }