From 83f321bec6823a1a90eba796fbec43c2064cdfb3 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 23:36:00 -0500 Subject: [PATCH] feat(moving platform): the server runs movers as live did and answers resyncs with their state The client's LWOPlatform logic (ArrivedAtWaypoint 0x00c86c30, ProcessStateChange 0x00cce380, GetNextWaypoint, the waypoint speeds) runs on the server: stopped at a waypoint (25 at an end) for its wait, then travelling (2), serialized on arrival and departure as in the NT mover captures; constructions carry the path and the mover state; RequestPlatformResync gets the current state, percent and position. The LOT 9483 timers are gone. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/MovingPlatformComponent.cpp | 449 +++++++++--------- dGame/dComponents/MovingPlatformComponent.h | 105 ++-- dGame/dGameMessages/MovementMessages.cpp | 11 + dZoneManager/Zone.h | 2 + .../MovingPlatformComponentTests.cpp | 80 +++- 5 files changed, 370 insertions(+), 277 deletions(-) diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 345020d4b..a12511195 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -14,31 +14,22 @@ #include "SimplePhysicsComponent.h" #include "Zone.h" #include "CDClientDatabase.h" +#include "MovementMessages.h" #include MoverSubComponent::MoverSubComponent(const NiPoint3& startPos) { - mPosition = {}; - - mState = eMovementPlatformState::Stopped; - mDesiredWaypointIndex = 0; // -1; - mInReverse = false; - mShouldStopAtDesiredWaypoint = false; - - mPercentBetweenPoints = 0.0f; - - mCurrentWaypointIndex = 0; - mNextWaypointIndex = 0; //mCurrentWaypointIndex + 1; - - mIdleTimeElapsed = 0.0f; + mPosition = startPos; } MoverSubComponent::~MoverSubComponent() = default; +// LWOPlatform::Deserialize 0x00c870e0: a dirty bit, the state, desired waypoint, whether to stop there, reverse, the +// percent between the waypoints, where it is, the current and next waypoints, the time waited and the time travelled void MoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { outBitStream.Write(true); - outBitStream.Write(mState); + outBitStream.Write(mState); outBitStream.Write(mDesiredWaypointIndex); outBitStream.Write(mShouldStopAtDesiredWaypoint); outBitStream.Write(mInReverse); @@ -53,7 +44,7 @@ void MoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIniti outBitStream.Write(mNextWaypointIndex); outBitStream.Write(mIdleTimeElapsed); - outBitStream.Write(0.0f); // Move time elapsed + outBitStream.Write(mMoveTimeElapsed); } //------------- SimpleMoverSubComponent below -------------- @@ -151,6 +142,18 @@ MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t c LOG("Path not found: %s", pathName.c_str()); } + // A mover starts at its attached_path_start waypoint, waiting there, and leaves after the waypoint's wait unless + // startPathingOnLoad is false (live: 25 with the time waited in the constructions) + if (IsMover()) { + auto* const mover = GetMoverSubComponent(); + mover->mTimeBased = m_Path->movingPlatform.timeBasedMovement != 0; + const auto start = m_Parent->HasVar(u"attached_path_start") ? m_Parent->GetVar(u"attached_path_start") : 0; + m_StartWaypoint = start >= 0 && static_cast(start) < m_Path->pathWaypoints.size() ? start : 0; + MoverProcessStateChange(m_StartWaypoint); + const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar(u"startPathingOnLoad"); + MoverStopAtWaypoint(startOnLoad); + } + // Live constructed every moving platform with simple physics as keyframed auto* const simplePhysics = m_Parent->GetComponent(); if (simplePhysics && !m_Parent->HasVar(u"motionType")) simplePhysics->SetPhysicsMotionState(SimplePhysicsComponent::MOTION_TYPE_KEYFRAMED); @@ -179,34 +182,23 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI return; } - // 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; - } - + // LWOMovingPlatformComponent::Deserialize: whether subcomponents follow, whether the path changed (then whether + // there is one: its name, starting waypoint and reverse), then the subcomponents. Live wrote the path with the + // construction and the mover's state with every serialization. outBitStream.Write(true); - auto hasPath = !m_PathingStopped && !m_PathName.empty(); - outBitStream.Write(hasPath); - - if (hasPath) { - // Is on rail - outBitStream.Write1(); - - outBitStream.Write(m_PathName.size()); - for (const auto& c : m_PathName) { - outBitStream.Write(c); + outBitStream.Write(bIsInitialUpdate); + if (bIsInitialUpdate) { + const auto hasPath = !m_PathName.empty() && m_Path != nullptr; + outBitStream.Write(hasPath); + if (hasPath) { + outBitStream.Write(m_PathName.size()); + for (const auto& c : m_PathName) { + outBitStream.Write(c); + } + outBitStream.Write(m_StartWaypoint); + outBitStream.Write(false); } - - // Starting point - outBitStream.Write(0); - - // Reverse - outBitStream.Write(false); } // A platform the client made no subcomponent for must not get one: it could not read it @@ -254,27 +246,54 @@ void MovingPlatformComponent::SetMovementState(eMovementPlatformState value) { Game::entityManager->SerializeEntity(m_Parent); return; } - - auto* subComponent = static_cast(m_MoverSubComponent); - - subComponent->mState = value; - + if (!IsMover()) return; + GetMoverSubComponent()->mState = static_cast(value); Game::entityManager->SerializeEntity(m_Parent); } +// Sends the mover to a waypoint, stopping there when stopAtWaypoint: it turns round if the waypoint is behind it void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) { if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { SimpleMoverGotoWaypoint(static_cast(index)); return; } + if (!IsMover() || index >= m_Path->pathWaypoints.size()) return; + auto* const mover = GetMoverSubComponent(); + const bool atWaypoint = (mover->mState & ePlatformStateFlag::ReachedDesiredWaypoint) != 0; + if (atWaypoint && mover->mCurrentWaypointIndex == index) { + // Already there + mover->mDesiredWaypointIndex = -1; + mover->mShouldStopAtDesiredWaypoint = false; + if (stopAtWaypoint) MoverStopAtWaypoint(false); + FireArrivalHooks(index, true, index == m_Path->pathWaypoints.size() - 1); + return; + } + mover->mDesiredWaypointIndex = static_cast(index); + mover->mShouldStopAtDesiredWaypoint = stopAtWaypoint; - auto* subComponent = static_cast(m_MoverSubComponent); - - subComponent->mDesiredWaypointIndex = index; - subComponent->mNextWaypointIndex = index; - subComponent->mShouldStopAtDesiredWaypoint = stopAtWaypoint; - - StartPathing(); + // LWOPlatform::SetMovingForwards / SetMovingBackwards: at a waypoint it sets off in the waypoint's direction; between + // two it carries on if the waypoint is ahead, else turns round (the waypoint it was heading to becomes the one it + // comes from) + if (atWaypoint) { + const bool reverse = index < mover->mCurrentWaypointIndex; + if (reverse != mover->mInReverse) { + mover->mInReverse = reverse; + MoverProcessStateChange(mover->mCurrentWaypointIndex); + } + } else { + const bool ahead = mover->mInReverse ? index <= mover->mNextWaypointIndex : index >= mover->mNextWaypointIndex; + if (!ahead) { + const float percent = mover->mPercentBetweenPoints; + const auto heading = mover->mNextWaypointIndex; + mover->mInReverse = !mover->mInReverse; + mover->mNextWaypointIndex = mover->mCurrentWaypointIndex; + mover->mCurrentWaypointIndex = heading; + mover->mPercentBetweenPoints = 1.0f - std::min(percent, 1.0f); + if (mover->mTimeBased) mover->mMoveTimeElapsed = mover->mCurrentSpeed * mover->mPercentBetweenPoints; + else std::swap(mover->mCurrentSpeed, mover->mNextSpeed); + } + } + MoverDepart(); } void MovingPlatformComponent::StartPathing() { @@ -283,161 +302,11 @@ void MovingPlatformComponent::StartPathing() { SimpleMoverGotoWaypoint(GetSimpleMoverSubComponent()->mCurrentWaypointIndex == 0 ? 1 : 0); return; } - //GameMessages::SendStartPathing(m_Parent); + if (!IsMover()) return; 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] { - FireArrivalHooks(subComponent->mNextWaypointIndex, subComponent->mNextWaypointIndex == static_cast(subComponent->mDesiredWaypointIndex)); - }); - - m_Parent->AddCallbackTimer(travelNext, [this] { - ContinuePathing(); - }); - - //GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS); - - Game::entityManager->SerializeEntity(m_Parent); -} - -void MovingPlatformComponent::ContinuePathing() { - if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) return; - auto* subComponent = static_cast(m_MoverSubComponent); - - subComponent->mState = eMovementPlatformState::Stationary; - - 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_Parent->GetLOT() == 9483) { - behavior = PathBehavior::Bounce; - } else { - return; - } - - 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] { - FireArrivalHooks(subComponent->mNextWaypointIndex, subComponent->mNextWaypointIndex == static_cast(subComponent->mDesiredWaypointIndex)); - }); - - m_Parent->AddCallbackTimer(travelNext, [this] { - ContinuePathing(); - }); - - Game::entityManager->SerializeEntity(m_Parent); + auto* const mover = GetMoverSubComponent(); + if ((mover->mState & ePlatformStateFlag::Travelling) != 0) return; + MoverDepart(); } void MovingPlatformComponent::StopPathing() { @@ -450,19 +319,157 @@ void MovingPlatformComponent::StopPathing() { Game::entityManager->SerializeEntity(m_Parent); return; } - //m_Parent->CancelCallbackTimers(); - - auto* subComponent = static_cast(m_MoverSubComponent); - + if (!IsMover()) return; m_PathingStopped = true; - - subComponent->mState = eMovementPlatformState::Stopped; - subComponent->mDesiredWaypointIndex = -1; - subComponent->mShouldStopAtDesiredWaypoint = false; - + auto* const mover = GetMoverSubComponent(); + // LWOPlatform::StopPlatform: stopped where it is, not leaving after a wait + mover->mState = (mover->mState & ~(ePlatformStateFlag::Travelling | ePlatformStateFlag::Waiting)) | ePlatformStateFlag::Stopped; + mover->mDepartAfterWait = false; + mover->mDesiredWaypointIndex = -1; + mover->mShouldStopAtDesiredWaypoint = false; Game::entityManager->SerializeEntity(m_Parent); +} - //GameMessages::SendPlatformResync(m_Parent, UNASSIGNED_SYSTEM_ADDRESS); +bool MovingPlatformComponent::IsMover() const { + return (m_MoverSubComponentType == eMoverSubComponentType::mover || m_MoverSubComponentType == eMoverSubComponentType::rotater) && + m_Path != nullptr && !m_Path->pathWaypoints.empty(); +} + +// LWOPlatform::ProcessStateChange: the waypoint becomes the current one, the next follows from the path's behavior and +// the direction (a bounce turns round at either end), with the two waypoints' speeds (GetWaypointSpeed) +void MovingPlatformComponent::MoverProcessStateChange(const uint32_t index) { + auto* const mover = GetMoverSubComponent(); + const auto& waypoints = m_Path->pathWaypoints; + const auto count = static_cast(waypoints.size()); + const auto behavior = static_cast(m_Path->pathBehavior); + const auto current = std::min(static_cast(index), count - 1); + + int32_t next{}; + if (!mover->mInReverse) { + next = current + 1; + if (next >= count) { + if (behavior == PathBehavior::Once) next = count - 1; + else if (behavior == PathBehavior::Bounce) { + next = count - 2; + mover->mInReverse = true; + } else next = 0; + } + } else { + next = current - 1; + if (next < 0) { + if (behavior == PathBehavior::Once) next = 0; + else if (behavior == PathBehavior::Bounce) { + next = 1; + mover->mInReverse = false; + } else next = count - 1; + } + } + next = std::clamp(next, 0, count - 1); + + mover->mCurrentWaypointIndex = current; + mover->mNextWaypointIndex = next; + mover->mPercentBetweenPoints = 0.0f; + mover->mMoveTimeElapsed = 0.0f; + mover->mPosition = waypoints[current].position; + mover->mSegmentLength = Vector3::Distance(waypoints[current].position, waypoints[next].position); + if (current == next) { + mover->mCurrentSpeed = mover->mNextSpeed = 0.0f; + } else if (mover->mTimeBased) { + mover->mCurrentSpeed = waypoints[std::min(current, next)].speed; + mover->mNextSpeed = 0.0f; + } else { + mover->mCurrentSpeed = waypoints[current].speed; + mover->mNextSpeed = waypoints[next].speed; + } +} + +void MovingPlatformComponent::MoverStopAtWaypoint(const bool depart) { + auto* const mover = GetMoverSubComponent(); + const auto last = static_cast(m_Path->pathWaypoints.size() - 1); + const bool atEnd = mover->mCurrentWaypointIndex == 0 || mover->mCurrentWaypointIndex == last; + mover->mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint | (atEnd ? ePlatformStateFlag::ReachedFinalDestination : 0); + mover->mIdleTimeElapsed = 0.0f; + // LWOPlatform::SetWaitingTime: at least 0.1 s + mover->mTimeToWait = std::max(0.1f, m_Path->pathWaypoints[mover->mCurrentWaypointIndex].movingPlatform.wait); + mover->mDepartAfterWait = depart; +} + +void MovingPlatformComponent::MoverDepart() { + auto* const mover = GetMoverSubComponent(); + mover->mState = ePlatformStateFlag::Travelling; + mover->mIdleTimeElapsed = 0.0f; + mover->mDepartAfterWait = false; + Game::entityManager->SerializeEntity(m_Parent); +} + +// LWOPlatform::ArrivedAtWaypoint and HandleWaypointArrived: at the next waypoint, it stops there if it was sent there +// to stop, or at an end of a path run once (which turns it round); otherwise it waits there and goes on +void MovingPlatformComponent::MoverArrive() { + auto* const mover = GetMoverSubComponent(); + const auto arrived = mover->mNextWaypointIndex; + const auto last = static_cast(m_Path->pathWaypoints.size() - 1); + const bool atEnd = arrived == last; + const bool atDesired = mover->mDesiredWaypointIndex == static_cast(arrived); + bool stop = false; + if (atDesired) { + stop = mover->mShouldStopAtDesiredWaypoint; + mover->mShouldStopAtDesiredWaypoint = false; + mover->mDesiredWaypointIndex = -1; + } + if ((arrived == 0 || atEnd) && static_cast(m_Path->pathBehavior) == PathBehavior::Once) { + stop = true; + mover->mInReverse = !mover->mInReverse; + } + MoverProcessStateChange(arrived); + MoverStopAtWaypoint(!stop); + Game::entityManager->SerializeEntity(m_Parent); + FireArrivalHooks(arrived, atDesired, atEnd); +} + +void MovingPlatformComponent::Update(const float deltaTime) { + if (!IsMover()) return; + auto* const mover = GetMoverSubComponent(); + if ((mover->mState & ePlatformStateFlag::Travelling) != 0) { + // LWOPlatform::Run / RunPlatform: time based paths count the time travelled; others move at the speed between + // the two waypoints' (CalculateSpeed), the client by its physics + if (mover->mTimeBased) { + mover->mMoveTimeElapsed += deltaTime; + mover->mPercentBetweenPoints = mover->mCurrentSpeed > 0.0f ? std::min(mover->mMoveTimeElapsed, mover->mCurrentSpeed) / mover->mCurrentSpeed : 1.0f; + } else if (mover->mSegmentLength > 0.0f) { + const float speed = mover->mCurrentSpeed + (mover->mNextSpeed - mover->mCurrentSpeed) * mover->mPercentBetweenPoints; + mover->mPercentBetweenPoints += std::max(speed, 0.0f) * deltaTime / mover->mSegmentLength; + } else { + mover->mPercentBetweenPoints = 1.0f; + } + const auto& from = m_Path->pathWaypoints[mover->mCurrentWaypointIndex].position; + const auto& to = m_Path->pathWaypoints[mover->mNextWaypointIndex].position; + const float percent = std::min(mover->mPercentBetweenPoints, 1.0f); + mover->mPosition = from + (to - from) * percent; + if (mover->mPercentBetweenPoints >= 1.0f) MoverArrive(); + } else if (mover->mDepartAfterWait) { + mover->mIdleTimeElapsed += deltaTime; + if (mover->mIdleTimeElapsed >= mover->mTimeToWait) MoverDepart(); + } else if ((mover->mState & ePlatformStateFlag::ReachedDesiredWaypoint) != 0) { + mover->mIdleTimeElapsed += deltaTime; + } +} + +void MovingPlatformComponent::FillResync(GameMessages::PlatformResync& resync) const { + auto* const mover = GetMoverSubComponent(); + resync.bReverse = mover->mInReverse; + resync.bStopAtDesiredWaypoint = mover->mShouldStopAtDesiredWaypoint; + resync.eState = static_cast(mover->mState); + resync.fIdleTimeElapsed = mover->mIdleTimeElapsed; + resync.fMoveTimeElapsed = mover->mMoveTimeElapsed; + resync.fPercentBetweenPoints = std::min(mover->mPercentBetweenPoints, 1.0f); + resync.iDesiredWaypointIndex = mover->mDesiredWaypointIndex; + resync.iIndex = static_cast(mover->mCurrentWaypointIndex); + resync.iNextIndex = static_cast(mover->mNextWaypointIndex); + resync.ptUnexpectedLocation = mover->mPosition; + const auto& from = m_Path->pathWaypoints[mover->mCurrentWaypointIndex].rotation; + const auto& to = m_Path->pathWaypoints[mover->mNextWaypointIndex].rotation; + const auto rotation = glm::slerp(glm::quat(from.w, from.x, from.y, from.z), glm::quat(to.w, to.x, to.y, to.z), resync.fPercentBetweenPoints); + resync.qUnexpectedRotation = NiQuaternion(rotation.w, rotation.x, rotation.y, rotation.z); } void MovingPlatformComponent::SetSerialized(bool value) { @@ -489,7 +496,10 @@ void MovingPlatformComponent::WarpToWaypoint(size_t index) { Game::entityManager->SerializeEntity(m_Parent); return; } + if (!IsMover() || index >= m_Path->pathWaypoints.size()) return; const auto& waypoint = m_Path->pathWaypoints[index]; + MoverProcessStateChange(static_cast(index)); + MoverStopAtWaypoint(false); m_Parent->SetPosition(waypoint.position); m_Parent->SetRotation(waypoint.rotation); @@ -544,15 +554,14 @@ void MovingPlatformComponent::OnSimpleMoverArrived(const int32_t index) { m_Parent->SetPosition(simpleMover->GetWaypointPosition(index)); Game::entityManager->SerializeEntity(m_Parent); - FireArrivalHooks(index, true); + FireArrivalHooks(index, true, index == 1); } // LWOPlatform::HandleWaypointArrived: Arrived, then ArrivedAtDesiredWaypoint at the waypoint it was sent to, then // PlatformAtLastWaypoint at either end of the path -void MovingPlatformComponent::FireArrivalHooks(const uint32_t index, const bool atDesiredWaypoint) { +void MovingPlatformComponent::FireArrivalHooks(const uint32_t index, const bool atDesiredWaypoint, const bool atLastWaypoint) { auto* const script = m_Parent->GetScript(); script->OnWaypointReached(m_Parent, index); if (atDesiredWaypoint) script->OnArrivedAtDesiredWaypoint(m_Parent, index); - const size_t last = m_MoverSubComponentType == eMoverSubComponentType::simpleMover ? 1 : (m_Path ? m_Path->pathWaypoints.size() - 1 : 0); - if (index == 0 || index == last) script->OnPlatformAtLastWaypoint(m_Parent); + if (atLastWaypoint) script->OnPlatformAtLastWaypoint(m_Parent); } diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index c84f60f72..216c46035 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -18,6 +18,7 @@ #include "eReplicaComponentType.h" class Path; +namespace GameMessages { struct PlatformResync; } /** * Different types of available platforms @@ -94,6 +95,13 @@ public: /** * Sub component for moving platforms that determine the actual current movement state */ +/** + * A mover (or rotater) along its path, run by the server as live ran it: the client's LWOPlatform logic (ArrivedAtWaypoint + * 0x00c86c30, ProcessStateChange 0x00cce380, GetNextWaypoint 0x00bd4490 / GetNextReversedWaypoint 0x00bd4420, the speeds + * of GetWaypointSpeed 0x00be90d0), with live's states: it travels (2), and at a waypoint it is stopped (1) with + * ReachedDesiredWaypoint (8), and ReachedFinalDestination (16) at either end, while it waits the waypoint's wait time; + * each arrival and departure is serialized (NT movers in the live captures: 25 on arrival, 2 on departure). + */ class MoverSubComponent { public: MoverSubComponent(const NiPoint3& startPos); @@ -101,60 +109,30 @@ public: 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 - */ + // ePlatformStateFlag bits + uint32_t mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint; + int32_t mDesiredWaypointIndex = -1; 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 - */ + // From the current waypoint (0) to the next (1) float mPercentBetweenPoints = 0; - - /** - * The current position of the platofrm - */ + // Where it is now (current waypoint + percent towards the next) 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 - */ + uint32_t mCurrentWaypointIndex = 0; + uint32_t mNextWaypointIndex = 0; + // Time spent at the waypoint, and how long it waits there (the waypoint's wait, at least 0.1 s) float mIdleTimeElapsed = 0; - - /** - * The speed the platform is currently moving at - */ - float mSpeed = 0; - - /** - * The time to wait before continuing movement - */ - float mWaitTime = 0; + float mTimeToWait = 0; + // Whether it leaves when the wait is over (false: stopped until told otherwise) + bool mDepartAfterWait = false; + // Time travelled since the current waypoint (time based paths only) + float mMoveTimeElapsed = 0; + // The current and next waypoints' speeds; time based paths: the seconds from one to the next in mCurrentSpeed + float mCurrentSpeed = 0; + float mNextSpeed = 0; + // From the current waypoint to the next + float mSegmentLength = 0; + bool mTimeBased = false; }; @@ -202,11 +180,6 @@ public: */ 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. */ @@ -278,6 +251,9 @@ private: */ bool m_PathingStopped = false; + // The waypoint the path starts at (attached_path_start), written with the path + uint32_t m_StartWaypoint = 0; + /** * The type of the subcomponent */ @@ -295,7 +271,26 @@ private: void SimpleMoverGotoWaypoint(int32_t index); - void FireArrivalHooks(uint32_t index, bool atDesiredWaypoint); + void FireArrivalHooks(uint32_t index, bool atDesiredWaypoint, bool atLastWaypoint); + + // The mover: index becomes the current waypoint (the next from the path's behavior and direction), at it + void MoverProcessStateChange(uint32_t index); + // At its current waypoint, stopped; it leaves after the waypoint's wait when depart + void MoverStopAtWaypoint(bool depart); + // Leaves (or carries on) towards the next waypoint + void MoverDepart(); + void MoverArrive(); + +public: + void Update(float deltaTime) override; + + // The mover's state as a PlatformResync gives it (RequestPlatformResync) + void FillResync(GameMessages::PlatformResync& resync) const; + + // A mover (or rotater) on a path, run by the server + bool IsMover() const; + +private: /** * Whether the platform shouldn't auto start diff --git a/dGame/dGameMessages/MovementMessages.cpp b/dGame/dGameMessages/MovementMessages.cpp index ea6fa48d5..df82310dc 100644 --- a/dGame/dGameMessages/MovementMessages.cpp +++ b/dGame/dGameMessages/MovementMessages.cpp @@ -1,4 +1,5 @@ #include "MovementMessages.h" +#include "MovingPlatformComponent.h" #include "BitStreamUtils.h" #include "CppScripts.h" @@ -120,6 +121,16 @@ namespace GameMessages { } void RequestPlatformResync::Handle(Entity& entity, const SystemAddress& sysAddr) { + // A mover answers with where the server has it (live: travelling 2 with its percent and position, or 25 at a + // waypoint with the time waited) + auto* const platform = entity.GetComponent(); + if (platform && platform->IsMover()) { + PlatformResync resync; + resync.target = entity.GetObjectID(); + platform->FillResync(resync); + resync.Send(sysAddr); + return; + } if (entity.GetLOT() == 6267 || entity.GetLOT() == 16141) return; PlatformResync(entity).Send(sysAddr); } diff --git a/dZoneManager/Zone.h b/dZoneManager/Zone.h index 5ebd0bae3..9ebf57169 100644 --- a/dZoneManager/Zone.h +++ b/dZoneManager/Zone.h @@ -41,6 +41,8 @@ public: LUTriggers::Trigger* GetTrigger(uint32_t sceneID, uint32_t triggerID) const; const Path* GetPath(std::string name) const; const std::vector& GetPaths() const { return m_Paths; } + // A path that isn't in the zone's files (tests); earlier GetPath pointers may move + void AddPath(Path path) { m_Paths.push_back(std::move(path)); } uint32_t GetWorldID() const { return m_WorldID; } [[nodiscard]] std::string GetZoneName() const { return m_ZoneName; } diff --git a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp index 4dd76b692..e64df48af 100644 --- a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp @@ -9,8 +9,11 @@ #include "CDClientDatabase.h" #include "Entity.h" #include "LDFFormat.h" +#include "MovementMessages.h" #include "MovingPlatformComponent.h" #include "SimplePhysicsComponent.h" +#include "Zone.h" +#include "dZoneManager.h" namespace { float Float(const uint32_t bits) { return std::bit_cast(bits); } @@ -148,7 +151,80 @@ TEST_F(MovingPlatformComponentTest, RotaterIsWrittenAsTypeSix) { RakNet::BitStream stream; platform->Serialize(stream, true); const auto bits = Bits(stream); - // 1 (subcomponents), 0 (no path), 1 (one follows), type 6 - EXPECT_EQ(bits.substr(0, 3 + 32), "101" "00000110000000000000000000000000"); + // 1 (subcomponents), 1 (path written with the construction), 0 (no path), 1 (one follows), type 6 + EXPECT_EQ(bits.substr(0, 4 + 32), "1101" "00000110000000000000000000000000"); EXPECT_EQ(bits.back(), '0'); } + +// A mover runs on the server as live ran it (the NT movers in the live captures): stopped at its first waypoint with +// the time waited (25), travelling after the waypoint's wait (2), arriving at the other end stopped there (25, turned +// round on a bounce path), and a GoToWaypoint that stops keeps it there +TEST_F(MovingPlatformComponentTest, MoverRunsLikeLive) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestMoverPath"; + path.pathBehavior = PathBehavior::Bounce; + PathWaypoint first; + first.position = NiPoint3(0.0f, 0.0f, 0.0f); + first.speed = 5.0f; + first.movingPlatform.wait = 2.0f; + PathWaypoint second = first; + second.position = NiPoint3(10.0f, 0.0f, 0.0f); + second.movingPlatform.wait = 1.0f; + path.pathWaypoints = { first, second }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + Entity entity(288300744895900108, info); + auto* const platform = entity.AddComponent(-1, "TestMoverPath"); + ASSERT_TRUE(platform->IsMover()); + auto* const mover = platform->GetMoverSubComponent(); + EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); + EXPECT_EQ(mover->mNextWaypointIndex, 1u); + + platform->Update(1.9f); + EXPECT_EQ(mover->mState, 25u); + EXPECT_FLOAT_EQ(mover->mIdleTimeElapsed, 1.9f); + platform->Update(0.2f); + EXPECT_EQ(mover->mState, 2u); + + // 5 units a second over 10 + platform->Update(1.0f); + EXPECT_NEAR(mover->mPercentBetweenPoints, 0.5f, 0.001f); + EXPECT_NEAR(mover->mPosition.x, 5.0f, 0.01f); + GameMessages::PlatformResync resync; + platform->FillResync(resync); + EXPECT_EQ(static_cast(resync.eState), 2u); + EXPECT_EQ(resync.iIndex, 0); + EXPECT_EQ(resync.iNextIndex, 1); + EXPECT_NEAR(resync.fPercentBetweenPoints, 0.5f, 0.001f); + + platform->Update(1.1f); + EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + EXPECT_EQ(mover->mNextWaypointIndex, 0u); + EXPECT_TRUE(mover->mInReverse); + EXPECT_EQ(mover->mPercentBetweenPoints, 0.0f); + + // Sent back to the start to stop there: it leaves at once and stays + platform->GotoWaypoint(0, true); + EXPECT_EQ(mover->mState, 2u); + EXPECT_EQ(mover->mDesiredWaypointIndex, 0); + platform->Update(2.5f); + EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); + EXPECT_EQ(mover->mDesiredWaypointIndex, -1); + platform->Update(10.0f); + EXPECT_EQ(mover->mState, 25u); + + // Stopped mid way, then started again + platform->StartPathing(); + platform->Update(1.0f); + platform->StopPathing(); + EXPECT_EQ(mover->mState & 1u, 1u); + const auto percent = mover->mPercentBetweenPoints; + platform->Update(1.0f); + EXPECT_EQ(mover->mPercentBetweenPoints, percent); + platform->StartPathing(); + EXPECT_EQ(mover->mState, 2u); +}