Fix use-after-free crash in ScenePlayer when remote player disconnects mid-animation

When a remote player's ROI is destroyed (disconnect, timeout, or respawn),
notify all active ScenePlayer instances to null out dangling references
before the ROI is freed. The animation engine already handles null ROI map
entries gracefully, so playback continues for remaining participants.
This commit is contained in:
Christian Semmler 2026-04-03 20:37:04 -07:00
parent 11bf290396
commit 3cf1a38600
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
6 changed files with 74 additions and 2 deletions

View File

@ -30,6 +30,7 @@ class PhonemePlayer {
); );
void Tick(float p_elapsedMs, const std::vector<SceneAnimData::PhonemeTrack>& p_tracks); void Tick(float p_elapsedMs, const std::vector<SceneAnimData::PhonemeTrack>& p_tracks);
void Cleanup(); void Cleanup();
void NotifyROIDestroyed(LegoROI* p_roi);
private: private:
std::vector<PhonemeState> m_states; std::vector<PhonemeState> m_states;

View File

@ -45,6 +45,7 @@ class ScenePlayer {
); );
void Tick(); void Tick();
void Stop(); void Stop();
void NotifyROIDestroyed(LegoROI* p_roi);
bool IsPlaying() const { return m_playing; } bool IsPlaying() const { return m_playing; }
bool IsObserverMode() const { return m_observerMode; } bool IsObserverMode() const { return m_observerMode; }

View File

@ -222,6 +222,7 @@ class NetworkManager : public MxCore {
void TickAnimation(); void TickAnimation();
void StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemotes); void StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemotes);
void StopAllPlayback(); void StopAllPlayback();
void NotifyAnimationsROIDestroyed(RemotePlayer* p_player);
void UnlockRemotesForAnim(uint16_t p_animIndex); void UnlockRemotesForAnim(uint16_t p_animIndex);
// Horn sound synchronization // Horn sound synchronization

View File

@ -188,6 +188,15 @@ void PhonemePlayer::Tick(float p_elapsedMs, const std::vector<SceneAnimData::Pho
} }
} }
void PhonemePlayer::NotifyROIDestroyed(LegoROI* p_roi)
{
for (auto& state : m_states) {
if (state.targetROI == p_roi) {
state.targetROI = nullptr;
}
}
}
void PhonemePlayer::Cleanup() void PhonemePlayer::Cleanup()
{ {
for (size_t i = 0; i < m_states.size(); i++) { for (size_t i = 0; i < m_states.size(); i++) {

View File

@ -567,9 +567,11 @@ void ScenePlayer::Stop()
m_roiMapSize = 0; m_roiMapSize = 0;
for (auto& p : m_participants) { for (auto& p : m_participants) {
if (p.roi) {
p.roi->WrappedSetLocal2WorldWithWorldDataUpdate(p.savedTransform); p.roi->WrappedSetLocal2WorldWithWorldDataUpdate(p.savedTransform);
p.roi->SetVisibility(TRUE); p.roi->SetVisibility(TRUE);
} }
}
m_participants.clear(); m_participants.clear();
BackgroundAudioManager()->RaiseVolume(); BackgroundAudioManager()->RaiseVolume();
@ -586,6 +588,46 @@ void ScenePlayer::Stop()
m_hideOnStop = false; m_hideOnStop = false;
} }
void ScenePlayer::NotifyROIDestroyed(LegoROI* p_roi)
{
if (!m_playing || !p_roi) {
return;
}
for (auto& p : m_participants) {
if (p.roi == p_roi) {
p.roi = nullptr;
}
if (p.vehicleROI == p_roi) {
p.vehicleROI = nullptr;
}
}
if (m_roiMap) {
for (MxU32 i = 0; i < m_roiMapSize; i++) {
if (m_roiMap[i] == p_roi) {
m_roiMap[i] = nullptr;
}
}
}
for (auto& roi : m_ptAtCamROIs) {
if (roi == p_roi) {
roi = nullptr;
}
}
if (m_animRootROI == p_roi) {
m_animRootROI = nullptr;
}
if (m_vehicleROI == p_roi) {
m_vehicleROI = nullptr;
}
m_phonemePlayer.NotifyROIDestroyed(p_roi);
}
void ScenePlayer::CleanupProps() void ScenePlayer::CleanupProps()
{ {
for (auto* propROI : m_propROIs) { for (auto* propROI : m_propROIs) {

View File

@ -1029,6 +1029,7 @@ void NetworkManager::HandleState(const PlayerStateMsg& p_msg)
// Respawn only if display actor changed (not on actorId change) // Respawn only if display actor changed (not on actorId change)
if (it->second->GetDisplayActorIndex() != p_msg.displayActorIndex) { if (it->second->GetDisplayActorIndex() != p_msg.displayActorIndex) {
NotifyAnimationsROIDestroyed(it->second.get());
if (it->second->GetROI()) { if (it->second->GetROI()) {
m_roiToPlayer.erase(it->second->GetROI()); m_roiToPlayer.erase(it->second->GetROI());
} }
@ -1261,6 +1262,7 @@ void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
break; break;
} }
} }
NotifyAnimationsROIDestroyed(it->second.get());
if (it->second->GetROI()) { if (it->second->GetROI()) {
m_roiToPlayer.erase(it->second->GetROI()); m_roiToPlayer.erase(it->second->GetROI());
} }
@ -1280,6 +1282,7 @@ void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
void NetworkManager::RemoveAllRemotePlayers() void NetworkManager::RemoveAllRemotePlayers()
{ {
for (auto& [peerId, player] : m_remotePlayers) { for (auto& [peerId, player] : m_remotePlayers) {
NotifyAnimationsROIDestroyed(player.get());
player->Despawn(); player->Despawn();
} }
m_remotePlayers.clear(); m_remotePlayers.clear();
@ -1428,6 +1431,21 @@ void NetworkManager::StopAllPlayback()
} }
} }
void NetworkManager::NotifyAnimationsROIDestroyed(RemotePlayer* p_player)
{
LegoROI* roi = p_player->GetROI();
LegoROI* vehicleROI = p_player->GetRideVehicleROI();
for (auto& [animIndex, scenePlayer] : m_playingAnims) {
if (roi) {
scenePlayer->NotifyROIDestroyed(roi);
}
if (vehicleROI) {
scenePlayer->NotifyROIDestroyed(vehicleROI);
}
}
}
void NetworkManager::UnlockRemotesForAnim(uint16_t p_animIndex) void NetworkManager::UnlockRemotesForAnim(uint16_t p_animIndex)
{ {
for (auto& [peerId, player] : m_remotePlayers) { for (auto& [peerId, player] : m_remotePlayers) {