diff --git a/extensions/include/extensions/multiplayer/animation/coordinator.h b/extensions/include/extensions/multiplayer/animation/coordinator.h index 388b2e51..33f42bdd 100644 --- a/extensions/include/extensions/multiplayer/animation/coordinator.h +++ b/extensions/include/extensions/multiplayer/animation/coordinator.h @@ -69,6 +69,8 @@ class Coordinator { void OnLocationChanged(const std::vector& p_locations, const Catalog* p_catalog); void Reset(); + void ResetLocalState(); + void RemoveSession(uint16_t p_animIndex); // Apply authoritative session state from host void ApplySessionUpdate( diff --git a/extensions/include/extensions/multiplayer/animation/sceneplayer.h b/extensions/include/extensions/multiplayer/animation/sceneplayer.h index 71d75ecd..33e5cf2b 100644 --- a/extensions/include/extensions/multiplayer/animation/sceneplayer.h +++ b/extensions/include/extensions/multiplayer/animation/sceneplayer.h @@ -45,8 +45,9 @@ class ScenePlayer { void Tick(); void Stop(); bool IsPlaying() const { return m_playing; } + bool IsObserverMode() const { return m_observerMode; } - void PreloadAsync(uint32_t p_objectId) { m_loader.PreloadAsync(p_objectId); } + void SetLoader(Loader* p_loader) { m_loader = p_loader; } private: void ComputeRebaseMatrix(); @@ -56,7 +57,7 @@ class ScenePlayer { void CleanupProps(); // Sub-components - Loader m_loader; + Loader* m_loader; AudioPlayer m_audioPlayer; PhonemePlayer m_phonemePlayer; diff --git a/extensions/include/extensions/multiplayer/animation/sessionhost.h b/extensions/include/extensions/multiplayer/animation/sessionhost.h index 228179b2..ec528680 100644 --- a/extensions/include/extensions/multiplayer/animation/sessionhost.h +++ b/extensions/include/extensions/multiplayer/animation/sessionhost.h @@ -33,12 +33,13 @@ class SessionHost { uint32_t p_peerId, uint16_t p_animIndex, uint8_t p_displayActorIndex, - std::vector& p_changedAnims); + std::vector& p_changedAnims + ); bool HandleCancel(uint32_t p_peerId, std::vector& p_changedAnims); bool HandlePlayerRemoved(uint32_t p_peerId, std::vector& p_changedAnims); - // Returns animIndex of session ready to play, or ANIM_INDEX_NONE - uint16_t Tick(uint32_t p_now); + // Returns animIndices of all sessions ready to play + std::vector Tick(uint32_t p_now); void StartCountdown(uint16_t p_animIndex); void RevertCountdown(uint16_t p_animIndex); @@ -66,7 +67,8 @@ class SessionHost { void RemovePlayerFromSessions( uint32_t p_peerId, bool p_includePlayingSessions, - std::vector& p_changedAnims); + std::vector& p_changedAnims + ); const Catalog* m_catalog = nullptr; std::map m_sessions; diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 6a764b11..b5cb3a91 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -199,15 +199,19 @@ class NetworkManager : public MxCore { // NPC animation playback Multiplayer::Animation::Catalog m_animCatalog; - Multiplayer::Animation::ScenePlayer m_scenePlayer; + Multiplayer::Animation::Loader m_animLoader; Multiplayer::Animation::LocationProximity m_locationProximity; Multiplayer::Animation::Coordinator m_animCoordinator; Multiplayer::Animation::SessionHost m_animSessionHost; int32_t m_localPendingAnimInterest; - uint16_t m_playingAnimIndex; + + // Concurrent animation playback: one ScenePlayer per playing animation + std::map> m_playingAnims; void TickAnimation(); - void StopScenePlayback(bool p_unlockRemotes); + void StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemotes); + void StopAllPlayback(); + void UnlockRemotesForAnim(uint16_t p_animIndex); // Animation state push bool m_animStateDirty; diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index d0817a84..79e48f85 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -2,6 +2,7 @@ #include "extensions/common/characteranimator.h" #include "extensions/common/customizestate.h" +#include "extensions/multiplayer/animation/catalog.h" #include "extensions/multiplayer/protocol.h" #include "mxtypes.h" @@ -58,8 +59,15 @@ class RemotePlayer { const char* GetDisplayName() const { return m_displayName; } - void SetAnimationLocked(bool p_locked) { m_animationLocked = p_locked; } - bool IsAnimationLocked() const { return m_animationLocked; } + void LockForAnimation(uint16_t p_animIndex) { m_lockedForAnimIndex = p_animIndex; } + void UnlockFromAnimation(uint16_t p_animIndex) + { + if (m_lockedForAnimIndex == p_animIndex) { + m_lockedForAnimIndex = Animation::ANIM_INDEX_NONE; + } + } + void ForceUnlockAnimation() { m_lockedForAnimIndex = Animation::ANIM_INDEX_NONE; } + bool IsAnimationLocked() const { return m_lockedForAnimIndex != Animation::ANIM_INDEX_NONE; } private: const char* GetDisplayActorName() const; @@ -100,7 +108,7 @@ class RemotePlayer { Extensions::Common::CustomizeState m_customizeState; bool m_allowRemoteCustomize; - bool m_animationLocked; + uint16_t m_lockedForAnimIndex; }; } // namespace Multiplayer diff --git a/extensions/src/multiplayer/animation/coordinator.cpp b/extensions/src/multiplayer/animation/coordinator.cpp index 5c0a5316..7f501471 100644 --- a/extensions/src/multiplayer/animation/coordinator.cpp +++ b/extensions/src/multiplayer/animation/coordinator.cpp @@ -185,6 +185,18 @@ void Coordinator::Reset() m_cancelPending = false; } +void Coordinator::ResetLocalState() +{ + m_state = CoordinationState::e_idle; + m_currentAnimIndex = ANIM_INDEX_NONE; + m_cancelPending = false; +} + +void Coordinator::RemoveSession(uint16_t p_animIndex) +{ + m_sessions.erase(p_animIndex); +} + void Coordinator::ApplySessionUpdate( uint16_t p_animIndex, uint8_t p_state, diff --git a/extensions/src/multiplayer/animation/sceneplayer.cpp b/extensions/src/multiplayer/animation/sceneplayer.cpp index 1b5a8d77..66e73087 100644 --- a/extensions/src/multiplayer/animation/sceneplayer.cpp +++ b/extensions/src/multiplayer/animation/sceneplayer.cpp @@ -61,9 +61,9 @@ static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex) } ScenePlayer::ScenePlayer() - : m_playing(false), m_rebaseComputed(false), m_startTime(0), m_currentData(nullptr), m_category(e_npcAnim), - m_animRootROI(nullptr), m_vehicleROI(nullptr), m_hiddenVehicleROI(nullptr), m_roiMap(nullptr), m_roiMapSize(0), - m_hasCamAnim(false), m_observerMode(false), m_hideOnStop(false) + : m_loader(nullptr), m_playing(false), m_rebaseComputed(false), m_startTime(0), m_currentData(nullptr), + m_category(e_npcAnim), m_animRootROI(nullptr), m_vehicleROI(nullptr), m_hiddenVehicleROI(nullptr), + m_roiMap(nullptr), m_roiMapSize(0), m_hasCamAnim(false), m_observerMode(false), m_hideOnStop(false) { } @@ -264,7 +264,7 @@ void ScenePlayer::Play( return; } - SceneAnimData* data = m_loader.EnsureCached(p_animInfo->m_objectId); + SceneAnimData* data = m_loader->EnsureCached(p_animInfo->m_objectId); if (!data || !data->anim) { return; } diff --git a/extensions/src/multiplayer/animation/sessionhost.cpp b/extensions/src/multiplayer/animation/sessionhost.cpp index 343a77e8..15b904c7 100644 --- a/extensions/src/multiplayer/animation/sessionhost.cpp +++ b/extensions/src/multiplayer/animation/sessionhost.cpp @@ -221,16 +221,16 @@ void SessionHost::RevertCountdown(uint16_t p_animIndex) } } -uint16_t SessionHost::Tick(uint32_t p_now) +std::vector SessionHost::Tick(uint32_t p_now) { + std::vector ready; for (auto& [animIndex, session] : m_sessions) { if (session.state == CoordinationState::e_countdown && p_now >= session.countdownEndTime) { session.state = CoordinationState::e_playing; - return animIndex; + ready.push_back(animIndex); } } - - return ANIM_INDEX_NONE; + return ready; } void SessionHost::Reset() diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index ab411df2..1c61f7e4 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -69,11 +69,10 @@ NetworkManager::NetworkManager() m_sequence(0), m_lastBroadcastTime(0), m_lastValidActorId(0), m_localAllowRemoteCustomize(true), m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false), - m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), - m_playingAnimIndex(Animation::ANIM_INDEX_NONE), m_showNameBubbles(true), m_lastCameraEnabled(false), - m_wasInRestrictedArea(false), m_animStateDirty(false), m_animInterestDirty(false), m_lastAnimPushTime(0), - m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_reconnectAttempt(0), m_reconnectDelay(0), - m_nextReconnectTime(0) + m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_showNameBubbles(true), + m_lastCameraEnabled(false), m_wasInRestrictedArea(false), m_animStateDirty(false), m_animInterestDirty(false), + m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_reconnectAttempt(0), + m_reconnectDelay(0), m_nextReconnectTime(0) { } @@ -105,8 +104,11 @@ MxResult NetworkManager::Tickle() // Cancel animation when camera is disabled (vehicle entry, restricted area, etc.) if (!cameraEnabled && m_animCoordinator.GetState() != Animation::CoordinationState::e_idle) { + uint16_t localAnim = m_animCoordinator.GetCurrentAnimIndex(); CancelLocalAnimInterest(); - StopScenePlayback(false); + if (localAnim != Animation::ANIM_INDEX_NONE) { + StopScenePlayback(localAnim, false); + } } if (m_localNameBubble) { @@ -314,14 +316,7 @@ void NetworkManager::CancelLocalAnimInterest() void NetworkManager::StopAnimation() { ResetAnimationState(); - - if (m_scenePlayer.IsPlaying()) { - m_scenePlayer.Stop(); - ThirdPersonCamera::Controller* cam = GetCamera(); - if (cam) { - cam->SetAnimPlaying(false); - } - } + StopAllPlayback(); } void NetworkManager::OnWorldEnabled(LegoWorld* p_world) @@ -562,8 +557,11 @@ void NetworkManager::ProcessPendingRequests() if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) { if (cam->IsEnabled()) { if (m_animCoordinator.GetState() != Animation::CoordinationState::e_idle) { + uint16_t localAnim = m_animCoordinator.GetCurrentAnimIndex(); CancelLocalAnimInterest(); - StopScenePlayback(false); + if (localAnim != Animation::ANIM_INDEX_NONE) { + StopScenePlayback(localAnim, false); + } } cam->Disable(); NotifyThirdPersonChanged(false); @@ -1189,53 +1187,104 @@ void NetworkManager::SendCustomize(uint32_t p_targetPeerId, uint8_t p_changeType SendMessage(msg); } -void NetworkManager::StopScenePlayback(bool p_unlockRemotes) +void NetworkManager::StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemotes) { - if (!m_scenePlayer.IsPlaying()) { + auto it = m_playingAnims.find(p_animIndex); + if (it == m_playingAnims.end()) { return; } - m_scenePlayer.Stop(); - m_playingAnimIndex = Animation::ANIM_INDEX_NONE; + // Save before Stop() which resets the flag + bool wasObserver = it->second->IsObserverMode(); + + if (it->second->IsPlaying()) { + it->second->Stop(); + } if (p_unlockRemotes) { - for (auto& [peerId, player] : m_remotePlayers) { - player->SetAnimationLocked(false); + UnlockRemotesForAnim(p_animIndex); + } + + // Release camera if local player was a participant (not observer) in this animation + if (!wasObserver) { + ThirdPersonCamera::Controller* cam = GetCamera(); + if (cam) { + cam->SetAnimPlaying(false); } } + m_playingAnims.erase(it); +} + +void NetworkManager::StopAllPlayback() +{ + for (auto& [animIndex, scenePlayer] : m_playingAnims) { + if (scenePlayer->IsPlaying()) { + scenePlayer->Stop(); + } + } + m_playingAnims.clear(); + + for (auto& [peerId, player] : m_remotePlayers) { + player->ForceUnlockAnimation(); + } + ThirdPersonCamera::Controller* cam = GetCamera(); if (cam) { cam->SetAnimPlaying(false); } } +void NetworkManager::UnlockRemotesForAnim(uint16_t p_animIndex) +{ + for (auto& [peerId, player] : m_remotePlayers) { + player->UnlockFromAnimation(p_animIndex); + } +} + void NetworkManager::TickAnimation() { - if (!m_scenePlayer.IsPlaying()) { + if (m_playingAnims.empty()) { return; } - m_scenePlayer.Tick(); + // Collect completed animations with their observer mode (Tick/Stop resets the flag) + std::vector> completed; - if (!m_scenePlayer.IsPlaying()) { - for (auto& [peerId, player] : m_remotePlayers) { - player->SetAnimationLocked(false); + for (auto& [animIndex, scenePlayer] : m_playingAnims) { + if (!scenePlayer->IsPlaying()) { + completed.push_back({animIndex, scenePlayer->IsObserverMode()}); + continue; } - ThirdPersonCamera::Controller* cam = GetCamera(); - if (cam) { - cam->SetAnimPlaying(false); + bool wasObserver = scenePlayer->IsObserverMode(); + scenePlayer->Tick(); + + if (!scenePlayer->IsPlaying()) { + completed.push_back({animIndex, wasObserver}); + } + } + + for (auto& [animIndex, wasObserver] : completed) { + UnlockRemotesForAnim(animIndex); + + // Release camera if local player was a participant (not observer) + if (!wasObserver) { + ThirdPersonCamera::Controller* cam = GetCamera(); + if (cam) { + cam->SetAnimPlaying(false); + } + m_animCoordinator.ResetLocalState(); + m_animCoordinator.RemoveSession(animIndex); } - if (IsHost() && m_playingAnimIndex != Animation::ANIM_INDEX_NONE) { - BroadcastAnimComplete(m_playingAnimIndex); // Must fire before EraseSession destroys participant data - m_animSessionHost.EraseSession(m_playingAnimIndex); - BroadcastAnimUpdate(m_playingAnimIndex); // Broadcast cleared state + if (IsHost()) { + BroadcastAnimComplete(animIndex); // Must fire before EraseSession destroys participant data + m_animSessionHost.EraseSession(animIndex); + BroadcastAnimUpdate(animIndex); // Broadcast cleared state } - m_playingAnimIndex = Animation::ANIM_INDEX_NONE; - m_animCoordinator.Reset(); + m_playingAnims.erase(animIndex); m_animStateDirty = true; m_animInterestDirty = true; } @@ -1286,7 +1335,7 @@ void NetworkManager::TickHostSessions() if (m_animCoordinator.IsLocalPlayerInSession(animIndex)) { const AnimInfo* ai = m_animCatalog.GetAnimInfo(animIndex); if (ai) { - m_scenePlayer.PreloadAsync(ai->m_objectId); + m_animLoader.PreloadAsync(ai->m_objectId); } } @@ -1300,9 +1349,9 @@ void NetworkManager::TickHostSessions() } } - // Check countdown expiry - uint16_t readyAnim = m_animSessionHost.Tick(SDL_GetTicks()); - if (readyAnim != Animation::ANIM_INDEX_NONE) { + // Check countdown expiry — multiple animations may be ready simultaneously + std::vector readyAnims = m_animSessionHost.Tick(SDL_GetTicks()); + for (uint16_t readyAnim : readyAnims) { BroadcastAnimStart(readyAnim); HandleAnimStartLocally(readyAnim, m_animCoordinator.IsLocalPlayerInSession(readyAnim)); } @@ -1363,6 +1412,7 @@ void NetworkManager::HandleAnimCancel(uint32_t p_peerId) return; } + uint16_t localAnimBefore = m_animCoordinator.GetCurrentAnimIndex(); Animation::CoordinationState oldState = m_animCoordinator.GetState(); std::vector changedAnims; @@ -1371,9 +1421,18 @@ void NetworkManager::HandleAnimCancel(uint32_t p_peerId) m_animInterestDirty = true; } + // Stop local player's animation if their session was erased if (oldState == Animation::CoordinationState::e_playing && - m_animCoordinator.GetState() == Animation::CoordinationState::e_idle) { - StopScenePlayback(true); + m_animCoordinator.GetState() == Animation::CoordinationState::e_idle && + localAnimBefore != Animation::ANIM_INDEX_NONE) { + StopScenePlayback(localAnimBefore, true); + } + + // Stop observer-mode playback for any erased playing sessions + for (uint16_t animIndex : changedAnims) { + if (animIndex != localAnimBefore && m_playingAnims.count(animIndex)) { + StopScenePlayback(animIndex, true); + } } } @@ -1383,6 +1442,7 @@ void NetworkManager::HandleAnimUpdate(const AnimUpdateMsg& p_msg) return; // Host already updated its own state } + uint16_t localAnimBefore = m_animCoordinator.GetCurrentAnimIndex(); Animation::CoordinationState oldState = m_animCoordinator.GetState(); uint32_t slots[8]; @@ -1393,7 +1453,7 @@ void NetworkManager::HandleAnimUpdate(const AnimUpdateMsg& p_msg) if (p_msg.state == static_cast(Animation::CoordinationState::e_countdown)) { const AnimInfo* ai = m_animCatalog.GetAnimInfo(p_msg.animIndex); if (ai) { - m_scenePlayer.PreloadAsync(ai->m_objectId); + m_animLoader.PreloadAsync(ai->m_objectId); } } @@ -1402,14 +1462,16 @@ void NetworkManager::HandleAnimUpdate(const AnimUpdateMsg& p_msg) m_localPendingAnimInterest = -1; } + // Stop local player's animation if their session was cleared if (oldState == Animation::CoordinationState::e_playing && - m_animCoordinator.GetState() == Animation::CoordinationState::e_idle) { - StopScenePlayback(true); + m_animCoordinator.GetState() == Animation::CoordinationState::e_idle && + localAnimBefore != Animation::ANIM_INDEX_NONE) { + StopScenePlayback(localAnimBefore, true); } // Stop observer playback when the observed session is cleared - if (m_scenePlayer.IsPlaying() && m_playingAnimIndex == p_msg.animIndex && p_msg.state == 0) { - StopScenePlayback(true); + if (m_playingAnims.count(p_msg.animIndex) && p_msg.state == 0) { + StopScenePlayback(p_msg.animIndex, true); } m_animStateDirty = true; @@ -1438,7 +1500,7 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn m_animSessionHost.EraseSession(p_animIndex); BroadcastAnimUpdate(p_animIndex); } - m_animCoordinator.Reset(); + m_animCoordinator.ResetLocalState(); } m_animStateDirty = true; }; @@ -1496,7 +1558,7 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn // Lock performers to prevent network updates from fighting animation if (!rp.IsSpectator()) { - it->second->SetAnimationLocked(true); + it->second->LockForAnimation(p_animIndex); } } } @@ -1515,26 +1577,31 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn return; } + auto scenePlayer = std::make_unique(); + scenePlayer->SetLoader(&m_animLoader); + if (!observerMode) { bool localIsPerformer = (localCharIndex >= 0); - cam->SetAnimPlaying(true, localIsPerformer, [this]() { m_scenePlayer.Stop(); }); + cam->SetAnimPlaying(true, localIsPerformer, [this, p_animIndex]() { + auto it = m_playingAnims.find(p_animIndex); + if (it != m_playingAnims.end()) { + it->second->Stop(); + } + }); } - m_scenePlayer.Play(animInfo, entry->category, participants.data(), (uint8_t) participants.size(), observerMode); + scenePlayer->Play(animInfo, entry->category, participants.data(), (uint8_t) participants.size(), observerMode); - if (!m_scenePlayer.IsPlaying()) { + if (!scenePlayer->IsPlaying()) { if (!observerMode) { cam->SetAnimPlaying(false); } - // Unlock remote players on failure - for (auto& [peerId, player] : m_remotePlayers) { - player->SetAnimationLocked(false); - } + UnlockRemotesForAnim(p_animIndex); abortSession(); return; } - m_playingAnimIndex = p_animIndex; + m_playingAnims[p_animIndex] = std::move(scenePlayer); m_localPendingAnimInterest = -1; m_animStateDirty = true; } @@ -1842,7 +1909,7 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg) it->second->GetCustomizeState(), p_msg.changeType == CHANGE_MOOD ); - if (!it->second->IsMoving() && !it->second->IsInMultiPartEmote() && !m_scenePlayer.IsPlaying()) { + if (!it->second->IsMoving() && !it->second->IsInMultiPartEmote() && !it->second->IsAnimationLocked()) { it->second->StopClickAnimation(); MxU32 clickAnimId = Common::CharacterCustomizer::PlayClickAnimation( it->second->GetROI(), diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index c3684df1..b8d2fb23 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -32,7 +32,8 @@ RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displ 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_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false, /*.propSuffix=*/p_peerId}), - m_vehicleROI(nullptr), m_nameBubble(nullptr), m_allowRemoteCustomize(true), m_animationLocked(false) + m_vehicleROI(nullptr), m_nameBubble(nullptr), m_allowRemoteCustomize(true), + m_lockedForAnimIndex(Animation::ANIM_INDEX_NONE) { m_displayName[0] = '\0'; const char* displayName = GetDisplayActorName(); @@ -205,7 +206,7 @@ void RemotePlayer::Tick(float p_deltaTime) // During animation playback, skip transform/animation updates (ScenePlayer drives // our ROI), but still update the name bubble so it follows the animated position. - if (m_animationLocked) { + if (IsAnimationLocked()) { if (m_nameBubble) { m_nameBubble->Update(m_roi); }