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) <noreply@anthropic.com>
This commit is contained in:
Christian Semmler 2026-03-27 18:56:04 -07:00
parent 734623cdd7
commit 964013203a
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
6 changed files with 118 additions and 17 deletions

View File

@ -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; }

View File

@ -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);

View File

@ -92,6 +92,9 @@ class ScenePlayer {
// Props created for the animation (cloned characters and prop models)
std::vector<LegoROI*> m_propROIs;
// ROIs cloned from scene (created by sharing LOD data, not registered in CharacterManager)
std::vector<LegoROI*> m_clonedSceneROIs;
bool m_hasCamAnim;
bool m_observerMode;
std::vector<LegoROI*> m_ptAtCamROIs;

View File

@ -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 <SDL3/SDL_stdinc.h>
#include <algorithm>
@ -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<ViewLODList*>(const_cast<LODListBase*>(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);

View File

@ -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) {

View File

@ -16,13 +16,13 @@
#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;
@ -81,12 +81,16 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo)
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());
@ -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();
}