isle-portable/extensions/src/multiplayer/animation/sceneplayer.cpp
Christian Semmler 32b2834c0b
Remove dead code hiding spectator ROIs during camera animations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 11:18:28 -07:00

570 lines
15 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 "legoanimationmanager.h"
#include "legocameracontroller.h"
#include "legocharactermanager.h"
#include "legovideomanager.h"
#include "legoworld.h"
#include "misc.h"
#include "misc/legotree.h"
#include "mxgeometry/mxgeometry3d.h"
#include "realtime/realtime.h"
#include "roi/legoroi.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;
// Defined in legoanimationmanager.cpp
extern LegoAnimationManager::Character g_characters[47];
enum VehicleCategory {
e_bike,
e_motorcycle,
e_skateboard,
e_unknownVehicle
};
static VehicleCategory GetVehicleCategory(MxU32 p_vehicleIdx)
{
if (p_vehicleIdx <= 3) {
return e_bike;
}
if (p_vehicleIdx <= 5) {
return e_motorcycle;
}
if (p_vehicleIdx == 6) {
return e_skateboard;
}
return e_unknownVehicle;
}
static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex)
{
if (p_charIndex < 0 || p_charIndex >= (int8_t) sizeOfArray(g_characters)) {
return false;
}
return !SDL_strcasecmp(p_actorName.c_str(), g_characters[p_charIndex].m_name);
}
ScenePlayer::ScenePlayer()
: 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);
auto addAlias = [&](const std::string& p_name, LegoROI* p_roi) {
aliasNames.push_back(p_name);
aliases.push_back({aliasNames.back().c_str(), p_roi});
};
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;
};
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) {
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) {
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);
}
}
else if (actorType == LegoAnimActorEntry::e_managedInvisibleRoiTrimmed || actorType == LegoAnimActorEntry::e_sceneRoi1 || actorType == LegoAnimActorEntry::e_sceneRoi2) {
createProp(lowered, AnimUtils::TrimLODSuffix(lowered).c_str());
}
else if (actorType == LegoAnimActorEntry::e_managedInvisibleRoi) {
createProp(lowered, lowered.c_str());
}
else {
// Type 0/1: check if this is a vehicle actor via ModelInfo flag
LegoROI* roi = nullptr;
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 (GetVehicleCategory(animVehicleIdx) == GetVehicleCategory(perfVehicleIdx)) {
m_vehicleROI = m_participants[p].vehicleROI;
addAlias(lowered, m_vehicleROI);
roi = m_vehicleROI;
break;
}
}
}
}
}
// Try creating as prop
if (!roi) {
roi = createProp(lowered, AnimUtils::TrimLODSuffix(lowered).c_str());
}
// 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);
}
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,
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_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_audioPlayer.Init(data->audioTracks);
// Observers don't get camera control — they watch the animation from their own viewpoint
m_hasCamAnim = (!m_observerMode && m_category == e_camAnim && m_currentData->anim->GetCamAnim() != nullptr);
if (m_category == e_camAnim && !m_observerMode) {
// 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;
}
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) {
p.roi->WrappedSetLocal2WorldWithWorldDataUpdate(p.savedTransform);
p.roi->SetVisibility(TRUE);
}
m_participants.clear();
m_ptAtCamROIs.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::CleanupProps()
{
for (auto* propROI : m_propROIs) {
if (propROI) {
CharacterManager()->ReleaseAutoROI(propROI);
}
}
m_propROIs.clear();
}