diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index b17b5a96..d4dc2c15 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -37,7 +37,6 @@ class NetworkManager : public MxCore { void Disconnect(); bool IsConnected() const; - // Called by the Multiplayer extension on world transitions void OnWorldEnabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world); @@ -46,6 +45,8 @@ class NetworkManager : public MxCore { void ProcessIncomingPackets(); void UpdateRemotePlayers(float p_deltaTime); + RemotePlayer* CreateAndSpawnPlayer(uint32_t p_peerId, uint8_t p_actorId); + void HandleJoin(const PlayerJoinMsg& p_msg); void HandleLeave(const PlayerLeaveMsg& p_msg); void HandleState(const PlayerStateMsg& p_msg); diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index 9f0ad69f..c93412bc 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -1,7 +1,6 @@ #pragma once #include "extensions/multiplayer/protocol.h" -#include "mxgeometry/mxmatrix.h" #include "mxtypes.h" #include @@ -23,8 +22,6 @@ class RemotePlayer { void Despawn(); void UpdateFromNetwork(const PlayerStateMsg& p_msg); void Tick(float p_deltaTime); - - // Re-add ROI to 3D scene after world transition void ReAddToScene(); uint32_t GetPeerId() const { return m_peerId; } @@ -51,17 +48,14 @@ class RemotePlayer { void EnterVehicle(int8_t p_vehicleType); void ExitVehicle(); - // Identity uint32_t m_peerId; uint8_t m_actorId; char m_uniqueName[32]; - // Visual LegoROI* m_roi; bool m_spawned; bool m_visible; - // Network state (latest received) float m_targetPosition[3]; float m_targetDirection[3]; float m_targetUp[3]; @@ -71,12 +65,10 @@ class RemotePlayer { uint32_t m_lastUpdateTime; bool m_hasReceivedUpdate; - // Interpolation state float m_currentPosition[3]; float m_currentDirection[3]; float m_currentUp[3]; - // Walk animation state LegoAnim* m_walkAnim; LegoROI** m_walkRoiMap; MxU32 m_walkRoiMapSize; @@ -84,19 +76,16 @@ class RemotePlayer { float m_idleTime; bool m_wasMoving; - // Idle animation state (CNs008xx - breathing/swaying) LegoAnim* m_idleAnim; LegoROI** m_idleRoiMap; MxU32 m_idleRoiMapSize; float m_idleAnimTime; - // Ride animation state (small vehicles) LegoAnim* m_rideAnim; LegoROI** m_rideRoiMap; MxU32 m_rideRoiMapSize; LegoROI* m_rideVehicleROI; - // Vehicle state LegoROI* m_vehicleROI; int8_t m_currentVehicleType; }; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index da501d82..14a4f9ab 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -6,8 +6,6 @@ #include "extensions/multiplayer/websockettransport.h" #endif -#include - using namespace Extensions; std::map MultiplayerExt::options; @@ -21,7 +19,6 @@ void MultiplayerExt::Initialize() relayUrl = options["multiplayer:relay url"]; if (relayUrl.empty()) { - SDL_Log("Multiplayer: no relay url configured, multiplayer will not connect"); return; } @@ -31,12 +28,7 @@ void MultiplayerExt::Initialize() s_networkManager = new Multiplayer::NetworkManager(); s_networkManager->Initialize(s_transport); - // Auto-connect to default room for MVP 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 } diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index db648395..18b6d6e3 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -8,7 +8,6 @@ #include "mxticklemanager.h" #include "roi/legoroi.h" -#include #include #include #include @@ -45,16 +44,13 @@ MxResult NetworkManager::Tickle() ProcessIncomingPackets(); UpdateRemotePlayers(0.016f); - // Timeout check - remove stale remote players. // Re-read time because ProcessIncomingPackets updates player timestamps // 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(); std::vector timedOut; for (auto& [peerId, player] : m_remotePlayers) { uint32_t lastUpdate = player->GetLastUpdateTime(); if (timeoutNow >= lastUpdate && (timeoutNow - lastUpdate) > TIMEOUT_MS) { - SDL_Log("Multiplayer: peer %u timed out", peerId); timedOut.push_back(peerId); } } @@ -110,9 +106,6 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world) 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) { TickleManager()->RegisterClient(this, 10); m_registered = true; @@ -121,19 +114,15 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world) if (p_world->GetWorldId() == LegoOmni::e_act1) { m_inIsleWorld = true; - // Re-add all remote player ROIs to the 3D scene for (auto& [peerId, player] : m_remotePlayers) { if (player->IsSpawned()) { player->ReAddToScene(); - // Only show if the remote player is also in ISLE if (player->GetWorldId() == (int8_t) LegoOmni::e_act1) { 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; } - // Don't broadcast if we haven't seen a valid character yet if (!IsValidActorId(actorId)) { return; } @@ -190,21 +178,6 @@ void NetworkManager::BroadcastLocalState() int8_t worldId = (int8_t) currentWorld->GetWorldId(); 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]; size_t len = SerializeStateMsg( buf, @@ -240,7 +213,6 @@ void NetworkManager::ProcessIncomingPackets() uint32_t assignedId; SDL_memcpy(&assignedId, data + 1, sizeof(uint32_t)); m_localPeerId = assignedId; - SDL_Log("Multiplayer: assigned peer ID %u", m_localPeerId); } break; } @@ -266,7 +238,6 @@ void NetworkManager::ProcessIncomingPackets() break; } default: - SDL_Log("Multiplayer: unknown message type %u (len=%zu)", msgType, length); 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(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(peerId, p_msg.actorId); - - // Spawn in current world if we're in ISLE if (m_inIsleWorld) { LegoWorld* world = CurrentWorld(); 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) @@ -313,65 +288,27 @@ void NetworkManager::HandleState(const PlayerStateMsg& p_msg) auto it = m_remotePlayers.find(peerId); if (it == m_remotePlayers.end()) { - // Auto-create remote player on first STATE if we haven't seen a JOIN if (!IsValidActorId(p_msg.actorId)) { return; } - SDL_Log("Multiplayer: new remote peer %u (actor %u)", peerId, p_msg.actorId); - auto player = std::make_unique(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); + CreateAndSpawnPlayer(peerId, p_msg.actorId); 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) { - SDL_Log("Multiplayer: peer %u changed actor %u -> %u", peerId, it->second->GetActorId(), p_msg.actorId); it->second->Despawn(); - auto player = std::make_unique(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); + m_remotePlayers.erase(it); + CreateAndSpawnPlayer(peerId, p_msg.actorId); it = m_remotePlayers.find(peerId); } it->second->UpdateFromNetwork(p_msg); - // Handle visibility based on worldId bool bothInIsle = m_inIsleWorld && (p_msg.worldId == (int8_t) LegoOmni::e_act1); - if (it->second->IsSpawned()) { - bool wasVisible = it->second->IsVisible(); 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); if (it != m_remotePlayers.end()) { - SDL_Log("Multiplayer: peer %u removed", p_peerId); it->second->Despawn(); m_remotePlayers.erase(it); } diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 76cd7b7c..da661d1a 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -9,9 +9,11 @@ #include "legoworld.h" #include "misc.h" #include "misc/legotree.h" +#include "mxgeometry/mxgeometry3d.h" +#include "realtime/realtime.h" #include "roi/legoroi.h" +#include -#include #include #include #include @@ -19,43 +21,19 @@ using namespace Multiplayer; -// Vehicle ROI LOD names, indexed by VehicleType enum -// Large vehicles: character hidden, show vehicle ROI only -// Small vehicles: character visible with ride animation +// clang-format off static const char* g_vehicleROINames[VEHICLE_COUNT] = { - "copter", // VEHICLE_HELICOPTER (large) - "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) + "copter", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul" }; -// Ride animation presenter names for small vehicles (NULL for large) static const char* g_rideAnimNames[VEHICLE_COUNT] = { - NULL, // VEHICLE_HELICOPTER - NULL, // VEHICLE_JETSKI - NULL, // VEHICLE_DUNEBUGGY - "CNs001Bd", // VEHICLE_BIKE - "CNs001sk", // VEHICLE_SKATEBOARD - "CNs011Ni", // VEHICLE_MOTOCYCLE - NULL, // VEHICLE_TOWTRACK - NULL // VEHICLE_AMBULANCE + NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL }; -// Vehicle variant ROI names used in ride animations (NULL for large) static const char* g_rideVehicleROINames[VEHICLE_COUNT] = { - NULL, // VEHICLE_HELICOPTER - NULL, // VEHICLE_JETSKI - NULL, // VEHICLE_DUNEBUGGY - "bikebd", // VEHICLE_BIKE - "board", // VEHICLE_SKATEBOARD - "motoni", // VEHICLE_MOTOCYCLE - NULL, // VEHICLE_TOWTRACK - NULL // VEHICLE_AMBULANCE + NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL }; +// clang-format on 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_memset(m_targetPosition, 0, sizeof(m_targetPosition)); + ZEROVEC3(m_targetPosition); m_targetDirection[0] = 0.0f; m_targetDirection[1] = 0.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[2] = 0.0f; - SDL_memcpy(m_currentPosition, m_targetPosition, sizeof(m_targetPosition)); - SDL_memcpy(m_currentDirection, m_targetDirection, sizeof(m_targetDirection)); - SDL_memcpy(m_currentUp, m_targetUp, sizeof(m_targetUp)); + SET3(m_currentPosition, m_targetPosition); + SET3(m_currentDirection, m_targetDirection); + SET3(m_currentUp, m_targetUp); } RemotePlayer::~RemotePlayer() @@ -104,31 +82,23 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) const char* actorName = LegoActor::GetActorName(m_actorId); if (!actorName) { - SDL_Log("Multiplayer: failed to get actor name for id %d", m_actorId); return; } - // Create a full multi-part character clone with body parts m_roi = charMgr->CreateCharacterClone(m_uniqueName, actorName); - if (!m_roi) { - SDL_Log("Multiplayer: failed to create character clone for %s", m_uniqueName); return; } - // Add ROI to the 3D scene and notify the 3D manager VideoManager()->Get3DManager()->Add(*m_roi); VideoManager()->Get3DManager()->Moved(*m_roi); - // Start hidden until we get a STATE update confirming worldId m_roi->SetVisibility(FALSE); m_spawned = true; m_visible = false; - // Build walk animation ROI map BuildWalkROIMap(p_isleWorld); - // Build idle animation ROI map (CNs008xx - breathing/swaying) MxCore* idlePresenter = p_isleWorld->Find("LegoAnimPresenter", "CNs008xx"); if (idlePresenter) { m_idleAnim = static_cast(idlePresenter)->GetAnimation(); @@ -136,14 +106,6 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) 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() @@ -152,7 +114,6 @@ void RemotePlayer::Despawn() return; } - // Clean up vehicle state first ExitVehicle(); if (m_roi) { @@ -176,31 +137,24 @@ void RemotePlayer::Despawn() m_idleAnim = nullptr; m_spawned = false; m_visible = false; - - SDL_Log("Multiplayer: despawned remote player %s", m_uniqueName); } void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) { - // Compute speed from position delta (GetWorldSpeed clamps backward movement to 0) - 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); + float posDelta = SDL_sqrtf(DISTSQRD3(p_msg.position, m_targetPosition)); - SDL_memcpy(m_targetPosition, p_msg.position, sizeof(float) * 3); - SDL_memcpy(m_targetDirection, p_msg.direction, sizeof(float) * 3); - SDL_memcpy(m_targetUp, p_msg.up, sizeof(float) * 3); + SET3(m_targetPosition, p_msg.position); + SET3(m_targetDirection, p_msg.direction); + SET3(m_targetUp, p_msg.up); m_targetSpeed = posDelta > 0.01f ? posDelta : 0.0f; m_targetVehicleType = p_msg.vehicleType; m_targetWorldId = p_msg.worldId; m_lastUpdateTime = SDL_GetTicks(); if (!m_hasReceivedUpdate) { - // Snap to position on first update (don't interpolate from origin) - SDL_memcpy(m_currentPosition, m_targetPosition, sizeof(float) * 3); - SDL_memcpy(m_currentDirection, m_targetDirection, sizeof(float) * 3); - SDL_memcpy(m_currentUp, m_targetUp, sizeof(float) * 3); + SET3(m_currentPosition, m_targetPosition); + SET3(m_currentDirection, m_targetDirection); + SET3(m_currentUp, m_targetUp); m_hasReceivedUpdate = true; } } @@ -211,20 +165,6 @@ void RemotePlayer::Tick(float p_deltaTime) 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(); UpdateTransform(p_deltaTime); UpdateAnimation(p_deltaTime); @@ -252,7 +192,6 @@ void RemotePlayer::SetVisible(bool p_visible) m_visible = p_visible; if (p_visible) { - // Visibility depends on vehicle state if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { m_roi->SetVisibility(FALSE); if (m_vehicleROI) { @@ -265,7 +204,6 @@ void RemotePlayer::SetVisible(bool p_visible) m_vehicleROI->SetVisibility(FALSE); } } - // Ride vehicle ROI visibility is managed by the animation (ApplyAnimationTransformation) } else { m_roi->SetVisibility(FALSE); @@ -284,72 +222,45 @@ void RemotePlayer::BuildWalkROIMap(LegoWorld* p_isleWorld) return; } - // Find the generic slow walk animation presenter "CNs001xx" MxCore* presenter = p_isleWorld->Find("LegoAnimPresenter", "CNs001xx"); if (!presenter) { - SDL_Log("Multiplayer: walk animation presenter CNs001xx not found"); return; } LegoAnimPresenter* animPresenter = static_cast(presenter); m_walkAnim = animPresenter->GetAnimation(); - if (!m_walkAnim) { - SDL_Log("Multiplayer: walk animation data is null"); return; } BuildROIMap(m_walkAnim, m_roi, nullptr, m_walkRoiMap, m_walkRoiMapSize); } -// Traverse the animation tree, assign ROI indices, and collect matched ROIs. -// This mirrors the game's UpdateStructMapAndROIIndex approach: ROI indices -// are assigned at runtime via SetROIIndex() and are NOT pre-stored in animation -// data (m_roiIndex starts at 0 for all nodes). +// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime +// via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes. static void AssignROIIndices( LegoTreeNode* p_node, LegoROI* p_parentROI, LegoROI* p_rootROI, LegoROI* p_extraROI, MxU32& p_nextIndex, - std::vector& p_entries, - int p_depth = 0 + std::vector& p_entries ) { LegoROI* roi = p_parentROI; LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData(); 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 != '-') { LegoROI* matchedROI = 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; matchedROI = p_rootROI; - SDL_Log("Multiplayer: [ROIMap] matched root node '%s' to rootROI", name); } else { - // Body part → search in parent's ROI hierarchy 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) { - // Try extra ROI hierarchy (vehicle variant for ride animations) matchedROI = p_extraROI->FindChildROI(name, p_extraROI); } } @@ -365,7 +276,7 @@ static void AssignROIIndices( } 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; } - // Traverse tree, assigning ROI indices and collecting matched ROIs MxU32 nextIndex = 1; std::vector entries; AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries); @@ -395,7 +305,7 @@ void RemotePlayer::BuildROIMap( 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_roiMap = new LegoROI*[p_roiMapSize]; p_roiMap[0] = nullptr; @@ -406,82 +316,21 @@ void RemotePlayer::BuildROIMap( void RemotePlayer::UpdateTransform(float p_deltaTime) { - // Interpolate position toward target - float lerpFactor = 0.2f; + LERP3(m_currentPosition, m_currentPosition, m_targetPosition, 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++) { - m_currentPosition[i] += (m_targetPosition[i] - m_currentPosition[i]) * lerpFactor; - m_currentDirection[i] += (m_targetDirection[i] - m_currentDirection[i]) * lerpFactor; - m_currentUp[i] += (m_targetUp[i] - m_currentUp[i]) * lerpFactor; - } + // Character clones need negated direction + Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]); + Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]); + 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; - mat[0][0] = x[0]; - 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; + CalcLocalTransform(pos, dir, up, mat); m_roi->WrappedSetLocal2WorldWithWorldDataUpdate(mat); VideoManager()->Get3DManager()->Moved(*m_roi); - // Also update vehicle ROI transform if in large vehicle if (m_vehicleROI && m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); VideoManager()->Get3DManager()->Moved(*m_vehicleROI); @@ -490,23 +339,19 @@ void RemotePlayer::UpdateTransform(float p_deltaTime) void RemotePlayer::UpdateAnimation(float p_deltaTime) { - // Determine which animation and ROI map to use LegoAnim* anim = nullptr; if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { - // Large vehicle: no animation, character is hidden return; } LegoROI** roiMap = nullptr; if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) { - // Small vehicle: use ride animation anim = m_rideAnim; roiMap = m_rideRoiMap; } else if (m_walkAnim && m_walkRoiMap) { - // On foot: use walk animation anim = m_walkAnim; roiMap = m_walkRoiMap; } @@ -514,16 +359,13 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) return; } - // Ensure all body parts are visible before animation (matches game's AnimateWithTransform) MxU32 roiMapSize = (roiMap == m_walkRoiMap) ? m_walkRoiMapSize : m_rideRoiMapSize; - MxU32 idleMapSize = m_idleRoiMapSize; for (MxU32 i = 1; i < roiMapSize; i++) { if (roiMap[i] != nullptr) { roiMap[i]->SetVisibility(TRUE); } } - // Also ensure idle ROI map parts are visible (may include different body parts) - for (MxU32 i = 1; i < idleMapSize; i++) { + for (MxU32 i = 1; i < m_idleRoiMapSize; i++) { if (m_idleRoiMap[i] != nullptr) { m_idleRoiMap[i]->SetVisibility(TRUE); } @@ -532,9 +374,6 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) bool inVehicle = (m_currentVehicleType != VEHICLE_NONE); 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) { m_animTime += p_deltaTime * 2000.0f; } @@ -553,7 +392,6 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) m_idleAnimTime = 0.0f; } else if (m_idleAnim && m_idleRoiMap) { - // Standing still on foot: use the dedicated idle animation (CNs008xx) if (m_wasMoving) { m_wasMoving = false; m_idleTime = 0.0f; @@ -562,8 +400,7 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) m_idleTime += p_deltaTime; - // Play idle animation: frame 0 for first 2.5s (standing pose), - // then continuously loop (breathing/swaying effect) + // Hold standing pose for 2.5s, then loop breathing/swaying if (m_idleTime >= 2.5f) { m_idleAnimTime += p_deltaTime * 1000.0f; } @@ -584,15 +421,10 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime) void RemotePlayer::UpdateVehicleState() { if (m_targetVehicleType != m_currentVehicleType) { - if (m_targetVehicleType == VEHICLE_NONE) { - // Exiting vehicle + if (m_currentVehicleType != VEHICLE_NONE) { ExitVehicle(); } - else { - // Entering vehicle (exit old one first if needed) - if (m_currentVehicleType != VEHICLE_NONE) { - ExitVehicle(); - } + if (m_targetVehicleType != VEHICLE_NONE) { EnterVehicle(m_targetVehicleType); } } @@ -608,27 +440,19 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType) m_animTime = 0.0f; if (IsLargeVehicle(p_vehicleType)) { - // Large vehicle: hide character, show vehicle ROI m_roi->SetVisibility(FALSE); - // Create vehicle ROI clone char vehicleName[48]; SDL_snprintf(vehicleName, sizeof(vehicleName), "%s_mp_%u", g_vehicleROINames[p_vehicleType], m_peerId); m_vehicleROI = CharacterManager()->CreateAutoROI(vehicleName, g_vehicleROINames[p_vehicleType], FALSE); if (m_vehicleROI) { - // CreateAutoROI already adds to 3D scene via Get3DManager()->Add() - // Position at current transform MxMatrix mat(m_roi->GetLocal2World()); m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); m_vehicleROI->SetVisibility(m_visible ? TRUE : FALSE); } - else { - SDL_Log("Multiplayer: failed to create vehicle ROI for type %d", p_vehicleType); - } } else { - // Small vehicle: find ride animation and build ride ROI map const char* rideAnimName = g_rideAnimNames[p_vehicleType]; const char* vehicleVariantName = g_rideVehicleROINames[p_vehicleType]; @@ -636,7 +460,6 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType) return; } - // Find the ride animation presenter LegoWorld* world = CurrentWorld(); if (!world) { return; @@ -644,31 +467,25 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType) MxCore* presenter = world->Find("LegoAnimPresenter", rideAnimName); if (!presenter) { - SDL_Log("Multiplayer: ride animation presenter %s not found", rideAnimName); return; } LegoAnimPresenter* animPresenter = static_cast(presenter); m_rideAnim = animPresenter->GetAnimation(); if (!m_rideAnim) { - SDL_Log("Multiplayer: ride animation data is null for %s", rideAnimName); return; } - // Create vehicle variant ROI for the ride animation char variantName[48]; SDL_snprintf(variantName, sizeof(variantName), "%s_mp_%u", vehicleVariantName, m_peerId); 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. - // CreateAutoROI sets name to unique variantName (e.g. "board_mp_2") but animation nodes - // expect the base name (e.g. "board"). ReleaseAutoROI uses pointer comparison, not name. + // Rename to base name so FindChildROI can match animation tree nodes. + // ReleaseAutoROI uses pointer comparison, not name. if (m_rideVehicleROI) { 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); } } @@ -679,14 +496,12 @@ void RemotePlayer::ExitVehicle() return; } - // Clean up large vehicle ROI if (m_vehicleROI) { VideoManager()->Get3DManager()->Remove(*m_vehicleROI); CharacterManager()->ReleaseAutoROI(m_vehicleROI); m_vehicleROI = nullptr; } - // Clean up ride animation state if (m_rideRoiMap) { delete[] m_rideRoiMap; m_rideRoiMap = nullptr; @@ -699,7 +514,6 @@ void RemotePlayer::ExitVehicle() } m_rideAnim = nullptr; - // Show character again if (m_visible) { m_roi->SetVisibility(TRUE); } diff --git a/extensions/src/multiplayer/websockettransport.cpp b/extensions/src/multiplayer/websockettransport.cpp index 6e4821ae..0f3ac0ac 100644 --- a/extensions/src/multiplayer/websockettransport.cpp +++ b/extensions/src/multiplayer/websockettransport.cpp @@ -2,7 +2,6 @@ #include "extensions/multiplayer/websockettransport.h" -#include #include #include @@ -36,8 +35,6 @@ void WebSocketTransport::Connect(const char* 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 m_socketId = MAIN_THREAD_EM_ASM_INT({ var url = UTF8ToString($0); @@ -78,11 +75,8 @@ void WebSocketTransport::Connect(const char* p_roomId) }, url.c_str(), &m_connectedFlag); // clang-format on - if (m_socketId > 0) { - SDL_Log("Multiplayer: connecting to %s", url.c_str()); - } - else { - SDL_Log("Multiplayer: failed to create WebSocket connection to %s", url.c_str()); + if (m_socketId <= 0) { + m_socketId = -1; } } @@ -100,7 +94,6 @@ void WebSocketTransport::Disconnect() }, m_socketId); // clang-format on - SDL_Log("Multiplayer: disconnected"); m_socketId = -1; m_connectedFlag = 0; } @@ -108,8 +101,6 @@ void WebSocketTransport::Disconnect() 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; }