mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Implement display actors (#6)
* Implement display actor override for multiplayer extension Add displayActorIndex to the multiplayer protocol, allowing players to choose any of the 66 character models from g_actorInfoInit via the multiplayer:actor INI setting. The visual display is decoupled from the gameplay actor ID while maintaining backward compatibility. - Protocol: Add displayActorIndex field to PlayerStateMsg and validation helpers - RemotePlayer: Use display actor name for cloning instead of actorId - NetworkManager: Broadcast/handle displayActorIndex, respawn on display change - ThirdPersonCamera: Create/manage display clone ROI for local player override - INI: Read multiplayer:actor setting and resolve to g_actorInfoInit index * Use array syntax for INI option access in display actor setup Consistent with how relayUrl and room are read from options. * Fix display actor ROI handling in 3rd person camera - Fix direction flip targeting display clone instead of native ROI in Disable(), ReinitForCharacter(), and OnCamAnimEnd(). The native ROI is the source of truth for TransformPointOfView and Tick() sync. - Fix use-after-free: DestroyDisplayClone() now nulls m_playerROI when it points to the destroyed clone, preventing dangling pointer access in BuildRideAnimation after a 3rd→1st→3rd person toggle on a vehicle. - Recreate display clone in ReinitForCharacter() vehicle branch. - Extract EnsureDisplayROI() helper to deduplicate clone setup pattern. - Move IsValidDisplayActorIndex() to charactercloner.h, replacing magic number 66 with sizeOfArray(g_actorInfoInit). * Remove display actors plan document
This commit is contained in:
parent
dcf3b66173
commit
ed4e248be4
@ -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<RemotePlayer>(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<LegoPathActor*>(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<int>(sizeOfArray(g_actorInfoInit)); i++) {
|
||||
if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) {
|
||||
return static_cast<uint8_t>(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
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 <SDL3/SDL_stdinc.h>
|
||||
|
||||
#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<int>(sizeOfArray(g_actorInfoInit)); i++) {
|
||||
if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) {
|
||||
return static_cast<uint8_t>(i);
|
||||
}
|
||||
}
|
||||
return Multiplayer::DISPLAY_ACTOR_NONE;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> 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());
|
||||
}
|
||||
|
||||
@ -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<RemotePlayer>(p_peerId, p_actorId);
|
||||
auto player = std::make_unique<RemotePlayer>(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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 <SDL3/SDL_stdinc.h>
|
||||
#include <cmath>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user