From 964013203aa679044c730de28499f72c0ccd2de9 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Fri, 27 Mar 2026 18:56:04 -0700 Subject: [PATCH] Add 6 prop-only animations and scene ROI cloning for missing models Manually override 6 spectator-only animations (557, 596, 709-711, 754) as cam anims in the catalog, with 754's location set to Hospital. For animations referencing world-placed models (BIRD, SHARK, CAT) whose LOD data isn't independently registered in the ViewLODListManager, add a scene ROI cloning fallback in SetupROIs. DeepCloneROI recursively clones the ROI hierarchy sharing LOD geometry via refcount, producing independent copies safe for concurrent playback. Moved to AnimUtils as a reusable utility. Co-Authored-By: Claude Opus 4.6 (1M context) --- LEGO1/realtime/roi.h | 1 + .../include/extensions/common/animutils.h | 4 + .../multiplayer/animation/sceneplayer.h | 3 + extensions/src/common/animutils.cpp | 35 +++++++++ .../src/multiplayer/animation/catalog.cpp | 14 +++- .../src/multiplayer/animation/sceneplayer.cpp | 78 +++++++++++++++---- 6 files changed, 118 insertions(+), 17 deletions(-) diff --git a/LEGO1/realtime/roi.h b/LEGO1/realtime/roi.h index 64964ad3..7d08c13f 100644 --- a/LEGO1/realtime/roi.h +++ b/LEGO1/realtime/roi.h @@ -143,6 +143,7 @@ class ROI { // FUNCTION: BETA10 0x10027110 const CompoundObject* GetComp() const { return comp; } + void SetComp(CompoundObject* p_comp) { comp = p_comp; } // FUNCTION: BETA10 0x10049e10 unsigned char GetVisibility() { return m_visible; } diff --git a/extensions/include/extensions/common/animutils.h b/extensions/include/extensions/common/animutils.h index 401d24a3..4256e4e2 100644 --- a/extensions/include/extensions/common/animutils.h +++ b/extensions/include/extensions/common/animutils.h @@ -93,6 +93,10 @@ inline void EnsureROIMapVisibility(LegoROI** p_roiMap, MxU32 p_roiMapSize) // Apply animation transformation to all root children of an animation tree. void ApplyTree(LegoAnim* p_anim, MxMatrix& p_transform, LegoTime p_time, LegoROI** p_roiMap); +// Deep-clone a ROI hierarchy, sharing LOD geometry via refcount. +// Each clone gets its own transform, safe for concurrent animation playback. +LegoROI* DeepCloneROI(LegoROI* p_source, const char* p_name); + // Strip trailing digits and underscores from a name to get the LOD base name. // Mirrors the digit-trimming in LegoAnimPresenter::CreateManagedActors/CreateSceneROIs. std::string TrimLODSuffix(const std::string& p_name); diff --git a/extensions/include/extensions/multiplayer/animation/sceneplayer.h b/extensions/include/extensions/multiplayer/animation/sceneplayer.h index 33e5cf2b..2ca0101e 100644 --- a/extensions/include/extensions/multiplayer/animation/sceneplayer.h +++ b/extensions/include/extensions/multiplayer/animation/sceneplayer.h @@ -92,6 +92,9 @@ class ScenePlayer { // Props created for the animation (cloned characters and prop models) std::vector m_propROIs; + // ROIs cloned from scene (created by sharing LOD data, not registered in CharacterManager) + std::vector m_clonedSceneROIs; + bool m_hasCamAnim; bool m_observerMode; std::vector m_ptAtCamROIs; diff --git a/extensions/src/common/animutils.cpp b/extensions/src/common/animutils.cpp index cf95c46a..35c6c2b9 100644 --- a/extensions/src/common/animutils.cpp +++ b/extensions/src/common/animutils.cpp @@ -2,10 +2,12 @@ #include "anim/legoanim.h" #include "legoanimpresenter.h" +#include "legovideomanager.h" #include "legoworld.h" #include "misc.h" #include "misc/legotree.h" #include "roi/legoroi.h" +#include "viewmanager/viewlodlist.h" #include #include @@ -311,6 +313,39 @@ void AnimUtils::ApplyTree(LegoAnim* p_anim, MxMatrix& p_transform, LegoTime p_ti } } +LegoROI* AnimUtils::DeepCloneROI(LegoROI* p_source, const char* p_name) +{ + Tgl::Renderer* renderer = VideoManager()->GetRenderer(); + ViewLODList* lodList = reinterpret_cast(const_cast(p_source->GetLODs())); + + LegoROI* clone; + if (lodList && lodList->Size() > 0) { + clone = new LegoROI(renderer, lodList); + } + else { + clone = new LegoROI(renderer); + } + + clone->SetName(p_name); + clone->SetBoundingSphere(p_source->GetBoundingSphere()); + + const CompoundObject* children = p_source->GetComp(); + if (children && !children->empty()) { + CompoundObject* clonedChildren = new CompoundObject(); + for (CompoundObject::const_iterator it = children->begin(); it != children->end(); it++) { + LegoROI* childSource = (LegoROI*) *it; + const char* childName = childSource->GetName() ? childSource->GetName() : ""; + LegoROI* childClone = DeepCloneROI(childSource, childName); + if (childClone) { + clonedChildren->push_back(childClone); + } + } + clone->SetComp(clonedChildren); + } + + return clone; +} + std::string AnimUtils::TrimLODSuffix(const std::string& p_name) { std::string result(p_name); diff --git a/extensions/src/multiplayer/animation/catalog.cpp b/extensions/src/multiplayer/animation/catalog.cpp index c99de6e9..9bd3c179 100644 --- a/extensions/src/multiplayer/animation/catalog.cpp +++ b/extensions/src/multiplayer/animation/catalog.cpp @@ -1,5 +1,6 @@ #include "extensions/multiplayer/animation/catalog.h" +#include "actions/isle_actions.h" #include "decomp.h" #include "legoactors.h" #include "legoanimationmanager.h" @@ -85,7 +86,18 @@ void Catalog::Refresh(LegoAnimationManager* p_am) static const uint64_t NAMED_CHARACTER_MASK = ((uint64_t(1) << 48) - 1) | (uint64_t(0xF) << 54); bool hasNamedPerformer = (entry.performerMask & NAMED_CHARACTER_MASK) != 0; - if (!hasNamedPerformer) { + // Manual overrides for prop-only animations that have no character + // performers but are valid scene animations with spectator-only slots. + MxU32 objectId = m_animsBase[i].m_objectId; + if (objectId == IsleScript::c_snsx31sh_RunAnim || objectId == IsleScript::c_fpz166p1_RunAnim || + objectId == IsleScript::c_nic002pr_RunAnim || objectId == IsleScript::c_nic003pr_RunAnim || + objectId == IsleScript::c_nic004pr_RunAnim || objectId == IsleScript::c_prp101pr_RunAnim) { + if (objectId == IsleScript::c_prp101pr_RunAnim) { + entry.location = 11; // Hospital + } + entry.category = e_camAnim; + } + else if (!hasNamedPerformer) { entry.category = e_otherAnim; } else if (entry.location == -1) { diff --git a/extensions/src/multiplayer/animation/sceneplayer.cpp b/extensions/src/multiplayer/animation/sceneplayer.cpp index 2a995721..80f86596 100644 --- a/extensions/src/multiplayer/animation/sceneplayer.cpp +++ b/extensions/src/multiplayer/animation/sceneplayer.cpp @@ -16,13 +16,13 @@ #include "mxgeometry/mxgeometry3d.h" #include "realtime/realtime.h" #include "roi/legoroi.h" +#include "viewmanager/viewmanager.h" #include #include #include #include #include -#include #include using namespace Multiplayer::Animation; @@ -81,12 +81,16 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) std::vector 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()); @@ -98,6 +102,32 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) 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 = AnimUtils::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); @@ -111,6 +141,7 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) 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++) { @@ -126,18 +157,15 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) } } - if (matched) { - continue; - } - - // No participant matched — create a clone - 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); + 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) { @@ -147,10 +175,11 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) createProp(lowered, lowered.c_str()); } else { - // Type 0/1: check if this is a vehicle actor via ModelInfo flag + // Type 0/1: scene actor, vehicle, or prop LegoROI* roi = nullptr; - bool isVehicleActor = false; + // 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) && @@ -182,11 +211,17 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) } } - // Try creating as prop + // Try creating from a registered LOD if (!roi) { roi = createProp(lowered, AnimUtils::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; @@ -226,6 +261,9 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo) 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); } @@ -571,4 +609,12 @@ void ScenePlayer::CleanupProps() } } m_propROIs.clear(); + + for (auto* clonedROI : m_clonedSceneROIs) { + if (clonedROI) { + VideoManager()->Get3DManager()->Remove(*clonedROI); + delete clonedROI; + } + } + m_clonedSceneROIs.clear(); }