mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
NotifyROIDestroyed now walks the parent chain to also invalidate child ROIs of the destroyed performer (head, limbs, etc.) that were placed into the roiMap by BuildROIMap. The ancestor walk happens once; all other fields are cleaned with simple pointer equality.
676 lines
18 KiB
C++
676 lines
18 KiB
C++
#include "extensions/multiplayer/animation/sceneplayer.h"
|
|
|
|
#include "3dmanager/lego3dmanager.h"
|
|
#include "anim/legoanim.h"
|
|
#include "extensions/common/animutils.h"
|
|
#include "extensions/common/charactercloner.h"
|
|
#include "extensions/multiplayer/mputils.h"
|
|
#include "legoactors.h"
|
|
#include "legoanimationmanager.h"
|
|
#include "legocameracontroller.h"
|
|
#include "legocharactermanager.h"
|
|
#include "legovideomanager.h"
|
|
#include "legoworld.h"
|
|
#include "misc.h"
|
|
#include "misc/legotree.h"
|
|
#include "mxbackgroundaudiomanager.h"
|
|
#include "mxgeometry/mxgeometry3d.h"
|
|
#include "realtime/realtime.h"
|
|
#include "roi/legoroi.h"
|
|
#include "viewmanager/viewmanager.h"
|
|
|
|
#include <SDL3/SDL_stdinc.h>
|
|
#include <SDL3/SDL_timer.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <vector>
|
|
|
|
using namespace Multiplayer::Animation;
|
|
namespace AnimUtils = Extensions::Common::AnimUtils;
|
|
using Extensions::Common::CharacterCloner;
|
|
|
|
static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex)
|
|
{
|
|
if (p_charIndex < 0 || p_charIndex >= (int8_t) sizeOfArray(g_actorInfoInit)) {
|
|
return false;
|
|
}
|
|
return !SDL_strcasecmp(p_actorName.c_str(), g_actorInfoInit[p_charIndex].m_name);
|
|
}
|
|
|
|
ScenePlayer::ScenePlayer()
|
|
: 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)
|
|
{
|
|
}
|
|
|
|
ScenePlayer::~ScenePlayer()
|
|
{
|
|
if (m_playing) {
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo)
|
|
{
|
|
LegoU32 numActors = m_currentData->anim->GetNumActors();
|
|
std::vector<LegoROI*> createdROIs;
|
|
std::vector<AnimUtils::ROIAlias> aliases;
|
|
std::deque<std::string> aliasNames;
|
|
|
|
std::vector<bool> participantMatched(m_participants.size(), false);
|
|
|
|
// Register an alias mapping an animation actor name to an ROI whose actual
|
|
// name differs (e.g. a participant's unique name, or a cloned scene ROI).
|
|
auto addAlias = [&](const std::string& p_name, LegoROI* p_roi) {
|
|
aliasNames.push_back(p_name);
|
|
aliases.push_back({aliasNames.back().c_str(), p_roi});
|
|
m_actorAliases.push_back({p_name, p_roi});
|
|
};
|
|
|
|
// Create a prop ROI from a registered LOD name. Returns nullptr if the
|
|
// LOD isn't in the ViewLODListManager.
|
|
auto createProp = [&](const std::string& p_name, const char* p_lodName) -> LegoROI* {
|
|
char uniqueName[64];
|
|
SDL_snprintf(uniqueName, sizeof(uniqueName), "npc_prop_%s", p_name.c_str());
|
|
LegoROI* roi = CharacterManager()->CreateAutoROI(uniqueName, p_lodName, FALSE);
|
|
if (roi) {
|
|
roi->SetName(p_name.c_str());
|
|
createdROIs.push_back(roi);
|
|
}
|
|
return roi;
|
|
};
|
|
|
|
// Clone a scene ROI by name. Creates an independent deep copy (shared LOD
|
|
// geometry via refcount) with a unique name and an alias for the ROI map.
|
|
auto cloneSceneROI = [&](const std::string& p_name) -> LegoROI* {
|
|
const CompoundObject& sceneROIs = VideoManager()->Get3DManager()->GetLego3DView()->GetViewManager()->GetROIs();
|
|
for (CompoundObject::const_iterator it = sceneROIs.begin(); it != sceneROIs.end(); it++) {
|
|
LegoROI* source = (LegoROI*) *it;
|
|
if (!source->GetName() || SDL_strcasecmp(source->GetName(), p_name.c_str())) {
|
|
continue;
|
|
}
|
|
|
|
static uint32_t s_counter = 0;
|
|
char uniqueName[64];
|
|
SDL_snprintf(uniqueName, sizeof(uniqueName), "npc_scene_%s_%u", p_name.c_str(), s_counter++);
|
|
|
|
LegoROI* clone = Multiplayer::DeepCloneROI(source, uniqueName);
|
|
if (clone) {
|
|
clone->SetVisibility(FALSE);
|
|
VideoManager()->Get3DManager()->Add(*clone);
|
|
m_clonedSceneROIs.push_back(clone);
|
|
addAlias(p_name, clone);
|
|
}
|
|
return clone;
|
|
}
|
|
return nullptr;
|
|
};
|
|
|
|
for (LegoU32 i = 0; i < numActors; i++) {
|
|
const char* actorName = m_currentData->anim->GetActorName(i);
|
|
LegoU32 actorType = m_currentData->anim->GetActorType(i);
|
|
|
|
if (!actorName || *actorName == '\0') {
|
|
continue;
|
|
}
|
|
|
|
const char* lookupName = (*actorName == '*') ? actorName + 1 : actorName;
|
|
std::string lowered(lookupName);
|
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(), ::tolower);
|
|
|
|
if (actorType == LegoAnimActorEntry::e_managedLegoActor) {
|
|
// Character actor: match to a participant or clone as NPC
|
|
bool matched = false;
|
|
|
|
for (size_t p = 0; p < m_participants.size(); p++) {
|
|
if (participantMatched[p] || m_participants[p].IsSpectator()) {
|
|
continue;
|
|
}
|
|
|
|
if (MatchesCharacter(lowered, m_participants[p].charIndex)) {
|
|
participantMatched[p] = true;
|
|
matched = true;
|
|
addAlias(lowered, m_participants[p].roi);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matched) {
|
|
char uniqueName[64];
|
|
SDL_snprintf(uniqueName, sizeof(uniqueName), "npc_char_%s", lowered.c_str());
|
|
LegoROI* roi = CharacterCloner::Clone(CharacterManager(), uniqueName, lowered.c_str());
|
|
if (roi) {
|
|
roi->SetName(lowered.c_str());
|
|
VideoManager()->Get3DManager()->Add(*roi);
|
|
createdROIs.push_back(roi);
|
|
}
|
|
}
|
|
}
|
|
else if (actorType == LegoAnimActorEntry::e_managedInvisibleRoiTrimmed || actorType == LegoAnimActorEntry::e_sceneRoi1 || actorType == LegoAnimActorEntry::e_sceneRoi2) {
|
|
createProp(lowered, Multiplayer::TrimLODSuffix(lowered).c_str());
|
|
}
|
|
else if (actorType == LegoAnimActorEntry::e_managedInvisibleRoi) {
|
|
createProp(lowered, lowered.c_str());
|
|
}
|
|
else {
|
|
// Type 0/1: scene actor, vehicle, or prop
|
|
LegoROI* roi = nullptr;
|
|
|
|
// Check if this is a vehicle actor via ModelInfo flag
|
|
bool isVehicleActor = false;
|
|
for (uint8_t m = 0; m < p_animInfo->m_modelCount; m++) {
|
|
if (p_animInfo->m_models[m].m_name &&
|
|
!SDL_strcasecmp(lowered.c_str(), p_animInfo->m_models[m].m_name) &&
|
|
p_animInfo->m_models[m].m_unk0x2c) {
|
|
isVehicleActor = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try matching a participant's vehicle by category
|
|
if (isVehicleActor && !m_vehicleROI) {
|
|
MxU32 animVehicleIdx;
|
|
if (AnimationManager()->FindVehicle(lowered.c_str(), animVehicleIdx)) {
|
|
for (size_t p = 0; p < m_participants.size(); p++) {
|
|
if (!m_participants[p].vehicleROI) {
|
|
continue;
|
|
}
|
|
|
|
MxU32 perfVehicleIdx;
|
|
if (AnimationManager()->FindVehicle(m_participants[p].vehicleROI->GetName(), perfVehicleIdx)) {
|
|
if (Catalog::GetVehicleCategory((int8_t) animVehicleIdx) ==
|
|
Catalog::GetVehicleCategory((int8_t) perfVehicleIdx)) {
|
|
m_vehicleROI = m_participants[p].vehicleROI;
|
|
addAlias(lowered, m_vehicleROI);
|
|
roi = m_vehicleROI;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try creating from a registered LOD
|
|
if (!roi) {
|
|
roi = createProp(lowered, Multiplayer::TrimLODSuffix(lowered).c_str());
|
|
}
|
|
|
|
// Fallback: clone an existing scene ROI (for models like BIRD
|
|
// whose LOD data is embedded in the world, not registered separately)
|
|
if (!roi) {
|
|
roi = cloneSceneROI(lowered);
|
|
}
|
|
|
|
// Final fallback: borrow local player's vehicle via alias
|
|
if (!roi && m_participants[0].vehicleROI && !m_vehicleROI) {
|
|
m_vehicleROI = m_participants[0].vehicleROI;
|
|
addAlias(lowered, m_vehicleROI);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_propROIs = std::move(createdROIs);
|
|
|
|
// Find root ROI: first non-spectator participant matched to an animation actor
|
|
LegoROI* rootROI = nullptr;
|
|
for (size_t p = 0; p < m_participants.size(); p++) {
|
|
if (!m_participants[p].IsSpectator() && participantMatched[p]) {
|
|
rootROI = m_participants[p].roi;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!rootROI && !m_participants.empty()) {
|
|
rootROI = m_participants[0].roi;
|
|
}
|
|
|
|
if (!rootROI) {
|
|
return;
|
|
}
|
|
|
|
m_animRootROI = rootROI;
|
|
|
|
// Collect extra ROIs (other matched participants + props + vehicle)
|
|
std::vector<LegoROI*> extras;
|
|
for (size_t p = 0; p < m_participants.size(); p++) {
|
|
if (m_participants[p].roi != rootROI && participantMatched[p]) {
|
|
extras.push_back(m_participants[p].roi);
|
|
}
|
|
}
|
|
for (auto* propROI : m_propROIs) {
|
|
extras.push_back(propROI);
|
|
}
|
|
for (auto* clonedROI : m_clonedSceneROIs) {
|
|
extras.push_back(clonedROI);
|
|
}
|
|
if (m_vehicleROI) {
|
|
extras.push_back(m_vehicleROI);
|
|
}
|
|
|
|
delete[] m_roiMap;
|
|
m_roiMap = nullptr;
|
|
m_roiMapSize = 0;
|
|
|
|
AnimUtils::BuildROIMap(
|
|
m_currentData->anim,
|
|
rootROI,
|
|
extras.empty() ? nullptr : extras.data(),
|
|
(int) extras.size(),
|
|
m_roiMap,
|
|
m_roiMapSize,
|
|
aliases.empty() ? nullptr : aliases.data(),
|
|
(int) aliases.size()
|
|
);
|
|
}
|
|
|
|
void ScenePlayer::Play(
|
|
const AnimInfo* p_animInfo,
|
|
int8_t p_worldId,
|
|
AnimCategory p_category,
|
|
const ParticipantROI* p_participants,
|
|
uint8_t p_participantCount,
|
|
bool p_observerMode
|
|
)
|
|
{
|
|
if (m_playing) {
|
|
Stop();
|
|
}
|
|
|
|
if (p_participantCount == 0 || !p_participants[0].roi || !p_animInfo) {
|
|
return;
|
|
}
|
|
|
|
SceneAnimData* data = m_loader->EnsureCached(p_worldId, p_animInfo->m_objectId);
|
|
if (!data || !data->anim) {
|
|
return;
|
|
}
|
|
|
|
m_currentData = data;
|
|
m_category = p_category;
|
|
m_hideOnStop = data->hideOnStop;
|
|
m_observerMode = p_observerMode;
|
|
|
|
// Build participant list with saved transforms for restoration
|
|
for (uint8_t i = 0; i < p_participantCount; i++) {
|
|
ParticipantROI participant;
|
|
participant.roi = p_participants[i].roi;
|
|
participant.vehicleROI = p_participants[i].vehicleROI;
|
|
participant.savedTransform = p_participants[i].roi->GetLocal2World();
|
|
participant.savedName = p_participants[i].roi->GetName();
|
|
participant.charIndex = p_participants[i].charIndex;
|
|
m_participants.push_back(participant);
|
|
}
|
|
|
|
SetupROIs(p_animInfo);
|
|
|
|
if (!m_roiMap) {
|
|
m_currentData = nullptr;
|
|
m_participants.clear();
|
|
return;
|
|
}
|
|
|
|
ResolvePtAtCamROIs();
|
|
m_phonemePlayer.Init(data->phonemeTracks, m_roiMap, m_roiMapSize, m_actorAliases);
|
|
m_audioPlayer.Init(data->audioTracks);
|
|
|
|
// Observers and spectators don't get camera control — they watch the animation from their own viewpoint
|
|
m_hasCamAnim =
|
|
(!m_observerMode && !m_participants[0].IsSpectator() && m_category == e_camAnim &&
|
|
m_currentData->anim->GetCamAnim() != nullptr);
|
|
|
|
if (m_category == e_camAnim && !m_observerMode && !m_participants[0].IsSpectator()) {
|
|
// Hide the player's ride vehicle — it would remain visible at the
|
|
// pre-animation position while the player is teleported
|
|
LegoROI* localVehicle = m_participants[0].vehicleROI;
|
|
if (localVehicle && localVehicle != m_vehicleROI) {
|
|
localVehicle->SetVisibility(FALSE);
|
|
m_hiddenVehicleROI = localVehicle;
|
|
}
|
|
}
|
|
|
|
m_startTime = 0;
|
|
m_playing = true;
|
|
|
|
BackgroundAudioManager()->LowerVolume();
|
|
}
|
|
|
|
void ScenePlayer::ComputeRebaseMatrix()
|
|
{
|
|
if (!m_animRootROI) {
|
|
m_rebaseMatrix.SetIdentity();
|
|
m_rebaseComputed = true;
|
|
return;
|
|
}
|
|
|
|
// Use the root performer's saved position as the rebase anchor
|
|
MxMatrix targetTransform;
|
|
targetTransform.SetIdentity();
|
|
for (const auto& p : m_participants) {
|
|
if (p.roi == m_animRootROI) {
|
|
targetTransform = p.savedTransform;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Find the root ROI's world transform at time 0 by walking the animation tree
|
|
std::function<bool(LegoTreeNode*, MxMatrix&)> findOrigin = [&](LegoTreeNode* node, MxMatrix& parentWorld) -> bool {
|
|
LegoAnimNodeData* data = (LegoAnimNodeData*) node->GetData();
|
|
MxU32 roiIdx = data ? data->GetROIIndex() : 0;
|
|
|
|
MxMatrix localMat;
|
|
LegoROI::CreateLocalTransform(data, 0, localMat);
|
|
MxMatrix worldMat;
|
|
worldMat.Product(localMat, parentWorld);
|
|
|
|
if (roiIdx != 0 && m_roiMap[roiIdx] == m_animRootROI) {
|
|
m_animPose0 = worldMat;
|
|
return true;
|
|
}
|
|
for (LegoU32 i = 0; i < node->GetNumChildren(); i++) {
|
|
if (findOrigin(node->GetChild(i), worldMat)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
MxMatrix identity;
|
|
identity.SetIdentity();
|
|
findOrigin(m_currentData->anim->GetRoot(), identity);
|
|
|
|
// Inverse of animPose0 (rigid body: transpose rotation, negate translated position)
|
|
MxMatrix invAnimPose0;
|
|
invAnimPose0.SetIdentity();
|
|
for (int r = 0; r < 3; r++) {
|
|
for (int c = 0; c < 3; c++) {
|
|
invAnimPose0[r][c] = m_animPose0[c][r];
|
|
}
|
|
}
|
|
for (int r = 0; r < 3; r++) {
|
|
invAnimPose0[3][r] =
|
|
-(invAnimPose0[0][r] * m_animPose0[3][0] + invAnimPose0[1][r] * m_animPose0[3][1] +
|
|
invAnimPose0[2][r] * m_animPose0[3][2]);
|
|
}
|
|
|
|
m_rebaseMatrix.Product(invAnimPose0, targetTransform);
|
|
m_rebaseComputed = true;
|
|
}
|
|
|
|
void ScenePlayer::ResolvePtAtCamROIs()
|
|
{
|
|
m_ptAtCamROIs.clear();
|
|
if (!m_currentData || m_currentData->ptAtCamNames.empty() || !m_roiMap) {
|
|
return;
|
|
}
|
|
|
|
for (const auto& name : m_currentData->ptAtCamNames) {
|
|
for (MxU32 i = 1; i < m_roiMapSize; i++) {
|
|
if (m_roiMap[i] && m_roiMap[i]->GetName() && !SDL_strcasecmp(name.c_str(), m_roiMap[i]->GetName())) {
|
|
m_ptAtCamROIs.push_back(m_roiMap[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScenePlayer::ApplyPtAtCam()
|
|
{
|
|
if (m_ptAtCamROIs.empty()) {
|
|
return;
|
|
}
|
|
|
|
LegoWorld* world = CurrentWorld();
|
|
if (!world || !world->GetCameraController()) {
|
|
return;
|
|
}
|
|
|
|
// Same math as LegoAnimPresenter::PutFrame
|
|
for (LegoROI* roi : m_ptAtCamROIs) {
|
|
if (!roi) {
|
|
continue;
|
|
}
|
|
|
|
MxMatrix mat(roi->GetLocal2World());
|
|
|
|
Vector3 pos(mat[0]);
|
|
Vector3 dir(mat[1]);
|
|
Vector3 up(mat[2]);
|
|
Vector3 und(mat[3]);
|
|
|
|
float possqr = sqrt(pos.LenSquared());
|
|
float dirsqr = sqrt(dir.LenSquared());
|
|
float upsqr = sqrt(up.LenSquared());
|
|
|
|
up = und;
|
|
up -= world->GetCameraController()->GetWorldLocation();
|
|
dir /= dirsqr;
|
|
pos.EqualsCross(dir, up);
|
|
pos.Unitize();
|
|
up.EqualsCross(pos, dir);
|
|
pos *= possqr;
|
|
dir *= dirsqr;
|
|
up *= upsqr;
|
|
|
|
roi->SetLocal2World(mat);
|
|
roi->WrappedUpdateWorldData();
|
|
}
|
|
}
|
|
|
|
void ScenePlayer::Tick()
|
|
{
|
|
if (!m_playing || !m_currentData || m_participants.empty()) {
|
|
return;
|
|
}
|
|
|
|
if (m_startTime == 0) {
|
|
m_startTime = SDL_GetTicks();
|
|
}
|
|
|
|
if (m_category == e_npcAnim && m_roiMap) {
|
|
AnimUtils::EnsureROIMapVisibility(m_roiMap, m_roiMapSize);
|
|
}
|
|
|
|
float elapsed = (float) (SDL_GetTicks() - m_startTime);
|
|
|
|
if (elapsed >= m_currentData->duration) {
|
|
Stop();
|
|
return;
|
|
}
|
|
|
|
// 1. Skeletal animation
|
|
if (m_currentData->anim && m_roiMap) {
|
|
if (!m_rebaseComputed) {
|
|
if (m_category == e_camAnim) {
|
|
// cam_anims use the action transform directly (keyframes are in world space)
|
|
if (m_currentData->actionTransform.valid) {
|
|
Mx3DPointFloat loc(
|
|
m_currentData->actionTransform.location[0],
|
|
m_currentData->actionTransform.location[1],
|
|
m_currentData->actionTransform.location[2]
|
|
);
|
|
Mx3DPointFloat dir(
|
|
m_currentData->actionTransform.direction[0],
|
|
m_currentData->actionTransform.direction[1],
|
|
m_currentData->actionTransform.direction[2]
|
|
);
|
|
Mx3DPointFloat up(
|
|
m_currentData->actionTransform.up[0],
|
|
m_currentData->actionTransform.up[1],
|
|
m_currentData->actionTransform.up[2]
|
|
);
|
|
CalcLocalTransform(loc, dir, up, m_rebaseMatrix);
|
|
}
|
|
else {
|
|
m_rebaseMatrix.SetIdentity();
|
|
}
|
|
m_rebaseComputed = true;
|
|
}
|
|
else {
|
|
ComputeRebaseMatrix();
|
|
}
|
|
}
|
|
|
|
AnimUtils::ApplyTree(m_currentData->anim, m_rebaseMatrix, (LegoTime) elapsed, m_roiMap);
|
|
}
|
|
|
|
// 2. Camera animation (cam_anim only)
|
|
if (m_hasCamAnim) {
|
|
MxMatrix camTransform(m_rebaseMatrix);
|
|
m_currentData->anim->GetCamAnim()->CalculateCameraTransform((LegoFloat) elapsed, camTransform);
|
|
|
|
LegoWorld* world = CurrentWorld();
|
|
if (world && world->GetCameraController()) {
|
|
world->GetCameraController()->TransformPointOfView(camTransform, FALSE);
|
|
}
|
|
}
|
|
|
|
// 3. PTATCAM post-processing
|
|
ApplyPtAtCam();
|
|
|
|
// 4. Audio
|
|
const char* audioROIName = m_animRootROI ? m_animRootROI->GetName() : nullptr;
|
|
m_audioPlayer.Tick(elapsed, audioROIName);
|
|
|
|
// 5. Phoneme frames
|
|
m_phonemePlayer.Tick(elapsed, m_currentData->phonemeTracks);
|
|
}
|
|
|
|
void ScenePlayer::Stop()
|
|
{
|
|
if (!m_playing) {
|
|
return;
|
|
}
|
|
|
|
m_audioPlayer.Cleanup();
|
|
m_phonemePlayer.Cleanup();
|
|
|
|
if (m_hideOnStop && m_roiMap) {
|
|
for (MxU32 i = 1; i < m_roiMapSize; i++) {
|
|
if (m_roiMap[i]) {
|
|
m_roiMap[i]->SetVisibility(FALSE);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_hiddenVehicleROI) {
|
|
m_hiddenVehicleROI->SetVisibility(TRUE);
|
|
m_hiddenVehicleROI = nullptr;
|
|
}
|
|
|
|
CleanupProps();
|
|
m_vehicleROI = nullptr;
|
|
|
|
delete[] m_roiMap;
|
|
m_roiMap = nullptr;
|
|
m_roiMapSize = 0;
|
|
|
|
for (auto& p : m_participants) {
|
|
if (p.roi) {
|
|
p.roi->WrappedSetLocal2WorldWithWorldDataUpdate(p.savedTransform);
|
|
p.roi->SetVisibility(TRUE);
|
|
}
|
|
}
|
|
m_participants.clear();
|
|
|
|
BackgroundAudioManager()->RaiseVolume();
|
|
|
|
m_ptAtCamROIs.clear();
|
|
m_actorAliases.clear();
|
|
m_playing = false;
|
|
m_rebaseComputed = false;
|
|
m_currentData = nullptr;
|
|
m_animRootROI = nullptr;
|
|
m_hasCamAnim = false;
|
|
m_observerMode = false;
|
|
m_startTime = 0;
|
|
m_hideOnStop = false;
|
|
}
|
|
|
|
void ScenePlayer::NotifyROIDestroyed(LegoROI* p_roi)
|
|
{
|
|
if (!m_playing || !p_roi) {
|
|
return;
|
|
}
|
|
|
|
// Walk the m_roiMap once to find p_roi and all its descendants (child ROIs
|
|
// are destroyed together with their parent). Collect them so every other
|
|
// field can be cleaned with simple pointer equality — the ancestor walk
|
|
// happens in exactly one place.
|
|
std::vector<LegoROI*> destroyed;
|
|
destroyed.push_back(p_roi);
|
|
|
|
if (m_roiMap) {
|
|
for (MxU32 i = 0; i < m_roiMapSize; i++) {
|
|
if (!m_roiMap[i]) {
|
|
continue;
|
|
}
|
|
|
|
for (OrientableROI* cur = m_roiMap[i]; cur != nullptr; cur = cur->GetParentROI()) {
|
|
if (cur == p_roi) {
|
|
if (m_roiMap[i] != p_roi) {
|
|
destroyed.push_back(m_roiMap[i]);
|
|
}
|
|
m_roiMap[i] = nullptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
auto isDestroyed = [&destroyed](LegoROI* roi) {
|
|
for (LegoROI* d : destroyed) {
|
|
if (roi == d) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
for (auto& p : m_participants) {
|
|
if (p.roi && isDestroyed(p.roi)) {
|
|
p.roi = nullptr;
|
|
}
|
|
if (p.vehicleROI && isDestroyed(p.vehicleROI)) {
|
|
p.vehicleROI = nullptr;
|
|
}
|
|
}
|
|
|
|
for (auto& roi : m_ptAtCamROIs) {
|
|
if (roi && isDestroyed(roi)) {
|
|
roi = nullptr;
|
|
}
|
|
}
|
|
|
|
if (m_animRootROI && isDestroyed(m_animRootROI)) {
|
|
m_animRootROI = nullptr;
|
|
}
|
|
|
|
if (m_vehicleROI && isDestroyed(m_vehicleROI)) {
|
|
m_vehicleROI = nullptr;
|
|
}
|
|
|
|
for (LegoROI* d : destroyed) {
|
|
m_phonemePlayer.NotifyROIDestroyed(d);
|
|
}
|
|
}
|
|
|
|
void ScenePlayer::CleanupProps()
|
|
{
|
|
for (auto* propROI : m_propROIs) {
|
|
if (propROI) {
|
|
CharacterManager()->ReleaseAutoROI(propROI);
|
|
}
|
|
}
|
|
m_propROIs.clear();
|
|
|
|
for (auto* clonedROI : m_clonedSceneROIs) {
|
|
if (clonedROI) {
|
|
VideoManager()->Get3DManager()->Remove(*clonedROI);
|
|
delete clonedROI;
|
|
}
|
|
}
|
|
m_clonedSceneROIs.clear();
|
|
}
|