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 {
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);

View File

@ -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;

View File

@ -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<uint8_t>(partIndex >= 0 ? partIndex : 0xFF)
);
mgr->SendCustomize(targetPeerId, changeType, static_cast<uint8_t>(partIndex >= 0 ? partIndex : 0xFF));
return TRUE;
}

View File

@ -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(

View File

@ -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<LegoActor*>(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);
}
}
}
}

View File

@ -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);

View File

@ -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;
}