Extract CharacterAnimator component, EncodeUsername utility, replace C stdlib with SDL

Extract ~420 lines of duplicated character animation logic from RemotePlayer
and ThirdPersonCamera into a shared CharacterAnimator component. Both classes
now compose a CharacterAnimator member that handles walk/idle/emote animation
playback, vehicle ride animations, click animation tracking, name bubbles,
and animation cache management.

Behavioral differences between consumers (emote transform save/restore) are
handled via CharacterAnimatorConfig.

Also extract duplicated username encoding (letter indices to ASCII) from
NetworkManager::BroadcastLocalState and ThirdPersonCamera::CreateNameBubble
into EncodeUsername() in protocol.cpp.

Replace C standard library usage across the multiplayer extension with SDL
equivalents: sprintf->SDL_snprintf, sscanf->SDL_sscanf, atoi->SDL_atoi,
strcmp->SDL_strcmp, fabsf->SDL_fabsf, floorf->SDL_floorf, and remove
unnecessary <cmath>, <cstdio>, <cstdlib> headers.
This commit is contained in:
Christian Semmler 2026-03-08 10:48:26 -07:00
parent 9e8ecd6d44
commit a5b2ea0ce9
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
14 changed files with 660 additions and 747 deletions

View File

@ -533,6 +533,7 @@ if (ISLE_EXTENSIONS)
extensions/src/textureloader.cpp
extensions/src/multiplayer.cpp
extensions/src/multiplayer/animutils.cpp
extensions/src/multiplayer/characteranimator.cpp
extensions/src/multiplayer/charactercloner.cpp
extensions/src/multiplayer/charactercustomizer.cpp
extensions/src/multiplayer/customizestate.cpp

View File

@ -0,0 +1,124 @@
#pragma once
#include "extensions/multiplayer/animutils.h"
#include "extensions/multiplayer/protocol.h"
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h"
#include <cstdint>
#include <map>
#include <string>
class LegoROI;
class LegoAnim;
namespace Multiplayer
{
class NameBubbleRenderer;
// Configuration for CharacterAnimator behavior that differs between consumers.
struct CharacterAnimatorConfig {
// When true, save/restore the parent ROI transform during emote playback
// to prevent scale accumulation (needed for ThirdPersonCamera's display clone).
bool saveEmoteTransform;
};
// Unified character animation component used by both RemotePlayer and ThirdPersonCamera.
// Handles walk/idle/emote animation playback, vehicle ride animations, click animation
// tracking, and name bubble management.
class CharacterAnimator {
public:
explicit CharacterAnimator(const CharacterAnimatorConfig& p_config);
~CharacterAnimator();
// Core animation tick. Call each frame with the character's ROI and movement state.
void Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving);
// Walk/idle animation selection
void SetWalkAnimId(uint8_t p_walkAnimId, LegoROI* p_roi);
void SetIdleAnimId(uint8_t p_idleAnimId, LegoROI* p_roi);
uint8_t GetWalkAnimId() const { return m_walkAnimId; }
uint8_t GetIdleAnimId() const { return m_idleAnimId; }
// Emote playback
void TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_isMoving);
// Click animation tracking
void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_clickAnimObjectId = p_clickAnimObjectId; }
void StopClickAnimation();
// Vehicle ride animation
void BuildRideAnimation(int8_t p_vehicleType, LegoROI* p_playerROI, uint32_t p_vehicleSuffix);
void ClearRideAnimation();
int8_t GetCurrentVehicleType() const { return m_currentVehicleType; }
void SetCurrentVehicleType(int8_t p_vehicleType) { m_currentVehicleType = p_vehicleType; }
bool IsInVehicle() const { return m_currentVehicleType != VEHICLE_NONE; }
LegoROI* GetRideVehicleROI() const { return m_rideVehicleROI; }
LegoAnim* GetRideAnim() const { return m_rideAnim; }
LegoROI** GetRideRoiMap() const { return m_rideRoiMap; }
MxU32 GetRideRoiMapSize() const { return m_rideRoiMapSize; }
// Animation cache management
void InitAnimCaches(LegoROI* p_roi);
void ClearAnimCaches();
void ClearAll();
void ApplyIdleFrame0(LegoROI* p_roi);
// Name bubble management
void CreateNameBubble(const char* p_name);
void DestroyNameBubble();
void SetNameBubbleVisible(bool p_visible);
void UpdateNameBubble(LegoROI* p_roi);
NameBubbleRenderer* GetNameBubble() const { return m_nameBubble; }
// Emote state accessors
bool IsEmoteActive() const { return m_emoteActive; }
// Animation time (needed for vehicle ride tick in ThirdPersonCamera)
float GetAnimTime() const { return m_animTime; }
void SetAnimTime(float p_time) { m_animTime = p_time; }
void ResetAnimState();
private:
using AnimCache = AnimUtils::AnimCache;
AnimCache* GetOrBuildAnimCache(LegoROI* p_roi, const char* p_animName);
CharacterAnimatorConfig m_config;
// Walk/idle state
uint8_t m_walkAnimId;
uint8_t m_idleAnimId;
AnimCache* m_walkAnimCache;
AnimCache* m_idleAnimCache;
float m_animTime;
float m_idleTime;
float m_idleAnimTime;
bool m_wasMoving;
// Emote state
AnimCache* m_emoteAnimCache;
float m_emoteTime;
float m_emoteDuration;
bool m_emoteActive;
MxMatrix m_emoteParentTransform;
// Click animation tracking (0 = none)
MxU32 m_clickAnimObjectId;
// ROI map cache: animation name -> cached ROI map
std::map<std::string, AnimCache> m_animCacheMap;
// Ride animation (vehicle-specific)
LegoAnim* m_rideAnim;
LegoROI** m_rideRoiMap;
MxU32 m_rideRoiMapSize;
LegoROI* m_rideVehicleROI;
int8_t m_currentVehicleType;
NameBubbleRenderer* m_nameBubble;
};
} // namespace Multiplayer

View File

@ -33,7 +33,7 @@ class NetworkManager : public MxCore {
MxBool IsA(const char* p_name) const override
{
return !strcmp(p_name, NetworkManager::ClassName()) || MxCore::IsA(p_name);
return !SDL_strcmp(p_name, NetworkManager::ClassName()) || MxCore::IsA(p_name);
}
void Initialize(NetworkTransport* p_transport, PlatformCallbacks* p_callbacks);
@ -53,8 +53,14 @@ class NetworkManager : public MxCore {
// Thread-safe request methods for cross-thread callers (e.g. WASM exports
// running on the browser main thread). Deferred to the game thread in Tickle().
void RequestToggleThirdPerson() { m_pendingToggleThirdPerson.store(true, std::memory_order_relaxed); }
void RequestSetWalkAnimation(uint8_t p_walkAnimId) { m_pendingWalkAnim.store(p_walkAnimId, std::memory_order_relaxed); }
void RequestSetIdleAnimation(uint8_t p_idleAnimId) { m_pendingIdleAnim.store(p_idleAnimId, std::memory_order_relaxed); }
void RequestSetWalkAnimation(uint8_t p_walkAnimId)
{
m_pendingWalkAnim.store(p_walkAnimId, std::memory_order_relaxed);
}
void RequestSetIdleAnimation(uint8_t p_idleAnimId)
{
m_pendingIdleAnim.store(p_idleAnimId, std::memory_order_relaxed);
}
void RequestSendEmote(uint8_t p_emoteId) { m_pendingEmote.store(p_emoteId, std::memory_order_relaxed); }
void RequestToggleNameBubbles() { m_pendingToggleNameBubbles.store(true, std::memory_order_relaxed); }
void RequestToggleAllowCustomize() { m_pendingToggleAllowCustomize.store(true, std::memory_order_relaxed); }

View File

@ -11,8 +11,8 @@ namespace Multiplayer
{
// Routing target constants for MessageHeader.target
const uint32_t TARGET_BROADCAST = 0; // Broadcast to all except sender
const uint32_t TARGET_HOST = 0xFFFFFFFF; // Send to host only
const uint32_t TARGET_BROADCAST = 0; // Broadcast to all except sender
const uint32_t TARGET_HOST = 0xFFFFFFFF; // Send to host only
const uint32_t TARGET_BROADCAST_ALL = 0xFFFFFFFE; // Broadcast to all including sender
enum MessageType : uint8_t {
@ -94,12 +94,12 @@ struct PlayerStateMsg {
float direction[3];
float up[3];
float speed;
uint8_t walkAnimId; // Index into walk animation table (0 = default)
uint8_t idleAnimId; // Index into idle animation table (0 = default)
char name[8]; // Player display name (7 chars + null terminator)
uint8_t walkAnimId; // Index into walk animation table (0 = default)
uint8_t idleAnimId; // Index into idle animation table (0 = default)
char name[8]; // Player display name (7 chars + null terminator)
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
uint8_t customizeData[5]; // Packed CustomizeState
uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize
uint8_t customizeData[5]; // Packed CustomizeState
uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize
};
// Server -> all: announces which peer is the host
@ -181,6 +181,10 @@ inline bool IsValidActorId(uint8_t p_actorId)
return p_actorId >= 1 && p_actorId <= 5;
}
// Convert LegoGameState::Username letter indices (0-25 = A-Z) to ASCII.
// Writes up to 7 characters + null terminator into p_out (must be at least 8 bytes).
void EncodeUsername(char p_out[8]);
static const uint8_t DISPLAY_ACTOR_NONE = 0xFF;
// Parse the message type from a buffer. Returns MSG type or 0 on error.

View File

@ -1,24 +1,19 @@
#pragma once
#include "extensions/multiplayer/animutils.h"
#include "extensions/multiplayer/characteranimator.h"
#include "extensions/multiplayer/customizestate.h"
#include "extensions/multiplayer/protocol.h"
#include "mxtypes.h"
#include <cstdint>
#include <map>
#include <string>
class LegoROI;
class LegoWorld;
class LegoAnim;
class LegoTreeNode;
namespace Multiplayer
{
class NameBubbleRenderer;
class RemotePlayer {
public:
RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex);
@ -48,18 +43,14 @@ class RemotePlayer {
const CustomizeState& GetCustomizeState() const { return m_customizeState; }
bool GetAllowRemoteCustomize() const { return m_allowRemoteCustomize; }
void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_clickAnimObjectId = p_clickAnimObjectId; }
void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_animator.SetClickAnimObjectId(p_clickAnimObjectId); }
void StopClickAnimation();
bool IsInVehicle() const { return m_currentVehicleType != VEHICLE_NONE; }
bool IsMoving() const { return m_currentVehicleType != VEHICLE_NONE || m_targetSpeed > 0.01f; }
bool IsInVehicle() const { return m_animator.IsInVehicle(); }
bool IsMoving() const { return m_animator.IsInVehicle() || m_targetSpeed > 0.01f; }
private:
using AnimCache = AnimUtils::AnimCache;
AnimCache* GetOrBuildAnimCache(const char* p_animName);
const char* GetDisplayActorName() const;
void UpdateTransform(float p_deltaTime);
void UpdateAnimation(float p_deltaTime);
void UpdateVehicleState();
void EnterVehicle(int8_t p_vehicleType);
void ExitVehicle();
@ -87,38 +78,9 @@ class RemotePlayer {
float m_currentDirection[3];
float m_currentUp[3];
// Animation state
uint8_t m_walkAnimId;
uint8_t m_idleAnimId;
AnimCache* m_walkAnimCache;
AnimCache* m_idleAnimCache;
float m_animTime;
float m_idleTime;
float m_idleAnimTime;
bool m_wasMoving;
// Emote state
AnimCache* m_emoteAnimCache;
float m_emoteTime;
float m_emoteDuration;
bool m_emoteActive;
// Click animation tracking (0 = none)
MxU32 m_clickAnimObjectId;
// ROI map cache: animation name -> cached ROI map (invalidated on world change)
std::map<std::string, AnimCache> m_animCacheMap;
// Ride animation (vehicle-specific, not cached globally)
LegoAnim* m_rideAnim;
LegoROI** m_rideRoiMap;
MxU32 m_rideRoiMapSize;
LegoROI* m_rideVehicleROI;
CharacterAnimator m_animator;
LegoROI* m_vehicleROI;
int8_t m_currentVehicleType;
NameBubbleRenderer* m_nameBubble;
CustomizeState m_customizeState;
bool m_allowRemoteCustomize;

View File

@ -1,28 +1,22 @@
#pragma once
#include "extensions/multiplayer/animutils.h"
#include "extensions/multiplayer/characteranimator.h"
#include "extensions/multiplayer/customizestate.h"
#include "extensions/multiplayer/protocol.h"
#include "mxgeometry/mxgeometry3d.h"
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h"
#include <SDL3/SDL_events.h>
#include <cstdint>
#include <map>
#include <string>
class IslePathActor;
class LegoPathActor;
class LegoROI;
class LegoWorld;
class LegoAnim;
namespace Multiplayer
{
class NameBubbleRenderer;
class ThirdPersonCamera {
public:
ThirdPersonCamera();
@ -50,9 +44,9 @@ class ThirdPersonCamera {
CustomizeState& GetCustomizeState() { return m_customizeState; }
void ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex);
void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_clickAnimObjectId = p_clickAnimObjectId; }
void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_animator.SetClickAnimObjectId(p_clickAnimObjectId); }
void StopClickAnimation();
bool IsInVehicle() const { return m_currentVehicleType != VEHICLE_NONE; }
bool IsInVehicle() const { return m_animator.IsInVehicle(); }
void SetNameBubbleVisible(bool p_visible);
@ -70,14 +64,7 @@ class ThirdPersonCamera {
void ClampPitch();
void ClampDistance();
using AnimCache = AnimUtils::AnimCache;
AnimCache* GetOrBuildAnimCache(const char* p_animName);
void ClearAnimCaches();
void SetupCamera(LegoPathActor* p_actor);
void BuildRideAnimation(int8_t p_vehicleType);
void ClearRideAnimation();
void ApplyIdleFrame0();
void ReinitForCharacter();
void CreateNameBubble();
@ -90,45 +77,17 @@ class ThirdPersonCamera {
bool m_enabled;
bool m_active;
bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies
bool m_roiUnflipped; // True when Disable() flipped the ROI direction; ReinitForCharacter re-applies
LegoROI* m_playerROI; // Borrowed, not owned
// Display actor override
uint8_t m_displayActorIndex;
LegoROI* m_displayROI; // Owned clone; nullptr = use native ROI
LegoROI* m_displayROI; // Owned clone; nullptr = use native ROI
char m_displayUniqueName[32];
CustomizeState m_customizeState;
// Walk/idle state (same pattern as RemotePlayer)
uint8_t m_walkAnimId;
uint8_t m_idleAnimId;
AnimCache* m_walkAnimCache;
AnimCache* m_idleAnimCache;
float m_animTime;
float m_idleTime;
float m_idleAnimTime;
bool m_wasMoving;
CharacterAnimator m_animator;
// Emote state
AnimCache* m_emoteAnimCache;
float m_emoteTime;
float m_emoteDuration;
bool m_emoteActive;
MxMatrix m_emoteParentTransform;
// Click animation tracking (0 = none)
MxU32 m_clickAnimObjectId;
// Vehicle ride state
int8_t m_currentVehicleType;
LegoAnim* m_rideAnim;
LegoROI** m_rideRoiMap;
MxU32 m_rideRoiMapSize;
LegoROI* m_rideVehicleROI;
std::map<std::string, AnimCache> m_animCacheMap;
NameBubbleRenderer* m_nameBubble;
bool m_showNameBubble;
// Orbit camera state

View File

@ -0,0 +1,366 @@
#include "extensions/multiplayer/characteranimator.h"
#include "3dmanager/lego3dmanager.h"
#include "anim/legoanim.h"
#include "extensions/multiplayer/charactercustomizer.h"
#include "extensions/multiplayer/namebubblerenderer.h"
#include "legoanimpresenter.h"
#include "legocharactermanager.h"
#include "legovideomanager.h"
#include "legoworld.h"
#include "misc.h"
#include "misc/legotree.h"
#include "realtime/realtime.h"
#include "roi/legoroi.h"
#include <SDL3/SDL_stdinc.h>
using namespace Multiplayer;
CharacterAnimator::CharacterAnimator(const CharacterAnimatorConfig& p_config)
: m_config(p_config), 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_clickAnimObjectId(0), m_rideAnim(nullptr),
m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE),
m_nameBubble(nullptr)
{
}
CharacterAnimator::~CharacterAnimator()
{
DestroyNameBubble();
ClearRideAnimation();
}
CharacterAnimator::AnimCache* CharacterAnimator::GetOrBuildAnimCache(LegoROI* p_roi, const char* p_animName)
{
return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, p_roi, p_animName);
}
void CharacterAnimator::Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving)
{
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
StopClickAnimation();
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) {
AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize);
}
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize);
}
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
bool isMoving = inVehicle || p_isMoving;
// Movement interrupts click animations and emotes
if (isMoving) {
StopClickAnimation();
if (m_emoteActive) {
m_emoteActive = false;
m_emoteAnimCache = nullptr;
}
}
if (isMoving) {
// Walking / riding
if (!walkAnim || !walkRoiMap) {
return;
}
if (p_isMoving) {
m_animTime += p_deltaTime * 2000.0f;
}
float duration = (float) walkAnim->GetDuration();
if (duration > 0.0f) {
float timeInCycle = m_animTime - duration * SDL_floorf(m_animTime / duration);
MxMatrix transform(p_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_config.saveEmoteTransform ? m_emoteParentTransform : p_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
);
}
// Restore player ROI transform (animation root overwrote it).
if (m_config.saveEmoteTransform) {
p_roi->WrappedSetLocal2WorldWithWorldDataUpdate(m_emoteParentTransform);
}
}
}
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 * SDL_floorf(m_idleAnimTime / duration);
MxMatrix transform(p_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 CharacterAnimator::SetWalkAnimId(uint8_t p_walkAnimId, LegoROI* p_roi)
{
if (p_walkAnimId >= g_walkAnimCount) {
return;
}
if (p_walkAnimId != m_walkAnimId) {
m_walkAnimId = p_walkAnimId;
if (p_roi) {
m_walkAnimCache = GetOrBuildAnimCache(p_roi, g_walkAnimNames[m_walkAnimId]);
}
}
}
void CharacterAnimator::SetIdleAnimId(uint8_t p_idleAnimId, LegoROI* p_roi)
{
if (p_idleAnimId >= g_idleAnimCount) {
return;
}
if (p_idleAnimId != m_idleAnimId) {
m_idleAnimId = p_idleAnimId;
if (p_roi) {
m_idleAnimCache = GetOrBuildAnimCache(p_roi, g_idleAnimNames[m_idleAnimId]);
}
}
}
void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_isMoving)
{
if (p_emoteId >= g_emoteAnimCount || !p_roi) {
return;
}
// Only play emotes when stationary
if (p_isMoving) {
return;
}
AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteAnimNames[p_emoteId]);
if (!cache || !cache->anim) {
return;
}
StopClickAnimation();
m_emoteAnimCache = cache;
m_emoteTime = 0.0f;
m_emoteDuration = (float) cache->anim->GetDuration();
m_emoteActive = true;
// Save clean transform to prevent scale accumulation during emote
if (m_config.saveEmoteTransform) {
m_emoteParentTransform = p_roi->GetLocal2World();
}
}
void CharacterAnimator::StopClickAnimation()
{
if (m_clickAnimObjectId != 0) {
CharacterCustomizer::StopClickAnimation(m_clickAnimObjectId);
m_clickAnimObjectId = 0;
}
}
void CharacterAnimator::BuildRideAnimation(int8_t p_vehicleType, LegoROI* p_playerROI, uint32_t p_vehicleSuffix)
{
if (p_vehicleType < 0 || p_vehicleType >= VEHICLE_COUNT) {
return;
}
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;
}
m_rideAnim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
if (!m_rideAnim) {
return;
}
// Create variant ROI, rename to match animation tree.
const char* baseName = g_vehicleROINames[p_vehicleType];
char variantName[48];
if (p_vehicleSuffix != 0) {
SDL_snprintf(variantName, sizeof(variantName), "%s_mp_%u", vehicleVariantName, p_vehicleSuffix);
}
else {
SDL_snprintf(variantName, sizeof(variantName), "tp_vehicle");
}
m_rideVehicleROI = CharacterManager()->CreateAutoROI(variantName, baseName, FALSE);
if (m_rideVehicleROI) {
m_rideVehicleROI->SetName(vehicleVariantName);
}
AnimUtils::BuildROIMap(m_rideAnim, p_playerROI, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
m_animTime = 0.0f;
}
void CharacterAnimator::ClearRideAnimation()
{
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;
m_currentVehicleType = VEHICLE_NONE;
}
void CharacterAnimator::InitAnimCaches(LegoROI* p_roi)
{
m_walkAnimCache = GetOrBuildAnimCache(p_roi, g_walkAnimNames[m_walkAnimId]);
m_idleAnimCache = GetOrBuildAnimCache(p_roi, g_idleAnimNames[m_idleAnimId]);
}
void CharacterAnimator::ClearAnimCaches()
{
m_walkAnimCache = nullptr;
m_idleAnimCache = nullptr;
m_emoteAnimCache = nullptr;
m_emoteActive = false;
}
void CharacterAnimator::ClearAll()
{
m_animCacheMap.clear();
ClearAnimCaches();
}
void CharacterAnimator::ResetAnimState()
{
m_animTime = 0.0f;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
m_wasMoving = false;
m_emoteActive = false;
}
void CharacterAnimator::ApplyIdleFrame0(LegoROI* p_roi)
{
if (!p_roi || !m_idleAnimCache || !m_idleAnimCache->anim || !m_idleAnimCache->roiMap) {
return;
}
MxMatrix transform(p_roi->GetLocal2World());
LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) 0.0f, m_idleAnimCache->roiMap);
}
}
void CharacterAnimator::CreateNameBubble(const char* p_name)
{
if (m_nameBubble || !p_name || p_name[0] == '\0') {
return;
}
m_nameBubble = new NameBubbleRenderer();
m_nameBubble->Create(p_name);
}
void CharacterAnimator::DestroyNameBubble()
{
if (m_nameBubble) {
delete m_nameBubble;
m_nameBubble = nullptr;
}
}
void CharacterAnimator::SetNameBubbleVisible(bool p_visible)
{
if (m_nameBubble) {
m_nameBubble->SetVisible(p_visible);
}
}
void CharacterAnimator::UpdateNameBubble(LegoROI* p_roi)
{
if (m_nameBubble) {
m_nameBubble->Update(p_roi);
}
}

View File

@ -11,7 +11,6 @@
#include "viewmanager/viewlodlist.h"
#include <SDL3/SDL_stdinc.h>
#include <cstdio>
#include <vec.h>
using namespace Multiplayer;
@ -69,7 +68,7 @@ LegoROI* CharacterCloner::Clone(LegoCharacterManager* p_charMgr, const char* p_u
ViewLODList* lodList = lodManager->Lookup(parentName);
MxS32 lodSize = lodList->Size();
sprintf(lodName, "%s%d", p_uniqueName, i);
SDL_snprintf(lodName, sizeof(lodName), "%s%d", p_uniqueName, i);
ViewLODList* dupLodList = lodManager->Create(lodName, lodSize);
for (MxS32 j = 0; j < lodSize; j++) {

View File

@ -1,11 +1,10 @@
#include "extensions/multiplayer/charactercustomizer.h"
#include "extensions/multiplayer/charactercloner.h"
#include "extensions/multiplayer/customizestate.h"
#include "extensions/multiplayer/protocol.h"
#include "3dmanager/lego3dmanager.h"
#include "3dmanager/lego3dview.h"
#include "extensions/multiplayer/charactercloner.h"
#include "extensions/multiplayer/customizestate.h"
#include "extensions/multiplayer/protocol.h"
#include "legoactors.h"
#include "legocharactermanager.h"
#include "legovideomanager.h"
@ -19,7 +18,6 @@
#include "viewmanager/viewmanager.h"
#include <SDL3/SDL_stdinc.h>
#include <cstdio>
using namespace Multiplayer;
@ -195,20 +193,15 @@ int CharacterCustomizer::MapClickedPartIndex(const char* p_partName)
return -1;
}
void CharacterCustomizer::ApplyFullState(
LegoROI* p_rootROI,
uint8_t p_actorInfoIndex,
const CustomizeState& p_state
)
void CharacterCustomizer::ApplyFullState(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state)
{
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return;
}
// Apply colors for the 6 independent colorable parts
static const int colorableParts[] = {
c_infohatPart, c_infogronPart, c_armlftPart, c_armrtPart, c_leglftPart, c_legrtPart
};
static const int colorableParts[] =
{c_infohatPart, c_infogronPart, c_armlftPart, c_armrtPart, c_leglftPart, c_legrtPart};
for (int i = 0; i < (int) sizeOfArray(colorableParts); i++) {
int partIndex = colorableParts[i];
@ -242,11 +235,7 @@ void CharacterCustomizer::ApplyFullState(
}
}
void CharacterCustomizer::ApplyHatVariant(
LegoROI* p_rootROI,
uint8_t p_actorInfoIndex,
const CustomizeState& p_state
)
void CharacterCustomizer::ApplyHatVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state)
{
if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) {
return;
@ -266,7 +255,7 @@ void CharacterCustomizer::ApplyHatVariant(
ViewLODList* lodList = GetViewLODListManager()->Lookup(part.m_partName[partNameIndex]);
MxS32 lodSize = lodList->Size();
sprintf(lodName, "%s_cv%u", p_rootROI->GetName(), s_variantCounter++);
SDL_snprintf(lodName, sizeof(lodName), "%s_cv%u", p_rootROI->GetName(), s_variantCounter++);
ViewLODList* dupLodList = GetViewLODListManager()->Create(lodName, lodSize);
Tgl::Renderer* renderer = VideoManager()->GetRenderer();
@ -300,8 +289,8 @@ void CharacterCustomizer::ApplyHatVariant(
void CharacterCustomizer::PlayClickSound(LegoROI* p_roi, const CustomizeState& p_state, bool p_basedOnMood)
{
MxU32 objectId = p_basedOnMood ? (p_state.mood + g_characterSoundIdMoodOffset)
: (p_state.sound + g_characterSoundIdOffset);
MxU32 objectId =
p_basedOnMood ? (p_state.mood + g_characterSoundIdMoodOffset) : (p_state.sound + g_characterSoundIdOffset);
if (objectId) {
MxDSAction action;

View File

@ -334,25 +334,7 @@ void NetworkManager::BroadcastLocalState()
msg.walkAnimId = m_localWalkAnimId;
msg.idleAnimId = m_localIdleAnimId;
// Convert Username letters (0-25 = A-Z) to ASCII string.
// The active player is always at m_players[0] after RegisterPlayer/SwitchPlayer.
SDL_memset(msg.name, 0, sizeof(msg.name));
LegoGameState* gs = GameState();
if (gs && gs->m_playerCount > 0) {
const LegoGameState::Username& username = gs->m_players[0];
for (int i = 0; i < 7; i++) {
MxS16 letter = username.m_letters[i];
if (letter < 0) {
break;
}
if (letter <= 25) {
msg.name[i] = (char) ('A' + letter);
}
else {
msg.name[i] = '?';
}
}
}
EncodeUsername(msg.name);
msg.displayActorIndex = m_localDisplayActorIndex;

View File

@ -1,8 +1,10 @@
#include "extensions/multiplayer/protocol.h"
#include "legogamestate.h"
#include "legopathactor.h"
#include "misc.h"
#include <cstddef>
#include <SDL3/SDL_stdinc.h>
namespace Multiplayer
{
@ -36,12 +38,10 @@ const char* const g_vehicleROINames[VEHICLE_COUNT] =
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
// Ride animation names for small vehicles (NULL = large vehicle, no ride anim)
const char* const g_rideAnimNames[VEHICLE_COUNT] =
{NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
const char* const g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
// Vehicle variant ROI names used in ride animations
const char* const g_rideVehicleROINames[VEHICLE_COUNT] =
{NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
const char* const g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
bool IsLargeVehicle(int8_t p_vehicleType)
{
@ -76,4 +76,25 @@ int8_t DetectVehicleType(LegoPathActor* p_actor)
return VEHICLE_NONE;
}
void EncodeUsername(char p_out[8])
{
SDL_memset(p_out, 0, 8);
LegoGameState* gs = GameState();
if (gs && gs->m_playerCount > 0) {
const LegoGameState::Username& username = gs->m_players[0];
for (int i = 0; i < 7; i++) {
MxS16 letter = username.m_letters[i];
if (letter < 0) {
break;
}
if (letter <= 25) {
p_out[i] = (char) ('A' + letter);
}
else {
p_out[i] = '?';
}
}
}
}
} // namespace Multiplayer

View File

@ -1,17 +1,12 @@
#include "extensions/multiplayer/remoteplayer.h"
#include "extensions/multiplayer/charactercustomizer.h"
#include "extensions/multiplayer/namebubblerenderer.h"
#include "3dmanager/lego3dmanager.h"
#include "anim/legoanim.h"
#include "extensions/multiplayer/charactercloner.h"
#include "legoanimpresenter.h"
#include "extensions/multiplayer/charactercustomizer.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"
@ -19,7 +14,6 @@
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h>
#include <cmath>
#include <vec.h>
using namespace Multiplayer;
@ -27,11 +21,8 @@ using namespace Multiplayer;
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex)
: m_peerId(p_peerId), m_actorId(p_actorId), m_displayActorIndex(p_displayActorIndex), 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_clickAnimObjectId(0), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0),
m_rideVehicleROI(nullptr), m_vehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE), m_nameBubble(nullptr),
m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false),
m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/false}), m_vehicleROI(nullptr),
m_allowRemoteCustomize(true)
{
m_displayName[0] = '\0';
@ -89,8 +80,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld)
m_customizeState.InitFromActorInfo(actorInfoIndex);
// Build initial walk and idle animation caches
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
m_animator.InitAnimCaches(m_roi);
// Create name bubble if we already have a name
if (m_displayName[0] != '\0') {
@ -104,7 +94,7 @@ void RemotePlayer::Despawn()
return;
}
StopClickAnimation();
m_animator.StopClickAnimation();
DestroyNameBubble();
ExitVehicle();
@ -115,11 +105,7 @@ void RemotePlayer::Despawn()
}
// Clear cached animation ROI maps (anim pointers are world-owned).
m_animCacheMap.clear();
m_walkAnimCache = nullptr;
m_idleAnimCache = nullptr;
m_emoteAnimCache = nullptr;
m_emoteActive = false;
m_animator.ClearAll();
m_spawned = false;
m_visible = false;
@ -187,15 +173,13 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg)
m_allowRemoteCustomize = (p_msg.customizeFlags & 0x01) != 0;
// 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]);
if (p_msg.walkAnimId != m_animator.GetWalkAnimId() && p_msg.walkAnimId < g_walkAnimCount) {
m_animator.SetWalkAnimId(p_msg.walkAnimId, m_roi);
}
// 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]);
if (p_msg.idleAnimId != m_animator.GetIdleAnimId() && p_msg.idleAnimId < g_idleAnimCount) {
m_animator.SetIdleAnimId(p_msg.idleAnimId, m_roi);
}
}
@ -207,12 +191,10 @@ void RemotePlayer::Tick(float p_deltaTime)
UpdateVehicleState();
UpdateTransform(p_deltaTime);
UpdateAnimation(p_deltaTime);
m_animator.Tick(p_deltaTime, m_roi, m_targetSpeed > 0.01f);
// Update name bubble position and billboard orientation
if (m_nameBubble) {
m_nameBubble->Update(m_roi);
}
m_animator.UpdateNameBubble(m_roi);
}
void RemotePlayer::ReAddToScene()
@ -223,8 +205,8 @@ void RemotePlayer::ReAddToScene()
if (m_vehicleROI) {
VideoManager()->Get3DManager()->Add(*m_vehicleROI);
}
if (m_rideVehicleROI) {
VideoManager()->Get3DManager()->Add(*m_rideVehicleROI);
if (m_animator.GetRideVehicleROI()) {
VideoManager()->Get3DManager()->Add(*m_animator.GetRideVehicleROI());
}
}
@ -237,7 +219,7 @@ void RemotePlayer::SetVisible(bool p_visible)
m_visible = p_visible;
if (p_visible) {
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE && IsLargeVehicle(m_animator.GetCurrentVehicleType())) {
m_roi->SetVisibility(FALSE);
if (m_vehicleROI) {
m_vehicleROI->SetVisibility(TRUE);
@ -255,42 +237,21 @@ void RemotePlayer::SetVisible(bool p_visible)
if (m_vehicleROI) {
m_vehicleROI->SetVisibility(FALSE);
}
if (m_rideVehicleROI) {
m_rideVehicleROI->SetVisibility(FALSE);
if (m_animator.GetRideVehicleROI()) {
m_animator.GetRideVehicleROI()->SetVisibility(FALSE);
}
}
}
RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName)
{
return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, m_roi, p_animName);
}
void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
{
if (p_emoteId >= g_emoteAnimCount || !m_spawned) {
if (!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;
}
StopClickAnimation();
m_emoteAnimCache = cache;
m_emoteTime = 0.0f;
m_emoteDuration = (float) cache->anim->GetDuration();
m_emoteActive = true;
m_animator.TriggerEmote(p_emoteId, m_roi, m_targetSpeed > 0.01f);
}
void RemotePlayer::UpdateTransform(float p_deltaTime)
{
LERP3(m_currentPosition, m_currentPosition, m_targetPosition, 0.2f);
@ -311,140 +272,17 @@ void RemotePlayer::UpdateTransform(float p_deltaTime)
m_roi->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
VideoManager()->Get3DManager()->Moved(*m_roi);
if (m_vehicleROI && m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
if (m_vehicleROI && m_animator.GetCurrentVehicleType() != VEHICLE_NONE &&
IsLargeVehicle(m_animator.GetCurrentVehicleType())) {
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
VideoManager()->Get3DManager()->Moved(*m_vehicleROI);
}
}
void RemotePlayer::UpdateAnimation(float p_deltaTime)
{
if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) {
StopClickAnimation();
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) {
AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize);
}
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize);
}
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
bool isMoving = inVehicle || m_targetSpeed > 0.01f;
// Movement interrupts click animations and emotes
if (isMoving) {
StopClickAnimation();
if (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) {
if (m_targetVehicleType != m_animator.GetCurrentVehicleType()) {
if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) {
ExitVehicle();
}
if (m_targetVehicleType != VEHICLE_NONE) {
@ -459,8 +297,8 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
return;
}
m_currentVehicleType = p_vehicleType;
m_animTime = 0.0f;
m_animator.SetCurrentVehicleType(p_vehicleType);
m_animator.SetAnimTime(0.0f);
if (IsLargeVehicle(p_vehicleType)) {
char vehicleName[48];
@ -475,50 +313,13 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
}
}
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);
}
AnimUtils::BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
m_animator.BuildRideAnimation(p_vehicleType, m_roi, m_peerId);
}
}
void RemotePlayer::ExitVehicle()
{
if (m_currentVehicleType == VEHICLE_NONE) {
if (m_animator.GetCurrentVehicleType() == VEHICLE_NONE) {
return;
}
@ -528,57 +329,31 @@ void RemotePlayer::ExitVehicle()
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;
m_animator.ClearRideAnimation();
if (m_visible) {
m_roi->SetVisibility(TRUE);
}
m_currentVehicleType = VEHICLE_NONE;
m_animTime = 0.0f;
m_wasMoving = false;
m_animator.SetAnimTime(0.0f);
}
void RemotePlayer::CreateNameBubble()
{
if (m_nameBubble || m_displayName[0] == '\0') {
return;
}
m_nameBubble = new NameBubbleRenderer();
m_nameBubble->Create(m_displayName);
m_animator.CreateNameBubble(m_displayName);
}
void RemotePlayer::DestroyNameBubble()
{
if (m_nameBubble) {
delete m_nameBubble;
m_nameBubble = nullptr;
}
m_animator.DestroyNameBubble();
}
void RemotePlayer::SetNameBubbleVisible(bool p_visible)
{
if (m_nameBubble) {
m_nameBubble->SetVisible(p_visible);
}
m_animator.SetNameBubbleVisible(p_visible);
}
void RemotePlayer::StopClickAnimation()
{
if (m_clickAnimObjectId != 0) {
CharacterCustomizer::StopClickAnimation(m_clickAnimObjectId);
m_clickAnimObjectId = 0;
}
m_animator.StopClickAnimation();
}

View File

@ -4,10 +4,7 @@
#include "anim/legoanim.h"
#include "extensions/multiplayer/charactercloner.h"
#include "extensions/multiplayer/charactercustomizer.h"
#include "extensions/multiplayer/namebubblerenderer.h"
#include "islepathactor.h"
#include "legogamestate.h"
#include "legoanimpresenter.h"
#include "legocameracontroller.h"
#include "legocharactermanager.h"
#include "legovideomanager.h"
@ -20,7 +17,6 @@
#include "roi/legoroi.h"
#include <SDL3/SDL_stdinc.h>
#include <cmath>
using namespace Multiplayer;
@ -39,13 +35,10 @@ static void FlipROIDirection(LegoROI* p_roi)
ThirdPersonCamera::ThirdPersonCamera()
: m_enabled(false), m_active(false), m_roiUnflipped(false), m_playerROI(nullptr),
m_displayActorIndex(DISPLAY_ACTOR_NONE), m_displayROI(nullptr), 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_clickAnimObjectId(0), m_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr),
m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_nameBubble(nullptr), m_showNameBubble(true),
m_orbitYaw(DEFAULT_ORBIT_YAW),
m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), m_touch{}
m_displayActorIndex(DISPLAY_ACTOR_NONE), m_displayROI(nullptr),
m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true}), m_showNameBubble(true),
m_orbitYaw(DEFAULT_ORBIT_YAW), m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE),
m_touch{}
{
SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName));
}
@ -94,9 +87,8 @@ void ThirdPersonCamera::Disable()
m_active = false;
DestroyNameBubble();
DestroyDisplayClone();
ClearRideAnimation();
m_animCacheMap.clear();
ClearAnimCaches();
m_animator.ClearRideAnimation();
m_animator.ClearAll();
ResetOrbitState();
}
@ -110,7 +102,7 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
// Always track vehicle type so OnActorExit can handle exits
// even if Enable() was called after entering the vehicle.
m_currentVehicleType = DetectVehicleType(userActor);
m_animator.SetCurrentVehicleType(DetectVehicleType(userActor));
// Enter() calls TurnAround(), so any previous undo is superseded.
m_roiUnflipped = false;
@ -124,9 +116,10 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
return;
}
if (m_currentVehicleType != VEHICLE_NONE) {
if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) {
// Large vehicles and helicopter: stay first-person.
if (IsLargeVehicle(m_currentVehicleType) || m_currentVehicleType == VEHICLE_HELICOPTER) {
if (IsLargeVehicle(m_animator.GetCurrentVehicleType()) ||
m_animator.GetCurrentVehicleType() == VEHICLE_HELICOPTER) {
// Hide walking character ROI (Enter doesn't call Exit on it).
if (m_playerROI) {
m_playerROI->SetVisibility(FALSE);
@ -150,7 +143,7 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
m_active = true;
SetupCamera(userActor);
BuildRideAnimation(m_currentVehicleType);
m_animator.BuildRideAnimation(m_animator.GetCurrentVehicleType(), m_playerROI, 0);
CreateNameBubble();
return;
}
@ -169,18 +162,11 @@ void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
VideoManager()->Get3DManager()->Remove(*m_playerROI);
VideoManager()->Get3DManager()->Add(*m_playerROI);
// Build animation caches
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
// Build animation caches and reset state
m_animator.InitAnimCaches(m_playerROI);
m_animator.ResetAnimState();
// Reset animation state
m_animTime = 0.0f;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
m_wasMoving = false;
m_emoteActive = false;
ApplyIdleFrame0();
m_animator.ApplyIdleFrame0(m_playerROI);
SetupCamera(userActor);
CreateNameBubble();
@ -194,7 +180,7 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor)
// For vehicle exit, p_actor is the vehicle, not UserActor —
// check m_currentVehicleType instead.
if (m_currentVehicleType != VEHICLE_NONE) {
if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) {
// When 3rd-person camera is active, movement inversion causes the
// vehicle to physically drive opposite to vanilla. CalculateTransform
// re-inverts to keep the ROI z backward. Exit()'s TurnAround restores
@ -206,9 +192,8 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor)
}
// Exiting a vehicle: reinitialize for the walking character.
ClearRideAnimation();
ClearAnimCaches();
m_animCacheMap.clear();
m_animator.ClearRideAnimation();
m_animator.ClearAll();
ReinitForCharacter();
}
else if (m_active && static_cast<LegoPathActor*>(p_actor) == UserActor()) {
@ -218,9 +203,8 @@ void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor)
m_playerROI->SetVisibility(FALSE);
VideoManager()->Get3DManager()->Remove(*m_playerROI);
}
ClearRideAnimation();
ClearAnimCaches();
m_currentVehicleType = VEHICLE_NONE;
m_animator.ClearRideAnimation();
m_animator.ClearAll();
m_playerROI = nullptr;
m_active = false;
}
@ -258,26 +242,24 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
// Update orbit camera position each frame so it tracks the player
ApplyOrbitCamera();
if (m_nameBubble) {
m_nameBubble->Update(m_playerROI);
}
m_animator.UpdateNameBubble(m_playerROI);
// Small vehicle with ride animation (like RemotePlayer)
if (m_currentVehicleType != VEHICLE_NONE) {
StopClickAnimation();
if (m_rideAnim && m_rideRoiMap) {
// Small vehicle with ride animation
if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) {
m_animator.StopClickAnimation();
if (m_animator.GetRideAnim() && m_animator.GetRideRoiMap()) {
LegoPathActor* actor = UserActor();
if (!actor || !actor->GetROI()) {
return;
}
// Force visibility of ride ROI map entries
AnimUtils::EnsureROIMapVisibility(m_rideRoiMap, m_rideRoiMapSize);
AnimUtils::EnsureROIMapVisibility(m_animator.GetRideRoiMap(), m_animator.GetRideRoiMapSize());
// Only advance animation time when actually moving
float speed = actor->GetWorldSpeed();
if (fabsf(speed) > 0.01f) {
m_animTime += p_deltaTime * 2000.0f;
if (SDL_fabsf(speed) > 0.01f) {
m_animator.SetAnimTime(m_animator.GetAnimTime() + p_deltaTime * 2000.0f);
}
// Use vehicle actor's transform as base.
@ -287,17 +269,18 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform);
m_playerROI->SetVisibility(TRUE);
float duration = (float) m_rideAnim->GetDuration();
float duration = (float) m_animator.GetRideAnim()->GetDuration();
if (duration > 0.0f) {
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
float timeInCycle =
m_animator.GetAnimTime() - duration * SDL_floorf(m_animator.GetAnimTime() / duration);
LegoTreeNode* root = m_rideAnim->GetRoot();
LegoTreeNode* root = m_animator.GetRideAnim()->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation(
root->GetChild(i),
transform,
(LegoTime) timeInCycle,
m_rideRoiMap
m_animator.GetRideRoiMap()
);
}
}
@ -320,170 +303,34 @@ void ThirdPersonCamera::Tick(float p_deltaTime)
}
}
// Determine the active walk animation and its ROI map
LegoAnim* walkAnim = nullptr;
LegoROI** walkRoiMap = nullptr;
MxU32 walkRoiMapSize = 0;
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) {
AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize);
}
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize);
}
float speed = userActor->GetWorldSpeed();
bool isMoving = fabsf(speed) > 0.01f;
bool isMoving = SDL_fabsf(speed) > 0.01f;
// Movement interrupts click animations and emotes
if (isMoving) {
StopClickAnimation();
if (m_emoteActive) {
m_emoteActive = false;
m_emoteAnimCache = nullptr;
}
}
if (isMoving) {
if (!walkAnim || !walkRoiMap) {
return;
}
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_playerROI->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) {
m_emoteTime += p_deltaTime * 1000.0f;
if (m_emoteTime >= m_emoteDuration) {
m_emoteActive = false;
m_emoteAnimCache = nullptr;
m_wasMoving = false;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
}
else {
// Use saved clean transform to prevent scale accumulation.
MxMatrix transform(m_emoteParentTransform);
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
);
}
// Restore player ROI transform (animation root overwrote it).
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(m_emoteParentTransform);
}
}
else if (m_idleAnimCache && m_idleAnimCache->anim && m_idleAnimCache->roiMap) {
if (m_wasMoving) {
m_wasMoving = false;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
}
m_idleTime += p_deltaTime;
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_playerROI->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
);
}
}
}
m_animator.Tick(p_deltaTime, m_playerROI, isMoving);
}
void ThirdPersonCamera::SetWalkAnimId(uint8_t p_walkAnimId)
{
if (p_walkAnimId >= g_walkAnimCount) {
return;
}
if (p_walkAnimId != m_walkAnimId) {
m_walkAnimId = p_walkAnimId;
if (m_active) {
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
}
}
m_animator.SetWalkAnimId(p_walkAnimId, m_active ? m_playerROI : nullptr);
}
void ThirdPersonCamera::SetIdleAnimId(uint8_t p_idleAnimId)
{
if (p_idleAnimId >= g_idleAnimCount) {
return;
}
if (p_idleAnimId != m_idleAnimId) {
m_idleAnimId = p_idleAnimId;
if (m_active) {
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
}
}
m_animator.SetIdleAnimId(p_idleAnimId, m_active ? m_playerROI : nullptr);
}
void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId)
{
if (p_emoteId >= g_emoteAnimCount || !m_active) {
if (!m_active) {
return;
}
LegoPathActor* userActor = UserActor();
if (!userActor || fabsf(userActor->GetWorldSpeed()) > 0.01f) {
if (!userActor) {
return;
}
AnimCache* cache = GetOrBuildAnimCache(g_emoteAnimNames[p_emoteId]);
if (!cache || !cache->anim) {
return;
}
StopClickAnimation();
m_emoteAnimCache = cache;
m_emoteTime = 0.0f;
m_emoteDuration = (float) cache->anim->GetDuration();
m_emoteActive = true;
// Save clean transform to prevent scale accumulation during emote
// (the animation root writes scaled values into the ROI each frame).
m_emoteParentTransform = m_playerROI->GetLocal2World();
m_animator.TriggerEmote(p_emoteId, m_playerROI, SDL_fabsf(userActor->GetWorldSpeed()) > 0.01f);
}
void ThirdPersonCamera::ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex)
@ -495,10 +342,7 @@ void ThirdPersonCamera::ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_par
void ThirdPersonCamera::StopClickAnimation()
{
if (m_clickAnimObjectId != 0) {
CharacterCustomizer::StopClickAnimation(m_clickAnimObjectId);
m_clickAnimObjectId = 0;
}
m_animator.StopClickAnimation();
}
void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world)
@ -508,8 +352,7 @@ void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world)
}
// Animation presenters may have been recreated.
m_animCacheMap.clear();
ClearAnimCaches();
m_animator.ClearAll();
ReinitForCharacter();
}
@ -525,22 +368,8 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world)
m_playerROI = nullptr;
DestroyNameBubble();
DestroyDisplayClone();
ClearRideAnimation();
m_animCacheMap.clear();
ClearAnimCaches();
}
ThirdPersonCamera::AnimCache* ThirdPersonCamera::GetOrBuildAnimCache(const char* p_animName)
{
return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, m_playerROI, p_animName);
}
void ThirdPersonCamera::ClearAnimCaches()
{
m_walkAnimCache = nullptr;
m_idleAnimCache = nullptr;
m_emoteAnimCache = nullptr;
m_emoteActive = false;
m_animator.ClearRideAnimation();
m_animator.ClearAll();
}
void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor)
@ -556,44 +385,6 @@ void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor)
p_actor->TransformPointOfView();
}
void ThirdPersonCamera::BuildRideAnimation(int8_t p_vehicleType)
{
if (p_vehicleType < 0 || p_vehicleType >= VEHICLE_COUNT) {
return;
}
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;
}
m_rideAnim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
if (!m_rideAnim) {
return;
}
// Create variant ROI, rename to match animation tree.
const char* baseName = g_vehicleROINames[p_vehicleType];
m_rideVehicleROI = CharacterManager()->CreateAutoROI("tp_vehicle", baseName, FALSE);
if (m_rideVehicleROI) {
m_rideVehicleROI->SetName(vehicleVariantName);
}
AnimUtils::BuildROIMap(m_rideAnim, m_playerROI, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
m_animTime = 0.0f;
}
void ThirdPersonCamera::SetDisplayActorIndex(uint8_t p_displayActorIndex)
{
if (m_displayActorIndex != p_displayActorIndex) {
@ -639,7 +430,7 @@ void ThirdPersonCamera::CreateDisplayClone()
void ThirdPersonCamera::DestroyDisplayClone()
{
StopClickAnimation();
m_animator.StopClickAnimation();
if (m_displayROI) {
if (m_playerROI == m_displayROI) {
m_playerROI = nullptr;
@ -652,97 +443,39 @@ void ThirdPersonCamera::DestroyDisplayClone()
void ThirdPersonCamera::CreateNameBubble()
{
if (m_nameBubble) {
return;
}
char name[8] = {};
LegoGameState* gs = GameState();
if (gs && gs->m_playerCount > 0) {
const LegoGameState::Username& username = gs->m_players[0];
for (int i = 0; i < 7; i++) {
MxS16 letter = username.m_letters[i];
if (letter < 0) {
break;
}
if (letter <= 25) {
name[i] = (char) ('A' + letter);
}
else {
name[i] = '?';
}
}
}
EncodeUsername(name);
if (name[0] == '\0') {
return;
}
m_nameBubble = new NameBubbleRenderer();
m_nameBubble->Create(name);
m_animator.CreateNameBubble(name);
if (!m_showNameBubble) {
m_nameBubble->SetVisible(false);
m_animator.SetNameBubbleVisible(false);
}
}
void ThirdPersonCamera::DestroyNameBubble()
{
if (m_nameBubble) {
delete m_nameBubble;
m_nameBubble = nullptr;
}
m_animator.DestroyNameBubble();
}
void ThirdPersonCamera::SetNameBubbleVisible(bool p_visible)
{
m_showNameBubble = p_visible;
if (m_nameBubble) {
m_nameBubble->SetVisible(p_visible);
}
m_animator.SetNameBubbleVisible(p_visible);
}
void ThirdPersonCamera::ClearRideAnimation()
{
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;
m_currentVehicleType = VEHICLE_NONE;
}
void ThirdPersonCamera::ApplyIdleFrame0()
{
if (!m_playerROI || !m_idleAnimCache || !m_idleAnimCache->anim || !m_idleAnimCache->roiMap) {
return;
}
MxMatrix transform(m_playerROI->GetLocal2World());
LegoTreeNode* root = m_idleAnimCache->anim->GetRoot();
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) 0.0f, m_idleAnimCache->roiMap);
}
}
void ThirdPersonCamera::ComputeOrbitVectors(
Mx3DPointFloat& p_at,
Mx3DPointFloat& p_dir,
Mx3DPointFloat& p_up
) const
void ThirdPersonCamera::ComputeOrbitVectors(Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const
{
// Convert spherical coordinates to camera offset in entity-local space.
// Entity local Z+ is "behind" (after TurnAround), which is where yaw=0 points.
float cosP = cosf(m_orbitPitch);
float sinP = sinf(m_orbitPitch);
float sinY = sinf(m_orbitYaw);
float cosY = cosf(m_orbitYaw);
float cosP = SDL_cosf(m_orbitPitch);
float sinP = SDL_sinf(m_orbitPitch);
float sinY = SDL_sinf(m_orbitYaw);
float cosY = SDL_cosf(m_orbitYaw);
p_at = Mx3DPointFloat(
m_orbitDistance * sinY * cosP,
@ -827,7 +560,7 @@ void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event)
if (m_touch.count == 2) {
float dx = m_touch.x[1] - m_touch.x[0];
float dy = m_touch.y[1] - m_touch.y[0];
m_touch.initialPinchDist = sqrtf(dx * dx + dy * dy);
m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy);
}
}
break;
@ -855,7 +588,7 @@ void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event)
// Pinch zoom
float dx = m_touch.x[1] - m_touch.x[0];
float dy = m_touch.y[1] - m_touch.y[0];
float newDist = sqrtf(dx * dx + dy * dy);
float newDist = SDL_sqrtf(dx * dx + dy * dy);
if (m_touch.initialPinchDist > 0.001f) {
float pinchDelta = m_touch.initialPinchDist - newDist;
@ -921,7 +654,7 @@ void ThirdPersonCamera::ReinitForCharacter()
return;
}
m_currentVehicleType = vehicleType;
m_animator.SetCurrentVehicleType(vehicleType);
if (vehicleType != VEHICLE_NONE) {
if (!EnsureDisplayROI()) {
@ -951,7 +684,7 @@ void ThirdPersonCamera::ReinitForCharacter()
VideoManager()->Get3DManager()->Add(*m_playerROI);
m_active = true;
SetupCamera(userActor);
BuildRideAnimation(vehicleType);
m_animator.BuildRideAnimation(vehicleType, m_playerROI, 0);
CreateNameBubble();
return;
}
@ -978,17 +711,11 @@ void ThirdPersonCamera::ReinitForCharacter()
VideoManager()->Get3DManager()->Remove(*m_playerROI);
VideoManager()->Get3DManager()->Add(*m_playerROI);
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
m_animTime = 0.0f;
m_idleTime = 0.0f;
m_idleAnimTime = 0.0f;
m_wasMoving = false;
m_emoteActive = false;
m_animator.InitAnimCaches(m_playerROI);
m_animator.ResetAnimState();
m_active = true;
ApplyIdleFrame0();
m_animator.ApplyIdleFrame0(m_playerROI);
SetupCamera(userActor);
CreateNameBubble();
}

View File

@ -16,8 +16,6 @@
#include "mxvariabletable.h"
#include <SDL3/SDL_stdinc.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
extern MxU8 g_counters[];
@ -49,7 +47,7 @@ void WorldStateSync::SaveSkyLightState()
{
const char* bgValue = GameState()->GetBackgroundColor()->GetValue()->GetData();
m_savedSkyColor = bgValue ? bgValue : "set 56 54 68";
m_savedLightPos = atoi(VariableTable()->GetVariable("lightposition"));
m_savedLightPos = SDL_atoi(VariableTable()->GetVariable("lightposition"));
}
void WorldStateSync::RestoreSkyLightState()
@ -64,7 +62,7 @@ void WorldStateSync::ApplySkyLightState(const char* p_skyColor, int p_lightPos)
SetLightPosition(p_lightPos);
char buf[32];
sprintf(buf, "%d", p_lightPos);
SDL_snprintf(buf, sizeof(buf), "%d", p_lightPos);
VariableTable()->SetVariable("lightposition", buf);
}
@ -118,7 +116,7 @@ void WorldStateSync::HandleWorldSnapshot(const uint8_t* p_data, size_t p_length)
if (remaining >= 4) {
char skyBuffer[32];
sprintf(skyBuffer, "set %d %d %d", extraData[0], extraData[1], extraData[2]);
SDL_snprintf(skyBuffer, sizeof(skyBuffer), "set %d %d %d", extraData[0], extraData[1], extraData[2]);
ApplySkyLightState(skyBuffer, extraData[3]);
}
@ -259,10 +257,10 @@ void WorldStateSync::SendWorldSnapshot(uint32_t p_targetPeerId)
int skyH = 56, skyS = 54, skyV = 68; // defaults matching "set 56 54 68"
const char* bgValue = GameState()->GetBackgroundColor()->GetValue()->GetData();
if (bgValue) {
sscanf(bgValue, "set %d %d %d", &skyH, &skyS, &skyV);
SDL_sscanf(bgValue, "set %d %d %d", &skyH, &skyS, &skyV);
}
int lightPos = atoi(VariableTable()->GetVariable("lightposition"));
int lightPos = SDL_atoi(VariableTable()->GetVariable("lightposition"));
stateBuffer[dataLength++] = (uint8_t) skyH;
stateBuffer[dataLength++] = (uint8_t) skyS;