mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 18:43:56 +00:00
Implement the animation system from the Phase 1 plan: Protocol: Add walkAnimId/idleAnimId fields to PlayerStateMsg (2 extra bytes per 15Hz tick), add MSG_EMOTE (type 9) with EmoteMsg struct, and define shared animation lookup tables (walk: 6 anims, idle: 3, emote: 2). NetworkManager: Store local walk/idle animation indices, include them in every state broadcast, handle incoming MSG_EMOTE by dispatching to the target remote player's TriggerEmote(). Add SetWalkAnimation(), SetIdleAnimation(), SendEmote(), GetPlayerCount() public API. RemotePlayer: Replace per-animation raw pointers with AnimCache struct and lazy m_animCacheMap (name -> ROI map, built on first use, cleared on Despawn). UpdateFromNetwork() detects walk/idle ID changes and swaps the active animation cache. UpdateAnimation() now has three states: moving (configurable walk anim), emote (one-shot with duration tracking, interrupted by movement), and idle (configurable idle anim after 2.5s timeout). Add TriggerEmote() for one-shot emote playback. WASM exports: mp_set_walk_animation(), mp_set_idle_animation(), mp_trigger_emote(), mp_get_player_count() with EMSCRIPTEN_KEEPALIVE. CMakeLists.txt adds EXPORTED_FUNCTIONS and EXPORTED_RUNTIME_METHODS for Svelte ccall/cwrap access. https://claude.ai/code/session_01BEYdu8gXr1QmYwzRRgaEA6
608 lines
16 KiB
C++
608 lines
16 KiB
C++
#include "extensions/multiplayer/remoteplayer.h"
|
|
|
|
#include "3dmanager/lego3dmanager.h"
|
|
#include "anim/legoanim.h"
|
|
#include "extensions/multiplayer/charactercloner.h"
|
|
#include "legoactor.h"
|
|
#include "legoanimpresenter.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 <cmath>
|
|
#include <vec.h>
|
|
#include <vector>
|
|
|
|
using namespace Multiplayer;
|
|
|
|
// LOD names for vehicle models. The helicopter is a compound ROI ("copter")
|
|
// with no standalone LOD; use its body part instead.
|
|
static const char* g_vehicleROINames[VEHICLE_COUNT] =
|
|
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
|
|
|
|
static const char* g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
|
|
|
|
static const char* g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
|
|
|
static bool IsLargeVehicle(int8_t p_vehicleType)
|
|
{
|
|
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
|
}
|
|
|
|
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
|
|
: m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f),
|
|
m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()),
|
|
m_hasReceivedUpdate(false), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr),
|
|
m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr),
|
|
m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_rideAnim(nullptr), m_rideRoiMap(nullptr),
|
|
m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE)
|
|
{
|
|
SDL_snprintf(m_uniqueName, sizeof(m_uniqueName), "%s_mp_%u", LegoActor::GetActorName(p_actorId), p_peerId);
|
|
|
|
ZEROVEC3(m_targetPosition);
|
|
m_targetDirection[0] = 0.0f;
|
|
m_targetDirection[1] = 0.0f;
|
|
m_targetDirection[2] = 1.0f;
|
|
m_targetUp[0] = 0.0f;
|
|
m_targetUp[1] = 1.0f;
|
|
m_targetUp[2] = 0.0f;
|
|
|
|
SET3(m_currentPosition, m_targetPosition);
|
|
SET3(m_currentDirection, m_targetDirection);
|
|
SET3(m_currentUp, m_targetUp);
|
|
}
|
|
|
|
RemotePlayer::~RemotePlayer()
|
|
{
|
|
Despawn();
|
|
}
|
|
|
|
void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
|
|
{
|
|
if (m_spawned) {
|
|
return;
|
|
}
|
|
|
|
LegoCharacterManager* charMgr = CharacterManager();
|
|
if (!charMgr) {
|
|
return;
|
|
}
|
|
|
|
const char* actorName = LegoActor::GetActorName(m_actorId);
|
|
if (!actorName) {
|
|
return;
|
|
}
|
|
|
|
m_roi = CharacterCloner::Clone(charMgr, m_uniqueName, actorName);
|
|
if (!m_roi) {
|
|
return;
|
|
}
|
|
|
|
VideoManager()->Get3DManager()->Add(*m_roi);
|
|
VideoManager()->Get3DManager()->Moved(*m_roi);
|
|
|
|
m_roi->SetVisibility(FALSE);
|
|
m_spawned = true;
|
|
m_visible = false;
|
|
|
|
// Build initial walk and idle animation caches
|
|
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
|
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
|
}
|
|
|
|
void RemotePlayer::Despawn()
|
|
{
|
|
if (!m_spawned) {
|
|
return;
|
|
}
|
|
|
|
ExitVehicle();
|
|
|
|
if (m_roi) {
|
|
VideoManager()->Get3DManager()->Remove(*m_roi);
|
|
CharacterManager()->ReleaseActor(m_uniqueName);
|
|
m_roi = nullptr;
|
|
}
|
|
|
|
// Clear all cached animation ROI maps (anim pointers are world-owned, not ours)
|
|
m_animCacheMap.clear();
|
|
m_walkAnimCache = nullptr;
|
|
m_idleAnimCache = nullptr;
|
|
m_emoteAnimCache = nullptr;
|
|
m_emoteActive = false;
|
|
|
|
m_spawned = false;
|
|
m_visible = false;
|
|
}
|
|
|
|
void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
|
|
{
|
|
float posDelta = SDL_sqrtf(DISTSQRD3(p_msg.position, m_targetPosition));
|
|
|
|
SET3(m_targetPosition, p_msg.position);
|
|
SET3(m_targetDirection, p_msg.direction);
|
|
SET3(m_targetUp, p_msg.up);
|
|
m_targetSpeed = posDelta > 0.01f ? posDelta : 0.0f;
|
|
m_targetVehicleType = p_msg.vehicleType;
|
|
m_targetWorldId = p_msg.worldId;
|
|
m_lastUpdateTime = SDL_GetTicks();
|
|
|
|
if (!m_hasReceivedUpdate) {
|
|
SET3(m_currentPosition, m_targetPosition);
|
|
SET3(m_currentDirection, m_targetDirection);
|
|
SET3(m_currentUp, m_targetUp);
|
|
m_hasReceivedUpdate = true;
|
|
}
|
|
|
|
// Swap walk animation if changed
|
|
if (p_msg.walkAnimId != m_walkAnimId && p_msg.walkAnimId < g_walkAnimCount) {
|
|
m_walkAnimId = p_msg.walkAnimId;
|
|
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
|
}
|
|
|
|
// Swap idle animation if changed
|
|
if (p_msg.idleAnimId != m_idleAnimId && p_msg.idleAnimId < g_idleAnimCount) {
|
|
m_idleAnimId = p_msg.idleAnimId;
|
|
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::Tick(float p_deltaTime)
|
|
{
|
|
if (!m_spawned || !m_visible) {
|
|
return;
|
|
}
|
|
|
|
UpdateVehicleState();
|
|
UpdateTransform(p_deltaTime);
|
|
UpdateAnimation(p_deltaTime);
|
|
}
|
|
|
|
void RemotePlayer::ReAddToScene()
|
|
{
|
|
if (m_spawned && m_roi) {
|
|
VideoManager()->Get3DManager()->Add(*m_roi);
|
|
}
|
|
if (m_vehicleROI) {
|
|
VideoManager()->Get3DManager()->Add(*m_vehicleROI);
|
|
}
|
|
if (m_rideVehicleROI) {
|
|
VideoManager()->Get3DManager()->Add(*m_rideVehicleROI);
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::SetVisible(bool p_visible)
|
|
{
|
|
if (!m_spawned || !m_roi) {
|
|
return;
|
|
}
|
|
|
|
m_visible = p_visible;
|
|
|
|
if (p_visible) {
|
|
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
|
|
m_roi->SetVisibility(FALSE);
|
|
if (m_vehicleROI) {
|
|
m_vehicleROI->SetVisibility(TRUE);
|
|
}
|
|
}
|
|
else {
|
|
m_roi->SetVisibility(TRUE);
|
|
if (m_vehicleROI) {
|
|
m_vehicleROI->SetVisibility(FALSE);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
m_roi->SetVisibility(FALSE);
|
|
if (m_vehicleROI) {
|
|
m_vehicleROI->SetVisibility(FALSE);
|
|
}
|
|
if (m_rideVehicleROI) {
|
|
m_rideVehicleROI->SetVisibility(FALSE);
|
|
}
|
|
}
|
|
}
|
|
|
|
RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName)
|
|
{
|
|
if (!p_animName || !m_roi) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Check if already cached
|
|
auto it = m_animCacheMap.find(p_animName);
|
|
if (it != m_animCacheMap.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 = m_animCacheMap[p_animName];
|
|
cache.anim = anim;
|
|
BuildROIMap(anim, m_roi, nullptr, cache.roiMap, cache.roiMapSize);
|
|
|
|
return &cache;
|
|
}
|
|
|
|
void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
|
|
{
|
|
if (p_emoteId >= g_emoteAnimCount || !m_spawned) {
|
|
return;
|
|
}
|
|
|
|
// Only play emotes when stationary
|
|
if (m_targetSpeed > 0.01f) {
|
|
return;
|
|
}
|
|
|
|
AnimCache* cache = GetOrBuildAnimCache(g_emoteAnimNames[p_emoteId]);
|
|
if (!cache || !cache->anim) {
|
|
return;
|
|
}
|
|
|
|
m_emoteAnimCache = cache;
|
|
m_emoteTime = 0.0f;
|
|
m_emoteDuration = (float) cache->anim->GetDuration();
|
|
m_emoteActive = true;
|
|
}
|
|
|
|
// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
|
|
// via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes.
|
|
static void AssignROIIndices(
|
|
LegoTreeNode* p_node,
|
|
LegoROI* p_parentROI,
|
|
LegoROI* p_rootROI,
|
|
LegoROI* p_extraROI,
|
|
MxU32& p_nextIndex,
|
|
std::vector<LegoROI*>& p_entries
|
|
)
|
|
{
|
|
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;
|
|
matchedROI = p_rootROI;
|
|
}
|
|
else {
|
|
matchedROI = p_parentROI->FindChildROI(name, p_parentROI);
|
|
if (matchedROI == nullptr && p_extraROI != nullptr) {
|
|
matchedROI = p_extraROI->FindChildROI(name, p_extraROI);
|
|
}
|
|
}
|
|
|
|
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_extraROI, p_nextIndex, p_entries);
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::BuildROIMap(
|
|
LegoAnim* p_anim,
|
|
LegoROI* p_rootROI,
|
|
LegoROI* p_extraROI,
|
|
LegoROI**& p_roiMap,
|
|
MxU32& p_roiMapSize
|
|
)
|
|
{
|
|
if (!p_anim || !p_rootROI) {
|
|
return;
|
|
}
|
|
|
|
LegoTreeNode* root = p_anim->GetRoot();
|
|
if (!root) {
|
|
return;
|
|
}
|
|
|
|
MxU32 nextIndex = 1;
|
|
std::vector<LegoROI*> entries;
|
|
AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries);
|
|
|
|
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];
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::UpdateTransform(float p_deltaTime)
|
|
{
|
|
LERP3(m_currentPosition, m_currentPosition, m_targetPosition, 0.2f);
|
|
LERP3(m_currentDirection, m_currentDirection, m_targetDirection, 0.2f);
|
|
LERP3(m_currentUp, m_currentUp, m_targetUp, 0.2f);
|
|
|
|
// Character clones need negated direction
|
|
Mx3DPointFloat pos(m_currentPosition[0], m_currentPosition[1], m_currentPosition[2]);
|
|
Mx3DPointFloat dir(-m_currentDirection[0], -m_currentDirection[1], -m_currentDirection[2]);
|
|
Mx3DPointFloat up(m_currentUp[0], m_currentUp[1], m_currentUp[2]);
|
|
|
|
MxMatrix mat;
|
|
CalcLocalTransform(pos, dir, up, mat);
|
|
|
|
m_roi->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
|
|
VideoManager()->Get3DManager()->Moved(*m_roi);
|
|
|
|
if (m_vehicleROI && m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
|
|
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
|
|
VideoManager()->Get3DManager()->Moved(*m_vehicleROI);
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::UpdateAnimation(float p_deltaTime)
|
|
{
|
|
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
|
|
return;
|
|
}
|
|
|
|
// Determine the active walk/ride animation and its ROI map
|
|
LegoAnim* walkAnim = nullptr;
|
|
LegoROI** walkRoiMap = nullptr;
|
|
MxU32 walkRoiMapSize = 0;
|
|
|
|
if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) {
|
|
walkAnim = m_rideAnim;
|
|
walkRoiMap = m_rideRoiMap;
|
|
walkRoiMapSize = m_rideRoiMapSize;
|
|
}
|
|
else if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) {
|
|
walkAnim = m_walkAnimCache->anim;
|
|
walkRoiMap = m_walkAnimCache->roiMap;
|
|
walkRoiMapSize = m_walkAnimCache->roiMapSize;
|
|
}
|
|
|
|
// Ensure visibility of all mapped ROIs
|
|
if (walkRoiMap) {
|
|
for (MxU32 i = 1; i < walkRoiMapSize; i++) {
|
|
if (walkRoiMap[i] != nullptr) {
|
|
walkRoiMap[i]->SetVisibility(TRUE);
|
|
}
|
|
}
|
|
}
|
|
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
|
|
for (MxU32 i = 1; i < m_idleAnimCache->roiMapSize; i++) {
|
|
if (m_idleAnimCache->roiMap[i] != nullptr) {
|
|
m_idleAnimCache->roiMap[i]->SetVisibility(TRUE);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
|
|
bool isMoving = inVehicle || m_targetSpeed > 0.01f;
|
|
|
|
// Movement interrupts emotes
|
|
if (isMoving && m_emoteActive) {
|
|
m_emoteActive = false;
|
|
m_emoteAnimCache = nullptr;
|
|
}
|
|
|
|
if (isMoving) {
|
|
// Walking / riding
|
|
if (!walkAnim || !walkRoiMap) {
|
|
return;
|
|
}
|
|
|
|
if (m_targetSpeed > 0.01f) {
|
|
m_animTime += p_deltaTime * 2000.0f;
|
|
}
|
|
float duration = (float) walkAnim->GetDuration();
|
|
if (duration > 0.0f) {
|
|
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
|
|
|
|
MxMatrix transform(m_roi->GetLocal2World());
|
|
LegoTreeNode* root = walkAnim->GetRoot();
|
|
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
|
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) timeInCycle, walkRoiMap);
|
|
}
|
|
}
|
|
m_wasMoving = true;
|
|
m_idleTime = 0.0f;
|
|
m_idleAnimTime = 0.0f;
|
|
}
|
|
else if (m_emoteActive && m_emoteAnimCache && m_emoteAnimCache->anim && m_emoteAnimCache->roiMap) {
|
|
// Emote playback (one-shot)
|
|
m_emoteTime += p_deltaTime * 1000.0f;
|
|
|
|
if (m_emoteTime >= m_emoteDuration) {
|
|
// Emote completed -- return to stationary flow
|
|
m_emoteActive = false;
|
|
m_emoteAnimCache = nullptr;
|
|
m_wasMoving = false;
|
|
m_idleTime = 0.0f;
|
|
m_idleAnimTime = 0.0f;
|
|
}
|
|
else {
|
|
MxMatrix transform(m_roi->GetLocal2World());
|
|
LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot();
|
|
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
|
LegoROI::ApplyAnimationTransformation(
|
|
root->GetChild(i),
|
|
transform,
|
|
(LegoTime) m_emoteTime,
|
|
m_emoteAnimCache->roiMap
|
|
);
|
|
}
|
|
}
|
|
}
|
|
else if (m_idleAnimCache && m_idleAnimCache->anim && m_idleAnimCache->roiMap) {
|
|
// Idle animation
|
|
if (m_wasMoving) {
|
|
m_wasMoving = false;
|
|
m_idleTime = 0.0f;
|
|
m_idleAnimTime = 0.0f;
|
|
}
|
|
|
|
m_idleTime += p_deltaTime;
|
|
|
|
// Hold standing pose for 2.5s, then loop breathing/swaying
|
|
if (m_idleTime >= 2.5f) {
|
|
m_idleAnimTime += p_deltaTime * 1000.0f;
|
|
}
|
|
|
|
float duration = (float) m_idleAnimCache->anim->GetDuration();
|
|
if (duration > 0.0f) {
|
|
float timeInCycle = m_idleAnimTime - duration * floorf(m_idleAnimTime / duration);
|
|
|
|
MxMatrix transform(m_roi->GetLocal2World());
|
|
LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
|
|
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
|
LegoROI::ApplyAnimationTransformation(
|
|
root->GetChild(i),
|
|
transform,
|
|
(LegoTime) timeInCycle,
|
|
m_idleAnimCache->roiMap
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::UpdateVehicleState()
|
|
{
|
|
if (m_targetVehicleType != m_currentVehicleType) {
|
|
if (m_currentVehicleType != VEHICLE_NONE) {
|
|
ExitVehicle();
|
|
}
|
|
if (m_targetVehicleType != VEHICLE_NONE) {
|
|
EnterVehicle(m_targetVehicleType);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
|
|
{
|
|
if (p_vehicleType < 0 || p_vehicleType >= VEHICLE_COUNT) {
|
|
return;
|
|
}
|
|
|
|
m_currentVehicleType = p_vehicleType;
|
|
m_animTime = 0.0f;
|
|
|
|
if (IsLargeVehicle(p_vehicleType)) {
|
|
char vehicleName[48];
|
|
SDL_snprintf(vehicleName, sizeof(vehicleName), "%s_mp_%u", g_vehicleROINames[p_vehicleType], m_peerId);
|
|
|
|
m_vehicleROI = CharacterManager()->CreateAutoROI(vehicleName, g_vehicleROINames[p_vehicleType], FALSE);
|
|
if (m_vehicleROI) {
|
|
m_roi->SetVisibility(FALSE);
|
|
MxMatrix mat(m_roi->GetLocal2World());
|
|
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
|
|
m_vehicleROI->SetVisibility(m_visible ? TRUE : FALSE);
|
|
}
|
|
}
|
|
else {
|
|
const char* rideAnimName = g_rideAnimNames[p_vehicleType];
|
|
const char* vehicleVariantName = g_rideVehicleROINames[p_vehicleType];
|
|
|
|
if (!rideAnimName || !vehicleVariantName) {
|
|
return;
|
|
}
|
|
|
|
LegoWorld* world = CurrentWorld();
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
MxCore* presenter = world->Find("LegoAnimPresenter", rideAnimName);
|
|
if (!presenter) {
|
|
return;
|
|
}
|
|
|
|
LegoAnimPresenter* animPresenter = static_cast<LegoAnimPresenter*>(presenter);
|
|
m_rideAnim = animPresenter->GetAnimation();
|
|
if (!m_rideAnim) {
|
|
return;
|
|
}
|
|
|
|
// Use the base vehicle LOD (e.g. "moto", "bike") which is always loaded as
|
|
// a world object. The ride-specific variant LODs (e.g. "motoni", "bikebd")
|
|
// are only available when the original animation pipeline starts locally.
|
|
const char* baseName = g_vehicleROINames[p_vehicleType];
|
|
char variantName[48];
|
|
SDL_snprintf(variantName, sizeof(variantName), "%s_mp_%u", vehicleVariantName, m_peerId);
|
|
m_rideVehicleROI = CharacterManager()->CreateAutoROI(variantName, baseName, FALSE);
|
|
|
|
// Rename to variant name so FindChildROI can match animation tree nodes
|
|
// (e.g. "MOTONI" in the anim tree matches ROI named "motoni").
|
|
if (m_rideVehicleROI) {
|
|
m_rideVehicleROI->SetName(vehicleVariantName);
|
|
}
|
|
|
|
BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
|
|
}
|
|
}
|
|
|
|
void RemotePlayer::ExitVehicle()
|
|
{
|
|
if (m_currentVehicleType == VEHICLE_NONE) {
|
|
return;
|
|
}
|
|
|
|
if (m_vehicleROI) {
|
|
VideoManager()->Get3DManager()->Remove(*m_vehicleROI);
|
|
CharacterManager()->ReleaseAutoROI(m_vehicleROI);
|
|
m_vehicleROI = nullptr;
|
|
}
|
|
|
|
if (m_rideRoiMap) {
|
|
delete[] m_rideRoiMap;
|
|
m_rideRoiMap = nullptr;
|
|
m_rideRoiMapSize = 0;
|
|
}
|
|
if (m_rideVehicleROI) {
|
|
VideoManager()->Get3DManager()->Remove(*m_rideVehicleROI);
|
|
CharacterManager()->ReleaseAutoROI(m_rideVehicleROI);
|
|
m_rideVehicleROI = nullptr;
|
|
}
|
|
m_rideAnim = nullptr;
|
|
|
|
if (m_visible) {
|
|
m_roi->SetVisibility(TRUE);
|
|
}
|
|
|
|
m_currentVehicleType = VEHICLE_NONE;
|
|
m_animTime = 0.0f;
|
|
m_wasMoving = false;
|
|
}
|