diff --git a/extensions/include/extensions/common/animutils.h b/extensions/include/extensions/common/animutils.h index 2c815a5a..7685084c 100644 --- a/extensions/include/extensions/common/animutils.h +++ b/extensions/include/extensions/common/animutils.h @@ -1,12 +1,13 @@ #pragma once -#include "mxtypes.h" #include "mxgeometry/mxmatrix.h" +#include "mxtypes.h" #include "realtime/vector.h" #include "roi/legoroi.h" #include #include +#include class LegoAnim; @@ -34,8 +35,7 @@ struct AnimCache { AnimCache(const AnimCache&) = delete; AnimCache& operator=(const AnimCache&) = delete; - AnimCache(AnimCache&& p_other) noexcept - : anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize) + AnimCache(AnimCache&& p_other) noexcept : anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize) { p_other.roiMap = nullptr; p_other.roiMapSize = 0; @@ -61,16 +61,15 @@ struct AnimCache { void BuildROIMap( LegoAnim* p_anim, LegoROI* p_rootROI, - LegoROI* p_extraROI, + LegoROI** p_extraROIs, + int p_extraROICount, LegoROI**& p_roiMap, MxU32& p_roiMapSize ); -AnimCache* GetOrBuildAnimCache( - std::map& p_cacheMap, - LegoROI* p_roi, - const char* p_animName -); +void CollectUnmatchedNodes(LegoAnim* p_anim, LegoROI* p_rootROI, std::vector& p_unmatchedNames); + +AnimCache* GetOrBuildAnimCache(std::map& p_cacheMap, LegoROI* p_roi, const char* p_animName); inline void EnsureROIMapVisibility(LegoROI** p_roiMap, MxU32 p_roiMapSize) { diff --git a/extensions/include/extensions/common/characteranimator.h b/extensions/include/extensions/common/characteranimator.h index a07f8778..9419b533 100644 --- a/extensions/include/extensions/common/characteranimator.h +++ b/extensions/include/extensions/common/characteranimator.h @@ -24,6 +24,19 @@ struct CharacterAnimatorConfig { // When true, save/restore the parent ROI transform during emote playback // to prevent scale accumulation (needed for ThirdPersonCameraExt's display clone). bool saveEmoteTransform; + + // Suffix used for unique naming of prop ROIs. + // Remote players use m_peerId, local player uses 0. + uint32_t propSuffix; +}; + +// A group of dynamically-created prop ROIs for an animation (ride or emote). +struct PropGroup { + LegoAnim* anim = nullptr; + LegoROI** roiMap = nullptr; + MxU32 roiMapSize = 0; + LegoROI** propROIs = nullptr; + uint8_t propCount = 0; }; // Unified character animation component used by both RemotePlayer and ThirdPersonCameraExt. @@ -61,10 +74,10 @@ class CharacterAnimator { 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; } + LegoROI* GetRideVehicleROI() const { return m_ridePropGroup.propCount > 0 ? m_ridePropGroup.propROIs[0] : nullptr; } + LegoAnim* GetRideAnim() const { return m_ridePropGroup.anim; } + LegoROI** GetRideRoiMap() const { return m_ridePropGroup.roiMap; } + MxU32 GetRideRoiMapSize() const { return m_ridePropGroup.roiMapSize; } // Animation cache management void InitAnimCaches(LegoROI* p_roi); @@ -94,6 +107,8 @@ class CharacterAnimator { AnimCache* GetOrBuildAnimCache(LegoROI* p_roi, const char* p_animName); void ClearFrozenState(); + void ClearPropGroup(PropGroup& p_group); + void BuildEmoteProps(PropGroup& p_group, LegoAnim* p_anim, LegoROI* p_playerROI); void PlayROISound(const char* p_key, LegoROI* p_roi); CharacterAnimatorConfig m_config; @@ -133,12 +148,11 @@ class CharacterAnimator { std::map m_animCacheMap; // Ride animation (vehicle-specific) - LegoAnim* m_rideAnim; - LegoROI** m_rideRoiMap; - MxU32 m_rideRoiMapSize; - LegoROI* m_rideVehicleROI; - + PropGroup m_ridePropGroup; int8_t m_currentVehicleType; + + // Emote prop animation (dynamically-created props for emotes like Toss) + PropGroup m_emotePropGroup; }; } // namespace Common diff --git a/extensions/src/common/animutils.cpp b/extensions/src/common/animutils.cpp index 37a9e7c6..fc2319ff 100644 --- a/extensions/src/common/animutils.cpp +++ b/extensions/src/common/animutils.cpp @@ -7,6 +7,7 @@ #include "misc/legotree.h" #include "roi/legoroi.h" +#include #include using namespace Extensions::Common; @@ -17,7 +18,8 @@ static void AssignROIIndices( LegoTreeNode* p_node, LegoROI* p_parentROI, LegoROI* p_rootROI, - LegoROI* p_extraROI, + LegoROI** p_extraROIs, + int p_extraROICount, MxU32& p_nextIndex, std::vector& p_entries, bool& p_rootClaimed @@ -36,11 +38,29 @@ static void AssignROIIndices( matchedROI = p_rootROI; p_rootClaimed = true; } + else if (*name == '*' && p_extraROICount > 0) { + // Subsequent *-prefixed node: search extra ROIs by stripped name. + // FindChildROI checks self first, then children recursively. + const char* stripped = name + 1; + for (int e = 0; e < p_extraROICount; e++) { + matchedROI = p_extraROIs[e]->FindChildROI(stripped, p_extraROIs[e]); + if (matchedROI != nullptr) { + break; + } + } + } } else { matchedROI = p_parentROI->FindChildROI(name, p_parentROI); - if (matchedROI == nullptr && p_extraROI != nullptr) { - matchedROI = p_extraROI->FindChildROI(name, p_extraROI); + if (matchedROI == nullptr) { + // FindChildROI checks self first, so this handles both + // direct name matches and child searches on extra ROIs. + for (int e = 0; e < p_extraROICount; e++) { + matchedROI = p_extraROIs[e]->FindChildROI(name, p_extraROIs[e]); + if (matchedROI != nullptr) { + break; + } + } } } @@ -55,14 +75,24 @@ static void AssignROIIndices( } for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) { - AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries, p_rootClaimed); + AssignROIIndices( + p_node->GetChild(i), + roi, + p_rootROI, + p_extraROIs, + p_extraROICount, + p_nextIndex, + p_entries, + p_rootClaimed + ); } } void AnimUtils::BuildROIMap( LegoAnim* p_anim, LegoROI* p_rootROI, - LegoROI* p_extraROI, + LegoROI** p_extraROIs, + int p_extraROICount, LegoROI**& p_roiMap, MxU32& p_roiMapSize ) @@ -79,7 +109,7 @@ void AnimUtils::BuildROIMap( MxU32 nextIndex = 1; std::vector entries; bool rootClaimed = false; - AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries, rootClaimed); + AssignROIIndices(root, nullptr, p_rootROI, p_extraROIs, p_extraROICount, nextIndex, entries, rootClaimed); if (entries.empty()) { return; @@ -129,7 +159,67 @@ AnimUtils::AnimCache* AnimUtils::GetOrBuildAnimCache( // Build and cache AnimCache& cache = p_cacheMap[p_animName]; cache.anim = anim; - BuildROIMap(anim, p_roi, nullptr, cache.roiMap, cache.roiMapSize); + BuildROIMap(anim, p_roi, nullptr, 0, cache.roiMap, cache.roiMapSize); return &cache; } + +// Read-only tree walk: collect names of animation nodes that don't match the player's ROI hierarchy. +static void CollectUnmatchedNodesRecursive( + LegoTreeNode* p_node, + LegoROI* p_parentROI, + LegoROI* p_rootROI, + std::vector& p_unmatchedNames, + bool& p_rootClaimed +) +{ + LegoROI* roi = p_parentROI; + LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData(); + const char* name = data ? data->GetName() : nullptr; + + if (name != nullptr && *name != '-') { + if (*name == '*' || p_parentROI == nullptr) { + roi = p_rootROI; + if (!p_rootClaimed) { + p_rootClaimed = true; + } + else if (*name == '*') { + // Subsequent *-prefixed node: strip prefix, add lowercased name + std::string stripped(name + 1); + std::transform(stripped.begin(), stripped.end(), stripped.begin(), ::tolower); + if (std::find(p_unmatchedNames.begin(), p_unmatchedNames.end(), stripped) == p_unmatchedNames.end()) { + p_unmatchedNames.push_back(stripped); + } + } + } + else { + LegoROI* matchedROI = p_parentROI->FindChildROI(name, p_parentROI); + if (matchedROI == nullptr) { + std::string lowered(name); + std::transform(lowered.begin(), lowered.end(), lowered.begin(), ::tolower); + if (std::find(p_unmatchedNames.begin(), p_unmatchedNames.end(), lowered) == p_unmatchedNames.end()) { + p_unmatchedNames.push_back(lowered); + } + } + } + } + + for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) { + CollectUnmatchedNodesRecursive(p_node->GetChild(i), roi, p_rootROI, p_unmatchedNames, p_rootClaimed); + } +} + +void AnimUtils::CollectUnmatchedNodes(LegoAnim* p_anim, LegoROI* p_rootROI, std::vector& p_unmatchedNames) +{ + if (!p_anim || !p_rootROI) { + return; + } + + LegoTreeNode* root = p_anim->GetRoot(); + if (!root) { + return; + } + + bool rootClaimed = false; + CollectUnmatchedNodesRecursive(root, nullptr, p_rootROI, p_unmatchedNames, rootClaimed); +} diff --git a/extensions/src/common/characteranimator.cpp b/extensions/src/common/characteranimator.cpp index d54bd035..35da0c2d 100644 --- a/extensions/src/common/characteranimator.cpp +++ b/extensions/src/common/characteranimator.cpp @@ -23,13 +23,13 @@ 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_currentEmoteId(0), m_frozenEmoteId(-1), - m_frozenAnimCache(nullptr), m_frozenAnimDuration(0.0f), m_clickAnimObjectId(0), m_rideAnim(nullptr), - m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) + m_frozenAnimCache(nullptr), m_frozenAnimDuration(0.0f), m_clickAnimObjectId(0), m_currentVehicleType(VEHICLE_NONE) { } CharacterAnimator::~CharacterAnimator() { + ClearPropGroup(m_emotePropGroup); ClearRideAnimation(); } @@ -50,10 +50,10 @@ void CharacterAnimator::Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving) LegoROI** walkRoiMap = nullptr; MxU32 walkRoiMapSize = 0; - if (m_currentVehicleType != VEHICLE_NONE && m_rideAnim && m_rideRoiMap) { - walkAnim = m_rideAnim; - walkRoiMap = m_rideRoiMap; - walkRoiMapSize = m_rideRoiMapSize; + if (m_currentVehicleType != VEHICLE_NONE && m_ridePropGroup.anim && m_ridePropGroup.roiMap) { + walkAnim = m_ridePropGroup.anim; + walkRoiMap = m_ridePropGroup.roiMap; + walkRoiMapSize = m_ridePropGroup.roiMapSize; } else if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) { walkAnim = m_walkAnimCache->anim; @@ -78,6 +78,7 @@ void CharacterAnimator::Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving) if (m_emoteActive) { m_emoteActive = false; m_emoteAnimCache = nullptr; + ClearPropGroup(m_emotePropGroup); } } @@ -127,12 +128,15 @@ void CharacterAnimator::Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving) // Emote completed -- return to stationary flow m_emoteActive = false; m_emoteAnimCache = nullptr; + ClearPropGroup(m_emotePropGroup); m_wasMoving = false; m_idleTime = 0.0f; m_idleAnimTime = 0.0f; } } else { + LegoROI** emoteRoiMap = + m_emotePropGroup.roiMap != nullptr ? m_emotePropGroup.roiMap : m_emoteAnimCache->roiMap; MxMatrix transform(m_config.saveEmoteTransform ? m_emoteParentTransform : p_roi->GetLocal2World()); LegoTreeNode* root = m_emoteAnimCache->anim->GetRoot(); @@ -141,7 +145,7 @@ void CharacterAnimator::Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving) root->GetChild(i), transform, (LegoTime) m_emoteTime, - m_emoteAnimCache->roiMap + emoteRoiMap ); } @@ -243,6 +247,7 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i } StopClickAnimation(); + ClearPropGroup(m_emotePropGroup); m_currentEmoteId = p_emoteId; m_emoteAnimCache = cache; @@ -250,6 +255,8 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i m_emoteDuration = (float) cache->anim->GetDuration(); m_emoteActive = true; + BuildEmoteProps(m_emotePropGroup, cache->anim, p_roi); + const char* sound = g_emoteEntries[p_emoteId].phases[1].sound; if (sound) { PlayROISound(sound, p_roi); @@ -279,6 +286,7 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i } StopClickAnimation(); + ClearPropGroup(m_emotePropGroup); m_currentEmoteId = p_emoteId; m_emoteAnimCache = cache; @@ -286,6 +294,8 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i m_emoteDuration = (float) cache->anim->GetDuration(); m_emoteActive = true; + BuildEmoteProps(m_emotePropGroup, cache->anim, p_roi); + const char* sound = g_emoteEntries[p_emoteId].phases[0].sound; if (sound) { PlayROISound(sound, p_roi); @@ -344,8 +354,8 @@ void CharacterAnimator::BuildRideAnimation(int8_t p_vehicleType, LegoROI* p_play return; } - m_rideAnim = static_cast(presenter)->GetAnimation(); - if (!m_rideAnim) { + m_ridePropGroup.anim = static_cast(presenter)->GetAnimation(); + if (!m_ridePropGroup.anim) { return; } @@ -358,28 +368,28 @@ void CharacterAnimator::BuildRideAnimation(int8_t p_vehicleType, LegoROI* p_play else { SDL_snprintf(variantName, sizeof(variantName), "tp_vehicle"); } - m_rideVehicleROI = CharacterManager()->CreateAutoROI(variantName, baseName, FALSE); - if (m_rideVehicleROI) { - m_rideVehicleROI->SetName(vehicleVariantName); + LegoROI* vehicleROI = CharacterManager()->CreateAutoROI(variantName, baseName, FALSE); + if (vehicleROI) { + vehicleROI->SetName(vehicleVariantName); + m_ridePropGroup.propROIs = new LegoROI*[1]; + m_ridePropGroup.propROIs[0] = vehicleROI; + m_ridePropGroup.propCount = 1; } - AnimUtils::BuildROIMap(m_rideAnim, p_playerROI, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize); + AnimUtils::BuildROIMap( + m_ridePropGroup.anim, + p_playerROI, + m_ridePropGroup.propROIs, + m_ridePropGroup.propCount, + m_ridePropGroup.roiMap, + m_ridePropGroup.roiMapSize + ); 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; + ClearPropGroup(m_ridePropGroup); m_currentVehicleType = VEHICLE_NONE; } @@ -417,6 +427,89 @@ void CharacterAnimator::ClearFrozenState() m_frozenEmoteId = -1; m_frozenAnimCache = nullptr; m_frozenAnimDuration = 0.0f; + ClearPropGroup(m_emotePropGroup); +} + +void CharacterAnimator::ClearPropGroup(PropGroup& p_group) +{ + delete[] p_group.roiMap; + p_group.roiMap = nullptr; + p_group.roiMapSize = 0; + + for (uint8_t i = 0; i < p_group.propCount; i++) { + if (p_group.propROIs[i]) { + VideoManager()->Get3DManager()->Remove(*p_group.propROIs[i]); + CharacterManager()->ReleaseAutoROI(p_group.propROIs[i]); + } + } + delete[] p_group.propROIs; + p_group.propROIs = nullptr; + p_group.propCount = 0; + p_group.anim = nullptr; +} + +// Maps animation tree node names to actual LOD names when they differ. +static const char* ResolvePropLODName(const char* p_nodeName) +{ + static const struct { + const char* nodePrefix; + const char* lodName; + } mappings[] = { + {"popmug", "pizpie"}, + }; + + for (const auto& m : mappings) { + if (!SDL_strncasecmp(p_nodeName, m.nodePrefix, SDL_strlen(m.nodePrefix))) { + return m.lodName; + } + } + return p_nodeName; +} + +void CharacterAnimator::BuildEmoteProps(PropGroup& p_group, LegoAnim* p_anim, LegoROI* p_playerROI) +{ + std::vector unmatchedNames; + AnimUtils::CollectUnmatchedNodes(p_anim, p_playerROI, unmatchedNames); + if (unmatchedNames.empty()) { + return; + } + + std::vector createdROIs; + for (const std::string& name : unmatchedNames) { + char uniqueName[64]; + if (m_config.propSuffix != 0) { + SDL_snprintf(uniqueName, sizeof(uniqueName), "%s_mp_%u", name.c_str(), m_config.propSuffix); + } + else { + SDL_snprintf(uniqueName, sizeof(uniqueName), "tp_prop_%s", name.c_str()); + } + + const char* lodName = ResolvePropLODName(name.c_str()); + LegoROI* propROI = CharacterManager()->CreateAutoROI(uniqueName, lodName, FALSE); + if (propROI) { + propROI->SetName(name.c_str()); + createdROIs.push_back(propROI); + } + } + + if (createdROIs.empty()) { + return; + } + + p_group.propCount = (uint8_t) createdROIs.size(); + p_group.propROIs = new LegoROI*[p_group.propCount]; + for (uint8_t i = 0; i < p_group.propCount; i++) { + p_group.propROIs[i] = createdROIs[i]; + } + + AnimUtils::BuildROIMap( + p_anim, + p_playerROI, + p_group.propROIs, + p_group.propCount, + p_group.roiMap, + p_group.roiMapSize + ); } void CharacterAnimator::ClearAnimCaches() diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 3d379c4b..23f616de 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -28,8 +28,8 @@ RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displ : 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_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false}), m_vehicleROI(nullptr), - m_nameBubble(nullptr), m_allowRemoteCustomize(true) + m_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false, /*.propSuffix=*/p_peerId}), + m_vehicleROI(nullptr), m_nameBubble(nullptr), m_allowRemoteCustomize(true) { m_displayName[0] = '\0'; const char* displayName = GetDisplayActorName(); diff --git a/extensions/src/thirdpersoncamera/controller.cpp b/extensions/src/thirdpersoncamera/controller.cpp index c6d77a32..d32878f8 100644 --- a/extensions/src/thirdpersoncamera/controller.cpp +++ b/extensions/src/thirdpersoncamera/controller.cpp @@ -28,8 +28,8 @@ using namespace Extensions::Common; using namespace Extensions::ThirdPersonCamera; Controller::Controller() - : m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true}), m_enabled(false), m_active(false), - m_pendingWorldTransition(false), m_playerROI(nullptr) + : m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true, /*.propSuffix=*/0}), m_enabled(false), + m_active(false), m_pendingWorldTransition(false), m_playerROI(nullptr) { }