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/textureloader.cpp
extensions/src/multiplayer.cpp extensions/src/multiplayer.cpp
extensions/src/multiplayer/animutils.cpp extensions/src/multiplayer/animutils.cpp
extensions/src/multiplayer/characteranimator.cpp
extensions/src/multiplayer/charactercloner.cpp extensions/src/multiplayer/charactercloner.cpp
extensions/src/multiplayer/charactercustomizer.cpp extensions/src/multiplayer/charactercustomizer.cpp
extensions/src/multiplayer/customizestate.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 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); 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 // 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(). // running on the browser main thread). Deferred to the game thread in Tickle().
void RequestToggleThirdPerson() { m_pendingToggleThirdPerson.store(true, std::memory_order_relaxed); } 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 RequestSetWalkAnimation(uint8_t p_walkAnimId)
void RequestSetIdleAnimation(uint8_t p_idleAnimId) { m_pendingIdleAnim.store(p_idleAnimId, std::memory_order_relaxed); } {
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 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 RequestToggleNameBubbles() { m_pendingToggleNameBubbles.store(true, std::memory_order_relaxed); }
void RequestToggleAllowCustomize() { m_pendingToggleAllowCustomize.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 // Routing target constants for MessageHeader.target
const uint32_t TARGET_BROADCAST = 0; // Broadcast to all except sender 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_HOST = 0xFFFFFFFF; // Send to host only
const uint32_t TARGET_BROADCAST_ALL = 0xFFFFFFFE; // Broadcast to all including sender const uint32_t TARGET_BROADCAST_ALL = 0xFFFFFFFE; // Broadcast to all including sender
enum MessageType : uint8_t { enum MessageType : uint8_t {
@ -94,12 +94,12 @@ struct PlayerStateMsg {
float direction[3]; float direction[3];
float up[3]; float up[3];
float speed; float speed;
uint8_t walkAnimId; // Index into walk animation table (0 = default) uint8_t walkAnimId; // Index into walk animation table (0 = default)
uint8_t idleAnimId; // Index into idle animation table (0 = default) uint8_t idleAnimId; // Index into idle animation table (0 = default)
char name[8]; // Player display name (7 chars + null terminator) char name[8]; // Player display name (7 chars + null terminator)
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65) uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
uint8_t customizeData[5]; // Packed CustomizeState uint8_t customizeData[5]; // Packed CustomizeState
uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize
}; };
// Server -> all: announces which peer is the host // 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; 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; static const uint8_t DISPLAY_ACTOR_NONE = 0xFF;
// Parse the message type from a buffer. Returns MSG type or 0 on error. // Parse the message type from a buffer. Returns MSG type or 0 on error.

View File

@ -1,24 +1,19 @@
#pragma once #pragma once
#include "extensions/multiplayer/animutils.h" #include "extensions/multiplayer/characteranimator.h"
#include "extensions/multiplayer/customizestate.h" #include "extensions/multiplayer/customizestate.h"
#include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/protocol.h"
#include "mxtypes.h" #include "mxtypes.h"
#include <cstdint> #include <cstdint>
#include <map>
#include <string> #include <string>
class LegoROI; class LegoROI;
class LegoWorld; class LegoWorld;
class LegoAnim;
class LegoTreeNode;
namespace Multiplayer namespace Multiplayer
{ {
class NameBubbleRenderer;
class RemotePlayer { class RemotePlayer {
public: public:
RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex); 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; } const CustomizeState& GetCustomizeState() const { return m_customizeState; }
bool GetAllowRemoteCustomize() const { return m_allowRemoteCustomize; } 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(); void StopClickAnimation();
bool IsInVehicle() const { return m_currentVehicleType != VEHICLE_NONE; } bool IsInVehicle() const { return m_animator.IsInVehicle(); }
bool IsMoving() const { return m_currentVehicleType != VEHICLE_NONE || m_targetSpeed > 0.01f; } bool IsMoving() const { return m_animator.IsInVehicle() || m_targetSpeed > 0.01f; }
private: private:
using AnimCache = AnimUtils::AnimCache;
AnimCache* GetOrBuildAnimCache(const char* p_animName);
const char* GetDisplayActorName() const; const char* GetDisplayActorName() const;
void UpdateTransform(float p_deltaTime); void UpdateTransform(float p_deltaTime);
void UpdateAnimation(float p_deltaTime);
void UpdateVehicleState(); void UpdateVehicleState();
void EnterVehicle(int8_t p_vehicleType); void EnterVehicle(int8_t p_vehicleType);
void ExitVehicle(); void ExitVehicle();
@ -87,38 +78,9 @@ class RemotePlayer {
float m_currentDirection[3]; float m_currentDirection[3];
float m_currentUp[3]; float m_currentUp[3];
// Animation state CharacterAnimator m_animator;
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;
LegoROI* m_vehicleROI; LegoROI* m_vehicleROI;
int8_t m_currentVehicleType;
NameBubbleRenderer* m_nameBubble;
CustomizeState m_customizeState; CustomizeState m_customizeState;
bool m_allowRemoteCustomize; bool m_allowRemoteCustomize;

View File

@ -1,28 +1,22 @@
#pragma once #pragma once
#include "extensions/multiplayer/animutils.h" #include "extensions/multiplayer/characteranimator.h"
#include "extensions/multiplayer/customizestate.h" #include "extensions/multiplayer/customizestate.h"
#include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/protocol.h"
#include "mxgeometry/mxgeometry3d.h" #include "mxgeometry/mxgeometry3d.h"
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h" #include "mxtypes.h"
#include <SDL3/SDL_events.h> #include <SDL3/SDL_events.h>
#include <cstdint> #include <cstdint>
#include <map>
#include <string>
class IslePathActor; class IslePathActor;
class LegoPathActor; class LegoPathActor;
class LegoROI; class LegoROI;
class LegoWorld; class LegoWorld;
class LegoAnim;
namespace Multiplayer namespace Multiplayer
{ {
class NameBubbleRenderer;
class ThirdPersonCamera { class ThirdPersonCamera {
public: public:
ThirdPersonCamera(); ThirdPersonCamera();
@ -50,9 +44,9 @@ class ThirdPersonCamera {
CustomizeState& GetCustomizeState() { return m_customizeState; } CustomizeState& GetCustomizeState() { return m_customizeState; }
void ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex); 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(); void StopClickAnimation();
bool IsInVehicle() const { return m_currentVehicleType != VEHICLE_NONE; } bool IsInVehicle() const { return m_animator.IsInVehicle(); }
void SetNameBubbleVisible(bool p_visible); void SetNameBubbleVisible(bool p_visible);
@ -70,14 +64,7 @@ class ThirdPersonCamera {
void ClampPitch(); void ClampPitch();
void ClampDistance(); void ClampDistance();
using AnimCache = AnimUtils::AnimCache;
AnimCache* GetOrBuildAnimCache(const char* p_animName);
void ClearAnimCaches();
void SetupCamera(LegoPathActor* p_actor); void SetupCamera(LegoPathActor* p_actor);
void BuildRideAnimation(int8_t p_vehicleType);
void ClearRideAnimation();
void ApplyIdleFrame0();
void ReinitForCharacter(); void ReinitForCharacter();
void CreateNameBubble(); void CreateNameBubble();
@ -90,45 +77,17 @@ class ThirdPersonCamera {
bool m_enabled; bool m_enabled;
bool m_active; 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 LegoROI* m_playerROI; // Borrowed, not owned
// Display actor override // Display actor override
uint8_t m_displayActorIndex; 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]; char m_displayUniqueName[32];
CustomizeState m_customizeState; CustomizeState m_customizeState;
// Walk/idle state (same pattern as RemotePlayer) CharacterAnimator m_animator;
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;
// 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; bool m_showNameBubble;
// Orbit camera state // 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 "viewmanager/viewlodlist.h"
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <cstdio>
#include <vec.h> #include <vec.h>
using namespace Multiplayer; using namespace Multiplayer;
@ -69,7 +68,7 @@ LegoROI* CharacterCloner::Clone(LegoCharacterManager* p_charMgr, const char* p_u
ViewLODList* lodList = lodManager->Lookup(parentName); ViewLODList* lodList = lodManager->Lookup(parentName);
MxS32 lodSize = lodList->Size(); 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); ViewLODList* dupLodList = lodManager->Create(lodName, lodSize);
for (MxS32 j = 0; j < lodSize; j++) { for (MxS32 j = 0; j < lodSize; j++) {

View File

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

View File

@ -334,25 +334,7 @@ void NetworkManager::BroadcastLocalState()
msg.walkAnimId = m_localWalkAnimId; msg.walkAnimId = m_localWalkAnimId;
msg.idleAnimId = m_localIdleAnimId; msg.idleAnimId = m_localIdleAnimId;
// Convert Username letters (0-25 = A-Z) to ASCII string. EncodeUsername(msg.name);
// 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] = '?';
}
}
}
msg.displayActorIndex = m_localDisplayActorIndex; msg.displayActorIndex = m_localDisplayActorIndex;

View File

@ -1,8 +1,10 @@
#include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/protocol.h"
#include "legogamestate.h"
#include "legopathactor.h" #include "legopathactor.h"
#include "misc.h"
#include <cstddef> #include <SDL3/SDL_stdinc.h>
namespace Multiplayer namespace Multiplayer
{ {
@ -36,12 +38,10 @@ const char* const g_vehicleROINames[VEHICLE_COUNT] =
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"}; {"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
// Ride animation names for small vehicles (NULL = large vehicle, no ride anim) // Ride animation names for small vehicles (NULL = large vehicle, no ride anim)
const char* const g_rideAnimNames[VEHICLE_COUNT] = const char* const g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
{NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
// Vehicle variant ROI names used in ride animations // Vehicle variant ROI names used in ride animations
const char* const g_rideVehicleROINames[VEHICLE_COUNT] = const char* const g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
{NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
bool IsLargeVehicle(int8_t p_vehicleType) bool IsLargeVehicle(int8_t p_vehicleType)
{ {
@ -76,4 +76,25 @@ int8_t DetectVehicleType(LegoPathActor* p_actor)
return VEHICLE_NONE; 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 } // namespace Multiplayer

View File

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

View File

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

View File

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