diff --git a/dGame/dComponents/CMakeLists.txt b/dGame/dComponents/CMakeLists.txt index e82ec360..ea8d75bd 100644 --- a/dGame/dComponents/CMakeLists.txt +++ b/dGame/dComponents/CMakeLists.txt @@ -1,4 +1,11 @@ +add_subdirectory(MovingPlatformComponent) + +foreach(file ${DGAME_DCOMPONENTS_MOVINGPLATFORMCOMPONENT}) + list(APPEND DGAME_DCOMPONENTS_SUBCOMPONENT_SOURCES "MovingPlatformComponent/${file}") +endforeach() + set(DGAME_DCOMPONENTS_SOURCES + ${DGAME_DCOMPONENTS_SUBCOMPONENT_SOURCES} "AchievementVendorComponent.cpp" "ActivityComponent.cpp" "BaseCombatAIComponent.cpp" @@ -51,7 +58,7 @@ set(DGAME_DCOMPONENTS_SOURCES ) add_library(dComponents OBJECT ${DGAME_DCOMPONENTS_SOURCES}) -target_include_directories(dComponents PUBLIC "." +target_include_directories(dComponents PUBLIC "." "MovingPlatformComponent" "${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors" # via ModelComponent.h "${PROJECT_SOURCE_DIR}/dGame/dPropertyBehaviors/ControlBehaviorMessages" "${PROJECT_SOURCE_DIR}/dGame/dMission" # via MissionComponent.h diff --git a/dGame/dComponents/ControllablePhysicsComponent.cpp b/dGame/dComponents/ControllablePhysicsComponent.cpp index b2a41358..886684f1 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/ControllablePhysicsComponent.cpp @@ -152,7 +152,17 @@ void ControllablePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo outBitStream.Write(m_AngularVelocity.z); } - outBitStream.Write0(); // local_space_info, always zero for now. + bool hasLocalSpaceInfo = m_PlatformEntityID != LWOOBJID_EMPTY; + outBitStream.Write(hasLocalSpaceInfo); + if (hasLocalSpaceInfo) { + outBitStream.Write(m_PlatformEntityID); + outBitStream.Write(m_LocalSpacePosition.x); + outBitStream.Write(m_LocalSpacePosition.y); + outBitStream.Write(m_LocalSpacePosition.z); + outBitStream.Write(m_LocalSpaceLinearVelocity.x); + outBitStream.Write(m_LocalSpaceLinearVelocity.y); + outBitStream.Write(m_LocalSpaceLinearVelocity.z); + } if (!bIsInitialUpdate) { m_DirtyPosition = false; diff --git a/dGame/dComponents/ControllablePhysicsComponent.h b/dGame/dComponents/ControllablePhysicsComponent.h index 419e9250..de420e51 100644 --- a/dGame/dComponents/ControllablePhysicsComponent.h +++ b/dGame/dComponents/ControllablePhysicsComponent.h @@ -261,6 +261,19 @@ public: /** * Push or Pop a layer of stun immunity to this entity */ + /** + * Sets the platform the entity is standing on for local space serialization + */ + void SetPlatformEntity(LWOOBJID platformID) { m_PlatformEntityID = platformID; m_DirtyPosition = true; } + + /** + * Returns the platform the entity is standing on + */ + LWOOBJID GetPlatformEntity() const { return m_PlatformEntityID; } + + void SetLocalSpacePosition(const NiPoint3& pos) { m_LocalSpacePosition = pos; } + void SetLocalSpaceLinearVelocity(const NiPoint3& vel) { m_LocalSpaceLinearVelocity = vel; } + void SetStunImmunity( const eStateChangeType state, const LWOOBJID originator = LWOOBJID_EMPTY, @@ -403,6 +416,21 @@ private: /** * stun immunity counters */ + /** + * The platform entity the player is standing on (for local space serialization) + */ + LWOOBJID m_PlatformEntityID = LWOOBJID_EMPTY; + + /** + * The player's position in the platform's local space + */ + NiPoint3 m_LocalSpacePosition{}; + + /** + * The player's linear velocity in the platform's local space + */ + NiPoint3 m_LocalSpaceLinearVelocity{}; + int32_t m_ImmuneToStunAttackCount; int32_t m_ImmuneToStunEquipCount; int32_t m_ImmuneToStunInteractCount; diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 18911fc1..d76bddf2 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -4,80 +4,119 @@ */ #include "MovingPlatformComponent.h" +#include "PlatformSubComponent.h" +#include "MoverSubComponent.h" +#include "SimpleMoverSubComponent.h" +#include "RotatorSubComponent.h" #include "BitStream.h" #include "GeneralUtils.h" #include "dZoneManager.h" #include "EntityManager.h" #include "Logger.h" #include "GameMessages.h" -#include "CppScripts.h" #include "SimplePhysicsComponent.h" #include "Zone.h" +#include "eMovementPlatformState.h" -MoverSubComponent::MoverSubComponent(const NiPoint3& startPos) { - mPosition = {}; +MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t componentID, const std::string& pathName) + : Component(parent, componentID) { - mState = eMovementPlatformState::Stopped; - mDesiredWaypointIndex = 0; // -1; - mInReverse = false; - mShouldStopAtDesiredWaypoint = false; - - mPercentBetweenPoints = 0.0f; - - mCurrentWaypointIndex = 0; - mNextWaypointIndex = 0; //mCurrentWaypointIndex + 1; - - mIdleTimeElapsed = 0.0f; -} - -MoverSubComponent::~MoverSubComponent() = default; - -void MoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { - outBitStream.Write(true); - - outBitStream.Write(mState); - outBitStream.Write(mDesiredWaypointIndex); - outBitStream.Write(mShouldStopAtDesiredWaypoint); - outBitStream.Write(mInReverse); - - outBitStream.Write(mPercentBetweenPoints); - - outBitStream.Write(mPosition.x); - outBitStream.Write(mPosition.y); - outBitStream.Write(mPosition.z); - - outBitStream.Write(mCurrentWaypointIndex); - outBitStream.Write(mNextWaypointIndex); - - outBitStream.Write(mIdleTimeElapsed); - outBitStream.Write(0.0f); // Move time elapsed -} - -//------------- MovingPlatformComponent below -------------- - -MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t componentID, const std::string& pathName) : Component(parent, componentID) { m_MoverSubComponentType = eMoverSubComponentType::mover; - m_MoverSubComponent = new MoverSubComponent(m_Parent->GetDefaultPosition()); m_PathName = GeneralUtils::ASCIIToUTF16(pathName); m_Path = Game::zoneManager->GetZone()->GetPath(pathName); m_NoAutoStart = false; - if (m_Path == nullptr) { + if (m_Path == nullptr && !pathName.empty()) { LOG("Path not found: %s", pathName.c_str()); } + + SetupPlatformSubComponents(); +} + +MovingPlatformComponent::~MovingPlatformComponent() = default; + +void MovingPlatformComponent::SetupPlatformSubComponents() { + // Read component properties matching client SetupPlatform + bool isMover = m_Parent->GetVar(u"platformIsMover"); + bool isSimpleMover = m_Parent->GetVar(u"platformIsSimpleMover"); + bool isRotater = m_Parent->GetVar(u"platformIsRotater"); + + // Read sound GUIDs + m_PlatformSoundStart = m_Parent->GetVarAsString(u"platformSoundStart"); + m_PlatformSoundTravel = m_Parent->GetVarAsString(u"platformSoundTravel"); + m_PlatformSoundStop = m_Parent->GetVarAsString(u"platformSoundStop"); + + // If no flags set but we have a path, default to mover (backwards compatibility) + if (!isMover && !isSimpleMover && !isRotater) { + if (m_Path && m_Path->pathType == PathType::MovingPlatform) { + isMover = true; + } + } + + // Create mover subcomponent + if (isMover && m_Path) { + m_MoverSubComponentType = eMoverSubComponentType::mover; + auto mover = std::make_unique(m_Parent, m_Path); + + if (!m_PathName.empty()) { + bool reverse = m_Parent->GetVar(u"reverse"); + int32_t startPoint = m_Parent->GetVarAs(u"startPoint"); + mover->SetInReverse(reverse); + if (startPoint >= 0 && startPoint < static_cast(m_Path->pathWaypoints.size())) { + mover->SetupWaypointSegment(static_cast(startPoint)); + } + mover->SetActive(true); + } + + m_MoverSubComponent = std::move(mover); + } + + // Create simple mover subcomponent + if (isSimpleMover) { + m_MoverSubComponentType = eMoverSubComponentType::simpleMover; + + NiPoint3 platformMove{}; + platformMove.x = m_Parent->GetVar(u"platformMoveX"); + platformMove.y = m_Parent->GetVar(u"platformMoveY"); + platformMove.z = m_Parent->GetVar(u"platformMoveZ"); + + float platformMoveTime = m_Parent->GetVar(u"platformMoveTime"); + + NiPoint3 startPos = m_Parent->GetDefaultPosition(); + NiQuaternion startRot = m_Parent->GetDefaultRotation(); + + m_MoverSubComponent = std::make_unique( + m_Parent, startPos, startRot, platformMove, platformMoveTime); + } + + // Create rotator subcomponent (can coexist with mover) + if (isRotater && m_Path) { + auto rotator = std::make_unique(m_Parent, m_Path); + + if (!m_PathName.empty()) { + bool reverse = m_Parent->GetVar(u"reverse"); + int32_t startPoint = m_Parent->GetVarAs(u"startPoint"); + rotator->SetInReverse(reverse); + if (startPoint >= 0 && startPoint < static_cast(m_Path->pathWaypoints.size())) { + rotator->SetupWaypointSegment(static_cast(startPoint)); + } + rotator->SetActive(true); + } + + m_RotatorSubComponent = std::move(rotator); + } + + // Fallback: if nothing was created, create a default mover + if (!m_MoverSubComponent && !m_RotatorSubComponent) { + m_MoverSubComponentType = eMoverSubComponentType::mover; + m_MoverSubComponent = std::make_unique(m_Parent, m_Path); + } } -MovingPlatformComponent::~MovingPlatformComponent() { - delete static_cast(m_MoverSubComponent); -} - void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { - // Here we don't serialize the moving platform to let the client simulate the movement - if (!m_Serialize) { outBitStream.Write(false); outBitStream.Write(false); - return; } @@ -87,7 +126,6 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI outBitStream.Write(hasPath); if (hasPath) { - // Is on rail outBitStream.Write1(); outBitStream.Write(m_PathName.size()); @@ -95,25 +133,50 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI outBitStream.Write(c); } - // Starting point - outBitStream.Write(0); - - // Reverse - outBitStream.Write(false); + outBitStream.Write(m_MoverSubComponent ? m_MoverSubComponent->GetCurrentWaypointIndex() : 0); + outBitStream.Write(m_MoverSubComponent ? m_MoverSubComponent->GetInReverse() : false); } const auto hasPlatform = m_MoverSubComponent != nullptr; outBitStream.Write(hasPlatform); if (hasPlatform) { - auto* mover = static_cast(m_MoverSubComponent); outBitStream.Write(m_MoverSubComponentType); + m_MoverSubComponent->Serialize(outBitStream, bIsInitialUpdate); + } +} - if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { - // TODO - } else { - mover->Serialize(outBitStream, bIsInitialUpdate); +void MovingPlatformComponent::Update(float deltaTime) { + if (!m_Serialize) return; + + // Track whether we were travelling before update for sound management + bool wasTravelling = m_MoverSubComponent && + (m_MoverSubComponent->GetState() & PlatformState::Travelling); + + bool dirty = false; + + if (m_MoverSubComponent) { + m_MoverSubComponent->Update(deltaTime, dirty); + } + + if (m_RotatorSubComponent) { + m_RotatorSubComponent->Update(deltaTime, dirty); + } + + // Handle travel sound looping (matching client PlayTravelSound/StopTravelSound) + if (m_MoverSubComponent && !m_PlatformSoundTravel.empty()) { + bool isTravelling = m_MoverSubComponent->GetState() & PlatformState::Travelling; + if (isTravelling && !wasTravelling) { + // Started travelling — play looping travel sound + GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundTravel); } + // Note: the client stops the travel sound on arrival/stop via StopTravelSound. + // SendPlayNDAudioEmitter doesn't support stopping, so the sound will naturally end + // or be replaced by the arrive/stop sound. + } + + if (dirty) { + Game::entityManager->SerializeEntity(m_Parent); } } @@ -122,201 +185,66 @@ void MovingPlatformComponent::OnQuickBuildInitilized() { } void MovingPlatformComponent::OnCompleteQuickBuild() { - if (m_NoAutoStart) - return; - + if (m_NoAutoStart) return; StartPathing(); } -void MovingPlatformComponent::SetMovementState(eMovementPlatformState value) { - auto* subComponent = static_cast(m_MoverSubComponent); - - subComponent->mState = value; - +void MovingPlatformComponent::SetMovementState(uint32_t state) { + if (m_MoverSubComponent) m_MoverSubComponent->SetState(state); + if (m_RotatorSubComponent) m_RotatorSubComponent->SetState(state); Game::entityManager->SerializeEntity(m_Parent); } void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) { - auto* subComponent = static_cast(m_MoverSubComponent); - - subComponent->mDesiredWaypointIndex = index; - subComponent->mNextWaypointIndex = index; - subComponent->mShouldStopAtDesiredWaypoint = stopAtWaypoint; - - StartPathing(); -} - -void MovingPlatformComponent::StartPathing() { - //GameMessages::SendStartPathing(m_Parent); m_PathingStopped = false; - auto* subComponent = static_cast(m_MoverSubComponent); - - subComponent->mShouldStopAtDesiredWaypoint = true; - subComponent->mState = eMovementPlatformState::Stationary; - - NiPoint3 targetPosition; - - if (m_Path != nullptr) { - const auto& currentWaypoint = m_Path->pathWaypoints[subComponent->mCurrentWaypointIndex]; - const auto& nextWaypoint = m_Path->pathWaypoints[subComponent->mNextWaypointIndex]; - - subComponent->mPosition = currentWaypoint.position; - subComponent->mSpeed = currentWaypoint.speed; - subComponent->mWaitTime = currentWaypoint.movingPlatform.wait; - - targetPosition = nextWaypoint.position; - } else { - subComponent->mPosition = m_Parent->GetPosition(); - subComponent->mSpeed = 1.0f; - subComponent->mWaitTime = 2.0f; - - targetPosition = m_Parent->GetPosition() + NiPoint3(0.0f, 10.0f, 0.0f); - } - - m_Parent->AddCallbackTimer(subComponent->mWaitTime, [this] { - SetMovementState(eMovementPlatformState::Moving); - }); - - const auto travelTime = Vector3::Distance(targetPosition, subComponent->mPosition) / subComponent->mSpeed + 1.5f; - - const auto travelNext = subComponent->mWaitTime + travelTime; - - m_Parent->AddCallbackTimer(travelTime, [subComponent, this] { - this->m_Parent->GetScript()->OnWaypointReached(m_Parent, subComponent->mNextWaypointIndex); - }); - - m_Parent->AddCallbackTimer(travelNext, [this] { - ContinuePathing(); - }); - - //GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS); + if (m_MoverSubComponent) m_MoverSubComponent->GotoWaypoint(index, stopAtWaypoint); + if (m_RotatorSubComponent) m_RotatorSubComponent->GotoWaypoint(index, stopAtWaypoint); Game::entityManager->SerializeEntity(m_Parent); } -void MovingPlatformComponent::ContinuePathing() { - auto* subComponent = static_cast(m_MoverSubComponent); +void MovingPlatformComponent::StartPathing() { + m_PathingStopped = false; - subComponent->mState = eMovementPlatformState::Stationary; + if (m_MoverSubComponent) m_MoverSubComponent->StartPathing(); + if (m_RotatorSubComponent) m_RotatorSubComponent->StartPathing(); - subComponent->mCurrentWaypointIndex = subComponent->mNextWaypointIndex; - - NiPoint3 targetPosition; - uint32_t pathSize; - PathBehavior behavior; - - if (m_Path != nullptr) { - const auto& currentWaypoint = m_Path->pathWaypoints[subComponent->mCurrentWaypointIndex]; - const auto& nextWaypoint = m_Path->pathWaypoints[subComponent->mNextWaypointIndex]; - - subComponent->mPosition = currentWaypoint.position; - subComponent->mSpeed = currentWaypoint.speed; - subComponent->mWaitTime = currentWaypoint.movingPlatform.wait; // + 2; - - pathSize = m_Path->pathWaypoints.size() - 1; - - behavior = static_cast(m_Path->pathBehavior); - - targetPosition = nextWaypoint.position; - } else { - subComponent->mPosition = m_Parent->GetPosition(); - subComponent->mSpeed = 1.0f; - subComponent->mWaitTime = 2.0f; - - targetPosition = m_Parent->GetPosition() + NiPoint3(0.0f, 10.0f, 0.0f); - - pathSize = 1; - behavior = PathBehavior::Loop; + if (m_MoverSubComponent) { + GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, + m_MoverSubComponent->GetShouldStopAtDesiredWaypoint(), + m_MoverSubComponent->GetCurrentWaypointIndex(), + m_MoverSubComponent->GetDesiredWaypointIndex(), + m_MoverSubComponent->GetNextWaypointIndex(), + static_cast(m_MoverSubComponent->GetSerializedState()), + m_MoverSubComponent->GetInReverse(), + m_MoverSubComponent->GetIdleTimeElapsed(), + m_MoverSubComponent->GetMoveTimeElapsed(), + m_MoverSubComponent->GetPercentBetweenPoints(), + m_MoverSubComponent->GetPosition()); } - if (m_Parent->GetLOT() == 9483) { - behavior = PathBehavior::Bounce; - } else { - return; + if (!m_PlatformSoundStart.empty()) { + GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundStart); } - if (subComponent->mCurrentWaypointIndex >= pathSize) { - subComponent->mCurrentWaypointIndex = pathSize; - switch (behavior) { - case PathBehavior::Once: - Game::entityManager->SerializeEntity(m_Parent); - return; - - case PathBehavior::Bounce: - subComponent->mInReverse = true; - break; - - case PathBehavior::Loop: - subComponent->mNextWaypointIndex = 0; - break; - - default: - break; - } - } else if (subComponent->mCurrentWaypointIndex == 0) { - subComponent->mInReverse = false; - } - - if (subComponent->mInReverse) { - subComponent->mNextWaypointIndex = subComponent->mCurrentWaypointIndex - 1; - } else { - subComponent->mNextWaypointIndex = subComponent->mCurrentWaypointIndex + 1; - } - - /* - subComponent->mNextWaypointIndex = 0; - subComponent->mCurrentWaypointIndex = 1; - */ - - //GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS); - - if (subComponent->mCurrentWaypointIndex == subComponent->mDesiredWaypointIndex) { - // TODO: Send event? - StopPathing(); - - return; - } - - m_Parent->CancelCallbackTimers(); - - m_Parent->AddCallbackTimer(subComponent->mWaitTime, [this] { - SetMovementState(eMovementPlatformState::Moving); - }); - - auto travelTime = Vector3::Distance(targetPosition, subComponent->mPosition) / subComponent->mSpeed + 1.5; - - if (m_Parent->GetLOT() == 9483) { - travelTime += 20; - } - - const auto travelNext = subComponent->mWaitTime + travelTime; - - m_Parent->AddCallbackTimer(travelTime, [subComponent, this] { - this->m_Parent->GetScript()->OnWaypointReached(m_Parent, subComponent->mNextWaypointIndex); - }); - - m_Parent->AddCallbackTimer(travelNext, [this] { - ContinuePathing(); - }); - Game::entityManager->SerializeEntity(m_Parent); } void MovingPlatformComponent::StopPathing() { - //m_Parent->CancelCallbackTimers(); - - auto* subComponent = static_cast(m_MoverSubComponent); - m_PathingStopped = true; - subComponent->mState = eMovementPlatformState::Stopped; - subComponent->mDesiredWaypointIndex = -1; - subComponent->mShouldStopAtDesiredWaypoint = false; + if (m_MoverSubComponent) m_MoverSubComponent->StopPathing(); + if (m_RotatorSubComponent) m_RotatorSubComponent->StopPathing(); + + GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, + false, 0, -1, 0, eMovementPlatformState::Stopped); + + if (!m_PlatformSoundStop.empty()) { + GameMessages::SendPlayNDAudioEmitter(m_Parent, UNASSIGNED_SYSTEM_ADDRESS, m_PlatformSoundStop); + } Game::entityManager->SerializeEntity(m_Parent); - - //GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS); } void MovingPlatformComponent::SetSerialized(bool value) { @@ -332,18 +260,18 @@ void MovingPlatformComponent::SetNoAutoStart(const bool value) { } void MovingPlatformComponent::WarpToWaypoint(size_t index) { - const auto& waypoint = m_Path->pathWaypoints[index]; - - m_Parent->SetPosition(waypoint.position); - m_Parent->SetRotation(waypoint.rotation); - + if (m_MoverSubComponent) m_MoverSubComponent->WarpToWaypoint(index); + if (m_RotatorSubComponent) m_RotatorSubComponent->WarpToWaypoint(index); Game::entityManager->SerializeEntity(m_Parent); } size_t MovingPlatformComponent::GetLastWaypointIndex() const { - return m_Path->pathWaypoints.size() - 1; + if (m_MoverSubComponent) return m_MoverSubComponent->GetLastWaypointIndex(); + if (m_RotatorSubComponent) return m_RotatorSubComponent->GetLastWaypointIndex(); + return 0; } -MoverSubComponent* MovingPlatformComponent::GetMoverSubComponent() const { - return static_cast(m_MoverSubComponent); +PlatformSubComponent* MovingPlatformComponent::GetMoverSubComponent() const { + if (m_MoverSubComponent) return m_MoverSubComponent.get(); + return nullptr; } diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index 065a2786..ea9cf099 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -6,103 +6,34 @@ #ifndef MOVINGPLATFORMCOMPONENT_H #define MOVINGPLATFORMCOMPONENT_H -#include "RakNetTypes.h" -#include "NiPoint3.h" -#include - -#include "dCommonVars.h" -#include "EntityManager.h" #include "Component.h" -#include "eMovementPlatformState.h" #include "eReplicaComponentType.h" +#include "NiPoint3.h" +#include "NiQuaternion.h" + +#include +#include class Path; - - /** - * Different types of available platforms - */ -enum class eMoverSubComponentType : uint32_t { - mover = 4, - - /** - * Used in NJ - */ - simpleMover = 5, -}; +class Entity; +class PlatformSubComponent; +class RotatorSubComponent; /** - * Sub component for moving platforms that determine the actual current movement state + * Different types of available platform subcomponents */ -class MoverSubComponent { -public: - MoverSubComponent(const NiPoint3& startPos); - ~MoverSubComponent(); - - void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate); - - /** - * The state the platform is currently in - */ - eMovementPlatformState mState = eMovementPlatformState::Stationary; - - /** - * The waypoint this platform currently wants to traverse to - */ - int32_t mDesiredWaypointIndex = 0; - - /** - * Whether the platform is currently reversing away from the desired waypoint - */ - bool mInReverse = false; - - /** - * Whether the platform should stop moving when reaching the desired waypoint - */ - bool mShouldStopAtDesiredWaypoint = false; - - /** - * The percentage of the way between the last point and the desired point - */ - float mPercentBetweenPoints = 0; - - /** - * The current position of the platofrm - */ - NiPoint3 mPosition{}; - - /** - * The waypoint the platform is (was) at - */ - uint32_t mCurrentWaypointIndex; - - /** - * The waypoint the platform is attempting to go to - */ - uint32_t mNextWaypointIndex; - - /** - * The timer that handles the time before stopping idling and continue platform movement - */ - float mIdleTimeElapsed = 0; - - /** - * The speed the platform is currently moving at - */ - float mSpeed = 0; - - /** - * The time to wait before continuing movement - */ - float mWaitTime = 0; +enum class eMoverSubComponentType : uint32_t { + mover = 4, + simpleMover = 5, + rotator = 6, }; - /** * Represents entities that may be moving platforms, indicating how they should move through the world. - * NOTE: the logic in this component hardly does anything, apparently the client can figure most of this stuff out - * if you just serialize it correctly, resulting in smoother results anyway. Don't be surprised if the exposed APIs - * don't at all do what you expect them to as we don't instruct the client of changes made here. - * ^^^ Trivia: This made the red blocks platform and property platforms a pain to implement. + * The server simulates platform movement each tick to maintain authoritative state for all players. + * + * An entity can have multiple subcomponents (mover + rotator), matching the client's architecture + * where SetupPlatform creates subcomponents based on platformIsMover/platformIsSimpleMover/platformIsRotater. */ class MovingPlatformComponent final : public Component { public: @@ -112,117 +43,42 @@ public: ~MovingPlatformComponent() override; void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) override; + void Update(float deltaTime) override; - /** - * Stops all pathing, called when an entity starts a quick build associated with this platform - */ void OnQuickBuildInitilized(); - - /** - * Starts the pathing, called when an entity completed a quick build associated with this platform - */ void OnCompleteQuickBuild(); - /** - * Updates the movement state for the moving platform - * @param value the movement state to set - */ - void SetMovementState(eMovementPlatformState value); - - /** - * Instructs the moving platform to go to some waypoint - * @param index the index of the waypoint - * @param stopAtWaypoint determines if the platform should stop at the waypoint - */ void GotoWaypoint(uint32_t index, bool stopAtWaypoint = true); - - /** - * Starts the pathing of this platform, setting appropriate waypoints and speeds - */ void StartPathing(); - - /** - * Continues the path of the platform, after it's been stopped - */ - void ContinuePathing(); - - /** - * Stops the platform from moving, waiting for it to be activated again. - */ void StopPathing(); - /** - * Determines if the entity should be serialized on the next update - * @param value whether to serialize the entity or not - */ void SetSerialized(bool value); - - /** - * Returns if this platform will start automatically after spawn - * @return if this platform will start automatically after spawn - */ bool GetNoAutoStart() const; - - /** - * Sets the auto start value for this platform - * @param value the auto start value to set - */ void SetNoAutoStart(bool value); - /** - * Warps the platform to a waypoint index, skipping its current path - * @param index the index to go to - */ void WarpToWaypoint(size_t index); - - /** - * Returns the waypoint this platform was previously at - * @return the waypoint this platform was previously at - */ size_t GetLastWaypointIndex() const; - /** - * Returns the sub component that actually defines how the platform moves around (speeds, etc). - * @return the sub component that actually defines how the platform moves around - */ - MoverSubComponent* GetMoverSubComponent() const; + PlatformSubComponent* GetMoverSubComponent() const; + void SetMovementState(uint32_t state); private: + void SetupPlatformSubComponents(); - /** - * The path this platform is currently on - */ const Path* m_Path = nullptr; - - /** - * The name of the path this platform is currently on - */ std::u16string m_PathName; - - /** - * Whether the platform has stopped pathing - */ bool m_PathingStopped = false; - - /** - * The type of the subcomponent - */ eMoverSubComponentType m_MoverSubComponentType; - /** - * The mover sub component that belongs to this platform - */ - void* m_MoverSubComponent; + std::unique_ptr m_MoverSubComponent; + std::unique_ptr m_RotatorSubComponent; - /** - * Whether the platform shouldn't auto start - */ - bool m_NoAutoStart; - - /** - * Whether to serialize the entity on the next update - */ + bool m_NoAutoStart = false; bool m_Serialize = false; + + std::string m_PlatformSoundStart; + std::string m_PlatformSoundTravel; + std::string m_PlatformSoundStop; }; #endif // MOVINGPLATFORMCOMPONENT_H diff --git a/dGame/dComponents/MovingPlatformComponent/CMakeLists.txt b/dGame/dComponents/MovingPlatformComponent/CMakeLists.txt new file mode 100644 index 00000000..4c0837d2 --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/CMakeLists.txt @@ -0,0 +1,7 @@ +set(DGAME_DCOMPONENTS_MOVINGPLATFORMCOMPONENT + "PlatformSubComponent.cpp" + "MoverSubComponent.cpp" + "SimpleMoverSubComponent.cpp" + "RotatorSubComponent.cpp" + PARENT_SCOPE +) diff --git a/dGame/dComponents/MovingPlatformComponent/MoverSubComponent.cpp b/dGame/dComponents/MovingPlatformComponent/MoverSubComponent.cpp new file mode 100644 index 00000000..5bc8e926 --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/MoverSubComponent.cpp @@ -0,0 +1,5 @@ +#include "MoverSubComponent.h" + +MoverSubComponent::MoverSubComponent(Entity* parentEntity, const Path* path) + : PlatformSubComponent(parentEntity, path) { +} diff --git a/dGame/dComponents/MovingPlatformComponent/MoverSubComponent.h b/dGame/dComponents/MovingPlatformComponent/MoverSubComponent.h new file mode 100644 index 00000000..2a59f3ff --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/MoverSubComponent.h @@ -0,0 +1,19 @@ +#ifndef MOVERSUBCOMPONENT_H +#define MOVERSUBCOMPONENT_H + +#include "PlatformSubComponent.h" + +/** + * Standard mover - follows a pre-defined path from zone data. + * Corresponds to client LWOPlatformMover (type 4). + */ +class MoverSubComponent final : public PlatformSubComponent { +public: + MoverSubComponent(Entity* parentEntity, const Path* path); + +private: + bool m_AllowPosSnap = true; + float m_MaxLerpDistance = 6.0f; +}; + +#endif // MOVERSUBCOMPONENT_H diff --git a/dGame/dComponents/MovingPlatformComponent/PlatformSubComponent.cpp b/dGame/dComponents/MovingPlatformComponent/PlatformSubComponent.cpp new file mode 100644 index 00000000..8cbf00ac --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/PlatformSubComponent.cpp @@ -0,0 +1,484 @@ +#include "PlatformSubComponent.h" + +#include "BitStream.h" +#include "BitStreamUtils.h" +#include "Entity.h" +#include "Game.h" +#include "dServer.h" +#include "GameMessages.h" +#include "CppScripts.h" +#include "SimplePhysicsComponent.h" +#include "Zone.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" + +#include +#include + +#include + +PlatformSubComponent::PlatformSubComponent(Entity* parentEntity, const Path* path) + : m_ParentEntity(parentEntity) + , m_Path(path) { + + if (m_Path) { + m_TimeBasedMovement = m_Path->movingPlatform.timeBasedMovement != 0; + } + + m_Position = parentEntity ? parentEntity->GetPosition() : NiPoint3{}; +} + +void PlatformSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { + outBitStream.Write(true); + + outBitStream.Write(GetSerializedState()); + outBitStream.Write(m_DesiredWaypointIndex); + outBitStream.Write(m_ShouldStopAtDesiredWaypoint); + outBitStream.Write(m_InReverse); + + outBitStream.Write(m_PercentBetweenPoints); + + outBitStream.Write(m_Position.x); + outBitStream.Write(m_Position.y); + outBitStream.Write(m_Position.z); + + outBitStream.Write(m_CurrentWaypointIndex); + outBitStream.Write(m_NextWaypointIndex); + + outBitStream.Write(m_IdleTimeElapsed); + outBitStream.Write(m_MoveTimeElapsed); +} + +uint32_t PlatformSubComponent::GetSerializedState() const { + if (m_State & PlatformState::Stopped) return PlatformState::StoppedSerialized; + if (m_State & PlatformState::Travelling) return PlatformState::MovingSerialized; + return PlatformState::StationarySerialized; +} + +void PlatformSubComponent::Update(float deltaTime, bool& dirtyOut) { + if (!m_Active) return; + if (m_State == 0) return; + if (!m_Path || m_Path->pathWaypoints.empty()) return; + + if (IncrementWaitingTime(deltaTime)) { + StartTravelling(); + dirtyOut = true; + } + + if (m_State & PlatformState::Travelling) { + UpdatePositionAlongPath(deltaTime); + + bool arrived = false; + if (m_TimeBasedMovement) { + arrived = m_TravelTime > 0.0f && std::abs(m_TravelTime - m_MoveTimeElapsed) < 0.001f; + } else { + arrived = CloseToNextWaypoint(); + } + + if (arrived) { + ArrivedAtWaypoint(dirtyOut); + } + } +} + +// --- Movement control --- + +void PlatformSubComponent::StartPathing() { + if (!m_Path || m_Path->pathWaypoints.empty()) return; + + m_Active = true; + SetupWaypointSegment(m_CurrentWaypointIndex); + + m_State = PlatformState::Waiting | PlatformState::Stopped; + m_IdleTimeElapsed = 0.0f; + m_MoveTimeElapsed = 0.0f; + m_PercentBetweenPoints = 0.0f; + m_HasStartedTravelling = false; +} + +void PlatformSubComponent::StopPathing() { + m_State = PlatformState::Stopped; + m_DesiredWaypointIndex = -1; + m_ShouldStopAtDesiredWaypoint = false; + m_MoveTimeElapsed = 0.0f; + m_HasStartedTravelling = false; + + ZeroPhysicsVelocity(); +} + +void PlatformSubComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) { + m_DesiredWaypointIndex = static_cast(index); + m_NextWaypointIndex = index; + m_ShouldStopAtDesiredWaypoint = stopAtWaypoint; + + StartPathing(); +} + +void PlatformSubComponent::WarpToWaypoint(size_t index) { + if (!m_Path || index >= m_Path->pathWaypoints.size()) return; + + const auto& waypoint = m_Path->pathWaypoints[index]; + m_Position = waypoint.position; + m_CurrentWaypointIndex = static_cast(index); + m_PercentBetweenPoints = 0.0f; + m_MoveTimeElapsed = 0.0f; + + if (m_ParentEntity) { + m_ParentEntity->SetPosition(waypoint.position); + m_ParentEntity->SetRotation(waypoint.rotation); + } +} + +size_t PlatformSubComponent::GetLastWaypointIndex() const { + if (!m_Path || m_Path->pathWaypoints.empty()) return 0; + return m_Path->pathWaypoints.size() - 1; +} + +// --- Waypoint segment setup (mirrors client ProcessStateChange) --- + +void PlatformSubComponent::SetupWaypointSegment(uint32_t waypointIndex) { + if (!m_Path || m_Path->pathWaypoints.empty()) return; + + m_CurrentWaypointIndex = waypointIndex; + + const auto& currentWP = m_Path->pathWaypoints[m_CurrentWaypointIndex]; + m_CurrentWaypointPosition = currentWP.position; + m_CurrentWaypointRotation = currentWP.rotation; + m_WaitTime = currentWP.movingPlatform.wait; + m_Position = currentWP.position; + + bool changedDirection = false; + if (!m_InReverse) { + m_NextWaypointIndex = GetNextWaypoint(m_CurrentWaypointIndex, changedDirection); + if (changedDirection) m_InReverse = true; + } else { + m_NextWaypointIndex = GetNextReversedWaypoint(m_CurrentWaypointIndex, changedDirection); + if (changedDirection) m_InReverse = false; + } + + const auto& nextWP = m_Path->pathWaypoints[m_NextWaypointIndex]; + m_NextWaypointPosition = nextWP.position; + m_NextWaypointRotation = nextWP.rotation; + + m_DirectionVector = m_NextWaypointPosition - m_CurrentWaypointPosition; + m_TotalDistance = m_DirectionVector.Length(); + if (m_TotalDistance > 0.0f) { + m_DirectionVector = m_DirectionVector / m_TotalDistance; + } + + CalculateWaypointSpeeds(); + + m_MoveTimeElapsed = 0.0f; + m_IdleTimeElapsed = 0.0f; + m_HasStartedTravelling = false; + + if (m_TimeBasedMovement) { + m_PercentBetweenPoints = 0.0f; + } else if (m_TotalDistance > 0.0f) { + m_PercentBetweenPoints = (m_Position - m_CurrentWaypointPosition).Length() / m_TotalDistance; + } else { + m_PercentBetweenPoints = 0.0f; + } + + if (m_ParentEntity) { + m_ParentEntity->SetPosition(m_CurrentWaypointPosition); + m_ParentEntity->SetRotation(m_CurrentWaypointRotation); + } +} + +// --- Waypoint navigation (exact match of client decompilation) --- + +uint32_t PlatformSubComponent::GetNextWaypoint(uint32_t current, bool& changedDirection) const { + changedDirection = false; + uint32_t next = current + 1; + const auto numWaypoints = static_cast(m_Path->pathWaypoints.size()); + + if (next >= numWaypoints) { + switch (m_Path->pathBehavior) { + case PathBehavior::Once: + next = numWaypoints - 1; + break; + case PathBehavior::Bounce: + next = numWaypoints >= 2 ? numWaypoints - 2 : 0; + changedDirection = true; + break; + case PathBehavior::Loop: + default: + next = 0; + break; + } + } + + return next; +} + +uint32_t PlatformSubComponent::GetNextReversedWaypoint(uint32_t current, bool& changedDirection) const { + changedDirection = false; + + if (current == 0) { + switch (m_Path->pathBehavior) { + case PathBehavior::Once: + return 0; + case PathBehavior::Bounce: + changedDirection = true; + return 1; + case PathBehavior::Loop: + default: + return static_cast(m_Path->pathWaypoints.size()) - 1; + } + } + + return current - 1; +} + +// --- Arrival detection --- + +bool PlatformSubComponent::CloseToNextWaypoint() const { + if (m_TimeBasedMovement) return false; + + const NiPoint3 toNext = m_NextWaypointPosition - m_Position; + const float distSq = toNext.SquaredLength(); + + if (distSq <= 0.001f) return true; + + const float dot = toNext.DotProduct(m_DirectionVector); + return dot <= 0.0f; +} + +// --- Travel time calculation --- + +float PlatformSubComponent::CalculateAcceleration(float vi, float vf, float d) { + if (d < 0.0001f) return 0.0f; + return (vf * vf - vi * vi) / (2.0f * d); +} + +float PlatformSubComponent::CalculateTime(float vi, float a, float d) { + if (d < 0.0001f) return 0.0f; + if (std::abs(a) < 0.0001f) { + return vi > 0.0f ? d / vi : 0.0f; + } + const float discriminant = 2.0f * a * d + vi * vi; + if (discriminant < 0.0f) return 0.0f; + return (std::sqrt(discriminant) - vi) / a; +} + +void PlatformSubComponent::CalculateWaypointSpeeds() { + if (m_CurrentWaypointIndex == m_NextWaypointIndex) { + m_TravelTime = 0.0f; + return; + } + + if (m_TimeBasedMovement) { + uint32_t minIdx = std::min(m_CurrentWaypointIndex, m_NextWaypointIndex); + m_CurrentSpeed = m_Path->pathWaypoints[minIdx].speed; + m_NextSpeed = 0.0f; + m_TravelTime = m_CurrentSpeed; + } else { + m_CurrentSpeed = m_Path->pathWaypoints[m_CurrentWaypointIndex].speed; + m_NextSpeed = m_Path->pathWaypoints[m_NextWaypointIndex].speed; + + float a = CalculateAcceleration(m_CurrentSpeed, m_NextSpeed, m_TotalDistance); + m_TravelTime = CalculateTime(m_CurrentSpeed, a, m_TotalDistance); + } +} + +float PlatformSubComponent::CalculateCurrentSpeed() const { + if (m_TimeBasedMovement) { + if (m_CurrentSpeed > 0.0f) { + return m_TotalDistance / m_CurrentSpeed; + } + return 0.0f; + } + + return (m_NextSpeed - m_CurrentSpeed) * m_PercentBetweenPoints + m_CurrentSpeed; +} + +// --- State machine helpers --- + +bool PlatformSubComponent::IncrementWaitingTime(float deltaTime) { + if (!(m_State & PlatformState::Waiting)) return false; + if (m_State & PlatformState::Travelling) return false; + + m_IdleTimeElapsed += deltaTime; + if (m_IdleTimeElapsed >= m_WaitTime) { + m_IdleTimeElapsed = 0.0f; + return true; + } + return false; +} + +void PlatformSubComponent::StartTravelling() { + m_State = (m_State & ~(PlatformState::Stopped | PlatformState::Waiting)) | PlatformState::Travelling; + m_MoveTimeElapsed = 0.0f; + m_HasStartedTravelling = false; +} + +void PlatformSubComponent::ArrivedAtWaypoint(bool& dirtyOut) { + dirtyOut = true; + + m_Position = m_NextWaypointPosition; + m_PercentBetweenPoints = 1.0f; + + if (m_ParentEntity) { + m_ParentEntity->SetPosition(m_NextWaypointPosition); + m_ParentEntity->SetRotation(m_NextWaypointRotation); + } + + PlayArriveSound(); + + if (m_ParentEntity) { + m_ParentEntity->GetScript()->OnWaypointReached(m_ParentEntity, m_NextWaypointIndex); + } + + bool isAtDesiredWaypoint = false; + bool stopAtDesired = false; + if (m_DesiredWaypointIndex >= 0 && + static_cast(m_DesiredWaypointIndex) == m_NextWaypointIndex) { + isAtDesiredWaypoint = true; + stopAtDesired = m_ShouldStopAtDesiredWaypoint; + m_ShouldStopAtDesiredWaypoint = false; + m_DesiredWaypointIndex = -1; + } + + if (isAtDesiredWaypoint && m_ParentEntity) { + CBITSTREAM; + CMSGHEADER; + bitStream.Write(m_ParentEntity->GetObjectID()); + bitStream.Write(MessageType::Game::ARRIVED_AT_DESIRED_WAYPOINT); + SEND_PACKET_BROADCAST; + } + + bool atEnd = false; + const auto numWaypoints = static_cast(m_Path->pathWaypoints.size()); + if (m_NextWaypointIndex == 0 || m_NextWaypointIndex == numWaypoints - 1) { + atEnd = true; + } + + if (atEnd && m_ParentEntity) { + CBITSTREAM; + CMSGHEADER; + bitStream.Write(m_ParentEntity->GetObjectID()); + bitStream.Write(MessageType::Game::PLATFORM_AT_LAST_WAYPOINT); + SEND_PACKET_BROADCAST; + } + + bool stopOnce = false; + if (atEnd && m_Path->pathBehavior == PathBehavior::Once) { + stopOnce = true; + m_InReverse = !m_InReverse; + } + + if (stopAtDesired || stopOnce) { + m_State = PlatformState::Stopped; + m_MoveTimeElapsed = 0.0f; + m_HasStartedTravelling = false; + ZeroPhysicsVelocity(); + + if (m_ParentEntity) { + CBITSTREAM; + CMSGHEADER; + bitStream.Write(m_ParentEntity->GetObjectID()); + bitStream.Write(MessageType::Game::ARRIVED); + SEND_PACKET_BROADCAST; + } + } else { + SetupWaypointSegment(m_NextWaypointIndex); + m_State = PlatformState::Waiting; + } +} + +void PlatformSubComponent::UpdatePositionAlongPath(float deltaTime) { + if (m_TotalDistance <= 0.0f && !m_TimeBasedMovement) return; + + m_MoveTimeElapsed += deltaTime; + + // Calculate percent between waypoints matching client CalculatePercentTravelledToWaypoint: + // Distance-based: percent = dist(position, currentWP) / dist(nextWP, currentWP) + // Time-based: percent = moveTimeElapsed / travelTime + if (m_TimeBasedMovement) { + if (m_TravelTime > 0.0f) { + m_MoveTimeElapsed = std::min(m_MoveTimeElapsed, m_TravelTime); + m_PercentBetweenPoints = m_MoveTimeElapsed / m_TravelTime; + } + } else if (m_TotalDistance > 0.0f) { + float distanceTravelled = (m_Position - m_CurrentWaypointPosition).Length(); + m_PercentBetweenPoints = std::min(distanceTravelled / m_TotalDistance, 1.0f); + } + + // Send Departed message on first travel frame (matching client RunPlatform) + if (!m_HasStartedTravelling) { + m_HasStartedTravelling = true; + PlayDepartSound(); + + if (m_ParentEntity) { + CBITSTREAM; + CMSGHEADER; + bitStream.Write(m_ParentEntity->GetObjectID()); + bitStream.Write(MessageType::Game::DEPARTED); + SEND_PACKET_BROADCAST; + } + } + + // Advance position using velocity and deltaTime (matching client physics model) + // The client sets velocity then lets the physics engine move the object. + // We do the same: calculate speed, derive velocity, advance position. + float speed = CalculateCurrentSpeed(); + NiPoint3 velocity = m_DirectionVector * speed; + m_Position = m_Position + velocity * deltaTime; + + // Clamp position to not overshoot the next waypoint + float distToNext = (m_NextWaypointPosition - m_Position).DotProduct(m_DirectionVector); + if (distToNext <= 0.0f) { + m_Position = m_NextWaypointPosition; + } + + if (m_ParentEntity) { + m_ParentEntity->SetPosition(m_Position); + SetPhysicsVelocity(velocity); + + // Slerp rotation between waypoints + auto interpRot = glm::slerp(m_CurrentWaypointRotation, m_NextWaypointRotation, m_PercentBetweenPoints); + m_ParentEntity->SetRotation(interpRot); + } +} + +// --- Physics velocity helpers --- + +void PlatformSubComponent::SetPhysicsVelocity(const NiPoint3& velocity) { + if (!m_ParentEntity) return; + auto* simplePhysics = m_ParentEntity->GetComponent(); + if (simplePhysics) { + simplePhysics->SetVelocity(velocity); + } +} + +void PlatformSubComponent::ZeroPhysicsVelocity() { + if (!m_ParentEntity) return; + auto* simplePhysics = m_ParentEntity->GetComponent(); + if (simplePhysics) { + simplePhysics->SetVelocity(NiPoint3Constant::ZERO); + simplePhysics->SetAngularVelocity(NiPoint3Constant::ZERO); + } +} + +// --- Sound helpers --- + +void PlatformSubComponent::PlayDepartSound() { + if (!m_ParentEntity || !m_Path) return; + if (m_CurrentWaypointIndex >= m_Path->pathWaypoints.size()) return; + + const auto& sound = m_Path->pathWaypoints[m_CurrentWaypointIndex].movingPlatform.departSound; + if (!sound.empty()) { + GameMessages::SendPlayNDAudioEmitter(m_ParentEntity, UNASSIGNED_SYSTEM_ADDRESS, sound); + } +} + +void PlatformSubComponent::PlayArriveSound() { + if (!m_ParentEntity || !m_Path) return; + if (m_NextWaypointIndex >= m_Path->pathWaypoints.size()) return; + + const auto& sound = m_Path->pathWaypoints[m_NextWaypointIndex].movingPlatform.arriveSound; + if (!sound.empty()) { + GameMessages::SendPlayNDAudioEmitter(m_ParentEntity, UNASSIGNED_SYSTEM_ADDRESS, sound); + } +} diff --git a/dGame/dComponents/MovingPlatformComponent/PlatformSubComponent.h b/dGame/dComponents/MovingPlatformComponent/PlatformSubComponent.h new file mode 100644 index 00000000..4b9e791d --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/PlatformSubComponent.h @@ -0,0 +1,127 @@ +#ifndef PLATFORMSUBCOMPONENT_H +#define PLATFORMSUBCOMPONENT_H + +#include "RakNetTypes.h" +#include "NiPoint3.h" +#include "NiQuaternion.h" + +#include + +class Entity; +class Path; +class SimplePhysicsComponent; + +/** + * Platform state flags (bitmask matching client LWOPlatform state bits) + */ +namespace PlatformState { + constexpr uint32_t Waiting = 1 << 0; // 0x01 - Waiting at waypoint + constexpr uint32_t Travelling = 1 << 1; // 0x02 - Moving between waypoints + constexpr uint32_t Stopped = 1 << 2; // 0x04 - Movement halted + + // These map to the old eMovementPlatformState values for serialization + constexpr uint32_t MovingSerialized = 0b00010; // Travelling + constexpr uint32_t StationarySerialized = 0b11001; // Waiting + constexpr uint32_t StoppedSerialized = 0b01100; // Stopped +}; + +/** + * Base class for platform subcomponents. Mirrors the client's LWOPlatform base. + * Handles the core state machine: waiting at waypoints, travelling between them, + * arrival detection, and waypoint navigation (loop/bounce/once). + */ +class PlatformSubComponent { +public: + PlatformSubComponent(Entity* parentEntity, const Path* path); + virtual ~PlatformSubComponent() = default; + + virtual void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate); + virtual void Update(float deltaTime, bool& dirtyOut); + + void StartPathing(); + void StopPathing(); + void GotoWaypoint(uint32_t index, bool stopAtWaypoint = true); + void WarpToWaypoint(size_t index); + void SetupWaypointSegment(uint32_t waypointIndex); + + // --- State accessors --- + + uint32_t GetState() const { return m_State; } + void SetState(uint32_t state) { m_State = state; } + + int32_t GetDesiredWaypointIndex() const { return m_DesiredWaypointIndex; } + bool GetInReverse() const { return m_InReverse; } + bool GetShouldStopAtDesiredWaypoint() const { return m_ShouldStopAtDesiredWaypoint; } + float GetPercentBetweenPoints() const { return m_PercentBetweenPoints; } + NiPoint3 GetPosition() const { return m_Position; } + uint32_t GetCurrentWaypointIndex() const { return m_CurrentWaypointIndex; } + uint32_t GetNextWaypointIndex() const { return m_NextWaypointIndex; } + float GetIdleTimeElapsed() const { return m_IdleTimeElapsed; } + float GetMoveTimeElapsed() const { return m_MoveTimeElapsed; } + float GetSpeed() const { return m_CurrentSpeed; } + float GetWaitTime() const { return m_WaitTime; } + size_t GetLastWaypointIndex() const; + bool IsActive() const { return m_Active; } + + void SetDesiredWaypointIndex(int32_t index) { m_DesiredWaypointIndex = index; } + void SetShouldStopAtDesiredWaypoint(bool value) { m_ShouldStopAtDesiredWaypoint = value; } + void SetInReverse(bool value) { m_InReverse = value; } + void SetActive(bool value) { m_Active = value; } + + uint32_t GetSerializedState() const; + +protected: + uint32_t GetNextWaypoint(uint32_t current, bool& changedDirection) const; + uint32_t GetNextReversedWaypoint(uint32_t current, bool& changedDirection) const; + bool CloseToNextWaypoint() const; + + static float CalculateAcceleration(float vi, float vf, float d); + static float CalculateTime(float vi, float a, float d); + void CalculateWaypointSpeeds(); + float CalculateCurrentSpeed() const; + + bool IncrementWaitingTime(float deltaTime); + void StartTravelling(); + void ArrivedAtWaypoint(bool& dirtyOut); + virtual void UpdatePositionAlongPath(float deltaTime); + + void SetPhysicsVelocity(const NiPoint3& velocity); + void ZeroPhysicsVelocity(); + void PlayDepartSound(); + void PlayArriveSound(); + + Entity* m_ParentEntity = nullptr; + const Path* m_Path = nullptr; + bool m_Active = false; + + uint32_t m_State = PlatformState::Stopped; + int32_t m_DesiredWaypointIndex = -1; + bool m_InReverse = false; + bool m_ShouldStopAtDesiredWaypoint = false; + + float m_PercentBetweenPoints = 0.0f; + NiPoint3 m_Position{}; + + uint32_t m_CurrentWaypointIndex = 0; + uint32_t m_NextWaypointIndex = 0; + + float m_IdleTimeElapsed = 0.0f; + float m_MoveTimeElapsed = 0.0f; + + float m_CurrentSpeed = 0.0f; + float m_NextSpeed = 0.0f; + float m_WaitTime = 0.0f; + + NiPoint3 m_CurrentWaypointPosition{}; + NiPoint3 m_NextWaypointPosition{}; + NiQuaternion m_CurrentWaypointRotation = QuatUtils::IDENTITY; + NiQuaternion m_NextWaypointRotation = QuatUtils::IDENTITY; + NiPoint3 m_DirectionVector{}; + float m_TotalDistance = 0.0f; + float m_TravelTime = 0.0f; + + bool m_TimeBasedMovement = false; + bool m_HasStartedTravelling = false; +}; + +#endif // PLATFORMSUBCOMPONENT_H diff --git a/dGame/dComponents/MovingPlatformComponent/RotatorSubComponent.cpp b/dGame/dComponents/MovingPlatformComponent/RotatorSubComponent.cpp new file mode 100644 index 00000000..57215fb8 --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/RotatorSubComponent.cpp @@ -0,0 +1,93 @@ +#include "RotatorSubComponent.h" +#include "Entity.h" +#include "SimplePhysicsComponent.h" + +#include +#include + +#include + +RotatorSubComponent::RotatorSubComponent(Entity* parentEntity, const Path* path) + : PlatformSubComponent(parentEntity, path) { +} + +void RotatorSubComponent::UpdatePositionAlongPath(float deltaTime) { + // Do the base linear movement (position, velocity, departed message) + PlatformSubComponent::UpdatePositionAlongPath(deltaTime); + + if (!m_ParentEntity) return; + + // Angular velocity calculation matching client LWOPlatform::SetAngularVelocity: + // 1. If current and next rotations are the same, just set current rotation and zero angular velocity + // 2. Otherwise, SLERP to the target rotation based on percent, then compute angular velocity + // to reach the next waypoint rotation in the remaining travel time + + if (m_CurrentWaypointRotation == m_NextWaypointRotation) { + m_ParentEntity->SetRotation(m_CurrentWaypointRotation); + auto* simplePhysics = m_ParentEntity->GetComponent(); + if (simplePhysics) { + simplePhysics->SetAngularVelocity(NiPoint3Constant::ZERO); + } + return; + } + + // SLERP to get the current target rotation (matching client CalculateSlerp) + NiQuaternion targetRot = glm::slerp(m_CurrentWaypointRotation, m_NextWaypointRotation, m_PercentBetweenPoints); + + // Check if we're already close enough to snap (matching client's angle threshold check) + NiQuaternion currentRot = m_ParentEntity->GetRotation(); + float dotProduct = glm::dot(targetRot, currentRot); + if (dotProduct < 0.0f) dotProduct = -dotProduct; + float angleDiff = std::acos(std::clamp(dotProduct, 0.0f, 1.0f)); + + bool snappedToTarget = false; + if (angleDiff < m_MaxLerpAngle) { + // Close enough — snap to current rotation, use the actual current rotation for angular vel calc + snappedToTarget = true; + targetRot = currentRot; + } else { + // Set the SLERP'd rotation on the entity + m_ParentEntity->SetRotation(targetRot); + } + + // Calculate remaining travel time for angular velocity + float remainingTime = 0.0f; + if (!m_TimeBasedMovement && m_TotalDistance > 0.0f) { + // Distance-based: calculate remaining time from remaining distance and speeds + NiPoint3 toNext = m_NextWaypointPosition - m_Position; + float remainingDist = toNext.Length(); + float currentSpeed = CalculateCurrentSpeed(); + if (currentSpeed > 0.0f) { + remainingTime = remainingDist / currentSpeed; + } + } else if (m_TimeBasedMovement) { + remainingTime = m_TravelTime - m_MoveTimeElapsed; + } + + if (remainingTime > 0.0f) { + // Compute angular velocity from quaternion difference (matching client LWOPhysicsCalcAngularVelocity) + // Angular velocity = axis * (angle / time) + NiQuaternion rotDiff = m_NextWaypointRotation * glm::inverse(targetRot); + + // Normalize to ensure valid quaternion + rotDiff = glm::normalize(rotDiff); + + float angle = 2.0f * std::acos(std::clamp(rotDiff.w, -1.0f, 1.0f)); + if (std::abs(angle) > 0.001f) { + float sinHalf = std::sqrt(1.0f - rotDiff.w * rotDiff.w); + NiPoint3 axis; + if (sinHalf > 0.001f) { + axis = NiPoint3(rotDiff.x / sinHalf, rotDiff.y / sinHalf, rotDiff.z / sinHalf); + } else { + axis = NiPoint3(0.0f, 1.0f, 0.0f); + } + + m_AngularVelocity = axis * (angle / remainingTime); + + auto* simplePhysics = m_ParentEntity->GetComponent(); + if (simplePhysics) { + simplePhysics->SetAngularVelocity(m_AngularVelocity); + } + } + } +} diff --git a/dGame/dComponents/MovingPlatformComponent/RotatorSubComponent.h b/dGame/dComponents/MovingPlatformComponent/RotatorSubComponent.h new file mode 100644 index 00000000..fe3d4f18 --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/RotatorSubComponent.h @@ -0,0 +1,24 @@ +#ifndef ROTATORSUBCOMPONENT_H +#define ROTATORSUBCOMPONENT_H + +#include "PlatformSubComponent.h" + +/** + * Rotator - follows a path like Mover but also applies angular velocity. + * Corresponds to client LWOPlatformRotator (type 6). + */ +class RotatorSubComponent final : public PlatformSubComponent { +public: + RotatorSubComponent(Entity* parentEntity, const Path* path); + + void UpdatePositionAlongPath(float deltaTime) override; + +private: + NiPoint3 m_RotationAxis{}; + float m_Rate = 0.0f; + NiPoint3 m_AngularVelocity{}; + bool m_AllowRotSnap = true; + float m_MaxLerpAngle = 0.785398f; // ~45 degrees +}; + +#endif // ROTATORSUBCOMPONENT_H diff --git a/dGame/dComponents/MovingPlatformComponent/SimpleMoverSubComponent.cpp b/dGame/dComponents/MovingPlatformComponent/SimpleMoverSubComponent.cpp new file mode 100644 index 00000000..b39a1093 --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/SimpleMoverSubComponent.cpp @@ -0,0 +1,55 @@ +#include "SimpleMoverSubComponent.h" +#include "Zone.h" + +SimpleMoverSubComponent::SimpleMoverSubComponent(Entity* parentEntity, const NiPoint3& startPos, + const NiQuaternion& startRot, const NiPoint3& platformMove, float platformMoveTime) + : PlatformSubComponent(parentEntity, nullptr) { + GeneratePath(startPos, startRot, platformMove, platformMoveTime); + m_Path = m_GeneratedPath.get(); + m_TimeBasedMovement = false; + + // Auto-activate and set up initial segment (matching client GenerateSimpleMoverPath) + if (m_Path && !m_Path->pathWaypoints.empty()) { + m_Active = true; + SetupWaypointSegment(m_CurrentWaypointIndex); + } +} + +void SimpleMoverSubComponent::GeneratePath(const NiPoint3& startPos, const NiQuaternion& startRot, + const NiPoint3& platformMove, float platformMoveTime) { + + auto path = std::make_unique(); + path->pathType = PathType::MovingPlatform; + path->pathBehavior = PathBehavior::Once; + path->pathName = "SimpleMoverPath"; + path->movingPlatform.timeBasedMovement = 0; + + // Waypoint 0: start position + PathWaypoint wp0; + wp0.position = startPos; + wp0.rotation = startRot; + wp0.movingPlatform.wait = 0.0f; + + // Waypoint 1: start + rotated platformMove + NiPoint3 move = platformMove; + NiPoint3 rotatedMove = move.RotateByQuaternion(startRot); + PathWaypoint wp1; + wp1.position = startPos + rotatedMove; + wp1.rotation = startRot; + wp1.movingPlatform.wait = 0.0f; + + // Calculate speed: length(platformMove) / platformMoveTime + float speed = 0.0f; + if (move.SquaredLength() > 0.0f && platformMoveTime > 0.0f) { + speed = platformMove.Length() / platformMoveTime; + } + + wp0.speed = speed; + wp1.speed = speed; + + path->pathWaypoints.push_back(wp0); + path->pathWaypoints.push_back(wp1); + path->waypointCount = 2; + + m_GeneratedPath = std::move(path); +} diff --git a/dGame/dComponents/MovingPlatformComponent/SimpleMoverSubComponent.h b/dGame/dComponents/MovingPlatformComponent/SimpleMoverSubComponent.h new file mode 100644 index 00000000..9db57151 --- /dev/null +++ b/dGame/dComponents/MovingPlatformComponent/SimpleMoverSubComponent.h @@ -0,0 +1,28 @@ +#ifndef SIMPLEMOVERSUBCOMPONENT_H +#define SIMPLEMOVERSUBCOMPONENT_H + +#include "PlatformSubComponent.h" + +#include + +class Path; + +/** + * Simple mover - auto-generates a 2-waypoint path from component properties. + * Corresponds to client LWOPlatformSimpleMover (type 5). + */ +class SimpleMoverSubComponent final : public PlatformSubComponent { +public: + SimpleMoverSubComponent(Entity* parentEntity, const NiPoint3& startPos, + const NiQuaternion& startRot, const NiPoint3& platformMove, float platformMoveTime); + + const Path* GetGeneratedPath() const { return m_GeneratedPath.get(); } + +private: + void GeneratePath(const NiPoint3& startPos, const NiQuaternion& startRot, + const NiPoint3& platformMove, float platformMoveTime); + + std::unique_ptr m_GeneratedPath; +}; + +#endif // SIMPLEMOVERSUBCOMPONENT_H diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp index c0efa544..68013036 100644 --- a/dGame/dComponents/SimplePhysicsComponent.cpp +++ b/dGame/dComponents/SimplePhysicsComponent.cpp @@ -12,6 +12,7 @@ #include "CDPhysicsComponentTable.h" #include "Entity.h" +#include "MovingPlatformComponent.h" #include "StringifiedEnum.h" #include "Amf3.h" @@ -39,6 +40,11 @@ SimplePhysicsComponent::~SimplePhysicsComponent() { void SimplePhysicsComponent::Update(const float deltaTime) { if (m_Velocity == NiPoint3Constant::ZERO) return; + + // If this entity has a MovingPlatformComponent, it owns position updates. + // Don't double-move by also applying velocity here. + if (m_Parent->GetComponent()) return; + m_Position += m_Velocity * deltaTime; m_DirtyPosition = true; Game::entityManager->SerializeEntity(m_Parent); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 03849d49..633932cd 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -365,31 +365,18 @@ void GameMessages::SendResetMissions(Entity* entity, const SystemAddress& sysAdd void GameMessages::SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint, int iIndex, int iDesiredWaypointIndex, int nextIndex, - eMovementPlatformState movementState) { + eMovementPlatformState movementState, + bool bReverse, float fIdleTimeElapsed, float fMoveTimeElapsed, + float fPercentBetweenPoints, NiPoint3 ptUnexpectedLocation, + NiQuaternion qUnexpectedRotation) { CBITSTREAM; CMSGHEADER; - const auto lot = entity->GetLOT(); - - if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308) { - iDesiredWaypointIndex = (lot == 11306 || lot == 11308) ? 1 : 0; - iIndex = 0; - nextIndex = 0; - bStopAtDesiredWaypoint = true; - movementState = eMovementPlatformState::Stationary; - } - bitStream.Write(entity->GetObjectID()); bitStream.Write(MessageType::Game::PLATFORM_RESYNC); - bool bReverse = false; int eCommand = 0; int eUnexpectedCommand = 0; - float fIdleTimeElapsed = 0.0f; - float fMoveTimeElapsed = 0.0f; - float fPercentBetweenPoints = 0.0f; - NiPoint3 ptUnexpectedLocation = NiPoint3Constant::ZERO; - NiQuaternion qUnexpectedRotation = QuatUtils::IDENTITY; bitStream.Write(bReverse); bitStream.Write(bStopAtDesiredWaypoint); diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 3b27b9f1..d1033c4d 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -104,7 +104,10 @@ namespace GameMessages { void SendStartPathing(Entity* entity); void SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint = false, int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1, - eMovementPlatformState movementState = eMovementPlatformState::Moving); + eMovementPlatformState movementState = eMovementPlatformState::Moving, + bool bReverse = false, float fIdleTimeElapsed = 0.0f, float fMoveTimeElapsed = 0.0f, + float fPercentBetweenPoints = 0.0f, NiPoint3 ptUnexpectedLocation = NiPoint3Constant::ZERO, + NiQuaternion qUnexpectedRotation = QuatUtils::IDENTITY); void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1); void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr);