From 275243cf8676aac14cfcd5e499840d41763d636a Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 13:36:25 -0600 Subject: [PATCH] feat(moving platform): SetCurrentPath puts a mover on a path LWOMovingPlatformComponent::SetCurrentPath (0x00d0f570, the SetCurrentPath game message of 0x00d0f5d0): the path, its starting waypoint and direction, then the subcomponents are made anew (SetupPlatform 0x00ccea50) and the mover takes the path at that waypoint. The path is written to the clients once more, whose component sets it the same way (Deserialize 0x00c88000), and the reverse flag of the path is no longer always written false. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/MovingPlatformComponent.cpp | 58 +++++++++++++++---- dGame/dComponents/MovingPlatformComponent.h | 12 ++++ .../MovingPlatformComponentTests.cpp | 53 +++++++++++++++++ 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 0f2bac7b5..11c9cb8b7 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -166,16 +166,10 @@ void MovingPlatformComponent::LoadConfigData() { LOG("Path not found: %s", pathName.c_str()); } - // LWOPlatform::SetupPath: a mover is at its attached_path_start waypoint, stopped (12, as live constructed movers - // nothing had started). Unless startPathingOnLoad is false it then waits the waypoint's wait and leaves (live: 9). 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"); - if (startOnLoad) MoverResumeTravelling(); + SetupMoverPath(); } else if (m_MoverSubComponentType == eMoverSubComponentType::rotater) { // LWOPlatformRotator: it only spins (rotX/rotY/rotZ at rate, its own path never arrives anywhere: its // HandleStoppedLogic 0x00bdc660 only keeps the angular velocity), while travelling @@ -188,6 +182,46 @@ void MovingPlatformComponent::LoadConfigData() { if (simplePhysics && !m_Parent->HasVar(u"motionType")) simplePhysics->SetPhysicsMotionState(SimplePhysicsComponent::MOTION_TYPE_KEYFRAMED); } +// LWOPlatform::SetupPath 0x00c17d10: a mover is at its starting waypoint, stopped (12, as live constructed movers nothing +// had started). Unless startPathingOnLoad is false it then waits the waypoint's wait and leaves (live: 9). +void MovingPlatformComponent::SetupMoverPath() { + auto* const mover = GetMoverSubComponent(); + mover->mTimeBased = m_Path->movingPlatform.timeBasedMovement != 0; + mover->mInReverse = m_PathReverse; + MoverProcessStateChange(m_StartWaypoint); + const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar(u"startPathingOnLoad"); + if (startOnLoad) MoverResumeTravelling(); +} + +// LWOMovingPlatformComponent::SetCurrentPath 0x00d0f570 (the SetCurrentPath game message, 0x00d0f5d0): the path, the +// waypoint it starts at and its direction, then SetupPlatform 0x00ccea50 makes the subcomponents anew and a mover takes +// the path. Live wrote the path to the clients with the mover (the GF shooting gallery's targets), whose +// LWOMovingPlatformComponent::Deserialize 0x00c88000 does the same there. +bool MovingPlatformComponent::SetCurrentPath(const std::string& pathName, const uint32_t startPoint, const bool reverse) { + m_PathName = GeneralUtils::ASCIIToUTF16(pathName); + m_Path = Game::zoneManager->GetZone()->GetPath(pathName); + m_PathReverse = reverse; + m_DirtyPath = true; + m_PathingStopped = false; + + for (auto& subComponent : m_SubComponents) { + if (subComponent->GetType() == eMoverSubComponentType::mover) subComponent = std::make_unique(m_Parent->GetPosition()); + else if (subComponent->GetType() == eMoverSubComponentType::rotater) subComponent = std::make_unique(m_Parent->GetPosition()); + } + + const bool mover = IsMover(); + if (mover) { + m_StartWaypoint = static_cast(startPoint) < m_Path->pathWaypoints.size() ? startPoint : 0; + SetupMoverPath(); + MoveObject(); + } else { + m_StartWaypoint = startPoint; + if (m_Path == nullptr) LOG("Cannot setup platform on path %s", pathName.c_str()); + } + Game::entityManager->SerializeEntity(m_Parent); + return mover; +} + MovingPlatformComponent::~MovingPlatformComponent() = default; void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { @@ -210,11 +244,13 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI // 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. + // construction and the mover's state with every serialization; a path set later (SetCurrentPath) is written once. outBitStream.Write(true); - outBitStream.Write(bIsInitialUpdate); - if (bIsInitialUpdate) { + const bool writePath = bIsInitialUpdate || m_DirtyPath; + outBitStream.Write(writePath); + if (writePath) { + if (!bIsInitialUpdate) m_DirtyPath = false; const auto hasPath = !m_PathName.empty() && m_Path != nullptr; outBitStream.Write(hasPath); if (hasPath) { @@ -223,7 +259,7 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI outBitStream.Write(c); } outBitStream.Write(m_StartWaypoint); - outBitStream.Write(false); + outBitStream.Write(m_PathReverse); } } diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index aae1bca82..25536a2a1 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -217,6 +217,12 @@ public: */ void GotoWaypoint(uint32_t index, bool stopAtWaypoint = true); + /** + * Puts the platform on a path of the zone, at one of its waypoints (the SetCurrentPath game message): a mover + * starts over on it. False when the platform is not a mover or the zone has no such path. + */ + bool SetCurrentPath(const std::string& pathName, uint32_t startPoint = 0, bool reverse = false); + /** * Starts the pathing of this platform, setting appropriate waypoints and speeds */ @@ -294,6 +300,10 @@ private: // The waypoint the path starts at (attached_path_start), written with the path uint32_t m_StartWaypoint = 0; + // Whether it runs the path backwards from there (SetCurrentPath), written with the path + bool m_PathReverse = false; + // The path changed since it was last written + bool m_DirtyPath = false; /** * The type of the first subcomponent, the one the server runs (none without one) @@ -320,6 +330,8 @@ private: // LWOPlatform::ProcessStateChange: index becomes the current waypoint (the next from the path's behavior and direction) void MoverProcessStateChange(uint32_t index); + // The mover at its starting waypoint on the path, started unless startPathingOnLoad is false + void SetupMoverPath(); // Puts the object where the mover is, without a serialization of its physics void MoveObject(); // LWOPlatform::SetWaitingTime: starts a wait of at least 0.1 s diff --git a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp index db210ca28..d231f02b6 100644 --- a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp @@ -337,3 +337,56 @@ TEST_F(MovingPlatformComponentTest, MoverCarriesItsObject) { EXPECT_EQ(entity.GetPosition(), NiPoint3(10.0f, 0.0f, 0.0f)); EXPECT_NEAR(std::abs(entity.GetRotation().y), 1.0f, 0.001f); } + +// SetCurrentPath (live's shooting gallery script puts its moving platform targets on their paths with it): the mover +// starts over at the path's waypoint, waiting there as live constructed the targets (9), and the path is written once +TEST_F(MovingPlatformComponentTest, SetCurrentPathPutsTheMoverOnThePath) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestTargetPath"; + path.pathBehavior = PathBehavior::Once; + PathWaypoint first; + first.position = NiPoint3(1.0f, 2.0f, 3.0f); + first.speed = 10.0f; + PathWaypoint second = first; + second.position = NiPoint3(1.0f, 2.0f, 23.0f); + PathWaypoint third = first; + third.position = NiPoint3(1.0f, 2.0f, 43.0f); + path.pathWaypoints = { first, second, third }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + Entity entity(288300744895900111, info); + entity.AddComponent(-1); + auto* const platform = entity.AddComponent(0, ""); + ASSERT_EQ(platform->GetMoverSubComponentType(), eMoverSubComponentType::mover); + EXPECT_FALSE(platform->IsMover()); + EXPECT_FALSE(platform->SetCurrentPath("NoSuchPath")); + + ASSERT_TRUE(platform->SetCurrentPath("TestTargetPath", 0)); + ASSERT_TRUE(platform->IsMover()); + auto* const mover = platform->GetMoverSubComponent(); + EXPECT_EQ(mover->mState, 9u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); + EXPECT_EQ(mover->mNextWaypointIndex, 1u); + EXPECT_EQ(entity.GetPosition(), NiPoint3(1.0f, 2.0f, 3.0f)); + + // 1 (subcomponents), 1 (the path changed), 1 (there is one), its name's length + RakNet::BitStream withPath; + platform->Serialize(withPath, false); + EXPECT_EQ(Bits(withPath).substr(0, 3 + 16), "111" "0000111000000000"); + RakNet::BitStream withoutPath; + platform->Serialize(withoutPath, false); + EXPECT_EQ(Bits(withoutPath).substr(0, 2), "10"); + + // It leaves after the shortest wait and passes the middle waypoint waiting there (9), as the live targets did + platform->Run(0.11f); + EXPECT_EQ(mover->mState, 2u); + platform->Run(2.1f); + EXPECT_EQ(mover->mState, 9u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + platform->Run(0.11f); + EXPECT_EQ(mover->mState, 2u); + platform->Run(2.1f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 2u); +}