mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
3rd person camera (#3)
* Add feasibility plan for reusing multiplayer animation system for third-person camera Evaluates reusing the multiplayer extension's RemotePlayer animation system (BuildROIMap, AssignROIIndices, ApplyAnimationTransformation) for the local player to enable a third-person camera mode. Conclusion: feasible with only 3 single-line extension hooks added to core game code. https://claude.ai/code/session_01NC3zdQZ4nqEcYjyvStqcdD * WIP: Third-person camera with animation reuse and movement fix * Fix third-person camera bugs: vehicles, remote facing, emote distortion (#2) - Fix spawn pose and building re-entry by applying idle frame 0 and reinitializing on world enable - Handle vehicle transitions: ride animations for small vehicles, first-person fallback for large vehicles and helicopter - Keep vehicle dashboards visible for exit controls - Disable third-person camera for large vehicles, fix ROI cleanup - Move HandleActorExit hook to end of Exit() for immediate reinit - Fix remote player facing 180 degrees wrong by negating direction in BroadcastLocalState when third-person camera is active - Fix Hat Tip emote distortion from compounding transform scale by saving clean parent transform at emote start and restoring after each frame's animation application * DRY cleanup for third-person camera branch - Extract shared DetectVehicleType() to protocol.h/cpp (was duplicated in ThirdPersonCamera and NetworkManager) - Remove no-op HandlePostApplyTransform hook chain (called every frame for every LegoPathActor but did nothing) - Add ThirdPersonCamera::ClearAnimCaches() helper (pattern repeated 5x) - Add AnimUtils::EnsureROIMapVisibility() inline helper (loop repeated 5x across ThirdPersonCamera and RemotePlayer) - Remove redundant static_cast in multiplayer.cpp (UserActor() already returns LegoPathActor*) - Delete THIRD_PERSON_CAMERA_ANIMATION_REUSE_PLAN.md development artifact
This commit is contained in:
parent
0997610bad
commit
37b328a595
@ -532,9 +532,12 @@ if (ISLE_EXTENSIONS)
|
|||||||
extensions/src/siloader.cpp
|
extensions/src/siloader.cpp
|
||||||
extensions/src/textureloader.cpp
|
extensions/src/textureloader.cpp
|
||||||
extensions/src/multiplayer.cpp
|
extensions/src/multiplayer.cpp
|
||||||
|
extensions/src/multiplayer/animutils.cpp
|
||||||
extensions/src/multiplayer/charactercloner.cpp
|
extensions/src/multiplayer/charactercloner.cpp
|
||||||
extensions/src/multiplayer/networkmanager.cpp
|
extensions/src/multiplayer/networkmanager.cpp
|
||||||
|
extensions/src/multiplayer/protocol.cpp
|
||||||
extensions/src/multiplayer/remoteplayer.cpp
|
extensions/src/multiplayer/remoteplayer.cpp
|
||||||
|
extensions/src/multiplayer/thirdpersoncamera.cpp
|
||||||
extensions/src/multiplayer/worldstatesync.cpp
|
extensions/src/multiplayer/worldstatesync.cpp
|
||||||
)
|
)
|
||||||
if(EMSCRIPTEN)
|
if(EMSCRIPTEN)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "islepathactor.h"
|
#include "islepathactor.h"
|
||||||
|
|
||||||
#include "3dmanager/lego3dmanager.h"
|
#include "3dmanager/lego3dmanager.h"
|
||||||
|
#include "extensions/multiplayer.h"
|
||||||
#include "isle_actions.h"
|
#include "isle_actions.h"
|
||||||
#include "jukebox_actions.h"
|
#include "jukebox_actions.h"
|
||||||
#include "legoanimationmanager.h"
|
#include "legoanimationmanager.h"
|
||||||
@ -16,6 +17,8 @@
|
|||||||
#include "scripts.h"
|
#include "scripts.h"
|
||||||
#include "viewmanager/viewmanager.h"
|
#include "viewmanager/viewmanager.h"
|
||||||
|
|
||||||
|
using namespace Extensions;
|
||||||
|
|
||||||
DECOMP_SIZE_ASSERT(IslePathActor, 0x160)
|
DECOMP_SIZE_ASSERT(IslePathActor, 0x160)
|
||||||
DECOMP_SIZE_ASSERT(IslePathActor::SpawnLocation, 0x38)
|
DECOMP_SIZE_ASSERT(IslePathActor::SpawnLocation, 0x38)
|
||||||
|
|
||||||
@ -95,6 +98,8 @@ void IslePathActor::Enter()
|
|||||||
TurnAround();
|
TurnAround();
|
||||||
TransformPointOfView();
|
TransformPointOfView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Extension<MultiplayerExt>::Call(HandleActorEnter, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FUNCTION: LEGO1 0x1001a3f0
|
// FUNCTION: LEGO1 0x1001a3f0
|
||||||
@ -154,6 +159,8 @@ void IslePathActor::Exit()
|
|||||||
TurnAround();
|
TurnAround();
|
||||||
TransformPointOfView();
|
TransformPointOfView();
|
||||||
ResetViewVelocity();
|
ResetViewVelocity();
|
||||||
|
|
||||||
|
Extension<MultiplayerExt>::Call(HandleActorExit, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GLOBAL: LEGO1 0x10102b28
|
// GLOBAL: LEGO1 0x10102b28
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "legopathactor.h"
|
#include "legopathactor.h"
|
||||||
|
|
||||||
#include "define.h"
|
#include "define.h"
|
||||||
|
#include "extensions/multiplayer.h"
|
||||||
#include "geom/legoorientededge.h"
|
#include "geom/legoorientededge.h"
|
||||||
#include "legocachesoundmanager.h"
|
#include "legocachesoundmanager.h"
|
||||||
#include "legocameracontroller.h"
|
#include "legocameracontroller.h"
|
||||||
@ -20,6 +21,8 @@
|
|||||||
#include <mxdebug.h>
|
#include <mxdebug.h>
|
||||||
#include <vec.h>
|
#include <vec.h>
|
||||||
|
|
||||||
|
using namespace Extensions;
|
||||||
|
|
||||||
DECOMP_SIZE_ASSERT(LegoPathActor, 0x154)
|
DECOMP_SIZE_ASSERT(LegoPathActor, 0x154)
|
||||||
DECOMP_SIZE_ASSERT(LegoPathEdgeContainer, 0x3c)
|
DECOMP_SIZE_ASSERT(LegoPathEdgeContainer, 0x3c)
|
||||||
|
|
||||||
@ -262,6 +265,11 @@ MxS32 LegoPathActor::CalculateTransform(float p_time, Matrix4& p_transform)
|
|||||||
|
|
||||||
m_worldSpeed = nav->GetLinearVel();
|
m_worldSpeed = nav->GetLinearVel();
|
||||||
|
|
||||||
|
MxBool invertDir = Extension<MultiplayerExt>::Call(ShouldInvertMovement, this).value_or(FALSE);
|
||||||
|
if (invertDir) {
|
||||||
|
dir *= -1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
if (nav->CalculateNewPosDir(pos, dir, newPos, newDir, m_boundary->GetUp())) {
|
if (nav->CalculateNewPosDir(pos, dir, newPos, newDir, m_boundary->GetUp())) {
|
||||||
Mx3DPointFloat newPosCopy;
|
Mx3DPointFloat newPosCopy;
|
||||||
newPosCopy = newPos;
|
newPosCopy = newPos;
|
||||||
@ -321,6 +329,10 @@ MxS32 LegoPathActor::CalculateTransform(float p_time, Matrix4& p_transform)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (invertDir) {
|
||||||
|
newDir *= -1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
p_transform.SetIdentity();
|
p_transform.SetIdentity();
|
||||||
|
|
||||||
Vector3 right(p_transform[0]);
|
Vector3 right(p_transform[0]);
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
namespace Extensions
|
namespace Extensions
|
||||||
{
|
{
|
||||||
@ -17,14 +18,26 @@ LEGO1_EXPORT void Enable(const char* p_key, std::map<std::string, std::string> p
|
|||||||
template <typename T>
|
template <typename T>
|
||||||
struct Extension {
|
struct Extension {
|
||||||
template <typename Function, typename... Args>
|
template <typename Function, typename... Args>
|
||||||
static auto Call(Function&& function, Args&&... args) -> std::optional<std::invoke_result_t<Function, Args...>>
|
static auto Call(Function&& function, Args&&... args)
|
||||||
{
|
{
|
||||||
|
using result_t = std::invoke_result_t<Function, Args...>;
|
||||||
|
if constexpr (std::is_void_v<result_t>) {
|
||||||
#ifdef EXTENSIONS
|
#ifdef EXTENSIONS
|
||||||
if (T::enabled) {
|
if (T::enabled) {
|
||||||
return std::invoke(std::forward<Function>(function), std::forward<Args>(args)...);
|
std::invoke(std::forward<Function>(function), std::forward<Args>(args)...);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
return std::nullopt;
|
}
|
||||||
|
else {
|
||||||
|
#ifdef EXTENSIONS
|
||||||
|
if (T::enabled) {
|
||||||
|
return std::optional<result_t>(
|
||||||
|
std::invoke(std::forward<Function>(function), std::forward<Args>(args)...)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return std::optional<result_t>(std::nullopt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}; // namespace Extensions
|
}; // namespace Extensions
|
||||||
|
|||||||
@ -6,7 +6,9 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
class IslePathActor;
|
||||||
class LegoEntity;
|
class LegoEntity;
|
||||||
|
class LegoPathActor;
|
||||||
class LegoWorld;
|
class LegoWorld;
|
||||||
|
|
||||||
namespace Multiplayer
|
namespace Multiplayer
|
||||||
@ -34,6 +36,10 @@ class MultiplayerExt {
|
|||||||
static std::string relayUrl;
|
static std::string relayUrl;
|
||||||
static std::string room;
|
static std::string room;
|
||||||
|
|
||||||
|
static void HandleActorEnter(IslePathActor* p_actor);
|
||||||
|
static void HandleActorExit(IslePathActor* p_actor);
|
||||||
|
static MxBool ShouldInvertMovement(LegoPathActor* p_actor);
|
||||||
|
|
||||||
// Returns true if the multiplayer connection was rejected (e.g. room full).
|
// Returns true if the multiplayer connection was rejected (e.g. room full).
|
||||||
static MxBool CheckRejected();
|
static MxBool CheckRejected();
|
||||||
|
|
||||||
@ -51,10 +57,16 @@ LEGO1_EXPORT bool IsMultiplayerRejected();
|
|||||||
|
|
||||||
constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
|
constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
|
||||||
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
|
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
|
||||||
|
constexpr auto HandleActorEnter = &MultiplayerExt::HandleActorEnter;
|
||||||
|
constexpr auto HandleActorExit = &MultiplayerExt::HandleActorExit;
|
||||||
|
constexpr auto ShouldInvertMovement = &MultiplayerExt::ShouldInvertMovement;
|
||||||
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected;
|
constexpr auto CheckRejected = &MultiplayerExt::CheckRejected;
|
||||||
#else
|
#else
|
||||||
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
|
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
|
||||||
|
constexpr decltype(&MultiplayerExt::HandleActorEnter) HandleActorEnter = nullptr;
|
||||||
|
constexpr decltype(&MultiplayerExt::HandleActorExit) HandleActorExit = nullptr;
|
||||||
|
constexpr decltype(&MultiplayerExt::ShouldInvertMovement) ShouldInvertMovement = nullptr;
|
||||||
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr;
|
constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
82
extensions/include/extensions/multiplayer/animutils.h
Normal file
82
extensions/include/extensions/multiplayer/animutils.h
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mxtypes.h"
|
||||||
|
#include "roi/legoroi.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class LegoAnim;
|
||||||
|
|
||||||
|
namespace Multiplayer
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace AnimUtils
|
||||||
|
{
|
||||||
|
|
||||||
|
// Cached ROI map entry for an animation
|
||||||
|
struct AnimCache {
|
||||||
|
LegoAnim* anim;
|
||||||
|
LegoROI** roiMap;
|
||||||
|
MxU32 roiMapSize;
|
||||||
|
|
||||||
|
AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {}
|
||||||
|
~AnimCache()
|
||||||
|
{
|
||||||
|
if (roiMap) {
|
||||||
|
delete[] roiMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
p_other.roiMap = nullptr;
|
||||||
|
p_other.roiMapSize = 0;
|
||||||
|
p_other.anim = nullptr;
|
||||||
|
}
|
||||||
|
AnimCache& operator=(AnimCache&& p_other) noexcept
|
||||||
|
{
|
||||||
|
if (this != &p_other) {
|
||||||
|
if (roiMap) {
|
||||||
|
delete[] roiMap;
|
||||||
|
}
|
||||||
|
anim = p_other.anim;
|
||||||
|
roiMap = p_other.roiMap;
|
||||||
|
roiMapSize = p_other.roiMapSize;
|
||||||
|
p_other.roiMap = nullptr;
|
||||||
|
p_other.roiMapSize = 0;
|
||||||
|
p_other.anim = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void BuildROIMap(
|
||||||
|
LegoAnim* p_anim,
|
||||||
|
LegoROI* p_rootROI,
|
||||||
|
LegoROI* p_extraROI,
|
||||||
|
LegoROI**& p_roiMap,
|
||||||
|
MxU32& p_roiMapSize
|
||||||
|
);
|
||||||
|
|
||||||
|
AnimCache* GetOrBuildAnimCache(
|
||||||
|
std::map<std::string, AnimCache>& p_cacheMap,
|
||||||
|
LegoROI* p_roi,
|
||||||
|
const char* p_animName
|
||||||
|
);
|
||||||
|
|
||||||
|
inline void EnsureROIMapVisibility(LegoROI** p_roiMap, MxU32 p_roiMapSize)
|
||||||
|
{
|
||||||
|
for (MxU32 i = 1; i < p_roiMapSize; i++) {
|
||||||
|
if (p_roiMap[i] != nullptr) {
|
||||||
|
p_roiMap[i]->SetVisibility(TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace AnimUtils
|
||||||
|
|
||||||
|
} // namespace Multiplayer
|
||||||
@ -4,6 +4,7 @@
|
|||||||
#include "extensions/multiplayer/platformcallbacks.h"
|
#include "extensions/multiplayer/platformcallbacks.h"
|
||||||
#include "extensions/multiplayer/protocol.h"
|
#include "extensions/multiplayer/protocol.h"
|
||||||
#include "extensions/multiplayer/remoteplayer.h"
|
#include "extensions/multiplayer/remoteplayer.h"
|
||||||
|
#include "extensions/multiplayer/thirdpersoncamera.h"
|
||||||
#include "extensions/multiplayer/worldstatesync.h"
|
#include "extensions/multiplayer/worldstatesync.h"
|
||||||
#include "mxcore.h"
|
#include "mxcore.h"
|
||||||
#include "mxtypes.h"
|
#include "mxtypes.h"
|
||||||
@ -49,6 +50,8 @@ class NetworkManager : public MxCore {
|
|||||||
void OnWorldEnabled(LegoWorld* p_world);
|
void OnWorldEnabled(LegoWorld* p_world);
|
||||||
void OnWorldDisabled(LegoWorld* p_world);
|
void OnWorldDisabled(LegoWorld* p_world);
|
||||||
|
|
||||||
|
ThirdPersonCamera& GetThirdPersonCamera() { return m_thirdPersonCamera; }
|
||||||
|
|
||||||
// Called from multiplayer extension when a plant/building entity is clicked.
|
// Called from multiplayer extension when a plant/building entity is clicked.
|
||||||
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
// Returns TRUE if the mutation should be suppressed locally (non-host).
|
||||||
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
MxBool HandleEntityMutation(LegoEntity* p_entity, MxU8 p_changeType);
|
||||||
@ -72,7 +75,6 @@ class NetworkManager : public MxCore {
|
|||||||
void RemoveAllRemotePlayers();
|
void RemoveAllRemotePlayers();
|
||||||
|
|
||||||
void NotifyPlayerCountChanged();
|
void NotifyPlayerCountChanged();
|
||||||
int8_t DetectLocalVehicleType();
|
|
||||||
|
|
||||||
// Serialize and send a fixed-size message via the transport
|
// Serialize and send a fixed-size message via the transport
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@ -81,6 +83,7 @@ class NetworkManager : public MxCore {
|
|||||||
NetworkTransport* m_transport;
|
NetworkTransport* m_transport;
|
||||||
PlatformCallbacks* m_callbacks;
|
PlatformCallbacks* m_callbacks;
|
||||||
WorldStateSync m_worldSync;
|
WorldStateSync m_worldSync;
|
||||||
|
ThirdPersonCamera m_thirdPersonCamera;
|
||||||
std::map<uint32_t, std::unique_ptr<RemotePlayer>> m_remotePlayers;
|
std::map<uint32_t, std::unique_ptr<RemotePlayer>> m_remotePlayers;
|
||||||
|
|
||||||
uint32_t m_localPeerId;
|
uint32_t m_localPeerId;
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
|
||||||
|
class LegoPathActor;
|
||||||
|
|
||||||
namespace Multiplayer
|
namespace Multiplayer
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -127,31 +129,25 @@ struct EmoteMsg {
|
|||||||
|
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
// Walk animation table: index -> CNs name
|
// Animation and vehicle tables (defined in protocol.cpp)
|
||||||
static const char* const g_walkAnimNames[] = {
|
extern const char* const g_walkAnimNames[];
|
||||||
"CNs001xx", // 0: Normal (default)
|
extern const int g_walkAnimCount;
|
||||||
"CNs002xx", // 1: Joyful
|
|
||||||
"CNs003xx", // 2: Gloomy
|
|
||||||
"CNs005xx", // 3: Leaning
|
|
||||||
"CNs006xx", // 4: Scared
|
|
||||||
"CNs007xx", // 5: Hyper
|
|
||||||
};
|
|
||||||
static const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
|
|
||||||
|
|
||||||
// Idle animation table: index -> CNs name
|
extern const char* const g_idleAnimNames[];
|
||||||
static const char* const g_idleAnimNames[] = {
|
extern const int g_idleAnimCount;
|
||||||
"CNs008xx", // 0: Sway (default)
|
|
||||||
"CNs009xx", // 1: Groove
|
|
||||||
"CNs010xx", // 2: Excited
|
|
||||||
};
|
|
||||||
static const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
|
|
||||||
|
|
||||||
// Emote table: index -> CNs name
|
extern const char* const g_emoteAnimNames[];
|
||||||
static const char* const g_emoteAnimNames[] = {
|
extern const int g_emoteAnimCount;
|
||||||
"CNs011xx", // 0: Wave
|
|
||||||
"CNs012xx", // 1: Hat Tip
|
extern const char* const g_vehicleROINames[VEHICLE_COUNT];
|
||||||
};
|
extern const char* const g_rideAnimNames[VEHICLE_COUNT];
|
||||||
static const int g_emoteAnimCount = sizeof(g_emoteAnimNames) / sizeof(g_emoteAnimNames[0]);
|
extern const char* const g_rideVehicleROINames[VEHICLE_COUNT];
|
||||||
|
|
||||||
|
// Returns true if the vehicle type has no ride animation (model swap instead)
|
||||||
|
bool IsLargeVehicle(int8_t p_vehicleType);
|
||||||
|
|
||||||
|
// Detect the vehicle type of a given actor, or VEHICLE_NONE if not a vehicle
|
||||||
|
int8_t DetectVehicleType(LegoPathActor* p_actor);
|
||||||
|
|
||||||
// Validate actorId is a playable character (1-5, not brickster)
|
// Validate actorId is a playable character (1-5, not brickster)
|
||||||
inline bool IsValidActorId(uint8_t p_actorId)
|
inline bool IsValidActorId(uint8_t p_actorId)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "extensions/multiplayer/animutils.h"
|
||||||
#include "extensions/multiplayer/protocol.h"
|
#include "extensions/multiplayer/protocol.h"
|
||||||
#include "mxtypes.h"
|
#include "mxtypes.h"
|
||||||
|
|
||||||
@ -37,53 +38,8 @@ class RemotePlayer {
|
|||||||
void TriggerEmote(uint8_t p_emoteId);
|
void TriggerEmote(uint8_t p_emoteId);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Cached ROI map entry for an animation
|
using AnimCache = AnimUtils::AnimCache;
|
||||||
struct AnimCache {
|
|
||||||
LegoAnim* anim;
|
|
||||||
LegoROI** roiMap;
|
|
||||||
MxU32 roiMapSize;
|
|
||||||
|
|
||||||
AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {}
|
|
||||||
~AnimCache()
|
|
||||||
{
|
|
||||||
if (roiMap) {
|
|
||||||
delete[] roiMap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
p_other.roiMap = nullptr;
|
|
||||||
p_other.roiMapSize = 0;
|
|
||||||
p_other.anim = nullptr;
|
|
||||||
}
|
|
||||||
AnimCache& operator=(AnimCache&& p_other) noexcept
|
|
||||||
{
|
|
||||||
if (this != &p_other) {
|
|
||||||
if (roiMap) {
|
|
||||||
delete[] roiMap;
|
|
||||||
}
|
|
||||||
anim = p_other.anim;
|
|
||||||
roiMap = p_other.roiMap;
|
|
||||||
roiMapSize = p_other.roiMapSize;
|
|
||||||
p_other.roiMap = nullptr;
|
|
||||||
p_other.roiMapSize = 0;
|
|
||||||
p_other.anim = nullptr;
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void BuildROIMap(
|
|
||||||
LegoAnim* p_anim,
|
|
||||||
LegoROI* p_rootROI,
|
|
||||||
LegoROI* p_extraROI,
|
|
||||||
LegoROI**& p_roiMap,
|
|
||||||
MxU32& p_roiMapSize
|
|
||||||
);
|
|
||||||
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
AnimCache* GetOrBuildAnimCache(const char* p_animName);
|
||||||
void UpdateTransform(float p_deltaTime);
|
void UpdateTransform(float p_deltaTime);
|
||||||
void UpdateAnimation(float p_deltaTime);
|
void UpdateAnimation(float p_deltaTime);
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
#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 IslePathActor;
|
||||||
|
class LegoPathActor;
|
||||||
|
class LegoROI;
|
||||||
|
class LegoWorld;
|
||||||
|
class LegoAnim;
|
||||||
|
|
||||||
|
namespace Multiplayer
|
||||||
|
{
|
||||||
|
|
||||||
|
class ThirdPersonCamera {
|
||||||
|
public:
|
||||||
|
ThirdPersonCamera();
|
||||||
|
|
||||||
|
void Enable();
|
||||||
|
void Disable();
|
||||||
|
bool IsEnabled() const { return m_enabled; }
|
||||||
|
bool IsActive() const { return m_active; }
|
||||||
|
|
||||||
|
// Core hooks
|
||||||
|
void OnActorEnter(IslePathActor* p_actor);
|
||||||
|
void OnActorExit(IslePathActor* p_actor);
|
||||||
|
|
||||||
|
// Called every frame from NetworkManager::Tickle()
|
||||||
|
void Tick(float p_deltaTime);
|
||||||
|
|
||||||
|
// Animation selection (forwarded from NetworkManager)
|
||||||
|
void SetWalkAnimId(uint8_t p_id);
|
||||||
|
void SetIdleAnimId(uint8_t p_id);
|
||||||
|
void TriggerEmote(uint8_t p_emoteId);
|
||||||
|
|
||||||
|
void OnWorldEnabled(LegoWorld* p_world);
|
||||||
|
void OnWorldDisabled(LegoWorld* p_world);
|
||||||
|
|
||||||
|
private:
|
||||||
|
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();
|
||||||
|
|
||||||
|
bool m_enabled;
|
||||||
|
bool m_active;
|
||||||
|
LegoROI* m_playerROI; // Borrowed, not owned
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Emote state
|
||||||
|
AnimCache* m_emoteAnimCache;
|
||||||
|
float m_emoteTime;
|
||||||
|
float m_emoteDuration;
|
||||||
|
bool m_emoteActive;
|
||||||
|
MxMatrix m_emoteParentTransform;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Multiplayer
|
||||||
@ -4,9 +4,11 @@
|
|||||||
#include "extensions/multiplayer/networkmanager.h"
|
#include "extensions/multiplayer/networkmanager.h"
|
||||||
#include "extensions/multiplayer/networktransport.h"
|
#include "extensions/multiplayer/networktransport.h"
|
||||||
#include "extensions/multiplayer/protocol.h"
|
#include "extensions/multiplayer/protocol.h"
|
||||||
|
#include "islepathactor.h"
|
||||||
#include "legoactor.h"
|
#include "legoactor.h"
|
||||||
#include "legoentity.h"
|
#include "legoentity.h"
|
||||||
#include "legogamestate.h"
|
#include "legogamestate.h"
|
||||||
|
#include "legopathactor.h"
|
||||||
#include "misc.h"
|
#include "misc.h"
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
@ -31,10 +33,6 @@ void MultiplayerExt::Initialize()
|
|||||||
relayUrl = options["multiplayer:relay url"];
|
relayUrl = options["multiplayer:relay url"];
|
||||||
room = options["multiplayer:room"];
|
room = options["multiplayer:room"];
|
||||||
|
|
||||||
if (relayUrl.empty() || room.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
s_transport = new Multiplayer::WebSocketTransport(relayUrl);
|
s_transport = new Multiplayer::WebSocketTransport(relayUrl);
|
||||||
s_callbacks = new Multiplayer::EmscriptenCallbacks();
|
s_callbacks = new Multiplayer::EmscriptenCallbacks();
|
||||||
@ -42,7 +40,12 @@ void MultiplayerExt::Initialize()
|
|||||||
s_networkManager = new Multiplayer::NetworkManager();
|
s_networkManager = new Multiplayer::NetworkManager();
|
||||||
s_networkManager->Initialize(s_transport, s_callbacks);
|
s_networkManager->Initialize(s_transport, s_callbacks);
|
||||||
|
|
||||||
s_networkManager->Connect(room.c_str());
|
// Third-person camera enabled by default, toggled via WASM export
|
||||||
|
s_networkManager->GetThirdPersonCamera().Enable();
|
||||||
|
|
||||||
|
if (!relayUrl.empty() && !room.empty()) {
|
||||||
|
s_networkManager->Connect(room.c_str());
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +110,29 @@ MxBool MultiplayerExt::HandleEntityNotify(LegoEntity* p_entity)
|
|||||||
return s_networkManager->HandleEntityMutation(p_entity, changeType);
|
return s_networkManager->HandleEntityMutation(p_entity, changeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MultiplayerExt::HandleActorEnter(IslePathActor* p_actor)
|
||||||
|
{
|
||||||
|
if (s_networkManager) {
|
||||||
|
s_networkManager->GetThirdPersonCamera().OnActorEnter(p_actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultiplayerExt::HandleActorExit(IslePathActor* p_actor)
|
||||||
|
{
|
||||||
|
if (s_networkManager) {
|
||||||
|
s_networkManager->GetThirdPersonCamera().OnActorExit(p_actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MxBool MultiplayerExt::ShouldInvertMovement(LegoPathActor* p_actor)
|
||||||
|
{
|
||||||
|
if (s_networkManager && UserActor() == p_actor) {
|
||||||
|
return s_networkManager->GetThirdPersonCamera().IsActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
MxBool MultiplayerExt::CheckRejected()
|
MxBool MultiplayerExt::CheckRejected()
|
||||||
{
|
{
|
||||||
if (s_networkManager && s_networkManager->WasRejected()) {
|
if (s_networkManager && s_networkManager->WasRejected()) {
|
||||||
|
|||||||
130
extensions/src/multiplayer/animutils.cpp
Normal file
130
extensions/src/multiplayer/animutils.cpp
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#include "extensions/multiplayer/animutils.h"
|
||||||
|
|
||||||
|
#include "anim/legoanim.h"
|
||||||
|
#include "legoanimpresenter.h"
|
||||||
|
#include "legoworld.h"
|
||||||
|
#include "misc.h"
|
||||||
|
#include "misc/legotree.h"
|
||||||
|
#include "roi/legoroi.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace Multiplayer;
|
||||||
|
|
||||||
|
// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
|
||||||
|
// via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes.
|
||||||
|
static void AssignROIIndices(
|
||||||
|
LegoTreeNode* p_node,
|
||||||
|
LegoROI* p_parentROI,
|
||||||
|
LegoROI* p_rootROI,
|
||||||
|
LegoROI* p_extraROI,
|
||||||
|
MxU32& p_nextIndex,
|
||||||
|
std::vector<LegoROI*>& p_entries
|
||||||
|
)
|
||||||
|
{
|
||||||
|
LegoROI* roi = p_parentROI;
|
||||||
|
LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData();
|
||||||
|
const char* name = data ? data->GetName() : nullptr;
|
||||||
|
|
||||||
|
if (name != nullptr && *name != '-') {
|
||||||
|
LegoROI* matchedROI = nullptr;
|
||||||
|
|
||||||
|
if (*name == '*' || p_parentROI == nullptr) {
|
||||||
|
roi = p_rootROI;
|
||||||
|
matchedROI = p_rootROI;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
matchedROI = p_parentROI->FindChildROI(name, p_parentROI);
|
||||||
|
if (matchedROI == nullptr && p_extraROI != nullptr) {
|
||||||
|
matchedROI = p_extraROI->FindChildROI(name, p_extraROI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedROI != nullptr) {
|
||||||
|
data->SetROIIndex(p_nextIndex);
|
||||||
|
p_entries.push_back(matchedROI);
|
||||||
|
p_nextIndex++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data->SetROIIndex(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) {
|
||||||
|
AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimUtils::BuildROIMap(
|
||||||
|
LegoAnim* p_anim,
|
||||||
|
LegoROI* p_rootROI,
|
||||||
|
LegoROI* p_extraROI,
|
||||||
|
LegoROI**& p_roiMap,
|
||||||
|
MxU32& p_roiMapSize
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!p_anim || !p_rootROI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoTreeNode* root = p_anim->GetRoot();
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MxU32 nextIndex = 1;
|
||||||
|
std::vector<LegoROI*> entries;
|
||||||
|
AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries);
|
||||||
|
|
||||||
|
if (entries.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1-indexed; index 0 reserved as NULL
|
||||||
|
p_roiMapSize = entries.size() + 1;
|
||||||
|
p_roiMap = new LegoROI*[p_roiMapSize];
|
||||||
|
p_roiMap[0] = nullptr;
|
||||||
|
for (MxU32 i = 0; i < entries.size(); i++) {
|
||||||
|
p_roiMap[i + 1] = entries[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimUtils::AnimCache* AnimUtils::GetOrBuildAnimCache(
|
||||||
|
std::map<std::string, AnimCache>& p_cacheMap,
|
||||||
|
LegoROI* p_roi,
|
||||||
|
const char* p_animName
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!p_animName || !p_roi) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already cached
|
||||||
|
auto it = p_cacheMap.find(p_animName);
|
||||||
|
if (it != p_cacheMap.end()) {
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the animation presenter in the current world
|
||||||
|
LegoWorld* world = CurrentWorld();
|
||||||
|
if (!world) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
MxCore* presenter = world->Find("LegoAnimPresenter", p_animName);
|
||||||
|
if (!presenter) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoAnim* anim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
|
||||||
|
if (!anim) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and cache
|
||||||
|
AnimCache& cache = p_cacheMap[p_animName];
|
||||||
|
cache.anim = anim;
|
||||||
|
BuildROIMap(anim, p_roi, nullptr, cache.roiMap, cache.roiMapSize);
|
||||||
|
|
||||||
|
return &cache;
|
||||||
|
}
|
||||||
@ -42,6 +42,8 @@ NetworkManager::~NetworkManager()
|
|||||||
|
|
||||||
MxResult NetworkManager::Tickle()
|
MxResult NetworkManager::Tickle()
|
||||||
{
|
{
|
||||||
|
m_thirdPersonCamera.Tick(0.016f);
|
||||||
|
|
||||||
if (!m_transport) {
|
if (!m_transport) {
|
||||||
return SUCCESS;
|
return SUCCESS;
|
||||||
}
|
}
|
||||||
@ -134,6 +136,8 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
|
|||||||
m_registered = true;
|
m_registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_thirdPersonCamera.OnWorldEnabled(p_world);
|
||||||
|
|
||||||
if (p_world->GetWorldId() == LegoOmni::e_act1) {
|
if (p_world->GetWorldId() == LegoOmni::e_act1) {
|
||||||
m_inIsleWorld = true;
|
m_inIsleWorld = true;
|
||||||
m_worldSync.SetInIsleWorld(true);
|
m_worldSync.SetInIsleWorld(true);
|
||||||
@ -158,6 +162,8 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_thirdPersonCamera.OnWorldDisabled(p_world);
|
||||||
|
|
||||||
if (p_world->GetWorldId() == LegoOmni::e_act1) {
|
if (p_world->GetWorldId() == LegoOmni::e_act1) {
|
||||||
m_inIsleWorld = false;
|
m_inIsleWorld = false;
|
||||||
m_worldSync.SetInIsleWorld(false);
|
m_worldSync.SetInIsleWorld(false);
|
||||||
@ -213,9 +219,19 @@ void NetworkManager::BroadcastLocalState()
|
|||||||
msg.header = {MSG_STATE, m_localPeerId, m_sequence++};
|
msg.header = {MSG_STATE, m_localPeerId, m_sequence++};
|
||||||
msg.actorId = actorId;
|
msg.actorId = actorId;
|
||||||
msg.worldId = (int8_t) currentWorld->GetWorldId();
|
msg.worldId = (int8_t) currentWorld->GetWorldId();
|
||||||
msg.vehicleType = DetectLocalVehicleType();
|
msg.vehicleType = DetectVehicleType(userActor);
|
||||||
SDL_memcpy(msg.position, pos, sizeof(msg.position));
|
SDL_memcpy(msg.position, pos, sizeof(msg.position));
|
||||||
SDL_memcpy(msg.direction, dir, sizeof(msg.direction));
|
SDL_memcpy(msg.direction, dir, sizeof(msg.direction));
|
||||||
|
|
||||||
|
// Third-person camera: ROI direction is opposite to actual movement direction
|
||||||
|
// (ShouldInvertMovement preserves TurnAround convention). Negate so remote
|
||||||
|
// players receive the true movement-facing direction.
|
||||||
|
if (m_thirdPersonCamera.IsActive()) {
|
||||||
|
msg.direction[0] = -msg.direction[0];
|
||||||
|
msg.direction[1] = -msg.direction[1];
|
||||||
|
msg.direction[2] = -msg.direction[2];
|
||||||
|
}
|
||||||
|
|
||||||
SDL_memcpy(msg.up, up, sizeof(msg.up));
|
SDL_memcpy(msg.up, up, sizeof(msg.up));
|
||||||
msg.speed = speed;
|
msg.speed = speed;
|
||||||
msg.walkAnimId = m_localWalkAnimId;
|
msg.walkAnimId = m_localWalkAnimId;
|
||||||
@ -407,6 +423,7 @@ void NetworkManager::SetWalkAnimation(uint8_t p_index)
|
|||||||
{
|
{
|
||||||
if (p_index < g_walkAnimCount) {
|
if (p_index < g_walkAnimCount) {
|
||||||
m_localWalkAnimId = p_index;
|
m_localWalkAnimId = p_index;
|
||||||
|
m_thirdPersonCamera.SetWalkAnimId(p_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,6 +431,7 @@ void NetworkManager::SetIdleAnimation(uint8_t p_index)
|
|||||||
{
|
{
|
||||||
if (p_index < g_idleAnimCount) {
|
if (p_index < g_idleAnimCount) {
|
||||||
m_localIdleAnimId = p_index;
|
m_localIdleAnimId = p_index;
|
||||||
|
m_thirdPersonCamera.SetIdleAnimId(p_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,6 +441,8 @@ void NetworkManager::SendEmote(uint8_t p_emoteId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_thirdPersonCamera.TriggerEmote(p_emoteId);
|
||||||
|
|
||||||
EmoteMsg msg{};
|
EmoteMsg msg{};
|
||||||
msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++};
|
msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++};
|
||||||
msg.emoteId = p_emoteId;
|
msg.emoteId = p_emoteId;
|
||||||
@ -476,31 +496,3 @@ void NetworkManager::NotifyPlayerCountChanged()
|
|||||||
m_callbacks->OnPlayerCountChanged(count);
|
m_callbacks->OnPlayerCountChanged(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
int8_t NetworkManager::DetectLocalVehicleType()
|
|
||||||
{
|
|
||||||
static const struct {
|
|
||||||
const char* className;
|
|
||||||
int8_t vehicleType;
|
|
||||||
} vehicleMap[] = {
|
|
||||||
{"Helicopter", VEHICLE_HELICOPTER},
|
|
||||||
{"Jetski", VEHICLE_JETSKI},
|
|
||||||
{"DuneBuggy", VEHICLE_DUNEBUGGY},
|
|
||||||
{"Bike", VEHICLE_BIKE},
|
|
||||||
{"SkateBoard", VEHICLE_SKATEBOARD},
|
|
||||||
{"Motorcycle", VEHICLE_MOTOCYCLE},
|
|
||||||
{"TowTrack", VEHICLE_TOWTRACK},
|
|
||||||
{"Ambulance", VEHICLE_AMBULANCE},
|
|
||||||
};
|
|
||||||
|
|
||||||
LegoPathActor* actor = UserActor();
|
|
||||||
if (!actor) {
|
|
||||||
return VEHICLE_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& entry : vehicleMap) {
|
|
||||||
if (actor->IsA(entry.className)) {
|
|
||||||
return entry.vehicleType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return VEHICLE_NONE;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -34,6 +34,20 @@ extern "C"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE void mp_toggle_third_person()
|
||||||
|
{
|
||||||
|
Multiplayer::NetworkManager* mgr = MultiplayerExt::GetNetworkManager();
|
||||||
|
if (mgr) {
|
||||||
|
Multiplayer::ThirdPersonCamera& cam = mgr->GetThirdPersonCamera();
|
||||||
|
if (cam.IsEnabled()) {
|
||||||
|
cam.Disable();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cam.Enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
79
extensions/src/multiplayer/protocol.cpp
Normal file
79
extensions/src/multiplayer/protocol.cpp
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
#include "extensions/multiplayer/protocol.h"
|
||||||
|
|
||||||
|
#include "legopathactor.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace Multiplayer
|
||||||
|
{
|
||||||
|
|
||||||
|
const char* const g_walkAnimNames[] = {
|
||||||
|
"CNs001xx", // 0: Normal (default)
|
||||||
|
"CNs002xx", // 1: Joyful
|
||||||
|
"CNs003xx", // 2: Gloomy
|
||||||
|
"CNs005xx", // 3: Leaning
|
||||||
|
"CNs006xx", // 4: Scared
|
||||||
|
"CNs007xx", // 5: Hyper
|
||||||
|
};
|
||||||
|
const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]);
|
||||||
|
|
||||||
|
const char* const g_idleAnimNames[] = {
|
||||||
|
"CNs008xx", // 0: Sway (default)
|
||||||
|
"CNs009xx", // 1: Groove
|
||||||
|
"CNs010xx", // 2: Excited
|
||||||
|
};
|
||||||
|
const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]);
|
||||||
|
|
||||||
|
const char* const g_emoteAnimNames[] = {
|
||||||
|
"CNs011xx", // 0: Wave
|
||||||
|
"CNs012xx", // 1: Hat Tip
|
||||||
|
};
|
||||||
|
const int g_emoteAnimCount = sizeof(g_emoteAnimNames) / sizeof(g_emoteAnimNames[0]);
|
||||||
|
|
||||||
|
// Vehicle model names (LOD names). The helicopter is a compound ROI ("copter")
|
||||||
|
// with no standalone LOD; use its body part instead.
|
||||||
|
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};
|
||||||
|
|
||||||
|
// Vehicle variant ROI names used in ride animations
|
||||||
|
const char* const g_rideVehicleROINames[VEHICLE_COUNT] =
|
||||||
|
{NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
||||||
|
|
||||||
|
bool IsLargeVehicle(int8_t p_vehicleType)
|
||||||
|
{
|
||||||
|
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t DetectVehicleType(LegoPathActor* p_actor)
|
||||||
|
{
|
||||||
|
static const struct {
|
||||||
|
const char* className;
|
||||||
|
int8_t vehicleType;
|
||||||
|
} vehicleMap[] = {
|
||||||
|
{"Helicopter", VEHICLE_HELICOPTER},
|
||||||
|
{"Jetski", VEHICLE_JETSKI},
|
||||||
|
{"DuneBuggy", VEHICLE_DUNEBUGGY},
|
||||||
|
{"Bike", VEHICLE_BIKE},
|
||||||
|
{"SkateBoard", VEHICLE_SKATEBOARD},
|
||||||
|
{"Motorcycle", VEHICLE_MOTOCYCLE},
|
||||||
|
{"TowTrack", VEHICLE_TOWTRACK},
|
||||||
|
{"Ambulance", VEHICLE_AMBULANCE},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!p_actor) {
|
||||||
|
return VEHICLE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : vehicleMap) {
|
||||||
|
if (p_actor->IsA(entry.className)) {
|
||||||
|
return entry.vehicleType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return VEHICLE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Multiplayer
|
||||||
@ -18,24 +18,9 @@
|
|||||||
#include <SDL3/SDL_timer.h>
|
#include <SDL3/SDL_timer.h>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <vec.h>
|
#include <vec.h>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace Multiplayer;
|
using namespace Multiplayer;
|
||||||
|
|
||||||
// LOD names for vehicle models. The helicopter is a compound ROI ("copter")
|
|
||||||
// with no standalone LOD; use its body part instead.
|
|
||||||
static const char* g_vehicleROINames[VEHICLE_COUNT] =
|
|
||||||
{"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"};
|
|
||||||
|
|
||||||
static const char* g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL};
|
|
||||||
|
|
||||||
static const char* g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL};
|
|
||||||
|
|
||||||
static bool IsLargeVehicle(int8_t p_vehicleType)
|
|
||||||
{
|
|
||||||
return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
|
RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId)
|
||||||
: m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f),
|
: m_peerId(p_peerId), m_actorId(p_actorId), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f),
|
||||||
m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()),
|
m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()),
|
||||||
@ -213,38 +198,7 @@ void RemotePlayer::SetVisible(bool p_visible)
|
|||||||
|
|
||||||
RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName)
|
RemotePlayer::AnimCache* RemotePlayer::GetOrBuildAnimCache(const char* p_animName)
|
||||||
{
|
{
|
||||||
if (!p_animName || !m_roi) {
|
return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, m_roi, p_animName);
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already cached
|
|
||||||
auto it = m_animCacheMap.find(p_animName);
|
|
||||||
if (it != m_animCacheMap.end()) {
|
|
||||||
return &it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up the animation presenter in the current world
|
|
||||||
LegoWorld* world = CurrentWorld();
|
|
||||||
if (!world) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
MxCore* presenter = world->Find("LegoAnimPresenter", p_animName);
|
|
||||||
if (!presenter) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
LegoAnim* anim = static_cast<LegoAnimPresenter*>(presenter)->GetAnimation();
|
|
||||||
if (!anim) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build and cache
|
|
||||||
AnimCache& cache = m_animCacheMap[p_animName];
|
|
||||||
cache.anim = anim;
|
|
||||||
BuildROIMap(anim, m_roi, nullptr, cache.roiMap, cache.roiMapSize);
|
|
||||||
|
|
||||||
return &cache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
|
void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
|
||||||
@ -269,83 +223,6 @@ void RemotePlayer::TriggerEmote(uint8_t p_emoteId)
|
|||||||
m_emoteActive = true;
|
m_emoteActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime
|
|
||||||
// via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes.
|
|
||||||
static void AssignROIIndices(
|
|
||||||
LegoTreeNode* p_node,
|
|
||||||
LegoROI* p_parentROI,
|
|
||||||
LegoROI* p_rootROI,
|
|
||||||
LegoROI* p_extraROI,
|
|
||||||
MxU32& p_nextIndex,
|
|
||||||
std::vector<LegoROI*>& p_entries
|
|
||||||
)
|
|
||||||
{
|
|
||||||
LegoROI* roi = p_parentROI;
|
|
||||||
LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData();
|
|
||||||
const char* name = data ? data->GetName() : nullptr;
|
|
||||||
|
|
||||||
if (name != nullptr && *name != '-') {
|
|
||||||
LegoROI* matchedROI = nullptr;
|
|
||||||
|
|
||||||
if (*name == '*' || p_parentROI == nullptr) {
|
|
||||||
roi = p_rootROI;
|
|
||||||
matchedROI = p_rootROI;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
matchedROI = p_parentROI->FindChildROI(name, p_parentROI);
|
|
||||||
if (matchedROI == nullptr && p_extraROI != nullptr) {
|
|
||||||
matchedROI = p_extraROI->FindChildROI(name, p_extraROI);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedROI != nullptr) {
|
|
||||||
data->SetROIIndex(p_nextIndex);
|
|
||||||
p_entries.push_back(matchedROI);
|
|
||||||
p_nextIndex++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
data->SetROIIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) {
|
|
||||||
AssignROIIndices(p_node->GetChild(i), roi, p_rootROI, p_extraROI, p_nextIndex, p_entries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemotePlayer::BuildROIMap(
|
|
||||||
LegoAnim* p_anim,
|
|
||||||
LegoROI* p_rootROI,
|
|
||||||
LegoROI* p_extraROI,
|
|
||||||
LegoROI**& p_roiMap,
|
|
||||||
MxU32& p_roiMapSize
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (!p_anim || !p_rootROI) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LegoTreeNode* root = p_anim->GetRoot();
|
|
||||||
if (!root) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MxU32 nextIndex = 1;
|
|
||||||
std::vector<LegoROI*> entries;
|
|
||||||
AssignROIIndices(root, nullptr, p_rootROI, p_extraROI, nextIndex, entries);
|
|
||||||
|
|
||||||
if (entries.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1-indexed; index 0 reserved as NULL
|
|
||||||
p_roiMapSize = entries.size() + 1;
|
|
||||||
p_roiMap = new LegoROI*[p_roiMapSize];
|
|
||||||
p_roiMap[0] = nullptr;
|
|
||||||
for (MxU32 i = 0; i < entries.size(); i++) {
|
|
||||||
p_roiMap[i + 1] = entries[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RemotePlayer::UpdateTransform(float p_deltaTime)
|
void RemotePlayer::UpdateTransform(float p_deltaTime)
|
||||||
{
|
{
|
||||||
@ -394,18 +271,10 @@ void RemotePlayer::UpdateAnimation(float p_deltaTime)
|
|||||||
|
|
||||||
// Ensure visibility of all mapped ROIs
|
// Ensure visibility of all mapped ROIs
|
||||||
if (walkRoiMap) {
|
if (walkRoiMap) {
|
||||||
for (MxU32 i = 1; i < walkRoiMapSize; i++) {
|
AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize);
|
||||||
if (walkRoiMap[i] != nullptr) {
|
|
||||||
walkRoiMap[i]->SetVisibility(TRUE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
|
if (m_idleAnimCache && m_idleAnimCache->roiMap) {
|
||||||
for (MxU32 i = 1; i < m_idleAnimCache->roiMapSize; i++) {
|
AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize);
|
||||||
if (m_idleAnimCache->roiMap[i] != nullptr) {
|
|
||||||
m_idleAnimCache->roiMap[i]->SetVisibility(TRUE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
|
bool inVehicle = (m_currentVehicleType != VEHICLE_NONE);
|
||||||
@ -569,7 +438,7 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
|
|||||||
m_rideVehicleROI->SetName(vehicleVariantName);
|
m_rideVehicleROI->SetName(vehicleVariantName);
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
|
AnimUtils::BuildROIMap(m_rideAnim, m_roi, m_rideVehicleROI, m_rideRoiMap, m_rideRoiMapSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
560
extensions/src/multiplayer/thirdpersoncamera.cpp
Normal file
560
extensions/src/multiplayer/thirdpersoncamera.cpp
Normal file
@ -0,0 +1,560 @@
|
|||||||
|
#include "extensions/multiplayer/thirdpersoncamera.h"
|
||||||
|
|
||||||
|
#include "3dmanager/lego3dmanager.h"
|
||||||
|
#include "anim/legoanim.h"
|
||||||
|
#include "islepathactor.h"
|
||||||
|
#include "legoanimpresenter.h"
|
||||||
|
#include "legocameracontroller.h"
|
||||||
|
#include "legocharactermanager.h"
|
||||||
|
#include "legovideomanager.h"
|
||||||
|
#include "legoworld.h"
|
||||||
|
#include "misc.h"
|
||||||
|
#include "misc/legotree.h"
|
||||||
|
#include "mxgeometry/mxgeometry3d.h"
|
||||||
|
#include "mxgeometry/mxmatrix.h"
|
||||||
|
#include "realtime/realtime.h"
|
||||||
|
#include "roi/legoroi.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using namespace Multiplayer;
|
||||||
|
|
||||||
|
ThirdPersonCamera::ThirdPersonCamera()
|
||||||
|
: m_enabled(false), m_active(false), m_playerROI(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_currentVehicleType(VEHICLE_NONE), m_rideAnim(nullptr), m_rideRoiMap(nullptr), m_rideRoiMapSize(0),
|
||||||
|
m_rideVehicleROI(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::Enable()
|
||||||
|
{
|
||||||
|
m_enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::Disable()
|
||||||
|
{
|
||||||
|
m_enabled = false;
|
||||||
|
m_active = false;
|
||||||
|
m_playerROI = nullptr;
|
||||||
|
ClearRideAnimation();
|
||||||
|
m_animCacheMap.clear();
|
||||||
|
ClearAnimCaches();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor)
|
||||||
|
{
|
||||||
|
if (!m_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoPathActor* userActor = UserActor();
|
||||||
|
if (static_cast<LegoPathActor*>(p_actor) != userActor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoROI* newROI = userActor->GetROI();
|
||||||
|
if (!newROI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect if we're entering a vehicle
|
||||||
|
int8_t vehicleType = DetectVehicleType(userActor);
|
||||||
|
|
||||||
|
if (vehicleType != VEHICLE_NONE) {
|
||||||
|
// Large vehicles and helicopter: stay first-person with dashboard.
|
||||||
|
// Track the vehicle type so OnActorExit can trigger reinit on exit.
|
||||||
|
if (IsLargeVehicle(vehicleType) || vehicleType == VEHICLE_HELICOPTER) {
|
||||||
|
// Hide the walking character ROI that we made visible earlier.
|
||||||
|
// Enter() doesn't call Exit() on the previous actor, so our
|
||||||
|
// OnActorExit never fires for the walking character.
|
||||||
|
if (m_playerROI) {
|
||||||
|
m_playerROI->SetVisibility(FALSE);
|
||||||
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
|
}
|
||||||
|
m_currentVehicleType = vehicleType;
|
||||||
|
m_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small vehicle: need the character ROI for ride animations.
|
||||||
|
if (!m_playerROI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentVehicleType = vehicleType;
|
||||||
|
m_active = true;
|
||||||
|
|
||||||
|
SetupCamera(userActor);
|
||||||
|
BuildRideAnimation(vehicleType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-vehicle (walking character) entry
|
||||||
|
m_playerROI = newROI;
|
||||||
|
m_currentVehicleType = VEHICLE_NONE;
|
||||||
|
m_active = true;
|
||||||
|
|
||||||
|
// Make the player model visible (Enter() hid it for first-person)
|
||||||
|
m_playerROI->SetVisibility(TRUE);
|
||||||
|
|
||||||
|
// SpawnPlayer() removes the ROI from the 3D manager before calling Enter().
|
||||||
|
// Re-add it so the character is actually rendered in third-person mode.
|
||||||
|
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]);
|
||||||
|
|
||||||
|
// Reset animation state
|
||||||
|
m_animTime = 0.0f;
|
||||||
|
m_idleTime = 0.0f;
|
||||||
|
m_idleAnimTime = 0.0f;
|
||||||
|
m_wasMoving = false;
|
||||||
|
m_emoteActive = false;
|
||||||
|
|
||||||
|
ApplyIdleFrame0();
|
||||||
|
|
||||||
|
SetupCamera(userActor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor)
|
||||||
|
{
|
||||||
|
if (!m_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hook fires at the end of Exit(), after UserActor() has been restored
|
||||||
|
// to the walking character. For vehicle exit, p_actor is the vehicle (not
|
||||||
|
// UserActor), so we check m_currentVehicleType instead of comparing actors.
|
||||||
|
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||||
|
// Exiting a vehicle: reinitialize immediately for the walking character.
|
||||||
|
ClearRideAnimation();
|
||||||
|
ClearAnimCaches();
|
||||||
|
m_animCacheMap.clear();
|
||||||
|
ReinitForCharacter();
|
||||||
|
}
|
||||||
|
else if (m_active && static_cast<LegoPathActor*>(p_actor) == UserActor()) {
|
||||||
|
// Exiting on foot (e.g., world transition): full teardown.
|
||||||
|
// Hide the player ROI and remove it from the 3D manager (we added it
|
||||||
|
// in OnActorEnter so the character would render in third-person).
|
||||||
|
if (m_playerROI) {
|
||||||
|
m_playerROI->SetVisibility(FALSE);
|
||||||
|
VideoManager()->Get3DManager()->Remove(*m_playerROI);
|
||||||
|
}
|
||||||
|
ClearRideAnimation();
|
||||||
|
ClearAnimCaches();
|
||||||
|
m_currentVehicleType = VEHICLE_NONE;
|
||||||
|
m_playerROI = nullptr;
|
||||||
|
m_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::Tick(float p_deltaTime)
|
||||||
|
{
|
||||||
|
if (!m_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_playerROI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small vehicle with ride animation (like RemotePlayer)
|
||||||
|
if (m_currentVehicleType != VEHICLE_NONE) {
|
||||||
|
if (m_rideAnim && m_rideRoiMap) {
|
||||||
|
LegoPathActor* actor = UserActor();
|
||||||
|
if (!actor || !actor->GetROI()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force visibility of ride ROI map entries
|
||||||
|
AnimUtils::EnsureROIMapVisibility(m_rideRoiMap, m_rideRoiMapSize);
|
||||||
|
|
||||||
|
// Only advance animation time when actually moving
|
||||||
|
float speed = actor->GetWorldSpeed();
|
||||||
|
if (fabsf(speed) > 0.01f) {
|
||||||
|
m_animTime += p_deltaTime * 2000.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use vehicle actor's transform as base (character ROI may be at old position)
|
||||||
|
MxMatrix transform(actor->GetROI()->GetLocal2World());
|
||||||
|
|
||||||
|
// Position character ROI at the vehicle so bones render at the right place
|
||||||
|
m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform);
|
||||||
|
m_playerROI->SetVisibility(TRUE);
|
||||||
|
|
||||||
|
float duration = (float) m_rideAnim->GetDuration();
|
||||||
|
if (duration > 0.0f) {
|
||||||
|
float timeInCycle = m_animTime - duration * floorf(m_animTime / duration);
|
||||||
|
|
||||||
|
LegoTreeNode* root = m_rideAnim->GetRoot();
|
||||||
|
for (LegoU32 i = 0; i < root->GetNumChildren(); i++) {
|
||||||
|
LegoROI::ApplyAnimationTransformation(
|
||||||
|
root->GetChild(i),
|
||||||
|
transform,
|
||||||
|
(LegoTime) timeInCycle,
|
||||||
|
m_rideRoiMap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoPathActor* userActor = UserActor();
|
||||||
|
if (!userActor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Movement interrupts emotes
|
||||||
|
if (isMoving && 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 the saved clean parent transform to prevent scale
|
||||||
|
// accumulation (see TriggerEmote for details).
|
||||||
|
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 the player ROI's transform — the animation's root
|
||||||
|
// node (ACTOR_01) wrote a scaled value into 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_id)
|
||||||
|
{
|
||||||
|
if (p_id >= g_walkAnimCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_id != m_walkAnimId) {
|
||||||
|
m_walkAnimId = p_id;
|
||||||
|
if (m_active) {
|
||||||
|
m_walkAnimCache = GetOrBuildAnimCache(g_walkAnimNames[m_walkAnimId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::SetIdleAnimId(uint8_t p_id)
|
||||||
|
{
|
||||||
|
if (p_id >= g_idleAnimCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_id != m_idleAnimId) {
|
||||||
|
m_idleAnimId = p_id;
|
||||||
|
if (m_active) {
|
||||||
|
m_idleAnimCache = GetOrBuildAnimCache(g_idleAnimNames[m_idleAnimId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId)
|
||||||
|
{
|
||||||
|
if (p_emoteId >= g_emoteAnimCount || !m_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoPathActor* userActor = UserActor();
|
||||||
|
if (!userActor || fabsf(userActor->GetWorldSpeed()) > 0.01f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimCache* cache = GetOrBuildAnimCache(g_emoteAnimNames[p_emoteId]);
|
||||||
|
if (!cache || !cache->anim) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_emoteAnimCache = cache;
|
||||||
|
m_emoteTime = 0.0f;
|
||||||
|
m_emoteDuration = (float) cache->anim->GetDuration();
|
||||||
|
m_emoteActive = true;
|
||||||
|
|
||||||
|
// Save the clean parent transform before the emote starts.
|
||||||
|
// The emote animation's root node (ACTOR_01) maps to the player ROI,
|
||||||
|
// so ApplyAnimationTransformation writes a scaled transform into
|
||||||
|
// m_playerROI->m_local2world each frame. When the character is
|
||||||
|
// stationary the engine's CalculateTransform does not run, so the ROI
|
||||||
|
// is never reset — causing the scale to compound across frames.
|
||||||
|
// Using the saved clean transform as parent prevents this feedback.
|
||||||
|
m_emoteParentTransform = m_playerROI->GetLocal2World();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world)
|
||||||
|
{
|
||||||
|
if (!m_enabled || !p_world) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear stale caches (animation presenters may have been recreated)
|
||||||
|
m_animCacheMap.clear();
|
||||||
|
ClearAnimCaches();
|
||||||
|
|
||||||
|
ReinitForCharacter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world)
|
||||||
|
{
|
||||||
|
if (!p_world) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_active = false;
|
||||||
|
m_playerROI = nullptr;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor)
|
||||||
|
{
|
||||||
|
LegoWorld* world = CurrentWorld();
|
||||||
|
if (!world || !world->GetCameraController()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After Enter()'s TurnAround, the ROI direction is negated.
|
||||||
|
// The mesh faces -z (local) = +path_forward (correct visual facing).
|
||||||
|
// +z in ROI-local is the negated direction, i.e. behind the visual model.
|
||||||
|
// Movement inversion is handled by ShouldInvertMovement in CalculateTransform.
|
||||||
|
Mx3DPointFloat at(0.0f, 2.5f, 3.0f);
|
||||||
|
Mx3DPointFloat dir(0.0f, -0.3f, -1.0f);
|
||||||
|
Mx3DPointFloat up(0.0f, 1.0f, 0.0f);
|
||||||
|
|
||||||
|
world->GetCameraController()->SetWorldTransform(at, dir, up);
|
||||||
|
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 from base vehicle name, rename for anim tree matching
|
||||||
|
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::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::ReinitForCharacter()
|
||||||
|
{
|
||||||
|
LegoPathActor* userActor = UserActor();
|
||||||
|
if (!userActor) {
|
||||||
|
m_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegoROI* roi = userActor->GetROI();
|
||||||
|
if (!roi) {
|
||||||
|
m_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t vehicleType = DetectVehicleType(userActor);
|
||||||
|
|
||||||
|
// Large vehicles and helicopter: stay first-person
|
||||||
|
if (vehicleType == VEHICLE_HELICOPTER || (vehicleType != VEHICLE_NONE && IsLargeVehicle(vehicleType))) {
|
||||||
|
m_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentVehicleType = vehicleType;
|
||||||
|
|
||||||
|
if (vehicleType != VEHICLE_NONE) {
|
||||||
|
if (!m_playerROI) {
|
||||||
|
m_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_active = true;
|
||||||
|
SetupCamera(userActor);
|
||||||
|
BuildRideAnimation(vehicleType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinitializing for walking character
|
||||||
|
m_playerROI = roi;
|
||||||
|
m_playerROI->SetVisibility(TRUE);
|
||||||
|
|
||||||
|
// Ensure the ROI is in the 3D manager so it gets rendered
|
||||||
|
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_active = true;
|
||||||
|
|
||||||
|
ApplyIdleFrame0();
|
||||||
|
SetupCamera(userActor);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user