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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 23:36:00 -05:00
parent 3e5acd42ab
commit 83f321bec6
5 changed files with 370 additions and 277 deletions

View File

@@ -14,31 +14,22 @@
#include "SimplePhysicsComponent.h"
#include "Zone.h"
#include "CDClientDatabase.h"
#include "MovementMessages.h"
#include <glm/gtc/quaternion.hpp>
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<bool>(true);
outBitStream.Write(mState);
outBitStream.Write<uint32_t>(mState);
outBitStream.Write<int32_t>(mDesiredWaypointIndex);
outBitStream.Write(mShouldStopAtDesiredWaypoint);
outBitStream.Write(mInReverse);
@@ -53,7 +44,7 @@ void MoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIniti
outBitStream.Write<uint32_t>(mNextWaypointIndex);
outBitStream.Write<float_t>(mIdleTimeElapsed);
outBitStream.Write<float_t>(0.0f); // Move time elapsed
outBitStream.Write<float_t>(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<int32_t>(u"attached_path_start") : 0;
m_StartWaypoint = start >= 0 && static_cast<size_t>(start) < m_Path->pathWaypoints.size() ? start : 0;
MoverProcessStateChange(m_StartWaypoint);
const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar<bool>(u"startPathingOnLoad");
MoverStopAtWaypoint(startOnLoad);
}
// Live constructed every moving platform with simple physics as keyframed
auto* const simplePhysics = m_Parent->GetComponent<SimplePhysicsComponent>();
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<bool>(false);
outBitStream.Write<bool>(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<bool>(true);
auto hasPath = !m_PathingStopped && !m_PathName.empty();
outBitStream.Write(hasPath);
if (hasPath) {
// Is on rail
outBitStream.Write1();
outBitStream.Write<uint16_t>(m_PathName.size());
for (const auto& c : m_PathName) {
outBitStream.Write<uint16_t>(c);
outBitStream.Write(bIsInitialUpdate);
if (bIsInitialUpdate) {
const auto hasPath = !m_PathName.empty() && m_Path != nullptr;
outBitStream.Write(hasPath);
if (hasPath) {
outBitStream.Write<uint16_t>(m_PathName.size());
for (const auto& c : m_PathName) {
outBitStream.Write<uint16_t>(c);
}
outBitStream.Write<uint32_t>(m_StartWaypoint);
outBitStream.Write<bool>(false);
}
// Starting point
outBitStream.Write<uint32_t>(0);
// Reverse
outBitStream.Write<bool>(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<MoverSubComponent*>(m_MoverSubComponent);
subComponent->mState = value;
if (!IsMover()) return;
GetMoverSubComponent()->mState = static_cast<uint32_t>(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<int32_t>(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<int32_t>(index);
mover->mShouldStopAtDesiredWaypoint = stopAtWaypoint;
auto* subComponent = static_cast<MoverSubComponent*>(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<MoverSubComponent*>(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<uint32_t>(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<MoverSubComponent*>(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<PathBehavior>(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<uint32_t>(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<MoverSubComponent*>(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<int32_t>(waypoints.size());
const auto behavior = static_cast<PathBehavior>(m_Path->pathBehavior);
const auto current = std::min<int32_t>(static_cast<int32_t>(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<uint32_t>(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<uint32_t>(m_Path->pathWaypoints.size() - 1);
const bool atEnd = arrived == last;
const bool atDesired = mover->mDesiredWaypointIndex == static_cast<int32_t>(arrived);
bool stop = false;
if (atDesired) {
stop = mover->mShouldStopAtDesiredWaypoint;
mover->mShouldStopAtDesiredWaypoint = false;
mover->mDesiredWaypointIndex = -1;
}
if ((arrived == 0 || atEnd) && static_cast<PathBehavior>(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<eMovementPlatformState>(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<int32_t>(mover->mCurrentWaypointIndex);
resync.iNextIndex = static_cast<int32_t>(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<uint32_t>(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);
}

View File

@@ -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

View File

@@ -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<MovingPlatformComponent>();
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);
}

View File

@@ -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<Path>& 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; }

View File

@@ -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<float>(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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
Entity entity(288300744895900108, info);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(-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<uint32_t>(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);
}