diff --git a/docs/display-actors.md b/docs/display-actors.md deleted file mode 100644 index d1c00503..00000000 --- a/docs/display-actors.md +++ /dev/null @@ -1,345 +0,0 @@ -# Display Actor Override for Multiplayer Extension - -## Context - -Currently, the multiplayer extension clones a character ROI based on the player's in-game actor ID (1-5: Pepper/Mama/Papa/Nick/Laura). This means all players appear as one of the 5 playable characters. We want to allow players to choose any of the 66 character models from `g_actorInfoInit` (e.g., "rhoda", "infoman") via an INI setting, decoupling the visual display from the gameplay actor ID. - -The actor ID continues to be communicated for future use, but the visual display is driven by a separate `displayActorIndex` — an index into `g_actorInfoInit[66]`. This works because all 66 characters share the same skeleton (`g_actorLODs`), so animations are compatible. - -## Files to Modify - -| File | Change | -|------|--------| -| `extensions/include/extensions/multiplayer/protocol.h` | Add `displayActorIndex` to `PlayerStateMsg`, add constants | -| `extensions/include/extensions/multiplayer/remoteplayer.h` | Add display actor fields, setter for actorId | -| `extensions/src/multiplayer/remoteplayer.cpp` | Use display actor name for cloning | -| `extensions/include/extensions/multiplayer/networkmanager.h` | Add `m_localDisplayActorIndex`, `SetDisplayActorIndex()` | -| `extensions/src/multiplayer/networkmanager.cpp` | Broadcast/handle display actor index, update respawn logic | -| `extensions/include/extensions/multiplayer/thirdpersoncamera.h` | Add display clone fields and methods | -| `extensions/src/multiplayer/thirdpersoncamera.cpp` | Create/manage display clone ROI, sync transform | -| `extensions/src/multiplayer.cpp` | Read INI setting, resolve to g_actorInfoInit index | - -## Implementation Steps - -### Step 1: Protocol — Add `displayActorIndex` to `PlayerStateMsg` - -**File:** `extensions/include/extensions/multiplayer/protocol.h` - -Add at the end of `PlayerStateMsg` (after `idleAnimId`): -```cpp -uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65) -``` - -Add constants/helper near `IsValidActorId`: -```cpp -static const uint8_t DISPLAY_ACTOR_NONE = 0xFF; - -inline bool IsValidDisplayActorIndex(uint8_t p_index) -{ - return p_index < 66; -} -``` - -The relay server (`extensions/src/multiplayer/server/`) does NOT parse MSG_STATE content — it just forwards raw bytes. No relay changes needed. - -### Step 2: RemotePlayer — Use display actor for cloning - -**File:** `extensions/include/extensions/multiplayer/remoteplayer.h` - -- Change constructor: `RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex)` -- Add member: `uint8_t m_displayActorIndex` -- Add getter: `uint8_t GetDisplayActorIndex() const { return m_displayActorIndex; }` -- Add setter: `void SetActorId(uint8_t p_actorId) { m_actorId = p_actorId; }` -- Add private helper declaration: `const char* GetDisplayActorName() const` - -**File:** `extensions/src/multiplayer/remoteplayer.cpp` - -Constructor: store `m_displayActorIndex`, use display actor name for unique name: -```cpp -const char* displayName = GetDisplayActorName(); -SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", displayName, p_peerId); -``` - -Add helper using existing APIs (`CharacterManager()->GetActorName()` at `legocharactermanager.h:72`, `LegoActor::GetActorName()` at `legoactor.cpp:125`): -```cpp -const char* RemotePlayer::GetDisplayActorName() const -{ - if (IsValidDisplayActorIndex(m_displayActorIndex)) { - return CharacterManager()->GetActorName(m_displayActorIndex); - } - return LegoActor::GetActorName(m_actorId); -} -``` - -In `Spawn()`, replace `LegoActor::GetActorName(m_actorId)` with `GetDisplayActorName()`: -```cpp -const char* actorName = GetDisplayActorName(); -``` - -### Step 3: NetworkManager — Broadcast and handle display actor index - -**File:** `extensions/include/extensions/multiplayer/networkmanager.h` - -- Add public method: `void SetDisplayActorIndex(uint8_t p_index)` -- Add private member: `uint8_t m_localDisplayActorIndex` -- Update `CreateAndSpawnPlayer` signature: add `uint8_t p_displayActorIndex` parameter - -**File:** `extensions/src/multiplayer/networkmanager.cpp` - -Initialize `m_localDisplayActorIndex(DISPLAY_ACTOR_NONE)` in constructor. - -`SetDisplayActorIndex`: store value and forward to third-person camera: -```cpp -void NetworkManager::SetDisplayActorIndex(uint8_t p_index) -{ - m_localDisplayActorIndex = p_index; - m_thirdPersonCamera.SetDisplayActorIndex(p_index); -} -``` - -`BroadcastLocalState`: always send a valid display index (resolve NONE to actorId-1): -```cpp -uint8_t displayIndex = m_localDisplayActorIndex; -if (displayIndex == DISPLAY_ACTOR_NONE) { - displayIndex = actorId - 1; // actorId already validated above -} -msg.displayActorIndex = displayIndex; -``` - -`HandleState` — replace the actorId-change respawn logic (lines 387-392) with display-actor-change logic: -```cpp -// Respawn only if display actor changed (not on actorId change) -if (it->second->GetDisplayActorIndex() != p_msg.displayActorIndex) { - it->second->Despawn(); - m_remotePlayers.erase(it); - CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.displayActorIndex); - it = m_remotePlayers.find(peerId); -} -else if (IsValidActorId(p_msg.actorId)) { - it->second->SetActorId(p_msg.actorId); // Update for future use, no visual change -} -``` - -Also update the fallback player creation (line 381) to pass displayActorIndex: -```cpp -CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.displayActorIndex); -``` - -`CreateAndSpawnPlayer`: pass displayActorIndex to RemotePlayer constructor: -```cpp -RemotePlayer* NetworkManager::CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex) -{ - auto player = std::make_unique(p_peerId, p_actorId, p_displayActorIndex); - // ... rest unchanged -} -``` - -### Step 4: ThirdPersonCamera — Display clone for local player - -**File:** `extensions/include/extensions/multiplayer/thirdpersoncamera.h` - -Add public: -```cpp -void SetDisplayActorIndex(uint8_t p_index); -``` - -Add private: -```cpp -uint8_t m_displayActorIndex; -LegoROI* m_displayROI; // Owned clone; nullptr = use native ROI -char m_displayUniqueName[32]; -void CreateDisplayClone(); -void DestroyDisplayClone(); -bool HasDisplayOverride() const { return m_displayROI != nullptr; } -``` - -**File:** `extensions/src/multiplayer/thirdpersoncamera.cpp` - -Add includes: `#include "extensions/multiplayer/charactercloner.h"`, `#include "legocharactermanager.h"` - -Constructor: initialize `m_displayActorIndex(DISPLAY_ACTOR_NONE)`, `m_displayROI(nullptr)`, zero `m_displayUniqueName`. - -`SetDisplayActorIndex`: just store the index: -```cpp -void ThirdPersonCamera::SetDisplayActorIndex(uint8_t p_index) -{ - m_displayActorIndex = p_index; -} -``` - -`CreateDisplayClone`: create clone via CharacterCloner (reuse existing `CharacterCloner::Clone` at `charactercloner.h:14`): -```cpp -void ThirdPersonCamera::CreateDisplayClone() -{ - if (!IsValidDisplayActorIndex(m_displayActorIndex)) { - return; - } - LegoCharacterManager* charMgr = CharacterManager(); - const char* actorName = charMgr->GetActorName(m_displayActorIndex); - if (!actorName) { - return; - } - SDL_snprintf(m_displayUniqueName, sizeof(m_displayUniqueName), "tp_display"); - m_displayROI = CharacterCloner::Clone(charMgr, m_displayUniqueName, actorName); -} -``` - -`DestroyDisplayClone`: clean up owned clone: -```cpp -void ThirdPersonCamera::DestroyDisplayClone() -{ - if (m_displayROI) { - VideoManager()->Get3DManager()->Remove(*m_displayROI); - CharacterManager()->ReleaseActor(m_displayUniqueName); - m_displayROI = nullptr; - } -} -``` - -`OnActorEnter` (walking character path, ~line 94): when display override active, use clone instead of native ROI: -```cpp -if (IsValidDisplayActorIndex(m_displayActorIndex)) { - newROI->SetVisibility(FALSE); // hide native ROI - if (!m_displayROI) { - CreateDisplayClone(); - } - if (!m_displayROI) { - return; // clone failed - } - m_playerROI = m_displayROI; -} else { - m_playerROI = newROI; -} -m_currentVehicleType = VEHICLE_NONE; -m_active = true; -m_playerROI->SetVisibility(TRUE); -VideoManager()->Get3DManager()->Remove(*m_playerROI); -VideoManager()->Get3DManager()->Add(*m_playerROI); -// ... build anim caches, setup camera (unchanged) -``` - -`OnActorExit` (walking exit, ~line 139): hide clone but don't destroy it (persists across actor changes): -```cpp -if (m_active && static_cast(p_actor) == UserActor()) { - if (m_playerROI) { - m_playerROI->SetVisibility(FALSE); - VideoManager()->Get3DManager()->Remove(*m_playerROI); - } - // ... existing cleanup (ClearRideAnimation, ClearAnimCaches, etc.) - m_playerROI = nullptr; - m_active = false; -} -``` - -`ReinitForCharacter` (~line 540, walking character init): use clone if override active: -```cpp -if (IsValidDisplayActorIndex(m_displayActorIndex)) { - if (!m_displayROI) { - CreateDisplayClone(); - } - if (!m_displayROI) { - m_active = false; - return; - } - roi->SetVisibility(FALSE); // hide native - m_playerROI = m_displayROI; -} else { - m_playerROI = roi; -} -// ... rest of init (SetVisibility, Add to 3D, build caches, etc.) -``` - -`OnWorldDisabled`: destroy the display clone (animation presenters are invalidated): -```cpp -// Add before existing cleanup: -DestroyDisplayClone(); -``` - -`Disable`: destroy display clone on full teardown: -```cpp -// Add to Disable(): -DestroyDisplayClone(); -``` - -`Tick` (walking mode, ~line 207): sync display clone position from actual player ROI before animation: -```cpp -LegoPathActor* userActor = UserActor(); -if (!userActor) { - return; -} - -// Sync display clone position from native ROI -if (m_displayROI && m_displayROI == m_playerROI) { - LegoROI* nativeROI = userActor->GetROI(); - if (nativeROI) { - MxMatrix mat(nativeROI->GetLocal2World()); - m_displayROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); - VideoManager()->Get3DManager()->Moved(*m_displayROI); - } -} -// ... rest of animation code uses m_playerROI (unchanged) -``` - -### Step 5: INI Setting — Read and resolve actor name - -**File:** `extensions/src/multiplayer.cpp` - -Add include: `#include "legoactors.h"` (provides `extern LegoActorInfo g_actorInfoInit[66]`) - -Add static resolver before `Initialize()`: -```cpp -static uint8_t ResolveDisplayActorIndex(const char* p_name) -{ - for (int i = 0; i < static_cast(sizeOfArray(g_actorInfoInit)); i++) { - if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) { - return static_cast(i); - } - } - return Multiplayer::DISPLAY_ACTOR_NONE; -} -``` - -In `Initialize()`, after creating NetworkManager, read and apply setting: -```cpp -auto actorIt = options.find("multiplayer:actor"); -if (actorIt != options.end() && !actorIt->second.empty()) { - uint8_t displayIndex = ResolveDisplayActorIndex(actorIt->second.c_str()); - if (displayIndex != Multiplayer::DISPLAY_ACTOR_NONE) { - s_networkManager->SetDisplayActorIndex(displayIndex); - } -} -``` - -## Key Reused Functions - -| Function | File | Purpose | -|----------|------|---------| -| `CharacterCloner::Clone(charMgr, uniqueName, characterType)` | `extensions/src/multiplayer/charactercloner.cpp` | Clone any actor by name | -| `CharacterManager()->GetActorName(index)` | `LEGO1/.../legocharactermanager.cpp:231` | Index → actor name | -| `CharacterManager()->GetNumActors()` | `LEGO1/.../legocharactermanager.cpp:243` | Returns 66 | -| `LegoActor::GetActorName(actorId)` | `LEGO1/.../legoactor.cpp:125` | ActorId → name (fallback) | -| `g_actorInfoInit[66]` | `LEGO1/.../legoactors.h:75` (extern) | Name lookup for INI resolution | -| `CharacterManager()->ReleaseActor(name)` | `LEGO1/.../legocharactermanager.h:83` | Clean up cloned ROI | - -## Edge Cases - -- **Invalid INI value**: `ResolveDisplayActorIndex` returns `DISPLAY_ACTOR_NONE`, player uses default actorId-based display -- **Actor change mid-game (with INI override)**: actorId changes but `displayActorIndex` stays the same (fixed by INI); remote players update stored actorId without respawning; local display clone persists unchanged -- **No INI override (backward-compatible behavior)**: `m_localDisplayActorIndex` stays `DISPLAY_ACTOR_NONE`; `BroadcastLocalState` resolves to `actorId - 1` dynamically each frame. When the player changes in-game character (e.g., Pepper→Nick), actorId goes 1→4, so `displayActorIndex` goes 0→3. Remote side detects the changed `displayActorIndex` and respawns with the new character model — preserving current behavior exactly. ThirdPersonCamera uses native ROI directly (no clone), so it also updates naturally with the new character -- **Vehicle entry**: Display clone is hidden for large vehicles (existing visibility logic in `SetVisible`/`OnActorEnter`). For small vehicles, ride animation uses `m_playerROI` (the clone) — works because all actors share the same skeleton -- **World transitions**: Display clone is destroyed on `OnWorldDisabled` (animation caches become stale). Recreated on next `ReinitForCharacter`/`OnActorEnter` via lazy `CreateDisplayClone` - -## Verification - -1. **Build**: Compile the project with `EXTENSIONS` enabled -2. **No override**: Run without `multiplayer:actor` in INI — verify existing behavior unchanged: - - Remote players show correct characters - - Third-person camera works with native ROI - - When a player changes in-game character (Pepper→Nick), the remote player ROI changes accordingly (displayActorIndex changes from 0→3, triggering respawn) -3. **With override**: Set `multiplayer:actor=rhoda` (or any g_actorInfoInit name) — verify: - - Local third-person camera shows the chosen actor model - - Remote players see the chosen actor model - - Changing in-game character (Pepper→Nick) doesn't change the visual display - - Vehicle entry/exit works correctly with display clone - - World transitions don't crash (clone recreated properly) -4. **Two players with different overrides**: Verify both display correctly to each other diff --git a/extensions/include/extensions/multiplayer/charactercloner.h b/extensions/include/extensions/multiplayer/charactercloner.h index 61dc8076..2d091980 100644 --- a/extensions/include/extensions/multiplayer/charactercloner.h +++ b/extensions/include/extensions/multiplayer/charactercloner.h @@ -1,11 +1,19 @@ #pragma once +#include "legoactors.h" +#include "misc.h" + class LegoCharacterManager; class LegoROI; namespace Multiplayer { +inline bool IsValidDisplayActorIndex(uint8_t p_index) +{ + return p_index < sizeOfArray(g_actorInfoInit); +} + class CharacterCloner { public: // Creates an independent multi-part character ROI clone. diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 68222e7e..b1d0bdb8 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -47,6 +47,7 @@ class NetworkManager : public MxCore { void SetWalkAnimation(uint8_t p_index); void SetIdleAnimation(uint8_t p_index); void SendEmote(uint8_t p_emoteId); + void SetDisplayActorIndex(uint8_t p_index); // 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(). @@ -71,7 +72,7 @@ class NetworkManager : public MxCore { void ProcessIncomingPackets(); void UpdateRemotePlayers(float p_deltaTime); - RemotePlayer* CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId); + RemotePlayer* CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex); void HandleJoin(const PlayerJoinMsg& p_msg); void HandleLeave(const PlayerLeaveMsg& p_msg); @@ -102,6 +103,7 @@ class NetworkManager : public MxCore { uint8_t m_lastValidActorId; uint8_t m_localWalkAnimId; uint8_t m_localIdleAnimId; + uint8_t m_localDisplayActorIndex; bool m_inIsleWorld; bool m_registered; diff --git a/extensions/include/extensions/multiplayer/protocol.h b/extensions/include/extensions/multiplayer/protocol.h index c26449f6..2e92e6ba 100644 --- a/extensions/include/extensions/multiplayer/protocol.h +++ b/extensions/include/extensions/multiplayer/protocol.h @@ -81,6 +81,7 @@ struct PlayerStateMsg { float speed; uint8_t walkAnimId; // Index into walk animation table (0 = default) uint8_t idleAnimId; // Index into idle animation table (0 = default) + uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65) }; // Server -> all: announces which peer is the host @@ -155,6 +156,8 @@ inline bool IsValidActorId(uint8_t p_actorId) return p_actorId >= 1 && p_actorId <= 5; } +static const uint8_t DISPLAY_ACTOR_NONE = 0xFF; + // Parse the message type from a buffer. Returns MSG type or 0 on error. inline uint8_t ParseMessageType(const uint8_t* p_data, size_t p_length) { diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index af1f42dd..a5aa51c2 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -18,7 +18,7 @@ namespace Multiplayer class RemotePlayer { public: - RemotePlayer(uint32_t p_peerId, uint8_t p_actorId); + RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex); ~RemotePlayer(); void Spawn(LegoWorld* p_isleWorld); @@ -29,6 +29,8 @@ class RemotePlayer { uint32_t GetPeerId() const { return m_peerId; } uint8_t GetActorId() const { return m_actorId; } + uint8_t GetDisplayActorIndex() const { return m_displayActorIndex; } + void SetActorId(uint8_t p_actorId) { m_actorId = p_actorId; } bool IsSpawned() const { return m_spawned; } bool IsVisible() const { return m_visible; } int8_t GetWorldId() const { return m_targetWorldId; } @@ -41,6 +43,7 @@ class RemotePlayer { using AnimCache = AnimUtils::AnimCache; AnimCache* GetOrBuildAnimCache(const char* p_animName); + const char* GetDisplayActorName() const; void UpdateTransform(float p_deltaTime); void UpdateAnimation(float p_deltaTime); void UpdateVehicleState(); @@ -49,6 +52,7 @@ class RemotePlayer { uint32_t m_peerId; uint8_t m_actorId; + uint8_t m_displayActorIndex; char m_uniqueName[32]; LegoROI* m_roi; diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h index c70de598..07156f3b 100644 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ b/extensions/include/extensions/multiplayer/thirdpersoncamera.h @@ -39,6 +39,7 @@ class ThirdPersonCamera { void SetWalkAnimId(uint8_t p_id); void SetIdleAnimId(uint8_t p_id); void TriggerEmote(uint8_t p_emoteId); + void SetDisplayActorIndex(uint8_t p_index); void OnWorldEnabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world); @@ -54,11 +55,21 @@ class ThirdPersonCamera { void ApplyIdleFrame0(); void ReinitForCharacter(); + bool EnsureDisplayROI(); + void CreateDisplayClone(); + void DestroyDisplayClone(); + bool HasDisplayOverride() const { return m_displayROI != nullptr; } + 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 + // Display actor override + uint8_t m_displayActorIndex; + LegoROI* m_displayROI; // Owned clone; nullptr = use native ROI + char m_displayUniqueName[32]; + // Walk/idle state (same pattern as RemotePlayer) uint8_t m_walkAnimId; uint8_t m_idleAnimId; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index 7373c993..01bda091 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -6,11 +6,14 @@ #include "extensions/multiplayer/protocol.h" #include "islepathactor.h" #include "legoactor.h" +#include "legoactors.h" #include "legoentity.h" #include "legogamestate.h" #include "legopathactor.h" #include "misc.h" +#include + #ifdef __EMSCRIPTEN__ #include "extensions/multiplayer/platforms/emscripten/callbacks.h" #include "extensions/multiplayer/platforms/emscripten/websockettransport.h" @@ -20,6 +23,16 @@ using namespace Extensions; +static uint8_t ResolveDisplayActorIndex(const char* p_name) +{ + for (int i = 0; i < static_cast(sizeOfArray(g_actorInfoInit)); i++) { + if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) { + return static_cast(i); + } + } + return Multiplayer::DISPLAY_ACTOR_NONE; +} + std::map MultiplayerExt::options; bool MultiplayerExt::enabled = false; std::string MultiplayerExt::relayUrl; @@ -43,6 +56,14 @@ void MultiplayerExt::Initialize() // Third-person camera enabled by default, toggled via WASM export s_networkManager->GetThirdPersonCamera().Enable(); + std::string actor = options["multiplayer:actor"]; + if (!actor.empty()) { + uint8_t displayIndex = ResolveDisplayActorIndex(actor.c_str()); + if (displayIndex != Multiplayer::DISPLAY_ACTOR_NONE) { + s_networkManager->SetDisplayActorIndex(displayIndex); + } + } + if (!relayUrl.empty() && !room.empty()) { s_networkManager->Connect(room.c_str()); } diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index e3f54cc9..4f7125e7 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -30,7 +30,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_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), + m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1) { @@ -265,6 +266,12 @@ void NetworkManager::BroadcastLocalState() msg.walkAnimId = m_localWalkAnimId; msg.idleAnimId = m_localIdleAnimId; + uint8_t displayIndex = m_localDisplayActorIndex; + if (displayIndex == DISPLAY_ACTOR_NONE) { + displayIndex = actorId - 1; // actorId already validated above + } + msg.displayActorIndex = displayIndex; + SendMessage(msg); } @@ -363,9 +370,9 @@ void NetworkManager::UpdateRemotePlayers(float p_deltaTime) } } -RemotePlayer* NetworkManager::CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId) +RemotePlayer* NetworkManager::CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex) { - auto player = std::make_unique(p_peerId, p_actorId); + auto player = std::make_unique(p_peerId, p_actorId, p_displayActorIndex); if (m_inIsleWorld) { LegoWorld* world = CurrentWorld(); @@ -387,7 +394,7 @@ void NetworkManager::HandleJoin(const PlayerJoinMsg& p_msg) return; } - CreateAndSpawnPlayer(peerId, p_msg.actorId); + CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.actorId - 1); NotifyPlayerCountChanged(); } @@ -406,18 +413,21 @@ void NetworkManager::HandleState(const PlayerStateMsg& p_msg) return; } - CreateAndSpawnPlayer(peerId, p_msg.actorId); + CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.displayActorIndex); NotifyPlayerCountChanged(); it = m_remotePlayers.find(peerId); } - // Handle actor change (e.g., Pepper -> Nick) - if (IsValidActorId(p_msg.actorId) && it->second->GetActorId() != p_msg.actorId) { + // Respawn only if display actor changed (not on actorId change) + if (it->second->GetDisplayActorIndex() != p_msg.displayActorIndex) { it->second->Despawn(); m_remotePlayers.erase(it); - CreateAndSpawnPlayer(peerId, p_msg.actorId); + CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.displayActorIndex); it = m_remotePlayers.find(peerId); } + else if (IsValidActorId(p_msg.actorId)) { + it->second->SetActorId(p_msg.actorId); // Update for future use, no visual change + } int8_t oldWorldId = it->second->GetWorldId(); @@ -477,6 +487,12 @@ void NetworkManager::SendEmote(uint8_t p_emoteId) SendMessage(msg); } +void NetworkManager::SetDisplayActorIndex(uint8_t p_index) +{ + m_localDisplayActorIndex = p_index; + m_thirdPersonCamera.SetDisplayActorIndex(p_index); +} + void NetworkManager::HandleEmote(const EmoteMsg& p_msg) { uint32_t peerId = p_msg.header.peerId; diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 0a82a31e..ce978475 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -21,15 +21,17 @@ using namespace Multiplayer; -RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId) - : m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f), - m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()), - m_hasReceivedUpdate(false), 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_rideAnim(nullptr), m_rideRoiMap(nullptr), - m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) +RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex) + : m_peerId(p_peerId), m_actorId(p_actorId), m_displayActorIndex(p_displayActorIndex), m_roi(nullptr), + m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), + m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false), 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_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), + m_currentVehicleType(VEHICLE_NONE) { - SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId); + const char* displayName = GetDisplayActorName(); + SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", displayName, p_peerId); ZEROVEC3(m_targetPosition); m_targetDirection[0] = 0.0f; @@ -60,7 +62,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) return; } - const char* actorName = LegoActor::GetActorName(m_actorId); + const char* actorName = GetDisplayActorName(); if (!actorName) { return; } @@ -107,6 +109,14 @@ void RemotePlayer::Despawn() m_visible = false; } +const char* RemotePlayer::GetDisplayActorName() const +{ + if (IsValidDisplayActorIndex(m_displayActorIndex)) { + return CharacterManager()->GetActorName(m_displayActorIndex); + } + return LegoActor::GetActorName(m_actorId); +} + void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) { float posDelta = SDL_sqrtf(DISTSQRD3(p_msg.position, m_targetPosition)); diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp index 8696cff6..14ab0e78 100644 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ b/extensions/src/multiplayer/thirdpersoncamera.cpp @@ -2,6 +2,7 @@ #include "3dmanager/lego3dmanager.h" #include "anim/legoanim.h" +#include "extensions/multiplayer/charactercloner.h" #include "islepathactor.h" #include "legoanimpresenter.h" #include "legocameracontroller.h" @@ -15,6 +16,7 @@ #include "realtime/realtime.h" #include "roi/legoroi.h" +#include #include using namespace Multiplayer; @@ -33,12 +35,14 @@ static void FlipROIDirection(LegoROI* p_roi) } ThirdPersonCamera::ThirdPersonCamera() - : m_enabled(false), m_active(false), m_roiUnflipped(false), m_playerROI(nullptr), m_walkAnimId(0), m_idleAnimId(0), + : m_enabled(false), m_active(false), m_roiUnflipped(false), m_playerROI(nullptr), + m_displayActorIndex(DISPLAY_ACTOR_NONE), m_displayROI(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), m_rideVehicleROI(nullptr) { + SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName)); } void ThirdPersonCamera::Enable() @@ -61,8 +65,11 @@ void ThirdPersonCamera::Disable() // 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); + // 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); if (turnAroundROI) { FlipROIDirection(turnAroundROI); @@ -84,6 +91,7 @@ void ThirdPersonCamera::Disable() } m_active = false; + DestroyDisplayClone(); ClearRideAnimation(); m_animCacheMap.clear(); ClearAnimCaches(); @@ -142,7 +150,15 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) } // Non-vehicle (walking character) entry — Enter() already called TurnAround. - m_playerROI = newROI; + if (IsValidDisplayActorIndex(m_displayActorIndex)) { + newROI->SetVisibility(FALSE); + if (!EnsureDisplayROI()) { + return; + } + } + else { + m_playerROI = newROI; + } m_roiUnflipped = false; m_active = true; @@ -216,7 +232,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. - LegoROI* roi = (m_currentVehicleType == VEHICLE_NONE) ? m_playerROI : p_actor->GetROI(); + // 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(); if (roi) { FlipROIDirection(roi); } @@ -281,6 +299,16 @@ void ThirdPersonCamera::Tick(float p_deltaTime) return; } + // Sync display clone position from native ROI + if (m_displayROI && m_displayROI == m_playerROI) { + LegoROI* nativeROI = userActor->GetROI(); + if (nativeROI) { + MxMatrix mat(nativeROI->GetLocal2World()); + m_displayROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); + VideoManager()->Get3DManager()->Moved(*m_displayROI); + } + } + // Determine the active walk animation and its ROI map LegoAnim* walkAnim = nullptr; LegoROI** walkRoiMap = nullptr; @@ -464,6 +492,7 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world) m_active = false; m_roiUnflipped = false; m_playerROI = nullptr; + DestroyDisplayClone(); ClearRideAnimation(); m_animCacheMap.clear(); ClearAnimCaches(); @@ -537,6 +566,52 @@ void ThirdPersonCamera::BuildRideAnimation(int8_t p_vehicleType) m_animTime = 0.0f; } +void ThirdPersonCamera::SetDisplayActorIndex(uint8_t p_index) +{ + m_displayActorIndex = p_index; +} + +bool ThirdPersonCamera::EnsureDisplayROI() +{ + if (!IsValidDisplayActorIndex(m_displayActorIndex)) { + return false; + } + if (!m_displayROI) { + CreateDisplayClone(); + } + if (!m_displayROI) { + return false; + } + m_playerROI = m_displayROI; + return true; +} + +void ThirdPersonCamera::CreateDisplayClone() +{ + if (!IsValidDisplayActorIndex(m_displayActorIndex)) { + return; + } + LegoCharacterManager* charMgr = CharacterManager(); + const char* actorName = charMgr->GetActorName(m_displayActorIndex); + if (!actorName) { + return; + } + SDL_snprintf(m_displayUniqueName, sizeof(m_displayUniqueName), "tp_display"); + m_displayROI = CharacterCloner::Clone(charMgr, m_displayUniqueName, actorName); +} + +void ThirdPersonCamera::DestroyDisplayClone() +{ + if (m_displayROI) { + if (m_playerROI == m_displayROI) { + m_playerROI = nullptr; + } + VideoManager()->Get3DManager()->Remove(*m_displayROI); + CharacterManager()->ReleaseActor(m_displayUniqueName); + m_displayROI = nullptr; + } +} + void ThirdPersonCamera::ClearRideAnimation() { if (m_rideRoiMap) { @@ -591,6 +666,11 @@ void ThirdPersonCamera::ReinitForCharacter() m_currentVehicleType = vehicleType; if (vehicleType != VEHICLE_NONE) { + if (IsValidDisplayActorIndex(m_displayActorIndex) && !EnsureDisplayROI()) { + m_active = false; + return; + } + if (!m_playerROI) { m_active = false; return; @@ -618,12 +698,23 @@ void ThirdPersonCamera::ReinitForCharacter() } // Reinitializing for walking character - m_playerROI = roi; + if (IsValidDisplayActorIndex(m_displayActorIndex)) { + roi->SetVisibility(FALSE); + if (!EnsureDisplayROI()) { + m_active = false; + return; + } + } + else { + 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. + // When a display actor override is active, flip the native ROI (not the + // display clone) since Tick() syncs the clone's transform from it. if (m_roiUnflipped) { - FlipROIDirection(m_playerROI); + FlipROIDirection(HasDisplayOverride() ? roi : m_playerROI); m_roiUnflipped = false; }