From a5b2ea0ce9714eb9e13a57cc1a2d12c57574dd51 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sun, 8 Mar 2026 10:48:26 -0700 Subject: [PATCH] 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 , , headers. --- CMakeLists.txt | 1 + .../multiplayer/characteranimator.h | 124 ++++++ .../extensions/multiplayer/networkmanager.h | 12 +- .../include/extensions/multiplayer/protocol.h | 18 +- .../extensions/multiplayer/remoteplayer.h | 48 +-- .../multiplayer/thirdpersoncamera.h | 53 +-- .../src/multiplayer/characteranimator.cpp | 366 ++++++++++++++++ .../src/multiplayer/charactercloner.cpp | 3 +- .../src/multiplayer/charactercustomizer.cpp | 31 +- extensions/src/multiplayer/networkmanager.cpp | 20 +- extensions/src/multiplayer/protocol.cpp | 31 +- extensions/src/multiplayer/remoteplayer.cpp | 291 ++----------- .../src/multiplayer/thirdpersoncamera.cpp | 397 +++--------------- extensions/src/multiplayer/worldstatesync.cpp | 12 +- 14 files changed, 660 insertions(+), 747 deletions(-) create mode 100644 extensions/include/extensions/multiplayer/characteranimator.h create mode 100644 extensions/src/multiplayer/characteranimator.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88ab179b..5cd0dbf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/extensions/include/extensions/multiplayer/characteranimator.h b/extensions/include/extensions/multiplayer/characteranimator.h new file mode 100644 index 00000000..130d1624 --- /dev/null +++ b/extensions/include/extensions/multiplayer/characteranimator.h @@ -0,0 +1,124 @@ +#pragma once + +#include "extensions/multiplayer/animutils.h" +#include "extensions/multiplayer/protocol.h" +#include "mxgeometry/mxmatrix.h" +#include "mxtypes.h" + +#include +#include +#include + +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 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 diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 4596efa4..37429fad 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -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); } diff --git a/extensions/include/extensions/multiplayer/protocol.h b/extensions/include/extensions/multiplayer/protocol.h index 8c086dd8..9a566068 100644 --- a/extensions/include/extensions/multiplayer/protocol.h +++ b/extensions/include/extensions/multiplayer/protocol.h @@ -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. diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index 53ccfa7f..46c31b09 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -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 -#include #include 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 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; diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h index e46d4581..474741fb 100644 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ b/extensions/include/extensions/multiplayer/thirdpersoncamera.h @@ -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 #include -#include -#include 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 m_animCacheMap; - - NameBubbleRenderer* m_nameBubble; bool m_showNameBubble; // Orbit camera state diff --git a/extensions/src/multiplayer/characteranimator.cpp b/extensions/src/multiplayer/characteranimator.cpp new file mode 100644 index 00000000..7706352f --- /dev/null +++ b/extensions/src/multiplayer/characteranimator.cpp @@ -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 + +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(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); + } +} diff --git a/extensions/src/multiplayer/charactercloner.cpp b/extensions/src/multiplayer/charactercloner.cpp index 81b60347..b0920f8f 100644 --- a/extensions/src/multiplayer/charactercloner.cpp +++ b/extensions/src/multiplayer/charactercloner.cpp @@ -11,7 +11,6 @@ #include "viewmanager/viewlodlist.h" #include -#include #include 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++) { diff --git a/extensions/src/multiplayer/charactercustomizer.cpp b/extensions/src/multiplayer/charactercustomizer.cpp index 8183acac..578e2d40 100644 --- a/extensions/src/multiplayer/charactercustomizer.cpp +++ b/extensions/src/multiplayer/charactercustomizer.cpp @@ -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 -#include 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; diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 677b10fc..4789e129 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -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; diff --git a/extensions/src/multiplayer/protocol.cpp b/extensions/src/multiplayer/protocol.cpp index a6e96870..b83be1e0 100644 --- a/extensions/src/multiplayer/protocol.cpp +++ b/extensions/src/multiplayer/protocol.cpp @@ -1,8 +1,10 @@ #include "extensions/multiplayer/protocol.h" +#include "legogamestate.h" #include "legopathactor.h" +#include "misc.h" -#include +#include 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 diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 0a7f515a..a0a2cb45 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -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 #include #include -#include #include 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(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(); } - diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp index d797042a..28bb7425 100644 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ b/extensions/src/multiplayer/thirdpersoncamera.cpp @@ -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 -#include 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(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(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(); } diff --git a/extensions/src/multiplayer/worldstatesync.cpp b/extensions/src/multiplayer/worldstatesync.cpp index e4655640..21e7ca8f 100644 --- a/extensions/src/multiplayer/worldstatesync.cpp +++ b/extensions/src/multiplayer/worldstatesync.cpp @@ -16,8 +16,6 @@ #include "mxvariabletable.h" #include -#include -#include #include 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;