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.
This commit is contained in:
Christian Semmler 2026-03-08 10:07:22 -07:00
parent 1fe1b732e0
commit a8c3ec7b2f
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
7 changed files with 62 additions and 72 deletions

View File

@ -13,7 +13,7 @@ struct CustomizeState;
class CharacterCustomizer { class CharacterCustomizer {
public: 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, static bool SwitchColor(LegoROI* p_rootROI, uint8_t p_actorInfoIndex,
CustomizeState& p_state, int p_partIndex); CustomizeState& p_state, int p_partIndex);

View File

@ -97,6 +97,7 @@ class NetworkManager : public MxCore {
void HandleEmote(const EmoteMsg& p_msg); void HandleEmote(const EmoteMsg& p_msg);
void HandleCustomize(const CustomizeMsg& p_msg); void HandleCustomize(const CustomizeMsg& p_msg);
void DeriveDisplayActorIndex(uint8_t p_actorId);
void ProcessPendingRequests(); void ProcessPendingRequests();
void RemoveRemotePlayer(uint32_t p_peerId); void RemoveRemotePlayer(uint32_t p_peerId);
void RemoveAllRemotePlayers(); void RemoveAllRemotePlayers();
@ -122,6 +123,7 @@ class NetworkManager : public MxCore {
uint8_t m_localWalkAnimId; uint8_t m_localWalkAnimId;
uint8_t m_localIdleAnimId; uint8_t m_localIdleAnimId;
uint8_t m_localDisplayActorIndex; uint8_t m_localDisplayActorIndex;
bool m_displayActorFrozen;
bool m_localAllowRemoteCustomize; bool m_localAllowRemoteCustomize;
bool m_inIsleWorld; bool m_inIsleWorld;
bool m_registered; bool m_registered;

View File

@ -108,7 +108,8 @@ MxBool MultiplayerExt::HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationP
Multiplayer::RemotePlayer* remote = mgr->FindPlayerByROI(p_rootROI); Multiplayer::RemotePlayer* remote = mgr->FindPlayerByROI(p_rootROI);
// Check if it's our own 3rd-person display actor override // Check if it's our own 3rd-person display actor override
bool isSelf = (mgr->GetThirdPersonCamera().GetDisplayROI() != nullptr && bool isSelf =
(mgr->GetThirdPersonCamera().GetDisplayROI() != nullptr &&
mgr->GetThirdPersonCamera().GetDisplayROI() == p_rootROI); mgr->GetThirdPersonCamera().GetDisplayROI() == p_rootROI);
if (!remote && !isSelf) { if (!remote && !isSelf) {
@ -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 // For remote targets this avoids flip-flop from stale state messages; for self targets
// it keeps the code path uniform. // it keeps the code path uniform.
uint32_t targetPeerId = remote ? remote->GetPeerId() : mgr->GetLocalPeerId(); uint32_t targetPeerId = remote ? remote->GetPeerId() : mgr->GetLocalPeerId();
mgr->SendCustomize( mgr->SendCustomize(targetPeerId, changeType, static_cast<uint8_t>(partIndex >= 0 ? partIndex : 0xFF));
targetPeerId,
changeType,
static_cast<uint8_t>(partIndex >= 0 ? partIndex : 0xFF)
);
return TRUE; return TRUE;
} }

View File

@ -50,19 +50,11 @@ LegoROI* CharacterCustomizer::FindChildROI(LegoROI* p_rootROI, const char* p_nam
// MARK: Public API // 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; return p_displayActorIndex;
} }
if (p_actorId >= 1 && p_actorId <= 5) {
return p_actorId - 1;
}
return 0;
}
bool CharacterCustomizer::SwitchColor( bool CharacterCustomizer::SwitchColor(
LegoROI* p_rootROI, LegoROI* p_rootROI,
uint8_t p_actorInfoIndex, uint8_t p_actorInfoIndex,

View File

@ -35,10 +35,10 @@ void NetworkManager::SendMessage(const T& p_msg)
NetworkManager::NetworkManager() NetworkManager::NetworkManager()
: m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), : 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_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0),
m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_localAllowRemoteCustomize(true), m_inIsleWorld(false), m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_displayActorFrozen(false), m_localAllowRemoteCustomize(true),
m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false), m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false),
m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false),
m_pendingToggleAllowCustomize(false), m_showNameBubbles(true) m_showNameBubbles(true)
{ {
} }
@ -49,6 +49,15 @@ NetworkManager::~NetworkManager()
MxResult NetworkManager::Tickle() 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<LegoActor*>(userActor)->GetActorId());
}
}
ProcessPendingRequests(); ProcessPendingRequests();
m_thirdPersonCamera.Tick(0.016f); m_thirdPersonCamera.Tick(0.016f);
@ -345,11 +354,7 @@ void NetworkManager::BroadcastLocalState()
} }
} }
uint8_t displayIndex = m_localDisplayActorIndex; msg.displayActorIndex = m_localDisplayActorIndex;
if (displayIndex == DISPLAY_ACTOR_NONE) {
displayIndex = actorId - 1; // actorId already validated above
}
msg.displayActorIndex = displayIndex;
m_thirdPersonCamera.GetCustomizeState().Pack(msg.customizeData); m_thirdPersonCamera.GetCustomizeState().Pack(msg.customizeData);
msg.customizeFlags = m_localAllowRemoteCustomize ? 0x01 : 0x00; msg.customizeFlags = m_localAllowRemoteCustomize ? 0x01 : 0x00;
@ -597,9 +602,22 @@ void NetworkManager::SendEmote(uint8_t p_emoteId)
void NetworkManager::SetDisplayActorIndex(uint8_t p_displayActorIndex) void NetworkManager::SetDisplayActorIndex(uint8_t p_displayActorIndex)
{ {
m_localDisplayActorIndex = p_displayActorIndex; m_localDisplayActorIndex = p_displayActorIndex;
m_displayActorFrozen = true;
m_thirdPersonCamera.SetDisplayActorIndex(p_displayActorIndex); 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) void NetworkManager::HandleEmote(const EmoteMsg& p_msg)
{ {
uint32_t peerId = p_msg.header.peerId; uint32_t peerId = p_msg.header.peerId;
@ -747,17 +765,19 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg)
if (effectROI) { if (effectROI) {
CharacterCustomizer::PlayClickSound( 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) // Only play click animation in 3rd person (not visible in 1st person)
if (m_thirdPersonCamera.GetDisplayROI() && !m_thirdPersonCamera.IsInVehicle()) { if (m_thirdPersonCamera.GetDisplayROI() && !m_thirdPersonCamera.IsInVehicle()) {
MxU32 clickAnimId = CharacterCustomizer::PlayClickAnimation( MxU32 clickAnimId = CharacterCustomizer::PlayClickAnimation(
m_thirdPersonCamera.GetDisplayROI(), m_thirdPersonCamera.GetCustomizeState() m_thirdPersonCamera.GetDisplayROI(),
m_thirdPersonCamera.GetCustomizeState()
); );
m_thirdPersonCamera.SetClickAnimObjectId(clickAnimId); m_thirdPersonCamera.SetClickAnimObjectId(clickAnimId);
} }
} }
} }
} }

View File

@ -6,7 +6,6 @@
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "anim/legoanim.h" #include "anim/legoanim.h"
#include "extensions/multiplayer/charactercloner.h" #include "extensions/multiplayer/charactercloner.h"
#include "legoactor.h"
#include "legoanimpresenter.h" #include "legoanimpresenter.h"
#include "legocharactermanager.h" #include "legocharactermanager.h"
#include "legovideomanager.h" #include "legovideomanager.h"
@ -86,7 +85,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
m_visible = false; m_visible = false;
// Initialize customize state from the display actor's info // 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); m_customizeState.InitFromActorInfo(actorInfoIndex);
// Build initial walk and idle animation caches // Build initial walk and idle animation caches
@ -128,11 +127,8 @@ void RemotePlayer::Despawn()
const char* RemotePlayer::GetDisplayActorName() const const char* RemotePlayer::GetDisplayActorName() const
{ {
if (IsValidDisplayActorIndex(m_displayActorIndex)) {
return CharacterManager()->GetActorName(m_displayActorIndex); return CharacterManager()->GetActorName(m_displayActorIndex);
} }
return LegoActor::GetActorName(m_actorId);
}
void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
{ {
@ -180,7 +176,7 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
newState.Unpack(p_msg.customizeData); newState.Unpack(p_msg.customizeData);
if (newState != m_customizeState) { if (newState != m_customizeState) {
uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex, m_actorId); uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex);
m_customizeState = newState; m_customizeState = newState;
if (m_spawned && m_roi) { if (m_spawned && m_roi) {
CharacterCustomizer::ApplyFullState(m_roi, actorInfoIndex, m_customizeState); CharacterCustomizer::ApplyFullState(m_roi, actorInfoIndex, m_customizeState);

View File

@ -68,13 +68,9 @@ void ThirdPersonCamera::Disable()
// direction. This keeps the 1st-person camera facing the same way // direction. This keeps the 1st-person camera facing the same way
// as the 3rd-person camera, and ensures the network direction stays // as the 3rd-person camera, and ensures the network direction stays
// consistent (no 180-degree flip for others). // consistent (no 180-degree flip for others).
// For walking characters the target is m_playerROI; for vehicles it // Flip the native ROI (not the display clone) since TransformPointOfView
// is the vehicle actor's ROI (UserActor() returns the vehicle). // uses it for the 1st-person camera.
// When a display actor override is active, flip the native ROI (not the LegoROI* turnAroundROI = userActor ? userActor->GetROI() : nullptr;
// display clone) since TransformPointOfView uses it for the 1st-person camera.
LegoROI* turnAroundROI = (m_currentVehicleType == VEHICLE_NONE && !HasDisplayOverride())
? m_playerROI
: (userActor ? userActor->GetROI() : nullptr);
if (turnAroundROI) { if (turnAroundROI) {
FlipROIDirection(turnAroundROI); FlipROIDirection(turnAroundROI);
@ -160,15 +156,10 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
} }
// Non-vehicle (walking character) entry — Enter() already called TurnAround. // Non-vehicle (walking character) entry — Enter() already called TurnAround.
if (IsValidDisplayActorIndex(m_displayActorIndex)) {
newROI->SetVisibility(FALSE); newROI->SetVisibility(FALSE);
if (!EnsureDisplayROI()) { if (!EnsureDisplayROI()) {
return; return;
} }
}
else {
m_playerROI = newROI;
}
m_roiUnflipped = false; m_roiUnflipped = false;
m_active = true; m_active = true;
@ -244,9 +235,9 @@ void ThirdPersonCamera::OnCamAnimEnd(LegoPathActor* p_actor)
// FUN_1004b6d0's PlaceActor set the ROI with standard direction // FUN_1004b6d0's PlaceActor set the ROI with standard direction
// (z = visual forward). The 3rd person camera needs backward-z. // (z = visual forward). The 3rd person camera needs backward-z.
// Flip the ROI direction, then re-setup the camera. // Flip the ROI direction, then re-setup the camera.
// When a display actor override is active, flip the native ROI (not the // Flip the native ROI (not the display clone) since Tick() syncs the
// display clone) since Tick() syncs the clone's transform from it. // clone's transform from it.
LegoROI* roi = (m_currentVehicleType == VEHICLE_NONE && !HasDisplayOverride()) ? m_playerROI : p_actor->GetROI(); LegoROI* roi = p_actor->GetROI();
if (roi) { if (roi) {
FlipROIDirection(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) void ThirdPersonCamera::ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex)
{ {
uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex( uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex);
m_displayActorIndex,
GameState() ? GameState()->GetActorId() : 0
);
CharacterCustomizer::ApplyChange(m_displayROI, actorInfoIndex, m_customizeState, p_changeType, p_partIndex); CharacterCustomizer::ApplyChange(m_displayROI, actorInfoIndex, m_customizeState, p_changeType, p_partIndex);
} }
@ -936,7 +924,7 @@ void ThirdPersonCamera::ReinitForCharacter()
m_currentVehicleType = vehicleType; m_currentVehicleType = vehicleType;
if (vehicleType != VEHICLE_NONE) { if (vehicleType != VEHICLE_NONE) {
if (IsValidDisplayActorIndex(m_displayActorIndex) && !EnsureDisplayROI()) { if (!EnsureDisplayROI()) {
m_active = false; m_active = false;
return; return;
} }
@ -969,23 +957,18 @@ void ThirdPersonCamera::ReinitForCharacter()
} }
// Reinitializing for walking character // Reinitializing for walking character
if (IsValidDisplayActorIndex(m_displayActorIndex)) {
roi->SetVisibility(FALSE); roi->SetVisibility(FALSE);
if (!EnsureDisplayROI()) { if (!EnsureDisplayROI()) {
m_active = false; m_active = false;
return; return;
} }
}
else {
m_playerROI = roi;
}
// Re-apply TurnAround if we undid it in Disable(). // Re-apply TurnAround if we undid it in Disable().
// Only set the local matrix here; the subsequent Add() will propagate world data. // 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 // Flip the native ROI (not the display clone) since Tick() syncs the
// display clone) since Tick() syncs the clone's transform from it. // clone's transform from it.
if (m_roiUnflipped) { if (m_roiUnflipped) {
FlipROIDirection(HasDisplayOverride() ? roi : m_playerROI); FlipROIDirection(roi);
m_roiUnflipped = false; m_roiUnflipped = false;
} }