Clean up multiplayer extension: use vec.h macros, remove debug logging

- Replace manual vector math in UpdateTransform with CalcLocalTransform and vec.h macros (LERP3, SET3, MV3, DISTSQRD3, ZEROVEC3)
- Remove all SDL_Log debug logging from multiplayer code
- Strip extraneous comments that restate the code
- Extract CreateAndSpawnPlayer helper to consolidate repeated spawn pattern
- Simplify UpdateVehicleState control flow
- Remove unused includes (SDL_log.h, mxgeometry/mxmatrix.h)
This commit is contained in:
Christian Semmler 2026-02-28 12:11:19 -08:00
parent 5c8a2ffd3b
commit fb1d596704
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
6 changed files with 62 additions and 339 deletions

View File

@ -37,7 +37,6 @@ class NetworkManager : public MxCore {
void Disconnect(); void Disconnect();
bool IsConnected() const; bool IsConnected() const;
// Called by the Multiplayer extension on world transitions
void OnWorldEnabled(LegoWorld* p_world); void OnWorldEnabled(LegoWorld* p_world);
void OnWorldDisabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world);
@ -46,6 +45,8 @@ 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);
void HandleJoin(const PlayerJoinMsg& p_msg); void HandleJoin(const PlayerJoinMsg& p_msg);
void HandleLeave(const PlayerLeaveMsg& p_msg); void HandleLeave(const PlayerLeaveMsg& p_msg);
void HandleState(const PlayerStateMsg& p_msg); void HandleState(const PlayerStateMsg& p_msg);

View File

@ -1,7 +1,6 @@
#pragma once #pragma once
#include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/protocol.h"
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h" #include "mxtypes.h"
#include <cstdint> #include <cstdint>
@ -23,8 +22,6 @@ class RemotePlayer {
void Despawn(); void Despawn();
void UpdateFromNetwork(const PlayerStateMsg& p_msg); void UpdateFromNetwork(const PlayerStateMsg& p_msg);
void Tick(float p_deltaTime); void Tick(float p_deltaTime);
// Re-add ROI to 3D scene after world transition
void ReAddToScene(); void ReAddToScene();
uint32_t GetPeerId() const { return m_peerId; } uint32_t GetPeerId() const { return m_peerId; }
@ -51,17 +48,14 @@ class RemotePlayer {
void EnterVehicle(int8_t p_vehicleType); void EnterVehicle(int8_t p_vehicleType);
void ExitVehicle(); void ExitVehicle();
// Identity
uint32_t m_peerId; uint32_t m_peerId;
uint8_t m_actorId; uint8_t m_actorId;
char m_uniqueName[32]; char m_uniqueName[32];
// Visual
LegoROI* m_roi; LegoROI* m_roi;
bool m_spawned; bool m_spawned;
bool m_visible; bool m_visible;
// Network state (latest received)
float m_targetPosition[3]; float m_targetPosition[3];
float m_targetDirection[3]; float m_targetDirection[3];
float m_targetUp[3]; float m_targetUp[3];
@ -71,12 +65,10 @@ class RemotePlayer {
uint32_t m_lastUpdateTime; uint32_t m_lastUpdateTime;
bool m_hasReceivedUpdate; bool m_hasReceivedUpdate;
// Interpolation state
float m_currentPosition[3]; float m_currentPosition[3];
float m_currentDirection[3]; float m_currentDirection[3];
float m_currentUp[3]; float m_currentUp[3];
// Walk animation state
LegoAnim* m_walkAnim; LegoAnim* m_walkAnim;
LegoROI** m_walkRoiMap; LegoROI** m_walkRoiMap;
MxU32 m_walkRoiMapSize; MxU32 m_walkRoiMapSize;
@ -84,19 +76,16 @@ class RemotePlayer {
float m_idleTime; float m_idleTime;
bool m_wasMoving; bool m_wasMoving;
// Idle animation state (CNs008xx - breathing/swaying)
LegoAnim* m_idleAnim; LegoAnim* m_idleAnim;
LegoROI** m_idleRoiMap; LegoROI** m_idleRoiMap;
MxU32 m_idleRoiMapSize; MxU32 m_idleRoiMapSize;
float m_idleAnimTime; float m_idleAnimTime;
// Ride animation state (small vehicles)
LegoAnim* m_rideAnim; LegoAnim* m_rideAnim;
LegoROI** m_rideRoiMap; LegoROI** m_rideRoiMap;
MxU32 m_rideRoiMapSize; MxU32 m_rideRoiMapSize;
LegoROI* m_rideVehicleROI; LegoROI* m_rideVehicleROI;
// Vehicle state
LegoROI* m_vehicleROI; LegoROI* m_vehicleROI;
int8_t m_currentVehicleType; int8_t m_currentVehicleType;
}; };

View File

@ -6,8 +6,6 @@
#include "extensions/multiplayer/websockettransport.h" #include "extensions/multiplayer/websockettransport.h"
#endif #endif
#include <SDL3/SDL_log.h>
using namespace Extensions; using namespace Extensions;
std::map<std::string, std::string> MultiplayerExt::options; std::map<std::string, std::string> MultiplayerExt::options;
@ -21,7 +19,6 @@ void MultiplayerExt::Initialize()
relayUrl = options["multiplayer:relay url"]; relayUrl = options["multiplayer:relay url"];
if (relayUrl.empty()) { if (relayUrl.empty()) {
SDL_Log("Multiplayer: no relay url configured, multiplayer will not connect");
return; return;
} }
@ -31,12 +28,7 @@ void MultiplayerExt::Initialize()
s_networkManager = new Multiplayer::NetworkManager(); s_networkManager = new Multiplayer::NetworkManager();
s_networkManager->Initialize(s_transport); s_networkManager->Initialize(s_transport);
// Auto-connect to default room for MVP
s_networkManager->Connect("default"); s_networkManager->Connect("default");
SDL_Log("Multiplayer: initialized with relay url %s", relayUrl.c_str());
#else
SDL_Log("Multiplayer: no transport available for this platform yet");
#endif #endif
} }

View File

@ -8,7 +8,6 @@
#include "mxticklemanager.h" #include "mxticklemanager.h"
#include "roi/legoroi.h" #include "roi/legoroi.h"
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h> #include <SDL3/SDL_timer.h>
#include <vector> #include <vector>
@ -45,16 +44,13 @@ MxResult NetworkManager::Tickle()
ProcessIncomingPackets(); ProcessIncomingPackets();
UpdateRemotePlayers(0.016f); UpdateRemotePlayers(0.016f);
// Timeout check - remove stale remote players.
// Re-read time because ProcessIncomingPackets updates player timestamps // Re-read time because ProcessIncomingPackets updates player timestamps
// via SDL_GetTicks(), which may be newer than the 'now' captured above. // via SDL_GetTicks(), which may be newer than the 'now' captured above.
// Using the stale 'now' would cause unsigned underflow (now < lastUpdate).
uint32_t timeoutNow = SDL_GetTicks(); uint32_t timeoutNow = SDL_GetTicks();
std::vector<uint32_t> timedOut; std::vector<uint32_t> timedOut;
for (auto& [peerId, player] : m_remotePlayers) { for (auto& [peerId, player] : m_remotePlayers) {
uint32_t lastUpdate = player->GetLastUpdateTime(); uint32_t lastUpdate = player->GetLastUpdateTime();
if (timeoutNow >= lastUpdate && (timeoutNow - lastUpdate) > TIMEOUT_MS) { if (timeoutNow >= lastUpdate && (timeoutNow - lastUpdate) > TIMEOUT_MS) {
SDL_Log("Multiplayer: peer %u timed out", peerId);
timedOut.push_back(peerId); timedOut.push_back(peerId);
} }
} }
@ -110,9 +106,6 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
return; return;
} }
SDL_Log("Multiplayer: OnWorldEnabled worldId=%d (e_act1=%d)", p_world->GetWorldId(), LegoOmni::e_act1);
// Register with tickle manager on first world enable (engine is now initialized)
if (!m_registered) { if (!m_registered) {
TickleManager()->RegisterClient(this, 10); TickleManager()->RegisterClient(this, 10);
m_registered = true; m_registered = true;
@ -121,19 +114,15 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
if (p_world->GetWorldId() == LegoOmni::e_act1) { if (p_world->GetWorldId() == LegoOmni::e_act1) {
m_inIsleWorld = true; m_inIsleWorld = true;
// Re-add all remote player ROIs to the 3D scene
for (auto& [peerId, player] : m_remotePlayers) { for (auto& [peerId, player] : m_remotePlayers) {
if (player->IsSpawned()) { if (player->IsSpawned()) {
player->ReAddToScene(); player->ReAddToScene();
// Only show if the remote player is also in ISLE
if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) { if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) {
player->SetVisible(true); player->SetVisible(true);
} }
} }
} }
SDL_Log("Multiplayer: ISLE world enabled, re-added %zu remote players", m_remotePlayers.size());
} }
} }
@ -182,7 +171,6 @@ void NetworkManager::BroadcastLocalState()
actorId = m_lastValidActorId; actorId = m_lastValidActorId;
} }
// Don't broadcast if we haven't seen a valid character yet
if (!IsValidActorId(actorId)) { if (!IsValidActorId(actorId)) {
return; return;
} }
@ -190,21 +178,6 @@ void NetworkManager::BroadcastLocalState()
int8_t worldId = (int8_t) currentWorld->GetWorldId(); int8_t worldId = (int8_t) currentWorld->GetWorldId();
int8_t vehicleType = DetectLocalVehicleType(); int8_t vehicleType = DetectLocalVehicleType();
// Log first broadcast per session for debugging
static bool firstBroadcast = true;
if (firstBroadcast) {
SDL_Log(
"Multiplayer: first broadcast actorId=%u worldId=%d vehicleType=%d pos=(%.1f,%.1f,%.1f)",
actorId,
worldId,
vehicleType,
pos[0],
pos[1],
pos[2]
);
firstBroadcast = false;
}
uint8_t buf[64]; uint8_t buf[64];
size_t len = SerializeStateMsg( size_t len = SerializeStateMsg(
buf, buf,
@ -240,7 +213,6 @@ void NetworkManager::ProcessIncomingPackets()
uint32_t assignedId; uint32_t assignedId;
SDL_memcpy(&assignedId, data + 1, sizeof(uint32_t)); SDL_memcpy(&assignedId, data + 1, sizeof(uint32_t));
m_localPeerId = assignedId; m_localPeerId = assignedId;
SDL_Log("Multiplayer: assigned peer ID %u", m_localPeerId);
} }
break; break;
} }
@ -266,7 +238,6 @@ void NetworkManager::ProcessIncomingPackets()
break; break;
} }
default: default:
SDL_Log("Multiplayer: unknown message type %u (len=%zu)", msgType, length);
break; break;
} }
}); });
@ -279,19 +250,10 @@ void NetworkManager::UpdateRemotePlayers(float p_deltaTime)
} }
} }
void NetworkManager::HandleJoin(const PlayerJoinMsg& p_msg) RemotePlayer* NetworkManager::CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId)
{ {
uint32_t peerId = p_msg.header.peerId; auto player = std::make_unique<RemotePlayer>(p_peerId, p_actorId);
if (m_remotePlayers.count(peerId)) {
return; // Already known
}
SDL_Log("Multiplayer: peer %u joined as actor %d (%s)", peerId, p_msg.actorId, p_msg.name);
auto player = std::make_unique<RemotePlayer>(peerId, p_msg.actorId);
// Spawn in current world if we're in ISLE
if (m_inIsleWorld) { if (m_inIsleWorld) {
LegoWorld* world = CurrentWorld(); LegoWorld* world = CurrentWorld();
if (world && world->GetWorldId() == LegoOmni::e_act1) { if (world && world->GetWorldId() == LegoOmni::e_act1) {
@ -299,7 +261,20 @@ void NetworkManager::HandleJoin(const PlayerJoinMsg& p_msg)
} }
} }
m_remotePlayers[peerId] = std::move(player); RemotePlayer* ptr = player.get();
m_remotePlayers[p_peerId] = std::move(player);
return ptr;
}
void NetworkManager::HandleJoin(const PlayerJoinMsg& p_msg)
{
uint32_t peerId = p_msg.header.peerId;
if (m_remotePlayers.count(peerId)) {
return;
}
CreateAndSpawnPlayer(peerId, p_msg.actorId);
} }
void NetworkManager::HandleLeave(const PlayerLeaveMsg& p_msg) void NetworkManager::HandleLeave(const PlayerLeaveMsg& p_msg)
@ -313,65 +288,27 @@ void NetworkManager::HandleState(const PlayerStateMsg& p_msg)
auto it = m_remotePlayers.find(peerId); auto it = m_remotePlayers.find(peerId);
if (it == m_remotePlayers.end()) { if (it == m_remotePlayers.end()) {
// Auto-create remote player on first STATE if we haven't seen a JOIN
if (!IsValidActorId(p_msg.actorId)) { if (!IsValidActorId(p_msg.actorId)) {
return; return;
} }
SDL_Log("Multiplayer: new remote peer %u (actor %u)", peerId, p_msg.actorId); CreateAndSpawnPlayer(peerId, p_msg.actorId);
auto player = std::make_unique<RemotePlayer>(peerId, p_msg.actorId);
if (m_inIsleWorld) {
LegoWorld* world = CurrentWorld();
if (world && world->GetWorldId() == LegoOmni::e_act1) {
player->Spawn(world);
}
}
m_remotePlayers[peerId] = std::move(player);
it = m_remotePlayers.find(peerId); it = m_remotePlayers.find(peerId);
} }
// Handle actor change (e.g., Pepper -> Nick): despawn and respawn with new actor // Handle actor change (e.g., Pepper -> Nick)
if (IsValidActorId(p_msg.actorId) && it->second->GetActorId() != p_msg.actorId) { if (IsValidActorId(p_msg.actorId) && it->second->GetActorId() != p_msg.actorId) {
SDL_Log("Multiplayer: peer %u changed actor %u -> %u", peerId, it->second->GetActorId(), p_msg.actorId);
it->second->Despawn(); it->second->Despawn();
auto player = std::make_unique<RemotePlayer>(peerId, p_msg.actorId); m_remotePlayers.erase(it);
CreateAndSpawnPlayer(peerId, p_msg.actorId);
if (m_inIsleWorld) {
LegoWorld* world = CurrentWorld();
if (world && world->GetWorldId() == LegoOmni::e_act1) {
player->Spawn(world);
}
}
m_remotePlayers[peerId] = std::move(player);
it = m_remotePlayers.find(peerId); it = m_remotePlayers.find(peerId);
} }
it->second->UpdateFromNetwork(p_msg); it->second->UpdateFromNetwork(p_msg);
// Handle visibility based on worldId
bool bothInIsle = m_inIsleWorld && (p_msg.worldId == (int8_t) LegoOmni::e_act1); bool bothInIsle = m_inIsleWorld && (p_msg.worldId == (int8_t) LegoOmni::e_act1);
if (it->second->IsSpawned()) { if (it->second->IsSpawned()) {
bool wasVisible = it->second->IsVisible();
it->second->SetVisible(bothInIsle); it->second->SetVisible(bothInIsle);
if (wasVisible != bothInIsle) {
SDL_Log(
"Multiplayer: peer %u visibility %d->%d (inIsle=%d, msgWorld=%d, e_act1=%d, spawned=%d)",
peerId,
wasVisible,
bothInIsle,
m_inIsleWorld,
p_msg.worldId,
(int8_t) LegoOmni::e_act1,
it->second->IsSpawned()
);
}
}
else {
SDL_Log("Multiplayer: peer %u not spawned, skipping visibility (inIsle=%d)", peerId, m_inIsleWorld);
} }
} }
@ -379,7 +316,6 @@ void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
{ {
auto it = m_remotePlayers.find(p_peerId); auto it = m_remotePlayers.find(p_peerId);
if (it != m_remotePlayers.end()) { if (it != m_remotePlayers.end()) {
SDL_Log("Multiplayer: peer %u removed", p_peerId);
it->second->Despawn(); it->second->Despawn();
m_remotePlayers.erase(it); m_remotePlayers.erase(it);
} }

View File

@ -9,9 +9,11 @@
#include "legoworld.h" #include "legoworld.h"
#include "misc.h" #include "misc.h"
#include "misc/legotree.h" #include "misc/legotree.h"
#include "mxgeometry/mxgeometry3d.h"
#include "realtime/realtime.h"
#include "roi/legoroi.h" #include "roi/legoroi.h"
#include <vec.h>
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h> #include <SDL3/SDL_timer.h>
#include <cmath> #include <cmath>
@ -19,43 +21,19 @@
using namespace Multiplayer; using namespace Multiplayer;
// Vehicle ROI LOD names, indexed by VehicleType enum // clang-format off
// Large vehicles: character hidden, show vehicle ROI only
// Small vehicles: character visible with ride animation
static const char* g_vehicleROINames[VEHICLE_COUNT] = { static const char* g_vehicleROINames[VEHICLE_COUNT] = {
"copter", // VEHICLE_HELICOPTER (large) "copter", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"
"jsuser", // VEHICLE_JETSKI (large)
"dunebugy", // VEHICLE_DUNEBUGGY (large)
"bike", // VEHICLE_BIKE (small)
"board", // VEHICLE_SKATEBOARD (small)
"moto", // VEHICLE_MOTOCYCLE (small)
"towtk", // VEHICLE_TOWTRACK (large)
"ambul" // VEHICLE_AMBULANCE (large)
}; };
// Ride animation presenter names for small vehicles (NULL for large)
static const char* g_rideAnimNames[VEHICLE_COUNT] = { static const char* g_rideAnimNames[VEHICLE_COUNT] = {
NULL, // VEHICLE_HELICOPTER NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL
NULL, // VEHICLE_JETSKI
NULL, // VEHICLE_DUNEBUGGY
"CNs001Bd", // VEHICLE_BIKE
"CNs001sk", // VEHICLE_SKATEBOARD
"CNs011Ni", // VEHICLE_MOTOCYCLE
NULL, // VEHICLE_TOWTRACK
NULL // VEHICLE_AMBULANCE
}; };
// Vehicle variant ROI names used in ride animations (NULL for large)
static const char* g_rideVehicleROINames[VEHICLE_COUNT] = { static const char* g_rideVehicleROINames[VEHICLE_COUNT] = {
NULL, // VEHICLE_HELICOPTER NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL
NULL, // VEHICLE_JETSKI
NULL, // VEHICLE_DUNEBUGGY
"bikebd", // VEHICLE_BIKE
"board", // VEHICLE_SKATEBOARD
"motoni", // VEHICLE_MOTOCYCLE
NULL, // VEHICLE_TOWTRACK
NULL // VEHICLE_AMBULANCE
}; };
// clang-format on
static bool IsLargeVehicle(int8_t p_vehicleType) static bool IsLargeVehicle(int8_t p_vehicleType)
{ {
@ -73,7 +51,7 @@ RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
{ {
SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId); SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId);
SDL_memset(m_targetPosition, 0, sizeof(m_targetPosition)); ZEROVEC3(m_targetPosition);
m_targetDirection[0] = 0.0f; m_targetDirection[0] = 0.0f;
m_targetDirection[1] = 0.0f; m_targetDirection[1] = 0.0f;
m_targetDirection[2] = 1.0f; m_targetDirection[2] = 1.0f;
@ -81,9 +59,9 @@ RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
m_targetUp[1] = 1.0f; m_targetUp[1] = 1.0f;
m_targetUp[2] = 0.0f; m_targetUp[2] = 0.0f;
SDL_memcpy(m_currentPosition, m_targetPosition, sizeof(m_targetPosition)); SET3(m_currentPosition, m_targetPosition);
SDL_memcpy(m_currentDirection, m_targetDirection, sizeof(m_targetDirection)); SET3(m_currentDirection, m_targetDirection);
SDL_memcpy(m_currentUp, m_targetUp, sizeof(m_targetUp)); SET3(m_currentUp, m_targetUp);
} }
RemotePlayer::~RemotePlayer() RemotePlayer::~RemotePlayer()
@ -104,31 +82,23 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
const char* actorName = LegoActor::GetActorName(m_actorId); const char* actorName = LegoActor::GetActorName(m_actorId);
if (!actorName) { if (!actorName) {
SDL_Log("Multiplayer: failed to get actor name for id %d", m_actorId);
return; return;
} }
// Create a full multi-part character clone with body parts
m_roi = charMgr->CreateCharacterClone(m_uniqueName, actorName); m_roi = charMgr->CreateCharacterClone(m_uniqueName, actorName);
if (!m_roi) { if (!m_roi) {
SDL_Log("Multiplayer: failed to create character clone for %s", m_uniqueName);
return; return;
} }
// Add ROI to the 3D scene and notify the 3D manager
VideoManager()->Get3DManager()->Add(*m_roi); VideoManager()->Get3DManager()->Add(*m_roi);
VideoManager()->Get3DManager()->Moved(*m_roi); VideoManager()->Get3DManager()->Moved(*m_roi);
// Start hidden until we get a STATE update confirming worldId
m_roi->SetVisibility(FALSE); m_roi->SetVisibility(FALSE);
m_spawned = true; m_spawned = true;
m_visible = false; m_visible = false;
// Build walk animation ROI map
BuildWalkROIMap(p_isleWorld); BuildWalkROIMap(p_isleWorld);
// Build idle animation ROI map (CNs008xx - breathing/swaying)
MxCore* idlePresenter = p_isleWorld->Find("LegoAnimPresenter", "CNs008xx"); MxCore* idlePresenter = p_isleWorld->Find("LegoAnimPresenter", "CNs008xx");
if (idlePresenter) { if (idlePresenter) {
m_idleAnim = static_cast<LegoAnimPresenter*>(idlePresenter)->GetAnimation(); m_idleAnim = static_cast<LegoAnimPresenter*>(idlePresenter)->GetAnimation();
@ -136,14 +106,6 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
BuildROIMap(m_idleAnim, m_roi, nullptr, m_idleRoiMap, m_idleRoiMapSize); BuildROIMap(m_idleAnim, m_roi, nullptr, m_idleRoiMap, m_idleRoiMapSize);
} }
} }
SDL_Log(
"Multiplayer: spawned remote player %s (roi=%p, walkRoiMap=%u, idleRoiMap=%u)",
m_uniqueName,
(void*) m_roi,
m_walkRoiMapSize,
m_idleRoiMapSize
);
} }
void RemotePlayer::Despawn() void RemotePlayer::Despawn()
@ -152,7 +114,6 @@ void RemotePlayer::Despawn()
return; return;
} }
// Clean up vehicle state first
ExitVehicle(); ExitVehicle();
if (m_roi) { if (m_roi) {
@ -176,31 +137,24 @@ void RemotePlayer::Despawn()
m_idleAnim = nullptr; m_idleAnim = nullptr;
m_spawned = false; m_spawned = false;
m_visible = false; m_visible = false;
SDL_Log("Multiplayer: despawned remote player %s", m_uniqueName);
} }
void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
{ {
// Compute speed from position delta (GetWorldSpeed clamps backward movement to 0) float posDelta = SDL_sqrtf(DISTSQRD3(p_msg.position, m_targetPosition));
float dx = p_msg.position[0] - m_targetPosition[0];
float dy = p_msg.position[1] - m_targetPosition[1];
float dz = p_msg.position[2] - m_targetPosition[2];
float posDelta = SDL_sqrtf(dx * dx + dy * dy + dz * dz);
SDL_memcpy(m_targetPosition, p_msg.position, sizeof(float) * 3); SET3(m_targetPosition, p_msg.position);
SDL_memcpy(m_targetDirection, p_msg.direction, sizeof(float) * 3); SET3(m_targetDirection, p_msg.direction);
SDL_memcpy(m_targetUp, p_msg.up, sizeof(float) * 3); SET3(m_targetUp, p_msg.up);
m_targetSpeed = posDelta > 0.01f ? posDelta : 0.0f; m_targetSpeed = posDelta > 0.01f ? posDelta : 0.0f;
m_targetVehicleType = p_msg.vehicleType; m_targetVehicleType = p_msg.vehicleType;
m_targetWorldId = p_msg.worldId; m_targetWorldId = p_msg.worldId;
m_lastUpdateTime = SDL_GetTicks(); m_lastUpdateTime = SDL_GetTicks();
if (!m_hasReceivedUpdate) { if (!m_hasReceivedUpdate) {
// Snap to position on first update (don't interpolate from origin) SET3(m_currentPosition, m_targetPosition);
SDL_memcpy(m_currentPosition, m_targetPosition, sizeof(float) * 3); SET3(m_currentDirection, m_targetDirection);
SDL_memcpy(m_currentDirection, m_targetDirection, sizeof(float) * 3); SET3(m_currentUp, m_targetUp);
SDL_memcpy(m_currentUp, m_targetUp, sizeof(float) * 3);
m_hasReceivedUpdate = true; m_hasReceivedUpdate = true;
} }
} }
@ -211,20 +165,6 @@ void RemotePlayer::Tick(float p_deltaTime)
return; return;
} }
// Log first tick to confirm the player is being updated
static uint32_t lastLoggedPeer = 0;
if (lastLoggedPeer != m_peerId) {
SDL_Log(
"Multiplayer: first tick for %s pos=(%.1f,%.1f,%.1f) hasUpdate=%d",
m_uniqueName,
m_currentPosition[0],
m_currentPosition[1],
m_currentPosition[2],
m_hasReceivedUpdate
);
lastLoggedPeer = m_peerId;
}
UpdateVehicleState(); UpdateVehicleState();
UpdateTransform(p_deltaTime); UpdateTransform(p_deltaTime);
UpdateAnimation(p_deltaTime); UpdateAnimation(p_deltaTime);
@ -252,7 +192,6 @@ void RemotePlayer::SetVisible(bool p_visible)
m_visible = p_visible; m_visible = p_visible;
if (p_visible) { if (p_visible) {
// Visibility depends on vehicle state
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
m_roi->SetVisibility(FALSE); m_roi->SetVisibility(FALSE);
if (m_vehicleROI) { if (m_vehicleROI) {
@ -265,7 +204,6 @@ void RemotePlayer::SetVisible(bool p_visible)
m_vehicleROI->SetVisibility(FALSE); m_vehicleROI->SetVisibility(FALSE);
} }
} }
// Ride vehicle ROI visibility is managed by the animation (ApplyAnimationTransformation)
} }
else { else {
m_roi->SetVisibility(FALSE); m_roi->SetVisibility(FALSE);
@ -284,72 +222,45 @@ void RemotePlayer::BuildWalkROIMap(LegoWorld* p_isleWorld)
return; return;
} }
// Find the generic slow walk animation presenter "CNs001xx"
MxCore* presenter = p_isleWorld->Find("LegoAnimPresenter", "CNs001xx"); MxCore* presenter = p_isleWorld->Find("LegoAnimPresenter", "CNs001xx");
if (!presenter) { if (!presenter) {
SDL_Log("Multiplayer: walk animation presenter CNs001xx not found");
return; return;
} }
LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter); LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter);
m_walkAnim = animPresenter->GetAnimation(); m_walkAnim = animPresenter->GetAnimation();
if (!m_walkAnim) { if (!m_walkAnim) {
SDL_Log("Multiplayer: walk animation data is null");
return; return;
} }
BuildROIMap(m_walkAnim, m_roi, nullptr, m_walkRoiMap, m_walkRoiMapSize); BuildROIMap(m_walkAnim, m_roi, nullptr, m_walkRoiMap, m_walkRoiMapSize);
} }
// Traverse the animation tree, assign ROI indices, and collect matched ROIs. // Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
// This mirrors the game's UpdateStructMapAndROIIndex approach: ROI indices // via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes.
// are assigned at runtime via SetROIIndex() and are NOT pre-stored in animation
// data (m_roiIndex starts at 0 for all nodes).
static void AssignROIIndices( static void AssignROIIndices(
LegoTreeNode* p_node, LegoTreeNode* p_node,
LegoROI* p_parentROI, LegoROI* p_parentROI,
LegoROI* p_rootROI, LegoROI* p_rootROI,
LegoROI* p_extraROI, LegoROI* p_extraROI,
MxU32& p_nextIndex, MxU32& p_nextIndex,
std::vector<LegoROI*>& p_entries, std::vector<LegoROI*>& p_entries
int p_depth = 0
) )
{ {
LegoROI* roi = p_parentROI; LegoROI* roi = p_parentROI;
LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData(); LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData();
const char* name = data ? data->GetName() : nullptr; const char* name = data ? data->GetName() : nullptr;
SDL_Log(
"Multiplayer: [ROIMap] depth=%d name='%s' parentROI=%p rootROI=%p children=%d",
p_depth,
name ? name : "(null)",
(void*) p_parentROI,
(void*) p_rootROI,
p_node->GetNumChildren()
);
if (name != nullptr && *name != '-') { if (name != nullptr && *name != '-') {
LegoROI* matchedROI = nullptr; LegoROI* matchedROI = nullptr;
if (*name == '*' || p_parentROI == nullptr) { if (*name == '*' || p_parentROI == nullptr) {
// Root-level node: either "*pepper" style or "actor_01" style variable reference.
// Game resolves via GetVariableOrIdentity + FindROI; we map directly to our clone.
roi = p_rootROI; roi = p_rootROI;
matchedROI = p_rootROI; matchedROI = p_rootROI;
SDL_Log("Multiplayer: [ROIMap] matched root node '%s' to rootROI", name);
} }
else { else {
// Body part → search in parent's ROI hierarchy
matchedROI = p_parentROI->FindChildROI(name, p_parentROI); matchedROI = p_parentROI->FindChildROI(name, p_parentROI);
SDL_Log(
"Multiplayer: [ROIMap] FindChildROI('%s', parentROI=%p) = %p",
name,
(void*) p_parentROI,
(void*) matchedROI
);
if (matchedROI == nullptr && p_extraROI != nullptr) { if (matchedROI == nullptr && p_extraROI != nullptr) {
// Try extra ROI hierarchy (vehicle variant for ride animations)
matchedROI = p_extraROI->FindChildROI(name, p_extraROI); matchedROI = p_extraROI->FindChildROI(name, p_extraROI);
} }
} }
@ -365,7 +276,7 @@ static void AssignROIIndices(
} }
for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) { for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) {
AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries, p_depth + 1); AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries);
} }
} }
@ -386,7 +297,6 @@ void RemotePlayer::BuildROIMap(
return; return;
} }
// Traverse tree, assigning ROI indices and collecting matched ROIs
MxU32 nextIndex = 1; MxU32 nextIndex = 1;
std::vector<LegoROI*> entries; std::vector<LegoROI*> entries;
AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries); AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries);
@ -395,7 +305,7 @@ void RemotePlayer::BuildROIMap(
return; return;
} }
// Build the ROI map array (1-indexed; index 0 reserved as NULL) // 1-indexed; index 0 reserved as NULL
p_roiMapSize = entries.size() + 1; p_roiMapSize = entries.size() + 1;
p_roiMap = new LegoROI*[p_roiMapSize]; p_roiMap = new LegoROI*[p_roiMapSize];
p_roiMap[0] = nullptr; p_roiMap[0] = nullptr;
@ -406,82 +316,21 @@ void RemotePlayer::BuildROIMap(
void RemotePlayer::UpdateTransform(float p_deltaTime) void RemotePlayer::UpdateTransform(float p_deltaTime)
{ {
// Interpolate position toward target LERP3(m_currentPosition, m_currentPosition, m_targetPosition, 0.2f);
float lerpFactor = 0.2f; LERP3(m_currentDirection, m_currentDirection, m_targetDirection, 0.2f);
LERP3(m_currentUp, m_currentUp, m_targetUp, 0.2f);
for (int i = 0; i < 3; i++) { // Character clones need negated direction
m_currentPosition[i] += (m_targetPosition[i] - m_currentPosition[i]) * lerpFactor; Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]);
m_currentDirection[i] += (m_targetDirection[i] - m_currentDirection[i]) * lerpFactor; Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]);
m_currentUp[i] += (m_targetUp[i] - m_currentUp[i]) * lerpFactor; Mx3DPointFloat up(m_currentUp[0], m_currentUp[1], m_currentUp[2]);
}
// Build transform using CalcLocalTransform convention from realtime.cpp:
// z = normalize(dir), y = normalize(up), x = y×z, y = z×x
// Non-player character clones need negated direction (see legopathactor.cpp:152)
float z[3], y[3], x[3];
z[0] = -m_currentDirection[0];
z[1] = -m_currentDirection[1];
z[2] = -m_currentDirection[2];
float zLen = SDL_sqrtf(z[0] * z[0] + z[1] * z[1] + z[2] * z[2]);
if (zLen > 0.001f) {
z[0] /= zLen;
z[1] /= zLen;
z[2] /= zLen;
}
float yLen = SDL_sqrtf(
m_currentUp[0] * m_currentUp[0] + m_currentUp[1] * m_currentUp[1] + m_currentUp[2] * m_currentUp[2]
);
y[0] = yLen > 0.001f ? m_currentUp[0] / yLen : 0.0f;
y[1] = yLen > 0.001f ? m_currentUp[1] / yLen : 1.0f;
y[2] = yLen > 0.001f ? m_currentUp[2] / yLen : 0.0f;
// x = y × z
x[0] = y[1] * z[2] - y[2] * z[1];
x[1] = y[2] * z[0] - y[0] * z[2];
x[2] = y[0] * z[1] - y[1] * z[0];
float xLen = SDL_sqrtf(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
if (xLen > 0.001f) {
x[0] /= xLen;
x[1] /= xLen;
x[2] /= xLen;
}
// y = z × x (re-orthogonalize)
y[0] = z[1] * x[2] - z[2] * x[1];
y[1] = z[2] * x[0] - z[0] * x[2];
y[2] = z[0] * x[1] - z[1] * x[0];
yLen = SDL_sqrtf(y[0] * y[0] + y[1] * y[1] + y[2] * y[2]);
if (yLen > 0.001f) {
y[0] /= yLen;
y[1] /= yLen;
y[2] /= yLen;
}
// Build 4x4 transform matrix [right, up, direction, position] as rows
MxMatrix mat; MxMatrix mat;
mat[0][0] = x[0]; CalcLocalTransform(pos, dir, up, mat);
mat[0][1] = x[1];
mat[0][2] = x[2];
mat[0][3] = 0.0f;
mat[1][0] = y[0];
mat[1][1] = y[1];
mat[1][2] = y[2];
mat[1][3] = 0.0f;
mat[2][0] = z[0];
mat[2][1] = z[1];
mat[2][2] = z[2];
mat[2][3] = 0.0f;
mat[3][0] = m_currentPosition[0];
mat[3][1] = m_currentPosition[1];
mat[3][2] = m_currentPosition[2];
mat[3][3] = 1.0f;
m_roi->WrappedSetLocal2WorldWithWorldDataUpdate(mat); m_roi->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
VideoManager()->Get3DManager()->Moved(*m_roi); VideoManager()->Get3DManager()->Moved(*m_roi);
// Also update vehicle ROI transform if in large vehicle
if (m_vehicleROI && m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { if (m_vehicleROI && m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
VideoManager()->Get3DManager()->Moved(*m_vehicleROI); VideoManager()->Get3DManager()->Moved(*m_vehicleROI);
@ -490,23 +339,19 @@ void RemotePlayer::UpdateTransform(float p_deltaTime)
void RemotePlayer::UpdateAnimation(float p_deltaTime) void RemotePlayer::UpdateAnimation(float p_deltaTime)
{ {
// Determine which animation and ROI map to use
LegoAnim* anim = nullptr; LegoAnim* anim = nullptr;
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
// Large vehicle: no animation, character is hidden
return; return;
} }
LegoROI** roiMap = nullptr; LegoROI** roiMap = nullptr;
if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) { if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) {
// Small vehicle: use ride animation
anim = m_rideAnim; anim = m_rideAnim;
roiMap = m_rideRoiMap; roiMap = m_rideRoiMap;
} }
else if (m_walkAnim && m_walkRoiMap) { else if (m_walkAnim && m_walkRoiMap) {
// On foot: use walk animation
anim = m_walkAnim; anim = m_walkAnim;
roiMap = m_walkRoiMap; roiMap = m_walkRoiMap;
} }
@ -514,16 +359,13 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
return; return;
} }
// Ensure all body parts are visible before animation (matches game's AnimateWithTransform)
MxU32 roiMapSize = (roiMap == m_walkRoiMap) ? m_walkRoiMapSize : m_rideRoiMapSize; MxU32 roiMapSize = (roiMap == m_walkRoiMap) ? m_walkRoiMapSize : m_rideRoiMapSize;
MxU32 idleMapSize = m_idleRoiMapSize;
for (MxU32 i = 1; i < roiMapSize; i++) { for (MxU32 i = 1; i < roiMapSize; i++) {
if (roiMap[i] != nullptr) { if (roiMap[i] != nullptr) {
roiMap[i]->SetVisibility(TRUE); roiMap[i]->SetVisibility(TRUE);
} }
} }
// Also ensure idle ROI map parts are visible (may include different body parts) for (MxU32 i = 1; i < m_idleRoiMapSize; i++) {
for (MxU32 i = 1; i < idleMapSize; i++) {
if (m_idleRoiMap[i] != nullptr) { if (m_idleRoiMap[i] != nullptr) {
m_idleRoiMap[i]->SetVisibility(TRUE); m_idleRoiMap[i]->SetVisibility(TRUE);
} }
@ -532,9 +374,6 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE); bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
if (inVehicle || m_targetSpeed > 0.01f) { if (inVehicle || m_targetSpeed > 0.01f) {
// Moving or in vehicle: advance animation time in LegoTime units (ms-scale)
// Game uses: m_actorTime += deltaTime_ms * worldSpeed (see legopathactor.cpp:359)
// When on a vehicle but standing still, freeze at frame 0
if (m_targetSpeed > 0.01f) { if (m_targetSpeed > 0.01f) {
m_animTime += p_deltaTime * 2000.0f; m_animTime += p_deltaTime * 2000.0f;
} }
@ -553,7 +392,6 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
m_idleAnimTime = 0.0f; m_idleAnimTime = 0.0f;
} }
else if (m_idleAnim && m_idleRoiMap) { else if (m_idleAnim && m_idleRoiMap) {
// Standing still on foot: use the dedicated idle animation (CNs008xx)
if (m_wasMoving) { if (m_wasMoving) {
m_wasMoving = false; m_wasMoving = false;
m_idleTime = 0.0f; m_idleTime = 0.0f;
@ -562,8 +400,7 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
m_idleTime += p_deltaTime; m_idleTime += p_deltaTime;
// Play idle animation: frame 0 for first 2.5s (standing pose), // Hold standing pose for 2.5s, then loop breathing/swaying
// then continuously loop (breathing/swaying effect)
if (m_idleTime >= 2.5f) { if (m_idleTime >= 2.5f) {
m_idleAnimTime += p_deltaTime * 1000.0f; m_idleAnimTime += p_deltaTime * 1000.0f;
} }
@ -584,15 +421,10 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
void RemotePlayer::UpdateVehicleState() void RemotePlayer::UpdateVehicleState()
{ {
if (m_targetVehicleType != m_currentVehicleType) { if (m_targetVehicleType != m_currentVehicleType) {
if (m_targetVehicleType == VEHICLE_NONE) { if (m_currentVehicleType != VEHICLE_NONE) {
// Exiting vehicle
ExitVehicle(); ExitVehicle();
} }
else { if (m_targetVehicleType != VEHICLE_NONE) {
// Entering vehicle (exit old one first if needed)
if (m_currentVehicleType != VEHICLE_NONE) {
ExitVehicle();
}
EnterVehicle(m_targetVehicleType); EnterVehicle(m_targetVehicleType);
} }
} }
@ -608,27 +440,19 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
m_animTime = 0.0f; m_animTime = 0.0f;
if (IsLargeVehicle(p_vehicleType)) { if (IsLargeVehicle(p_vehicleType)) {
// Large vehicle: hide character, show vehicle ROI
m_roi->SetVisibility(FALSE); m_roi->SetVisibility(FALSE);
// Create vehicle ROI clone
char vehicleName[48]; char vehicleName[48];
SDL_snprintf(vehicleName, sizeof(vehicleName), "%s_mp_%u", g_vehicleROINames[p_vehicleType], m_peerId); SDL_snprintf(vehicleName, sizeof(vehicleName), "%s_mp_%u", g_vehicleROINames[p_vehicleType], m_peerId);
m_vehicleROI = CharacterManager()->CreateAutoROI(vehicleName, g_vehicleROINames[p_vehicleType], FALSE); m_vehicleROI = CharacterManager()->CreateAutoROI(vehicleName, g_vehicleROINames[p_vehicleType], FALSE);
if (m_vehicleROI) { if (m_vehicleROI) {
// CreateAutoROI already adds to 3D scene via Get3DManager()->Add()
// Position at current transform
MxMatrix mat(m_roi->GetLocal2World()); MxMatrix mat(m_roi->GetLocal2World());
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
m_vehicleROI->SetVisibility(m_visible ? TRUE : FALSE); m_vehicleROI->SetVisibility(m_visible ? TRUE : FALSE);
} }
else {
SDL_Log("Multiplayer: failed to create vehicle ROI for type %d", p_vehicleType);
}
} }
else { else {
// Small vehicle: find ride animation and build ride ROI map
const char* rideAnimName = g_rideAnimNames[p_vehicleType]; const char* rideAnimName = g_rideAnimNames[p_vehicleType];
const char* vehicleVariantName = g_rideVehicleROINames[p_vehicleType]; const char* vehicleVariantName = g_rideVehicleROINames[p_vehicleType];
@ -636,7 +460,6 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
return; return;
} }
// Find the ride animation presenter
LegoWorld* world = CurrentWorld(); LegoWorld* world = CurrentWorld();
if (!world) { if (!world) {
return; return;
@ -644,31 +467,25 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
MxCore* presenter = world->Find("LegoAnimPresenter", rideAnimName); MxCore* presenter = world->Find("LegoAnimPresenter", rideAnimName);
if (!presenter) { if (!presenter) {
SDL_Log("Multiplayer: ride animation presenter %s not found", rideAnimName);
return; return;
} }
LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter); LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter);
m_rideAnim = animPresenter->GetAnimation(); m_rideAnim = animPresenter->GetAnimation();
if (!m_rideAnim) { if (!m_rideAnim) {
SDL_Log("Multiplayer: ride animation data is null for %s", rideAnimName);
return; return;
} }
// Create vehicle variant ROI for the ride animation
char variantName[48]; char variantName[48];
SDL_snprintf(variantName, sizeof(variantName), "%s_mp_%u", vehicleVariantName, m_peerId); SDL_snprintf(variantName, sizeof(variantName), "%s_mp_%u", vehicleVariantName, m_peerId);
m_rideVehicleROI = CharacterManager()->CreateAutoROI(variantName, vehicleVariantName, FALSE); m_rideVehicleROI = CharacterManager()->CreateAutoROI(variantName, vehicleVariantName, FALSE);
// CreateAutoROI already adds to 3D scene via Get3DManager()->Add()
// Rename to base name so FindChildROI in AssignROIIndices can match animation tree nodes. // Rename to base name so FindChildROI can match animation tree nodes.
// CreateAutoROI sets name to unique variantName (e.g. "board_mp_2") but animation nodes // ReleaseAutoROI uses pointer comparison, not name.
// expect the base name (e.g. "board"). ReleaseAutoROI uses pointer comparison, not name.
if (m_rideVehicleROI) { if (m_rideVehicleROI) {
m_rideVehicleROI->SetName(vehicleVariantName); m_rideVehicleROI->SetName(vehicleVariantName);
} }
// Build the ride ROI map with both character body parts and vehicle variant
BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize); BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
} }
} }
@ -679,14 +496,12 @@ void RemotePlayer::ExitVehicle()
return; return;
} }
// Clean up large vehicle ROI
if (m_vehicleROI) { if (m_vehicleROI) {
VideoManager()->Get3DManager()->Remove(*m_vehicleROI); VideoManager()->Get3DManager()->Remove(*m_vehicleROI);
CharacterManager()->ReleaseAutoROI(m_vehicleROI); CharacterManager()->ReleaseAutoROI(m_vehicleROI);
m_vehicleROI = nullptr; m_vehicleROI = nullptr;
} }
// Clean up ride animation state
if (m_rideRoiMap) { if (m_rideRoiMap) {
delete[] m_rideRoiMap; delete[] m_rideRoiMap;
m_rideRoiMap = nullptr; m_rideRoiMap = nullptr;
@ -699,7 +514,6 @@ void RemotePlayer::ExitVehicle()
} }
m_rideAnim = nullptr; m_rideAnim = nullptr;
// Show character again
if (m_visible) { if (m_visible) {
m_roi->SetVisibility(TRUE); m_roi->SetVisibility(TRUE);
} }

View File

@ -2,7 +2,6 @@
#include "extensions/multiplayer/websockettransport.h" #include "extensions/multiplayer/websockettransport.h"
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <emscripten.h> #include <emscripten.h>
@ -36,8 +35,6 @@ void WebSocketTransport::Connect(const char* p_roomId)
std::string url = m_relayBaseUrl + "/room/" + p_roomId; std::string url = m_relayBaseUrl + "/room/" + p_roomId;
// Pass the address of m_connectedFlag so JS callbacks can update it
// directly via shared WASM heap memory, avoiding proxy calls for IsConnected().
// clang-format off // clang-format off
m_socketId = MAIN_THREAD_EM_ASM_INT({ m_socketId = MAIN_THREAD_EM_ASM_INT({
var url = UTF8ToString($0); var url = UTF8ToString($0);
@ -78,11 +75,8 @@ void WebSocketTransport::Connect(const char* p_roomId)
}, url.c_str(), &m_connectedFlag); }, url.c_str(), &m_connectedFlag);
// clang-format on // clang-format on
if (m_socketId > 0) { if (m_socketId <= 0) {
SDL_Log("Multiplayer: connecting to %s", url.c_str()); m_socketId = -1;
}
else {
SDL_Log("Multiplayer: failed to create WebSocket connection to %s", url.c_str());
} }
} }
@ -100,7 +94,6 @@ void WebSocketTransport::Disconnect()
}, m_socketId); }, m_socketId);
// clang-format on // clang-format on
SDL_Log("Multiplayer: disconnected");
m_socketId = -1; m_socketId = -1;
m_connectedFlag = 0; m_connectedFlag = 0;
} }
@ -108,8 +101,6 @@ void WebSocketTransport::Disconnect()
bool WebSocketTransport::IsConnected() const bool WebSocketTransport::IsConnected() const
{ {
// Read the shared flag directly from WASM heap memory.
// No proxy call needed - the JS callbacks update this via Atomics.store.
return m_socketId > 0 && m_connectedFlag != 0; return m_socketId > 0 && m_connectedFlag != 0;
} }