diff --git a/extensions/include/extensions/multiplayer/animation/coordinator.h b/extensions/include/extensions/multiplayer/animation/coordinator.h index ffd1b5c5..388b2e51 100644 --- a/extensions/include/extensions/multiplayer/animation/coordinator.h +++ b/extensions/include/extensions/multiplayer/animation/coordinator.h @@ -65,8 +65,8 @@ class Coordinator { uint8_t p_proximityCount ) const; - // Auto-clear interest if current animation is not available at the new location. - void OnLocationChanged(int16_t p_location, const Catalog* p_catalog); + // Auto-clear interest if current animation is not available at any of the new locations. + void OnLocationChanged(const std::vector& p_locations, const Catalog* p_catalog); void Reset(); diff --git a/extensions/include/extensions/multiplayer/animation/locationproximity.h b/extensions/include/extensions/multiplayer/animation/locationproximity.h index 32b1a357..e45fdd5b 100644 --- a/extensions/include/extensions/multiplayer/animation/locationproximity.h +++ b/extensions/include/extensions/multiplayer/animation/locationproximity.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Multiplayer::Animation { @@ -11,23 +12,22 @@ class LocationProximity { public: LocationProximity(); - // Returns true if nearest location changed since last call + // Returns true if location set changed since last call bool Update(float p_x, float p_z); - int16_t GetNearestLocation() const { return m_nearestLocation; } - float GetNearestDistance() const { return m_nearestDistance; } + // All locations within radius (sorted by index for stable comparison) + const std::vector& GetLocations() const { return m_locations; } + bool IsAtLocation(int16_t p_location) const; - void SetRadius(float p_radius) { m_radius = p_radius; } float GetRadius() const { return m_radius; } void Reset(); - // Static version for computing any position's nearest location - static int16_t ComputeNearest(float p_x, float p_z, float p_radius); + // Static version returning all locations within radius (sorted by index) + static std::vector ComputeAll(float p_x, float p_z, float p_radius); private: - int16_t m_nearestLocation; - float m_nearestDistance; float m_radius; + std::vector m_locations; }; } // namespace Multiplayer::Animation diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 523a065a..6a764b11 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -142,7 +142,7 @@ class NetworkManager : public MxCore { void BroadcastAnimStart(uint16_t p_animIndex); void BroadcastAnimComplete(uint16_t p_animIndex); void HandleAnimComplete(const AnimCompleteMsg& p_msg); - int16_t GetPeerLocation(uint32_t p_peerId) const; + bool IsPeerAtLocation(uint32_t p_peerId, int16_t p_location) const; bool GetPeerPosition(uint32_t p_peerId, float& p_x, float& p_z) const; bool IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ) const; bool ValidateSessionLocations(uint16_t p_animIndex); diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index d364072a..d0817a84 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -7,6 +7,7 @@ #include #include +#include class LegoROI; class LegoWorld; @@ -36,8 +37,9 @@ class RemotePlayer { bool IsSpawned() const { return m_spawned; } bool IsVisible() const { return m_visible; } int8_t GetWorldId() const { return m_targetWorldId; } - int16_t GetNearestLocation() const { return m_nearestLocation; } - void SetNearestLocation(int16_t p_location) { m_nearestLocation = p_location; } + const std::vector& GetLocations() const { return m_locations; } + void SetLocations(std::vector p_locations) { m_locations = std::move(p_locations); } + bool IsAtLocation(int16_t p_location) const; uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; } void SetVisible(bool p_visible); void TriggerEmote(uint8_t p_emoteId); @@ -84,7 +86,7 @@ class RemotePlayer { int8_t m_targetWorldId; uint32_t m_lastUpdateTime; bool m_hasReceivedUpdate; - int16_t m_nearestLocation; + std::vector m_locations; float m_currentPosition[3]; float m_currentDirection[3]; diff --git a/extensions/src/multiplayer/animation/coordinator.cpp b/extensions/src/multiplayer/animation/coordinator.cpp index 2bbe34fc..5c0a5316 100644 --- a/extensions/src/multiplayer/animation/coordinator.cpp +++ b/extensions/src/multiplayer/animation/coordinator.cpp @@ -145,20 +145,33 @@ std::vector Coordinator::ComputeEligibility( return result; } -void Coordinator::OnLocationChanged(int16_t p_location, const Catalog* p_catalog) +void Coordinator::OnLocationChanged(const std::vector& p_locations, const Catalog* p_catalog) { if (m_state != CoordinationState::e_interested || !p_catalog) { return; } - auto anims = p_catalog->GetAnimationsAtLocation(p_location); - for (const auto* e : anims) { - if (e->animIndex == m_currentAnimIndex) { - return; // still available + // Check if the currently interested animation is still available at any of the locations + for (int16_t loc : p_locations) { + auto anims = p_catalog->GetAnimationsAtLocation(loc); + for (const auto* e : anims) { + if (e->animIndex == m_currentAnimIndex) { + return; // still available at this location + } } } - // Animation not at new location — clear interest + // Also check NPC anims when at no location + if (p_locations.empty()) { + auto anims = p_catalog->GetAnimationsAtLocation(-1); + for (const auto* e : anims) { + if (e->animIndex == m_currentAnimIndex) { + return; + } + } + } + + // Animation not at any current location — clear interest m_state = CoordinationState::e_idle; m_currentAnimIndex = ANIM_INDEX_NONE; m_cancelPending = true; diff --git a/extensions/src/multiplayer/animation/locationproximity.cpp b/extensions/src/multiplayer/animation/locationproximity.cpp index b5e126bd..41fd83c4 100644 --- a/extensions/src/multiplayer/animation/locationproximity.cpp +++ b/extensions/src/multiplayer/animation/locationproximity.cpp @@ -3,58 +3,52 @@ #include "decomp.h" #include "legolocations.h" +#include #include using namespace Multiplayer::Animation; -static const float DEFAULT_RADIUS = NPC_ANIM_PROXIMITY; +static const float DEFAULT_RADIUS = 5.0f; // Location 0 is the camera origin, and the last location is overhead — skip both static const int FIRST_VALID_LOCATION = 1; static const int LAST_VALID_LOCATION = sizeOfArray(g_locations) - 2; -LocationProximity::LocationProximity() : m_nearestLocation(-1), m_nearestDistance(0.0f), m_radius(DEFAULT_RADIUS) +LocationProximity::LocationProximity() : m_radius(DEFAULT_RADIUS) { } bool LocationProximity::Update(float p_x, float p_z) { - int16_t prev = m_nearestLocation; - m_nearestLocation = ComputeNearest(p_x, p_z, m_radius); + std::vector prev = m_locations; + m_locations = ComputeAll(p_x, p_z, m_radius); + return m_locations != prev; +} - if (m_nearestLocation >= 0) { - float dx = p_x - g_locations[m_nearestLocation].m_position[0]; - float dz = p_z - g_locations[m_nearestLocation].m_position[2]; - m_nearestDistance = std::sqrt(dx * dx + dz * dz); - } - else { - m_nearestDistance = 0.0f; - } - - return m_nearestLocation != prev; +bool LocationProximity::IsAtLocation(int16_t p_location) const +{ + return std::find(m_locations.begin(), m_locations.end(), p_location) != m_locations.end(); } void LocationProximity::Reset() { - m_nearestLocation = -1; - m_nearestDistance = 0.0f; + m_locations.clear(); } -int16_t LocationProximity::ComputeNearest(float p_x, float p_z, float p_radius) +std::vector LocationProximity::ComputeAll(float p_x, float p_z, float p_radius) { - float bestDist = p_radius; - int16_t bestLocation = -1; + std::vector result; for (int i = FIRST_VALID_LOCATION; i <= LAST_VALID_LOCATION; i++) { float dx = p_x - g_locations[i].m_position[0]; float dz = p_z - g_locations[i].m_position[2]; float dist = std::sqrt(dx * dx + dz * dz); - if (dist < bestDist) { - bestDist = dist; - bestLocation = static_cast(i); + if (dist < p_radius) { + result.push_back(static_cast(i)); } } - return bestLocation; + // Sorted by index (iteration order is already ascending), which gives stable comparison + return result; } diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 34c5774b..ab411df2 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include using namespace Extensions; @@ -39,7 +41,7 @@ static constexpr float NPC_ANIM_NEARBY_RADIUS_SQ = (Animation::NPC_ANIM_PROXIMITY + 5.0f) * (Animation::NPC_ANIM_PROXIMITY + 5.0f); static const char* IDLE_ANIM_STATE_JSON = - "{\"location\":-1,\"state\":0,\"currentAnimIndex\":65535,\"pendingInterest\":-1,\"animations\":[]}"; + "{\"locations\":[],\"state\":0,\"currentAnimIndex\":65535,\"pendingInterest\":-1,\"animations\":[]}"; static void ExtractSlotPeerIds(const AnimUpdateMsg& p_msg, uint32_t p_out[8]) { @@ -137,11 +139,10 @@ MxResult NetworkManager::Tickle() if (userActor && userActor->GetROI()) { const float* pos = userActor->GetROI()->GetWorldPosition(); if (m_locationProximity.Update(pos[0], pos[2])) { - int16_t loc = m_locationProximity.GetNearestLocation(); m_animStateDirty = true; Animation::CoordinationState oldState = m_animCoordinator.GetState(); - m_animCoordinator.OnLocationChanged(loc, &m_animCatalog); + m_animCoordinator.OnLocationChanged(m_locationProximity.GetLocations(), &m_animCatalog); // Location change cleared interest — send cancel to host if (oldState != Animation::CoordinationState::e_idle && @@ -861,23 +862,29 @@ void NetworkManager::ProcessIncomingPackets() void NetworkManager::UpdateRemotePlayers(float p_deltaTime) { float radius = m_locationProximity.GetRadius(); - int16_t localLoc = m_locationProximity.GetNearestLocation(); + const auto& localLocs = m_locationProximity.GetLocations(); bool anyInIsle = false; for (auto& [peerId, player] : m_remotePlayers) { player->Tick(p_deltaTime); - // Derive nearest location from remote player's current position + // Derive locations from remote player's current position // Skip players not in the isle world — their position is stale if (player->IsSpawned() && player->GetROI() && player->GetWorldId() == (int8_t) LegoOmni::e_act1) { anyInIsle = true; - int16_t oldLoc = player->GetNearestLocation(); + auto oldLocs = player->GetLocations(); const float* pos = player->GetROI()->GetWorldPosition(); - int16_t newLoc = Animation::LocationProximity::ComputeNearest(pos[0], pos[2], radius); - player->SetNearestLocation(newLoc); - if (oldLoc != newLoc && (oldLoc == localLoc || newLoc == localLoc)) { - m_animStateDirty = true; + auto newLocs = Animation::LocationProximity::ComputeAll(pos[0], pos[2], radius); + player->SetLocations(std::move(newLocs)); + if (oldLocs != player->GetLocations()) { + // Dirty if remote's locations changed and any overlap with local player's locations + for (int16_t loc : localLocs) { + if (player->IsAtLocation(loc) || std::find(oldLocs.begin(), oldLocs.end(), loc) != oldLocs.end()) { + m_animStateDirty = true; + break; + } + } } } } @@ -1058,8 +1065,12 @@ void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId) { auto it = m_remotePlayers.find(p_peerId); if (it != m_remotePlayers.end()) { - if (it->second->GetNearestLocation() == m_locationProximity.GetNearestLocation()) { - m_animStateDirty = true; + const auto& localLocs = m_locationProximity.GetLocations(); + for (int16_t loc : it->second->GetLocations()) { + if (std::find(localLocs.begin(), localLocs.end(), loc) != localLocs.end()) { + m_animStateDirty = true; + break; + } } if (it->second->GetROI()) { m_roiToPlayer.erase(it->second->GetROI()); @@ -1251,7 +1262,7 @@ void NetworkManager::TickHostSessions() if (entry && entry->location >= 0) { std::vector toRemove; for (const auto& slot : session->slots) { - if (slot.peerId != 0 && GetPeerLocation(slot.peerId) != entry->location) { + if (slot.peerId != 0 && !IsPeerAtLocation(slot.peerId, entry->location)) { toRemove.push_back(slot.peerId); } } @@ -1311,7 +1322,7 @@ void NetworkManager::HandleAnimInterest(uint32_t p_peerId, uint16_t p_animIndex, // For location-bound animations, player must be at that location const Animation::CatalogEntry* entry = m_animCatalog.FindEntry(p_animIndex); if (entry && entry->location >= 0) { - if (GetPeerLocation(p_peerId) != entry->location) { + if (!IsPeerAtLocation(p_peerId, entry->location)) { return; } } @@ -1704,16 +1715,16 @@ void NetworkManager::HandleAnimComplete(const AnimCompleteMsg& p_msg) m_callbacks->OnAnimationCompleted(json.c_str()); } -int16_t NetworkManager::GetPeerLocation(uint32_t p_peerId) const +bool NetworkManager::IsPeerAtLocation(uint32_t p_peerId, int16_t p_location) const { if (p_peerId == m_localPeerId) { - return m_locationProximity.GetNearestLocation(); + return m_locationProximity.IsAtLocation(p_location); } auto it = m_remotePlayers.find(p_peerId); if (it != m_remotePlayers.end()) { - return it->second->GetNearestLocation(); + return it->second->IsAtLocation(p_location); } - return -1; + return false; } bool NetworkManager::GetPeerPosition(uint32_t p_peerId, float& p_x, float& p_z) const @@ -1775,8 +1786,7 @@ bool NetworkManager::ValidateSessionLocations(uint16_t p_animIndex) if (slot.peerId == 0) { continue; } - int16_t loc = GetPeerLocation(slot.peerId); - if (loc >= 0 && loc != entry->location) { + if (!IsPeerAtLocation(slot.peerId, entry->location)) { return false; } } @@ -1978,7 +1988,7 @@ void NetworkManager::PushAnimationState() return; } - int16_t location = m_locationProximity.GetNearestLocation(); + const auto& locations = m_locationProximity.GetLocations(); uint8_t displayActorIndex = cam->GetDisplayActorIndex(); int8_t localCharIndex = Animation::Catalog::DisplayActorToCharacterIndex(displayActorIndex); @@ -1992,46 +2002,75 @@ void NetworkManager::PushAnimationState() const float* localPos = userActor->GetROI()->GetWorldPosition(); float localX = localPos[0], localZ = localPos[2]; - // Build two sets of character indices: - // - locationCharIndices: players at the same location (for cam anims) - // - proximityCharIndices: players within NPC_ANIM_PROXIMITY (for NPC anims) - std::vector locationCharIndices; + // Build proximity character indices (for NPC anims — position-based, not location-based) std::vector proximityCharIndices; - locationCharIndices.push_back(localCharIndex); proximityCharIndices.push_back(localCharIndex); for (const auto& [peerId, player] : m_remotePlayers) { if (!player->IsSpawned() || !player->GetROI() || player->GetWorldId() != (int8_t) LegoOmni::e_act1) { continue; } - int8_t charIdx = Animation::Catalog::DisplayActorToCharacterIndex(player->GetDisplayActorIndex()); - if (player->GetNearestLocation() == location) { - locationCharIndices.push_back(charIdx); - } // Exact NPC_ANIM_PROXIMITY radius for triggering eligibility // (tighter than IsPeerNearby's NPC_ANIM_NEARBY_RADIUS_SQ used for session visibility) const float* rpos = player->GetROI()->GetWorldPosition(); float dx = rpos[0] - localX; float dz = rpos[2] - localZ; if ((dx * dx + dz * dz) <= (Animation::NPC_ANIM_PROXIMITY * Animation::NPC_ANIM_PROXIMITY)) { + int8_t charIdx = Animation::Catalog::DisplayActorToCharacterIndex(player->GetDisplayActorIndex()); proximityCharIndices.push_back(charIdx); } } - auto eligibility = m_animCoordinator.ComputeEligibility( - location, - locationCharIndices.data(), - static_cast(locationCharIndices.size()), - proximityCharIndices.data(), - static_cast(proximityCharIndices.size()) - ); + // Compute eligibility across all overlapping locations. + // Each call returns NPC anims + cam anims for that specific location. + // NPC anims are identical across calls (same proximityChars), so we deduplicate by animIndex. + std::vector eligibility; + std::set seenAnimIndices; + + // If at no location, still process once with -1 to get NPC anims + std::vector locationsToProcess = locations.empty() ? std::vector{int16_t(-1)} : locations; + + for (int16_t loc : locationsToProcess) { + // Build per-location character indices (for cam anims at this location) + std::vector locationCharIndices; + locationCharIndices.push_back(localCharIndex); + + for (const auto& [peerId, player] : m_remotePlayers) { + if (!player->IsSpawned() || !player->GetROI() || player->GetWorldId() != (int8_t) LegoOmni::e_act1) { + continue; + } + if (player->IsAtLocation(loc)) { + int8_t charIdx = Animation::Catalog::DisplayActorToCharacterIndex(player->GetDisplayActorIndex()); + locationCharIndices.push_back(charIdx); + } + } + + auto locEligibility = m_animCoordinator.ComputeEligibility( + loc, + locationCharIndices.data(), + static_cast(locationCharIndices.size()), + proximityCharIndices.data(), + static_cast(proximityCharIndices.size()) + ); + + for (auto& info : locEligibility) { + if (seenAnimIndices.insert(info.animIndex).second) { + eligibility.push_back(std::move(info)); + } + } + } // Build JSON std::string json; json.reserve(2048); - json += "{\"location\":"; - json += std::to_string(location); - json += ",\"state\":"; + json += "{\"locations\":["; + for (size_t i = 0; i < locations.size(); i++) { + if (i > 0) { + json += ','; + } + json += std::to_string(locations[i]); + } + json += "],\"state\":"; json += std::to_string(static_cast(m_animCoordinator.GetState())); json += ",\"currentAnimIndex\":"; json += std::to_string(m_animCoordinator.GetCurrentAnimIndex()); diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index bba4c15e..c3684df1 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -15,6 +15,7 @@ #include #include +#include #include using namespace Extensions; @@ -29,7 +30,7 @@ using Common::WORLD_NOT_VISIBLE; RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex) : m_peerId(p_peerId), m_actorId(p_actorId), m_displayActorIndex(p_displayActorIndex), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), - m_targetWorldId(WORLD_NOT_VISIBLE), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false), m_nearestLocation(-1), + m_targetWorldId(WORLD_NOT_VISIBLE), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false), m_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false, /*.propSuffix=*/p_peerId}), m_vehicleROI(nullptr), m_nameBubble(nullptr), m_allowRemoteCustomize(true), m_animationLocked(false) { @@ -55,6 +56,11 @@ RemotePlayer::~RemotePlayer() Despawn(); } +bool RemotePlayer::IsAtLocation(int16_t p_location) const +{ + return std::find(m_locations.begin(), m_locations.end(), p_location) != m_locations.end(); +} + void RemotePlayer::Spawn(LegoWorld* p_isleWorld) { if (m_spawned) {