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
|
#pragma once
|
||||||
|
|
||||||
|
#include "legoactors.h"
|
||||||
|
#include "misc.h"
|
||||||
|
|
||||||
class LegoCharacterManager;
|
class LegoCharacterManager;
|
||||||
class LegoROI;
|
class LegoROI;
|
||||||
|
|
||||||
namespace Multiplayer
|
namespace Multiplayer
|
||||||
{
|
{
|
||||||
|
|
||||||
|
inline bool IsValidDisplayActorIndex(uint8_t p_index)
|
||||||
|
{
|
||||||
|
return p_index < sizeOfArray(g_actorInfoInit);
|
||||||
|
}
|
||||||
|
|
||||||
class CharacterCloner {
|
class CharacterCloner {
|
||||||
public:
|
public:
|
||||||
// Creates an independent multi-part character ROI clone.
|
// Creates an independent multi-part character ROI clone.
|
||||||
|
|||||||
@ -47,6 +47,7 @@ class NetworkManager : public MxCore {
|
|||||||
void SetWalkAnimation(uint8_t p_index);
|
void SetWalkAnimation(uint8_t p_index);
|
||||||
void SetIdleAnimation(uint8_t p_index);
|
void SetIdleAnimation(uint8_t p_index);
|
||||||
void SendEmote(uint8_t p_emoteId);
|
void SendEmote(uint8_t p_emoteId);
|
||||||
|
void SetDisplayActorIndex(uint8_t p_index);
|
||||||
|
|
||||||
// Thread-safe request methods for cross-thread callers (e.g. WASM exports
|
// 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().
|
// running on the browser main thread). Deferred to the game thread in Tickle().
|
||||||
@ -71,7 +72,7 @@ class NetworkManager : public MxCore {
|
|||||||
void ProcessIncomingPackets();
|
void ProcessIncomingPackets();
|
||||||
void UpdateRemotePlayers(float p_deltaTime);
|
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 HandleJoin(const PlayerJoinMsg& p_msg);
|
||||||
void HandleLeave(const PlayerLeaveMsg& p_msg);
|
void HandleLeave(const PlayerLeaveMsg& p_msg);
|
||||||
@ -102,6 +103,7 @@ class NetworkManager : public MxCore {
|
|||||||
uint8_t m_lastValidActorId;
|
uint8_t m_lastValidActorId;
|
||||||
uint8_t m_localWalkAnimId;
|
uint8_t m_localWalkAnimId;
|
||||||
uint8_t m_localIdleAnimId;
|
uint8_t m_localIdleAnimId;
|
||||||
|
uint8_t m_localDisplayActorIndex;
|
||||||
bool m_inIsleWorld;
|
bool m_inIsleWorld;
|
||||||
bool m_registered;
|
bool m_registered;
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,7 @@ struct PlayerStateMsg {
|
|||||||
float speed;
|
float speed;
|
||||||
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
||||||
uint8_t idleAnimId; // Index into idle 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
|
// 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;
|
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.
|
// 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)
|
inline uint8_t ParseMessageType(const uint8_t* p_data, size_t p_length)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -18,7 +18,7 @@ namespace Multiplayer
|
|||||||
|
|
||||||
class RemotePlayer {
|
class RemotePlayer {
|
||||||
public:
|
public:
|
||||||
RemotePlayer(uint32_t p_peerId, uint8_t p_actorId);
|
RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex);
|
||||||
~RemotePlayer();
|
~RemotePlayer();
|
||||||
|
|
||||||
void Spawn(LegoWorld* p_isleWorld);
|
void Spawn(LegoWorld* p_isleWorld);
|
||||||
@ -29,6 +29,8 @@ class RemotePlayer {
|
|||||||
|
|
||||||
uint32_t GetPeerId() const { return m_peerId; }
|
uint32_t GetPeerId() const { return m_peerId; }
|
||||||
uint8_t GetActorId() const { return m_actorId; }
|
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 IsSpawned() const { return m_spawned; }
|
||||||
bool IsVisible() const { return m_visible; }
|
bool IsVisible() const { return m_visible; }
|
||||||
int8_t GetWorldId() const { return m_targetWorldId; }
|
int8_t GetWorldId() const { return m_targetWorldId; }
|
||||||
@ -41,6 +43,7 @@ class RemotePlayer {
|
|||||||
using AnimCache = AnimUtils::AnimCache;
|
using AnimCache = AnimUtils::AnimCache;
|
||||||
|
|
||||||
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
||||||
|
const char* GetDisplayActorName() const;
|
||||||
void UpdateTransform(float p_deltaTime);
|
void UpdateTransform(float p_deltaTime);
|
||||||
void UpdateAnimation(float p_deltaTime);
|
void UpdateAnimation(float p_deltaTime);
|
||||||
void UpdateVehicleState();
|
void UpdateVehicleState();
|
||||||
@ -49,6 +52,7 @@ class RemotePlayer {
|
|||||||
|
|
||||||
uint32_t m_peerId;
|
uint32_t m_peerId;
|
||||||
uint8_t m_actorId;
|
uint8_t m_actorId;
|
||||||
|
uint8_t m_displayActorIndex;
|
||||||
char m_uniqueName[32];
|
char m_uniqueName[32];
|
||||||
|
|
||||||
LegoROI* m_roi;
|
LegoROI* m_roi;
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class ThirdPersonCamera {
|
|||||||
void SetWalkAnimId(uint8_t p_id);
|
void SetWalkAnimId(uint8_t p_id);
|
||||||
void SetIdleAnimId(uint8_t p_id);
|
void SetIdleAnimId(uint8_t p_id);
|
||||||
void TriggerEmote(uint8_t p_emoteId);
|
void TriggerEmote(uint8_t p_emoteId);
|
||||||
|
void SetDisplayActorIndex(uint8_t p_index);
|
||||||
|
|
||||||
void OnWorldEnabled(LegoWorld* p_world);
|
void OnWorldEnabled(LegoWorld* p_world);
|
||||||
void OnWorldDisabled(LegoWorld* p_world);
|
void OnWorldDisabled(LegoWorld* p_world);
|
||||||
@ -54,11 +55,21 @@ class ThirdPersonCamera {
|
|||||||
void ApplyIdleFrame0();
|
void ApplyIdleFrame0();
|
||||||
void ReinitForCharacter();
|
void ReinitForCharacter();
|
||||||
|
|
||||||
|
bool EnsureDisplayROI();
|
||||||
|
void CreateDisplayClone();
|
||||||
|
void DestroyDisplayClone();
|
||||||
|
bool HasDisplayOverride() const { return m_displayROI != nullptr; }
|
||||||
|
|
||||||
bool m_enabled;
|
bool m_enabled;
|
||||||
bool m_active;
|
bool m_active;
|
||||||
bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies
|
bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies
|
||||||
LegoROI* m_playerROI; // Borrowed, not owned
|
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)
|
// Walk/idle state (same pattern as RemotePlayer)
|
||||||
uint8_t m_walkAnimId;
|
uint8_t m_walkAnimId;
|
||||||
uint8_t m_idleAnimId;
|
uint8_t m_idleAnimId;
|
||||||
|
|||||||
@ -6,11 +6,14 @@
|
|||||||
#include "extensions/multiplayer/protocol.h"
|
#include "extensions/multiplayer/protocol.h"
|
||||||
#include "islepathactor.h"
|
#include "islepathactor.h"
|
||||||
#include "legoactor.h"
|
#include "legoactor.h"
|
||||||
|
#include "legoactors.h"
|
||||||
#include "legoentity.h"
|
#include "legoentity.h"
|
||||||
#include "legogamestate.h"
|
#include "legogamestate.h"
|
||||||
#include "legopathactor.h"
|
#include "legopathactor.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_stdinc.h>
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
#include "extensions/multiplayer/platforms/emscripten/callbacks.h"
|
#include "extensions/multiplayer/platforms/emscripten/callbacks.h"
|
||||||
#include "extensions/multiplayer/platforms/emscripten/websockettransport.h"
|
#include "extensions/multiplayer/platforms/emscripten/websockettransport.h"
|
||||||
@ -20,6 +23,16 @@
|
|||||||
|
|
||||||
using namespace Extensions;
|
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;
|
std::map<std::string, std::string> MultiplayerExt::options;
|
||||||
bool MultiplayerExt::enabled = false;
|
bool MultiplayerExt::enabled = false;
|
||||||
std::string MultiplayerExt::relayUrl;
|
std::string MultiplayerExt::relayUrl;
|
||||||
@ -43,6 +56,14 @@ void MultiplayerExt::Initialize()
|
|||||||
// Third-person camera enabled by default, toggled via WASM export
|
// Third-person camera enabled by default, toggled via WASM export
|
||||||
s_networkManager->GetThirdPersonCamera().Enable();
|
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()) {
|
if (!relayUrl.empty() && !room.empty()) {
|
||||||
s_networkManager->Connect(room.c_str());
|
s_networkManager->Connect(room.c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,8 @@ 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_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_registered(false), m_pendingToggleThirdPerson(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1),
|
||||||
m_pendingEmote(-1)
|
m_pendingEmote(-1)
|
||||||
{
|
{
|
||||||
@ -265,6 +266,12 @@ void NetworkManager::BroadcastLocalState()
|
|||||||
msg.walkAnimId = m_localWalkAnimId;
|
msg.walkAnimId = m_localWalkAnimId;
|
||||||
msg.idleAnimId = m_localIdleAnimId;
|
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);
|
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) {
|
if (m_inIsleWorld) {
|
||||||
LegoWorld* world = CurrentWorld();
|
LegoWorld* world = CurrentWorld();
|
||||||
@ -387,7 +394,7 @@ void NetworkManager::HandleJoin(const PlayerJoinMsg& p_msg)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateAndSpawnPlayer(peerId, p_msg.actorId);
|
CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.actorId - 1);
|
||||||
NotifyPlayerCountChanged();
|
NotifyPlayerCountChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,18 +413,21 @@ void NetworkManager::HandleState(const PlayerStateMsg& p_msg)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateAndSpawnPlayer(peerId, p_msg.actorId);
|
CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.displayActorIndex);
|
||||||
NotifyPlayerCountChanged();
|
NotifyPlayerCountChanged();
|
||||||
it = m_remotePlayers.find(peerId);
|
it = m_remotePlayers.find(peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle actor change (e.g., Pepper -> Nick)
|
// Respawn only if display actor changed (not on actorId change)
|
||||||
if (IsValidActorId(p_msg.actorId) && it->second->GetActorId() != p_msg.actorId) {
|
if (it->second->GetDisplayActorIndex() != p_msg.displayActorIndex) {
|
||||||
it->second->Despawn();
|
it->second->Despawn();
|
||||||
m_remotePlayers.erase(it);
|
m_remotePlayers.erase(it);
|
||||||
CreateAndSpawnPlayer(peerId, p_msg.actorId);
|
CreateAndSpawnPlayer(peerId, p_msg.actorId, p_msg.displayActorIndex);
|
||||||
it = m_remotePlayers.find(peerId);
|
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();
|
int8_t oldWorldId = it->second->GetWorldId();
|
||||||
|
|
||||||
@ -477,6 +487,12 @@ void NetworkManager::SendEmote(uint8_t p_emoteId)
|
|||||||
SendMessage(msg);
|
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)
|
void NetworkManager::HandleEmote(const EmoteMsg& p_msg)
|
||||||
{
|
{
|
||||||
uint32_t peerId = p_msg.header.peerId;
|
uint32_t peerId = p_msg.header.peerId;
|
||||||
|
|||||||
@ -21,15 +21,17 @@
|
|||||||
|
|
||||||
using namespace Multiplayer;
|
using namespace Multiplayer;
|
||||||
|
|
||||||
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
|
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex)
|
||||||
: m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f),
|
: m_peerId(p_peerId), m_actorId(p_actorId), m_displayActorIndex(p_displayActorIndex), m_roi(nullptr),
|
||||||
m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()),
|
m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1),
|
||||||
m_hasReceivedUpdate(false), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr),
|
m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false), m_walkAnimId(0), m_idleAnimId(0),
|
||||||
m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr),
|
m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f),
|
||||||
m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_rideAnim(nullptr), m_rideRoiMap(nullptr),
|
m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false),
|
||||||
m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE)
|
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);
|
ZEROVEC3(m_targetPosition);
|
||||||
m_targetDirection[0] = 0.0f;
|
m_targetDirection[0] = 0.0f;
|
||||||
@ -60,7 +62,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* actorName = LegoActor::GetActorName(m_actorId);
|
const char* actorName = GetDisplayActorName();
|
||||||
if (!actorName) {
|
if (!actorName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -107,6 +109,14 @@ void RemotePlayer::Despawn()
|
|||||||
m_visible = false;
|
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)
|
void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
|
||||||
{
|
{
|
||||||
float posDelta = SDL_sqrtf(DISTSQRD3(p_msg.position, m_targetPosition));
|
float posDelta = SDL_sqrtf(DISTSQRD3(p_msg.position, m_targetPosition));
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "3dmanager/lego3dmanager.h"
|
#include "3dmanager/lego3dmanager.h"
|
||||||
#include "anim/legoanim.h"
|
#include "anim/legoanim.h"
|
||||||
|
#include "extensions/multiplayer/charactercloner.h"
|
||||||
#include "islepathactor.h"
|
#include "islepathactor.h"
|
||||||
#include "legoanimpresenter.h"
|
#include "legoanimpresenter.h"
|
||||||
#include "legocameracontroller.h"
|
#include "legocameracontroller.h"
|
||||||
@ -15,6 +16,7 @@
|
|||||||
#include "realtime/realtime.h"
|
#include "realtime/realtime.h"
|
||||||
#include "roi/legoroi.h"
|
#include "roi/legoroi.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_stdinc.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
using namespace Multiplayer;
|
using namespace Multiplayer;
|
||||||
@ -33,12 +35,14 @@ static void FlipROIDirection(LegoROI* p_roi)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThirdPersonCamera::ThirdPersonCamera()
|
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_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_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_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0),
|
||||||
m_rideVehicleROI(nullptr)
|
m_rideVehicleROI(nullptr)
|
||||||
{
|
{
|
||||||
|
SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThirdPersonCamera::Enable()
|
void ThirdPersonCamera::Enable()
|
||||||
@ -61,8 +65,11 @@ void ThirdPersonCamera::Disable()
|
|||||||
// 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
|
// For walking characters the target is m_playerROI; for vehicles it
|
||||||
// is the vehicle actor's ROI (UserActor() returns the vehicle).
|
// is the vehicle actor's ROI (UserActor() returns the vehicle).
|
||||||
LegoROI* turnAroundROI =
|
// When a display actor override is active, flip the native ROI (not the
|
||||||
(m_currentVehicleType == VEHICLE_NONE) ? m_playerROI : (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);
|
||||||
@ -84,6 +91,7 @@ void ThirdPersonCamera::Disable()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_active = false;
|
m_active = false;
|
||||||
|
DestroyDisplayClone();
|
||||||
ClearRideAnimation();
|
ClearRideAnimation();
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
ClearAnimCaches();
|
ClearAnimCaches();
|
||||||
@ -142,7 +150,15 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-vehicle (walking character) entry — Enter() already called TurnAround.
|
// 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_roiUnflipped = false;
|
||||||
m_active = true;
|
m_active = true;
|
||||||
|
|
||||||
@ -216,7 +232,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.
|
||||||
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) {
|
if (roi) {
|
||||||
FlipROIDirection(roi);
|
FlipROIDirection(roi);
|
||||||
}
|
}
|
||||||
@ -281,6 +299,16 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
|
|||||||
return;
|
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
|
// Determine the active walk animation and its ROI map
|
||||||
LegoAnim* walkAnim = nullptr;
|
LegoAnim* walkAnim = nullptr;
|
||||||
LegoROI** walkRoiMap = nullptr;
|
LegoROI** walkRoiMap = nullptr;
|
||||||
@ -464,6 +492,7 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world)
|
|||||||
m_active = false;
|
m_active = false;
|
||||||
m_roiUnflipped = false;
|
m_roiUnflipped = false;
|
||||||
m_playerROI = nullptr;
|
m_playerROI = nullptr;
|
||||||
|
DestroyDisplayClone();
|
||||||
ClearRideAnimation();
|
ClearRideAnimation();
|
||||||
m_animCacheMap.clear();
|
m_animCacheMap.clear();
|
||||||
ClearAnimCaches();
|
ClearAnimCaches();
|
||||||
@ -537,6 +566,52 @@ void ThirdPersonCamera::BuildRideAnimation(int8_t p_vehicleType)
|
|||||||
m_animTime = 0.0f;
|
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()
|
void ThirdPersonCamera::ClearRideAnimation()
|
||||||
{
|
{
|
||||||
if (m_rideRoiMap) {
|
if (m_rideRoiMap) {
|
||||||
@ -591,6 +666,11 @@ void ThirdPersonCamera::ReinitForCharacter()
|
|||||||
m_currentVehicleType = vehicleType;
|
m_currentVehicleType = vehicleType;
|
||||||
|
|
||||||
if (vehicleType != VEHICLE_NONE) {
|
if (vehicleType != VEHICLE_NONE) {
|
||||||
|
if (IsValidDisplayActorIndex(m_displayActorIndex) && !EnsureDisplayROI()) {
|
||||||
|
m_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_playerROI) {
|
if (!m_playerROI) {
|
||||||
m_active = false;
|
m_active = false;
|
||||||
return;
|
return;
|
||||||
@ -618,12 +698,23 @@ void ThirdPersonCamera::ReinitForCharacter()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reinitializing for walking character
|
// 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().
|
// 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
|
||||||
|
// display clone) since Tick() syncs the clone's transform from it.
|
||||||
if (m_roiUnflipped) {
|
if (m_roiUnflipped) {
|
||||||
FlipROIDirection(m_playerROI);
|
FlipROIDirection(HasDisplayOverride() ? roi : m_playerROI);
|
||||||
m_roiUnflipped = false;
|
m_roiUnflipped = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user