Support concurrent animation playback by independent player groups

Replace the single ScenePlayer/m_playingAnimIndex with a map of
ScenePlayers keyed by animation index, allowing non-overlapping groups
of players to play different animations simultaneously. Each player can
still only participate in one animation at a time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Semmler 2026-03-27 15:57:12 -07:00
parent 343610ece5
commit f92967735f
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
10 changed files with 176 additions and 79 deletions

View File

@ -69,6 +69,8 @@ class Coordinator {
void OnLocationChanged(const std::vector<int16_t>& p_locations, const Catalog* p_catalog); void OnLocationChanged(const std::vector<int16_t>& p_locations, const Catalog* p_catalog);
void Reset(); void Reset();
void ResetLocalState();
void RemoveSession(uint16_t p_animIndex);
// Apply authoritative session state from host // Apply authoritative session state from host
void ApplySessionUpdate( void ApplySessionUpdate(

View File

@ -45,8 +45,9 @@ class ScenePlayer {
void Tick(); void Tick();
void Stop(); void Stop();
bool IsPlaying() const { return m_playing; } 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: private:
void ComputeRebaseMatrix(); void ComputeRebaseMatrix();
@ -56,7 +57,7 @@ class ScenePlayer {
void CleanupProps(); void CleanupProps();
// Sub-components // Sub-components
Loader m_loader; Loader* m_loader;
AudioPlayer m_audioPlayer; AudioPlayer m_audioPlayer;
PhonemePlayer m_phonemePlayer; PhonemePlayer m_phonemePlayer;

View File

@ -33,12 +33,13 @@ class SessionHost {
uint32_t p_peerId, uint32_t p_peerId,
uint16_t p_animIndex, uint16_t p_animIndex,
uint8_t p_displayActorIndex, uint8_t p_displayActorIndex,
std::vector<uint16_t>& p_changedAnims); std::vector<uint16_t>& p_changedAnims
);
bool HandleCancel(uint32_t p_peerId, std::vector<uint16_t>& p_changedAnims); bool HandleCancel(uint32_t p_peerId, std::vector<uint16_t>& p_changedAnims);
bool HandlePlayerRemoved(uint32_t p_peerId, std::vector<uint16_t>& p_changedAnims); bool HandlePlayerRemoved(uint32_t p_peerId, std::vector<uint16_t>& p_changedAnims);
// Returns animIndex of session ready to play, or ANIM_INDEX_NONE // Returns animIndices of all sessions ready to play
uint16_t Tick(uint32_t p_now); std::vector<uint16_t> Tick(uint32_t p_now);
void StartCountdown(uint16_t p_animIndex); void StartCountdown(uint16_t p_animIndex);
void RevertCountdown(uint16_t p_animIndex); void RevertCountdown(uint16_t p_animIndex);
@ -66,7 +67,8 @@ class SessionHost {
void RemovePlayerFromSessions( void RemovePlayerFromSessions(
uint32_t p_peerId, uint32_t p_peerId,
bool p_includePlayingSessions, bool p_includePlayingSessions,
std::vector<uint16_t>& p_changedAnims); std::vector<uint16_t>& p_changedAnims
);
const Catalog* m_catalog = nullptr; const Catalog* m_catalog = nullptr;
std::map<uint16_t, AnimSession> m_sessions; std::map<uint16_t, AnimSession> m_sessions;

View File

@ -199,15 +199,19 @@ class NetworkManager : public MxCore {
// NPC animation playback // NPC animation playback
Multiplayer::Animation::Catalog m_animCatalog; Multiplayer::Animation::Catalog m_animCatalog;
Multiplayer::Animation::ScenePlayer m_scenePlayer; Multiplayer::Animation::Loader m_animLoader;
Multiplayer::Animation::LocationProximity m_locationProximity; Multiplayer::Animation::LocationProximity m_locationProximity;
Multiplayer::Animation::Coordinator m_animCoordinator; Multiplayer::Animation::Coordinator m_animCoordinator;
Multiplayer::Animation::SessionHost m_animSessionHost; Multiplayer::Animation::SessionHost m_animSessionHost;
int32_t m_localPendingAnimInterest; int32_t m_localPendingAnimInterest;
uint16_t m_playingAnimIndex;
// Concurrent animation playback: one ScenePlayer per playing animation
std::map<uint16_t, std::unique_ptr<Multiplayer::Animation::ScenePlayer>> m_playingAnims;
void TickAnimation(); 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 // Animation state push
bool m_animStateDirty; bool m_animStateDirty;

View File

@ -2,6 +2,7 @@
#include "extensions/common/characteranimator.h" #include "extensions/common/characteranimator.h"
#include "extensions/common/customizestate.h" #include "extensions/common/customizestate.h"
#include "extensions/multiplayer/animation/catalog.h"
#include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/protocol.h"
#include "mxtypes.h" #include "mxtypes.h"
@ -58,8 +59,15 @@ class RemotePlayer {
const char* GetDisplayName() const { return m_displayName; } const char* GetDisplayName() const { return m_displayName; }
void SetAnimationLocked(bool p_locked) { m_animationLocked = p_locked; } void LockForAnimation(uint16_t p_animIndex) { m_lockedForAnimIndex = p_animIndex; }
bool IsAnimationLocked() const { return m_animationLocked; } 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: private:
const char* GetDisplayActorName() const; const char* GetDisplayActorName() const;
@ -100,7 +108,7 @@ class RemotePlayer {
Extensions::Common::CustomizeState m_customizeState; Extensions::Common::CustomizeState m_customizeState;
bool m_allowRemoteCustomize; bool m_allowRemoteCustomize;
bool m_animationLocked; uint16_t m_lockedForAnimIndex;
}; };
} // namespace Multiplayer } // namespace Multiplayer

View File

@ -185,6 +185,18 @@ void Coordinator::Reset()
m_cancelPending = false; 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( void Coordinator::ApplySessionUpdate(
uint16_t p_animIndex, uint16_t p_animIndex,
uint8_t p_state, uint8_t p_state,

View File

@ -61,9 +61,9 @@ static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex)
} }
ScenePlayer::ScenePlayer() ScenePlayer::ScenePlayer()
: m_playing(false), m_rebaseComputed(false), m_startTime(0), m_currentData(nullptr), m_category(e_npcAnim), : m_loader(nullptr), m_playing(false), m_rebaseComputed(false), m_startTime(0), m_currentData(nullptr),
m_animRootROI(nullptr), m_vehicleROI(nullptr), m_hiddenVehicleROI(nullptr), m_roiMap(nullptr), m_roiMapSize(0), m_category(e_npcAnim), m_animRootROI(nullptr), m_vehicleROI(nullptr), m_hiddenVehicleROI(nullptr),
m_hasCamAnim(false), m_observerMode(false), m_hideOnStop(false) m_roiMap(nullptr), m_roiMapSize(0), m_hasCamAnim(false), m_observerMode(false), m_hideOnStop(false)
{ {
} }
@ -264,7 +264,7 @@ void ScenePlayer::Play(
return; return;
} }
SceneAnimData* data = m_loader.EnsureCached(p_animInfo->m_objectId); SceneAnimData* data = m_loader->EnsureCached(p_animInfo->m_objectId);
if (!data || !data->anim) { if (!data || !data->anim) {
return; return;
} }

View File

@ -221,16 +221,16 @@ void SessionHost::RevertCountdown(uint16_t p_animIndex)
} }
} }
uint16_t SessionHost::Tick(uint32_t p_now) std::vector<uint16_t> SessionHost::Tick(uint32_t p_now)
{ {
std::vector<uint16_t> ready;
for (auto& [animIndex, session] : m_sessions) { for (auto& [animIndex, session] : m_sessions) {
if (session.state == CoordinationState::e_countdown && p_now >= session.countdownEndTime) { if (session.state == CoordinationState::e_countdown && p_now >= session.countdownEndTime) {
session.state = CoordinationState::e_playing; session.state = CoordinationState::e_playing;
return animIndex; ready.push_back(animIndex);
} }
} }
return ready;
return ANIM_INDEX_NONE;
} }
void SessionHost::Reset() void SessionHost::Reset()

View File

@ -69,11 +69,10 @@ NetworkManager::NetworkManager()
m_sequence(0), m_lastBroadcastTime(0), m_lastValidActorId(0), m_localAllowRemoteCustomize(true), 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_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false),
m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false),
m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_showNameBubbles(true),
m_playingAnimIndex(Animation::ANIM_INDEX_NONE), m_showNameBubbles(true), m_lastCameraEnabled(false), m_lastCameraEnabled(false), m_wasInRestrictedArea(false), m_animStateDirty(false), m_animInterestDirty(false),
m_wasInRestrictedArea(false), m_animStateDirty(false), m_animInterestDirty(false), m_lastAnimPushTime(0), m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_reconnectAttempt(0),
m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_reconnectAttempt(0), m_reconnectDelay(0), m_reconnectDelay(0), m_nextReconnectTime(0)
m_nextReconnectTime(0)
{ {
} }
@ -105,8 +104,11 @@ MxResult NetworkManager::Tickle()
// Cancel animation when camera is disabled (vehicle entry, restricted area, etc.) // Cancel animation when camera is disabled (vehicle entry, restricted area, etc.)
if (!cameraEnabled && m_animCoordinator.GetState() != Animation::CoordinationState::e_idle) { if (!cameraEnabled && m_animCoordinator.GetState() != Animation::CoordinationState::e_idle) {
uint16_t localAnim = m_animCoordinator.GetCurrentAnimIndex();
CancelLocalAnimInterest(); CancelLocalAnimInterest();
StopScenePlayback(false); if (localAnim != Animation::ANIM_INDEX_NONE) {
StopScenePlayback(localAnim, false);
}
} }
if (m_localNameBubble) { if (m_localNameBubble) {
@ -314,14 +316,7 @@ void NetworkManager::CancelLocalAnimInterest()
void NetworkManager::StopAnimation() void NetworkManager::StopAnimation()
{ {
ResetAnimationState(); ResetAnimationState();
StopAllPlayback();
if (m_scenePlayer.IsPlaying()) {
m_scenePlayer.Stop();
ThirdPersonCamera::Controller* cam = GetCamera();
if (cam) {
cam->SetAnimPlaying(false);
}
}
} }
void NetworkManager::OnWorldEnabled(LegoWorld* p_world) void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
@ -562,8 +557,11 @@ void NetworkManager::ProcessPendingRequests()
if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) { if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) {
if (cam->IsEnabled()) { if (cam->IsEnabled()) {
if (m_animCoordinator.GetState() != Animation::CoordinationState::e_idle) { if (m_animCoordinator.GetState() != Animation::CoordinationState::e_idle) {
uint16_t localAnim = m_animCoordinator.GetCurrentAnimIndex();
CancelLocalAnimInterest(); CancelLocalAnimInterest();
StopScenePlayback(false); if (localAnim != Animation::ANIM_INDEX_NONE) {
StopScenePlayback(localAnim, false);
}
} }
cam->Disable(); cam->Disable();
NotifyThirdPersonChanged(false); NotifyThirdPersonChanged(false);
@ -1189,19 +1187,46 @@ void NetworkManager::SendCustomize(uint32_t p_targetPeerId, uint8_t p_changeType
SendMessage(msg); 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; return;
} }
m_scenePlayer.Stop(); // Save before Stop() which resets the flag
m_playingAnimIndex = Animation::ANIM_INDEX_NONE; bool wasObserver = it->second->IsObserverMode();
if (it->second->IsPlaying()) {
it->second->Stop();
}
if (p_unlockRemotes) { if (p_unlockRemotes) {
for (auto& [peerId, player] : m_remotePlayers) { UnlockRemotesForAnim(p_animIndex);
player->SetAnimationLocked(false);
} }
// 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(); ThirdPersonCamera::Controller* cam = GetCamera();
@ -1210,32 +1235,56 @@ void NetworkManager::StopScenePlayback(bool p_unlockRemotes)
} }
} }
void NetworkManager::UnlockRemotesForAnim(uint16_t p_animIndex)
{
for (auto& [peerId, player] : m_remotePlayers) {
player->UnlockFromAnimation(p_animIndex);
}
}
void NetworkManager::TickAnimation() void NetworkManager::TickAnimation()
{ {
if (!m_scenePlayer.IsPlaying()) { if (m_playingAnims.empty()) {
return; return;
} }
m_scenePlayer.Tick(); // Collect completed animations with their observer mode (Tick/Stop resets the flag)
std::vector<std::pair<uint16_t, bool>> completed;
if (!m_scenePlayer.IsPlaying()) { for (auto& [animIndex, scenePlayer] : m_playingAnims) {
for (auto& [peerId, player] : m_remotePlayers) { if (!scenePlayer->IsPlaying()) {
player->SetAnimationLocked(false); completed.push_back({animIndex, scenePlayer->IsObserverMode()});
continue;
} }
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(); ThirdPersonCamera::Controller* cam = GetCamera();
if (cam) { if (cam) {
cam->SetAnimPlaying(false); cam->SetAnimPlaying(false);
} }
m_animCoordinator.ResetLocalState();
if (IsHost() && m_playingAnimIndex != Animation::ANIM_INDEX_NONE) { m_animCoordinator.RemoveSession(animIndex);
BroadcastAnimComplete(m_playingAnimIndex); // Must fire before EraseSession destroys participant data
m_animSessionHost.EraseSession(m_playingAnimIndex);
BroadcastAnimUpdate(m_playingAnimIndex); // Broadcast cleared state
} }
m_playingAnimIndex = Animation::ANIM_INDEX_NONE; if (IsHost()) {
m_animCoordinator.Reset(); BroadcastAnimComplete(animIndex); // Must fire before EraseSession destroys participant data
m_animSessionHost.EraseSession(animIndex);
BroadcastAnimUpdate(animIndex); // Broadcast cleared state
}
m_playingAnims.erase(animIndex);
m_animStateDirty = true; m_animStateDirty = true;
m_animInterestDirty = true; m_animInterestDirty = true;
} }
@ -1286,7 +1335,7 @@ void NetworkManager::TickHostSessions()
if (m_animCoordinator.IsLocalPlayerInSession(animIndex)) { if (m_animCoordinator.IsLocalPlayerInSession(animIndex)) {
const AnimInfo* ai = m_animCatalog.GetAnimInfo(animIndex); const AnimInfo* ai = m_animCatalog.GetAnimInfo(animIndex);
if (ai) { if (ai) {
m_scenePlayer.PreloadAsync(ai->m_objectId); m_animLoader.PreloadAsync(ai->m_objectId);
} }
} }
@ -1300,9 +1349,9 @@ void NetworkManager::TickHostSessions()
} }
} }
// Check countdown expiry // Check countdown expiry — multiple animations may be ready simultaneously
uint16_t readyAnim = m_animSessionHost.Tick(SDL_GetTicks()); std::vector<uint16_t> readyAnims = m_animSessionHost.Tick(SDL_GetTicks());
if (readyAnim != Animation::ANIM_INDEX_NONE) { for (uint16_t readyAnim : readyAnims) {
BroadcastAnimStart(readyAnim); BroadcastAnimStart(readyAnim);
HandleAnimStartLocally(readyAnim, m_animCoordinator.IsLocalPlayerInSession(readyAnim)); HandleAnimStartLocally(readyAnim, m_animCoordinator.IsLocalPlayerInSession(readyAnim));
} }
@ -1363,6 +1412,7 @@ void NetworkManager::HandleAnimCancel(uint32_t p_peerId)
return; return;
} }
uint16_t localAnimBefore = m_animCoordinator.GetCurrentAnimIndex();
Animation::CoordinationState oldState = m_animCoordinator.GetState(); Animation::CoordinationState oldState = m_animCoordinator.GetState();
std::vector<uint16_t> changedAnims; std::vector<uint16_t> changedAnims;
@ -1371,9 +1421,18 @@ void NetworkManager::HandleAnimCancel(uint32_t p_peerId)
m_animInterestDirty = true; m_animInterestDirty = true;
} }
// Stop local player's animation if their session was erased
if (oldState == Animation::CoordinationState::e_playing && if (oldState == Animation::CoordinationState::e_playing &&
m_animCoordinator.GetState() == Animation::CoordinationState::e_idle) { m_animCoordinator.GetState() == Animation::CoordinationState::e_idle &&
StopScenePlayback(true); 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 return; // Host already updated its own state
} }
uint16_t localAnimBefore = m_animCoordinator.GetCurrentAnimIndex();
Animation::CoordinationState oldState = m_animCoordinator.GetState(); Animation::CoordinationState oldState = m_animCoordinator.GetState();
uint32_t slots[8]; uint32_t slots[8];
@ -1393,7 +1453,7 @@ void NetworkManager::HandleAnimUpdate(const AnimUpdateMsg& p_msg)
if (p_msg.state == static_cast<uint8_t>(Animation::CoordinationState::e_countdown)) { if (p_msg.state == static_cast<uint8_t>(Animation::CoordinationState::e_countdown)) {
const AnimInfo* ai = m_animCatalog.GetAnimInfo(p_msg.animIndex); const AnimInfo* ai = m_animCatalog.GetAnimInfo(p_msg.animIndex);
if (ai) { 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; m_localPendingAnimInterest = -1;
} }
// Stop local player's animation if their session was cleared
if (oldState == Animation::CoordinationState::e_playing && if (oldState == Animation::CoordinationState::e_playing &&
m_animCoordinator.GetState() == Animation::CoordinationState::e_idle) { m_animCoordinator.GetState() == Animation::CoordinationState::e_idle &&
StopScenePlayback(true); localAnimBefore != Animation::ANIM_INDEX_NONE) {
StopScenePlayback(localAnimBefore, true);
} }
// Stop observer playback when the observed session is cleared // Stop observer playback when the observed session is cleared
if (m_scenePlayer.IsPlaying() && m_playingAnimIndex == p_msg.animIndex && p_msg.state == 0) { if (m_playingAnims.count(p_msg.animIndex) && p_msg.state == 0) {
StopScenePlayback(true); StopScenePlayback(p_msg.animIndex, true);
} }
m_animStateDirty = true; m_animStateDirty = true;
@ -1438,7 +1500,7 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn
m_animSessionHost.EraseSession(p_animIndex); m_animSessionHost.EraseSession(p_animIndex);
BroadcastAnimUpdate(p_animIndex); BroadcastAnimUpdate(p_animIndex);
} }
m_animCoordinator.Reset(); m_animCoordinator.ResetLocalState();
} }
m_animStateDirty = true; 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 // Lock performers to prevent network updates from fighting animation
if (!rp.IsSpectator()) { 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; return;
} }
auto scenePlayer = std::make_unique<Animation::ScenePlayer>();
scenePlayer->SetLoader(&m_animLoader);
if (!observerMode) { if (!observerMode) {
bool localIsPerformer = (localCharIndex >= 0); 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) { if (!observerMode) {
cam->SetAnimPlaying(false); cam->SetAnimPlaying(false);
} }
// Unlock remote players on failure UnlockRemotesForAnim(p_animIndex);
for (auto& [peerId, player] : m_remotePlayers) {
player->SetAnimationLocked(false);
}
abortSession(); abortSession();
return; return;
} }
m_playingAnimIndex = p_animIndex; m_playingAnims[p_animIndex] = std::move(scenePlayer);
m_localPendingAnimInterest = -1; m_localPendingAnimInterest = -1;
m_animStateDirty = true; m_animStateDirty = true;
} }
@ -1842,7 +1909,7 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg)
it->second->GetCustomizeState(), it->second->GetCustomizeState(),
p_msg.changeType == CHANGE_MOOD 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(); it->second->StopClickAnimation();
MxU32 clickAnimId = Common::CharacterCustomizer::PlayClickAnimation( MxU32 clickAnimId = Common::CharacterCustomizer::PlayClickAnimation(
it->second->GetROI(), it->second->GetROI(),

View File

@ -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_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_targetWorldId(WORLD_NOT_VISIBLE), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false),
m_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false, /*.propSuffix=*/p_peerId}), 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'; m_displayName[0] = '\0';
const char* displayName = GetDisplayActorName(); const char* displayName = GetDisplayActorName();
@ -205,7 +206,7 @@ void RemotePlayer::Tick(float p_deltaTime)
// During animation playback, skip transform/animation updates (ScenePlayer drives // During animation playback, skip transform/animation updates (ScenePlayer drives
// our ROI), but still update the name bubble so it follows the animated position. // our ROI), but still update the name bubble so it follows the animated position.
if (m_animationLocked) { if (IsAnimationLocked()) {
if (m_nameBubble) { if (m_nameBubble) {
m_nameBubble->Update(m_roi); m_nameBubble->Update(m_roi);
} }