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); +}