fix(moving platform): a simple mover without a quick build sets off when it is loaded

A simple mover is a LWOPlatform like a mover: made stopped at its
waypoint (12) and, unless startPathingOnLoad is false, waiting there (9)
for the shortest wait before it leaves for its other end
(ResumeTravelling 0x00c86b90, SetWaitingTime 0x00bdc860, RunPlatform
0x00c86e10). Live showed it on the NJ counterweights, which wait for
their quick build first; one without a quick build was left waiting for
a script here.

The client only reads startPathingOnLoad (LWOMovingPlatformComponent::
LoadConfigData 0x00d0f690) and never starts a platform by itself, so the
rule is the server's, taken from what live did with movers (12 with the
setting false, else 9 and off) and from the scripts: L_NJ_SPINNER_SERVER
stops its platform when it starts up, simple movers included, and the
simple movers that wait for an event without that script all have
startPathingOnLoad 0 in the maps.

The wait now runs in the component for both, in place of the timer a
built counterweight got, and a simple mover with startPathingOnLoad
false is constructed stopped (12).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 16:49:24 -06:00
parent 4e8e0e64ef
commit 22c3168587
3 changed files with 84 additions and 14 deletions

View File

@@ -157,6 +157,18 @@ void MovingPlatformComponent::LoadConfigData() {
if (m_Parent->HasVar(u"platformMoveTime")) simpleMover->mMoveTime = m_Parent->GetVar<float>(u"platformMoveTime");
}
simpleMover->mCurrentWaypointIndex = simpleMover->mStartAtEnd ? 1 : 0;
// As a mover on its path (LWOPlatform::ResumeTravelling 0x00c86b90 and SetWaitingTime 0x00bdc860): unless
// startPathingOnLoad is false it waits at its waypoint (9), for the shortest wait as its waypoints have none.
// Live's scripts for the NJ spinners, which are simple movers that only move when used, stop them when they
// start up (L_NJ_SPINNER_SERVER: StopPathing), and the simple movers that wait for an event without such a script
// have startPathingOnLoad 0.
const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar<bool>(u"startPathingOnLoad");
if (startOnLoad) {
simpleMover->mState = ePlatformStateFlag::Waiting | ePlatformStateFlag::ReachedDesiredWaypoint;
simpleMover->mTimeToWait = 0.1f;
simpleMover->mWaitSet = true;
}
}
const auto pathName = GeneralUtils::UTF16ToWTF8(m_PathName);
@@ -313,17 +325,8 @@ void MovingPlatformComponent::OnCompleteQuickBuild() {
// about 0.1 s later travelling (2) to its other end, where it stops without a serialization and
// PlatformAtLastWaypoint tells the scripts. It is the client's LWOPlatform at work: active once its quick build
// is complete (SendMessage 0x00bdc930), it waits the shortest wait (SetWaitingTime 0x00bdc860) and sets off
// (RunPlatform 0x00c86e10).
auto* const simpleMover = GetSimpleMoverSubComponent();
simpleMover->mDirtyState = true;
const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar<bool>(u"startPathingOnLoad");
if (!startOnLoad || (simpleMover->mState & ePlatformStateFlag::Travelling) != 0) return;
const auto generation = ++m_MoveGeneration;
m_Parent->AddCallbackTimer(0.1f, [this, generation] {
if (generation != m_MoveGeneration) return;
auto* const simpleMover = GetSimpleMoverSubComponent();
if (simpleMover) SimpleMoverGotoWaypoint(simpleMover->mCurrentWaypointIndex == 0 ? 1 : 0);
});
// (RunPlatform 0x00c86e10): RunSimpleMover.
GetSimpleMoverSubComponent()->mDirtyState = true;
return;
}
@@ -694,6 +697,20 @@ NiPoint3 CalcRotatorAngularVelocity(const float rotX, const float rotY, const fl
return NiPoint3(q[0] * scale, q[1] * scale, q[2] * scale);
}
// LWOPlatform::RunPlatform 0x00c86e10 for a simple mover waiting at its waypoint: once the wait is over it sets off for
// its other end (LWOPlatform::IncrementWaitingTime 0x00bd4370, StartTravelling 0x0058b170). A quick build's does not
// run until it is built (LWOPlatform::SendMessage 0x00bdc930). Live: a built counterweight was serialized waiting (9)
// and about 0.1 s later travelling (2).
void MovingPlatformComponent::RunSimpleMover(const float deltaTime) {
auto* const simpleMover = GetSimpleMoverSubComponent();
if (!UpdateActive(*simpleMover)) return;
if ((simpleMover->mState & ePlatformStateFlag::Waiting) == 0 || !simpleMover->mWaitSet) return;
simpleMover->mIdleTimeElapsed += deltaTime;
if (simpleMover->mIdleTimeElapsed < simpleMover->mTimeToWait) return;
simpleMover->mIdleTimeElapsed = 0.0f;
SimpleMoverGotoWaypoint(simpleMover->mCurrentWaypointIndex == 0 ? 1 : 0);
}
// LWOPlatform::RunPlatform 0x00c86e10 for a rotator: its wait never ends (IncrementWaitingTime 0x004a0390) and it never
// arrives (HandleStoppedLogic 0x00bdc660 keeps the object's angular velocity at the rotator's); it has no linear
// velocity (SetLinearVelocity 0x00bd1660). The client's physics turn the object at that velocity; the server turns its
@@ -735,6 +752,10 @@ void MovingPlatformComponent::Run(const float deltaTime) {
RunRotator(deltaTime);
return;
}
if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) {
RunSimpleMover(deltaTime);
return;
}
if (!IsMover()) return;
auto* mover = GetMoverSubComponent();
UpdateActive(*mover);

View File

@@ -171,12 +171,12 @@ public:
/**
* A simple mover (5), as the client's LWOPlatformSimpleMover: between a start point and that point moved by
* platformMove (in the start rotation), taking platformMoveTime seconds, as GenerateSimpleMoverPath builds its two
* waypoint path. At an end it has stopped there (28), as any platform on a path run once.
* waypoint path. At an end it has stopped there (28), as any platform on a path run once. Like a mover it is made
* stopped at its waypoint (12) and, unless startPathingOnLoad is false, waits there (9, as live constructed the NJ
* counterweights) the shortest wait before it sets off for its other end, a quick build's once it is built.
*/
class SimpleMoverSubComponent final : public PlatformSubComponent {
public:
// Live constructed the NJ counterweights waiting at their waypoint (9)
SimpleMoverSubComponent() { mState = ePlatformStateFlag::Waiting | ePlatformStateFlag::ReachedDesiredWaypoint; }
eMoverSubComponentType GetType() const override { return eMoverSubComponentType::simpleMover; }
// LWOPlatformSimpleMover::Deserialize: a dirty bit, then whether there is a starting point with its position and
@@ -369,6 +369,8 @@ private:
void SetupMoverPath();
// Puts the object where the mover is, without a serialization of its physics
void MoveObject();
// A simple mover's wait at its waypoint before it sets off by itself
void RunSimpleMover(float deltaTime);
// A rotator's settings from the level config, and where it is on its path
void SetupRotator();
// Turns a travelling rotator's object by its angular velocity

View File

@@ -575,3 +575,50 @@ TEST_F(MovingPlatformComponentTest, MoverKeepsItsVelocityThroughAWaitLikeLive) {
EXPECT_EQ(mover->mPosition, NiPoint3(30.0f, 0.0f, 30.0f));
EXPECT_EQ(entity.GetPosition(), NiPoint3(30.0f, 0.0f, 30.0f));
}
// A simple mover without a quick build runs from when it is loaded, as a mover does: waiting at its waypoint (9) for the
// shortest wait, then off to its other end, where it has stopped (28). With startPathingOnLoad false it stays as it was
// made (12), and so does one its script stops when it starts up (live's NJ spinner script, L_NJ_SPINNER_SERVER).
TEST_F(MovingPlatformComponentTest, SimpleMoverSetsOffOnLoadUnlessToldNotTo) {
info.lot = 16141;
info.pos = NiPoint3(10.0f, 20.0f, 30.0f);
info.rot = QuatUtils::IDENTITY;
{
Entity entity(288300744895900115, info);
entity.AddComponent<SimplePhysicsComponent>(-1);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(41, "");
auto* const mover = platform->GetSimpleMoverSubComponent();
EXPECT_EQ(mover->mState, 9u);
entity.Update(0.05f);
EXPECT_EQ(mover->mState, 9u);
entity.Update(0.06f);
EXPECT_EQ(mover->mState, 2u);
entity.Update(0.0f);
entity.Update(1.1f);
EXPECT_EQ(mover->mState, 28u);
EXPECT_EQ(mover->mCurrentWaypointIndex, 1u);
// Stopped at its end it does not set off again by itself
entity.Update(1.0f);
EXPECT_EQ(mover->mState, 28u);
}
{
Entity entity(288300744895900116, info);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(41, "");
auto* const mover = platform->GetSimpleMoverSubComponent();
platform->StopPathing();
EXPECT_EQ(mover->mState, 12u);
entity.Update(1.0f);
EXPECT_EQ(mover->mState, 12u);
EXPECT_EQ(mover->mCurrentWaypointIndex, 0u);
}
{
info.settings.Insert<bool>(u"startPathingOnLoad", false);
Entity entity(288300744895900117, info);
info.settings.values.clear();
auto* const platform = entity.AddComponent<MovingPlatformComponent>(41, "");
auto* const mover = platform->GetSimpleMoverSubComponent();
EXPECT_EQ(mover->mState, 12u);
entity.Update(1.0f);
EXPECT_EQ(mover->mState, 12u);
}
}