isle-portable/extensions/src/common/animutils.cpp
foxtacles 7e4a86fb39
Some checks failed
CI / clang-format (push) Has been cancelled
CI / ${{ matrix.name }} (false, --toolchain /usr/local/vitasdk/share/vita.toolchain.cmake, false, false, Ninja, Vita, ubuntu-latest, true, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0.26100.0, false, false, Visual Studio 17 2022, true, Xbox One, windows-latest, amd64, false, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/3DS.cmake, false, devkitpro/devkitarm:latest, false, Ninja, true, Nintendo 3DS, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake, false, devkitpro/devkita64:latest, false, Ninja, Nintendo Switch, true, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, emcmake, false, false, true, Ninja, Emscripten, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, false, false, Ninja, true, MSVC (arm64), windows-latest, amd64_arm64, false) (push) Has been cancelled
CI / ${{ matrix.name }} (false, false, true, Ninja, true, MSVC (x86), windows-latest, amd64_x86, false) (push) Has been cancelled
CI / ${{ matrix.name }} (false, true, false, Ninja, true, MSVC (x64), windows-latest, amd64, false) (push) Has been cancelled
CI / ${{ matrix.name }} (false, true, true, false, Ninja, true, MSVC (x64 Debug), windows-latest, amd64, false) (push) Has been cancelled
CI / ${{ matrix.name }} (true, false, -DCMAKE_SYSTEM_NAME=iOS, false, false, Xcode, true, iOS, macos-15, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, false, false, false, Ninja, Android, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, false, true, false, Ninja, macOS, macos-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, true, false, Ninja, true, mingw-w64-x86_64, mingw64, msys2 mingw64, windows-latest, msys2 {0}, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, true, true, false, Ninja, true, Linux (Debug), ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, true, true, false, Ninja, true, Linux, ubuntu-latest, true) (push) Has been cancelled
CI / Flatpak (${{ matrix.arch }}) (aarch64, ubuntu-22.04-arm) (push) Has been cancelled
CI / Flatpak (${{ matrix.arch }}) (x86_64, ubuntu-latest) (push) Has been cancelled
CI / C++ (push) Has been cancelled
Docker / Publish web port (push) Has been cancelled
CI / Release (push) Has been cancelled
Add third person camera extension (#786)
Introduces a third person camera system with orbit camera, input handling
(mouse/keyboard/touch/gamepad), display actor cloning, and camera-relative
movement. Includes shared character utilities (animator, cloner, customizer)
and an IExtraAnimHandler interface for optional animation extensions.
Also includes generic base game fixes and extension system improvements.
2026-03-31 01:00:07 +02:00

271 lines
6.7 KiB
C++

#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 <SDL3/SDL_stdinc.h>
#include <vector>
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<LegoROI*>& 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<LegoROI*> 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<std::string, AnimCache>& 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<LegoAnimPresenter*>(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);
}
}