#include "extensions/common/animutils.h" #include "anim/legoanim.h" #include "legoanimpresenter.h" #include "legoworld.h" #include "misc.h" #include "misc/legotree.h" #include "roi/legoroi.h" #include #include using namespace Extensions::Common; // Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime // via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes. // // Intentional divergences from LegoAnimPresenter::BuildROIMap (legoanimpresenter.cpp:413-530): // 1. No variable substitution -- we bypass the streaming pipeline, so the variable // table lacks our entries. Direct name comparison instead. // 2. *-prefixed nodes search extraROIs -- the original's GetActorName() depends on // presenter action context (m_action->GetUnknown24()). We search created extra // ROIs directly. // 3. No LegoAnimStructMap dedup -- sequential indices, functionally correct. // Look up an animation node name in the alias map (case-insensitive). static LegoROI* FindAlias(const char* p_name, const AnimUtils::ROIAlias* p_aliases, int p_aliasCount) { for (int i = 0; i < p_aliasCount; i++) { if (p_aliases[i].animName && !SDL_strcasecmp(p_name, p_aliases[i].animName)) { return p_aliases[i].roi; } } return nullptr; } static void AssignROIIndices( LegoTreeNode* p_node, LegoROI* p_parentROI, LegoROI* p_rootROI, LegoROI** p_extraROIs, int p_extraROICount, const AnimUtils::ROIAlias* p_aliases, int p_aliasCount, MxU32& p_nextIndex, std::vector& p_entries, bool& p_rootClaimed ) { LegoROI* roi = p_parentROI; LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData(); const char* name = data ? data->GetName() : nullptr; if (name != nullptr && *name != '-') { LegoROI* matchedROI = nullptr; if (*name == '*' || p_parentROI == nullptr) { roi = p_rootROI; const char* searchName = (*name == '*') ? name + 1 : name; bool matchedExtra = false; // Check aliases first (participant ROIs mapped by character name). // Claiming root prevents subsequent sibling nodes from also claiming it. matchedROI = FindAlias(searchName, p_aliases, p_aliasCount); if (matchedROI) { roi = matchedROI; matchedExtra = true; p_rootClaimed = true; } // Then check extra ROIs by name. // This handles cases like BIKESY appearing before SY in the tree: // BIKESY should match the vehicle extra, not claim the root. if (!matchedExtra && p_extraROICount > 0) { for (int e = 0; e < p_extraROICount; e++) { matchedROI = p_extraROIs[e]->FindChildROI(searchName, p_extraROIs[e]); if (matchedROI != nullptr) { roi = matchedROI; matchedExtra = true; break; } } } if (!matchedExtra) { if (!p_rootClaimed) { matchedROI = p_rootROI; p_rootClaimed = true; } } } else { matchedROI = p_parentROI->FindChildROI(name, p_parentROI); if (matchedROI == nullptr) { // Check aliases — also update roi so children resolve against the alias ROI matchedROI = FindAlias(name, p_aliases, p_aliasCount); if (matchedROI) { roi = matchedROI; } } if (matchedROI == nullptr) { for (int e = 0; e < p_extraROICount; e++) { matchedROI = p_extraROIs[e]->FindChildROI(name, p_extraROIs[e]); if (matchedROI != nullptr) { break; } } } // Mirrors original game (legoanimpresenter.cpp:486-490): // If FindChildROI fails, the node might be a top-level actor that isn't // a child of the current parent. Re-run this node with p_parentROI=NULL // so it enters the root-claiming / top-level search path instead. if (matchedROI == nullptr) { bool isTopLevel = false; // Check aliases for top-level match if (FindAlias(name, p_aliases, p_aliasCount) != nullptr) { isTopLevel = true; } if (!isTopLevel && !p_rootClaimed && p_rootROI->GetName() && !SDL_strcasecmp(name, p_rootROI->GetName())) { isTopLevel = true; } if (!isTopLevel) { for (int e = 0; e < p_extraROICount; e++) { if (p_extraROIs[e]->GetName() && !SDL_strcasecmp(name, p_extraROIs[e]->GetName())) { isTopLevel = true; break; } } } if (isTopLevel) { AssignROIIndices( p_node, nullptr, p_rootROI, p_extraROIs, p_extraROICount, p_aliases, p_aliasCount, p_nextIndex, p_entries, p_rootClaimed ); return; } } } if (matchedROI != nullptr) { data->SetROIIndex(p_nextIndex); p_entries.push_back(matchedROI); p_nextIndex++; } else { data->SetROIIndex(0); } } for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) { AssignROIIndices( p_node->GetChild(i), roi, p_rootROI, p_extraROIs, p_extraROICount, p_aliases, p_aliasCount, p_nextIndex, p_entries, p_rootClaimed ); } } void AnimUtils::BuildROIMap( LegoAnim* p_anim, LegoROI* p_rootROI, LegoROI** p_extraROIs, int p_extraROICount, LegoROI**& p_roiMap, MxU32& p_roiMapSize, const ROIAlias* p_aliases, int p_aliasCount ) { if (!p_anim || !p_rootROI) { return; } LegoTreeNode* root = p_anim->GetRoot(); if (!root) { return; } MxU32 nextIndex = 1; std::vector entries; bool rootClaimed = false; AssignROIIndices( root, nullptr, p_rootROI, p_extraROIs, p_extraROICount, p_aliases, p_aliasCount, nextIndex, entries, rootClaimed ); if (entries.empty()) { return; } // 1-indexed; index 0 reserved as NULL p_roiMapSize = entries.size() + 1; p_roiMap = new LegoROI*[p_roiMapSize]; p_roiMap[0] = nullptr; for (MxU32 i = 0; i < entries.size(); i++) { p_roiMap[i + 1] = entries[i]; } } AnimUtils::AnimCache* AnimUtils::GetOrBuildAnimCache( std::map& p_cacheMap, LegoROI* p_roi, const char* p_animName ) { if (!p_animName || !p_roi) { return nullptr; } // Check if already cached auto it = p_cacheMap.find(p_animName); if (it != p_cacheMap.end()) { return &it->second; } // Look up the animation presenter in the current world LegoWorld* world = CurrentWorld(); if (!world) { return nullptr; } MxCore* presenter = world->Find("LegoAnimPresenter", p_animName); if (!presenter) { return nullptr; } LegoAnim* anim = static_cast(presenter)->GetAnimation(); if (!anim) { return nullptr; } // Build and cache AnimCache& cache = p_cacheMap[p_animName]; cache.anim = anim; BuildROIMap(anim, p_roi, nullptr, 0, cache.roiMap, cache.roiMapSize); return &cache; } void AnimUtils::ApplyTree(LegoAnim* p_anim, MxMatrix& p_transform, LegoTime p_time, LegoROI** p_roiMap) { LegoTreeNode* root = p_anim->GetRoot(); for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { LegoROI::ApplyAnimationTransformation(root->GetChild(i), p_transform, p_time, p_roiMap); } }