diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 01ab84284..65de8dbe7 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -171,12 +171,9 @@ void MovingPlatformComponent::LoadConfigData() { m_StartWaypoint = start >= 0 && static_cast(start) < m_Path->pathWaypoints.size() ? start : 0; 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. Only one on a path runs - // (LWOMovingPlatformComponent::SetupPlatform 0x00ccea50), and the client reads a platform that is not at a - // waypoint from its path (LWOPlatform::Deserialize 0x00c870e0), so one without a path stays as it was made (12). - const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar(u"startPathingOnLoad"); - if (startOnLoad && m_Path != nullptr) GetMoverSubComponent()->mState = ePlatformStateFlag::Travelling; + const auto start = m_Parent->HasVar(u"attached_path_start") ? m_Parent->GetVar(u"attached_path_start") : 0; + m_StartWaypoint = start >= 0 ? start : 0; + SetupRotator(); } // Live constructed every moving platform with simple physics as keyframed @@ -195,6 +192,30 @@ void MovingPlatformComponent::SetupMoverPath() { if (startOnLoad) MoverResumeTravelling(); } +// LWOPlatformRotator: on its path it only turns (rotX/rotY/rotZ at rate; its HandleStoppedLogic 0x00bdc660 only keeps +// the angular velocity, so it never arrives anywhere), while travelling. Only one on a path runs +// (LWOMovingPlatformComponent::SetupPlatform 0x00ccea50), and the client reads a platform that is not at a waypoint +// from its path (LWOPlatform::Deserialize 0x00c870e0), so one without a path stays as it was made (12). The client's +// rotator starts turning when it is sent travelling (Deserialize: StartTravelling, ResumeTravelling, UpdateVelocities). +void MovingPlatformComponent::SetupRotator() { + auto* const rotator = static_cast(GetMoverSubComponent()); + // LWOPlatformRotator::LoadConfigData 0x00c33e10 + constexpr float pi = 3.14159265358979323846f; + if (m_Parent->HasVar(u"rotX")) rotator->mRotationX = m_Parent->GetVar(u"rotX") * pi; + if (m_Parent->HasVar(u"rotY")) rotator->mRotationY = m_Parent->GetVar(u"rotY") * pi; + if (m_Parent->HasVar(u"rotZ")) rotator->mRotationZ = m_Parent->GetVar(u"rotZ") * pi; + rotator->mRate = m_Parent->GetVar(u"rate"); + + if (m_Path == nullptr || m_Path->pathWaypoints.empty()) return; + // LWOPlatform::SetupPath 0x00c17d10 + rotator->mTimeBased = m_Path->movingPlatform.timeBasedMovement != 0; + rotator->mInReverse = m_PathReverse; + if (static_cast(m_StartWaypoint) >= m_Path->pathWaypoints.size()) m_StartWaypoint = 0; + MoverProcessStateChange(m_StartWaypoint); + const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar(u"startPathingOnLoad"); + if (startOnLoad) rotator->mState = ePlatformStateFlag::Travelling; +} + // 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 @@ -219,6 +240,7 @@ bool MovingPlatformComponent::SetCurrentPath(const std::string& pathName, const } else { m_StartWaypoint = startPoint; if (m_Path == nullptr) LOG("Cannot setup platform on path %s", pathName.c_str()); + else if (m_MoverSubComponentType == eMoverSubComponentType::rotater) SetupRotator(); } Game::entityManager->SerializeEntity(m_Parent); return mover; @@ -579,24 +601,123 @@ void MovingPlatformComponent::MoveObject() { } } -// LWOPlatform::Run 0x00c87080 and RunPlatform 0x00c86e10. The client's platform is moved by its physics, at the -// velocity SetLinearVelocity 0x00be9200 gives it; the server moves it here by the same speed. -void MovingPlatformComponent::Run(const float deltaTime) { - if (!IsMover()) return; - auto* mover = GetMoverSubComponent(); - - // LWOPlatform::SetupPath 0x00c17d10 and SendMessage 0x00bdc930 (RebuildNotifyState): a quick build's platform does not - // run until it is built, nor once the quick build is open again +// LWOPlatform::SetupPath 0x00c17d10 and SendMessage 0x00bdc930 (RebuildNotifyState): a quick build's platform does not +// run until it is built, nor once the quick build is open again +bool MovingPlatformComponent::UpdateActive(PlatformSubComponent& platform) { if (!m_CheckedQuickBuild) { m_CheckedQuickBuild = true; m_QuickBuild = m_Parent->GetComponent(); - if (m_QuickBuild) mover->mActive = false; + if (m_QuickBuild) platform.mActive = false; } if (m_QuickBuild) { const auto quickBuildState = m_QuickBuild->GetState(); - if (quickBuildState == eQuickBuildState::OPEN) mover->mActive = false; - else if (quickBuildState == eQuickBuildState::COMPLETED) mover->mActive = true; + if (quickBuildState == eQuickBuildState::OPEN) platform.mActive = false; + else if (quickBuildState == eQuickBuildState::COMPLETED) platform.mActive = true; } + return platform.mActive; +} + +NiPoint3 CalcRotatorAngularVelocity(const float rotX, const float rotY, const float rotZ, const float rate, const bool reverse) { + // NiMatrix3::MakeXRotation 0x0116c070, MakeYRotation 0x0116c020 and MakeZRotation 0x0116bfd0 + const float sx = static_cast(std::sin(static_cast(rotX))), cx = static_cast(std::cos(static_cast(rotX))); + const float sy = static_cast(std::sin(static_cast(rotY))), cy = static_cast(std::cos(static_cast(rotY))); + const float sz = static_cast(std::sin(static_cast(rotZ))), cz = static_cast(std::cos(static_cast(rotZ))); + const float x[3][3] = { { 1.0f, 0.0f, 0.0f }, { 0.0f, cx, sx }, { 0.0f, -sx, cx } }; + const float y[3][3] = { { cy, 0.0f, -sy }, { 0.0f, 1.0f, 0.0f }, { sy, 0.0f, cy } }; + const float z[3][3] = { { cz, sz, 0.0f }, { -sz, cz, 0.0f }, { 0.0f, 0.0f, 1.0f } }; + // NiMatrix3::FromEulerAnglesXYZ 0x0116c3a0: x * (y * z) + float yz[3][3]{}; + float m[3][3]{}; + for (int32_t i = 0; i < 3; i++) for (int32_t j = 0; j < 3; j++) for (int32_t k = 0; k < 3; k++) yz[i][j] += y[i][k] * z[k][j]; + for (int32_t i = 0; i < 3; i++) for (int32_t j = 0; j < 3; j++) for (int32_t k = 0; k < 3; k++) m[i][j] += x[i][k] * yz[k][j]; + + // NiQuaternion::FromRotation + float q[4]{}; // x, y, z, w + const float trace = m[0][0] + m[1][1] + m[2][2]; + if (trace > 0.0f) { + float root = std::sqrt(trace + 1.0f); + q[3] = 0.5f * root; + root = 0.5f / root; + q[0] = (m[2][1] - m[1][2]) * root; + q[1] = (m[0][2] - m[2][0]) * root; + q[2] = (m[1][0] - m[0][1]) * root; + } else { + constexpr int32_t next[3] = { 1, 2, 0 }; + int32_t i = 0; + if (m[1][1] > m[0][0]) i = 1; + if (m[2][2] > m[i][i]) i = 2; + const int32_t j = next[i]; + const int32_t k = next[j]; + float root = std::sqrt(m[i][i] - m[j][j] - m[k][k] + 1.0f); + q[i] = 0.5f * root; + root = 0.5f / root; + q[3] = (m[k][j] - m[j][k]) * root; + q[j] = (m[j][i] + m[i][j]) * root; + q[k] = (m[k][i] + m[i][k]) * root; + } + + // hkpKeyFrameUtility::KeyFrameInfo::setUsingPositionOrientationPair 0x012a4050: the rotation from the first to the + // second (the rotation itself, or its inverse in reverse), normalized; without an axis there is no velocity; else + // the axis (the other way for a negative w) times twice the arc cosine of |w| over the time + if (reverse) { + q[0] = -q[0]; + q[1] = -q[1]; + q[2] = -q[2]; + } + const float length = std::sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); + if (length <= 0.0f || rate == 0.0f) return NiPoint3Constant::ZERO; + for (auto& part : q) part /= length; + const float axisLength = std::sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2]); + if (!(axisLength > std::numeric_limits::epsilon())) return NiPoint3Constant::ZERO; + const float angle = 2.0f * std::acos(std::min(std::abs(q[3]), 1.0f)); + const float scale = (q[3] < 0.0f ? -1.0f : 1.0f) / axisLength * angle * (1.0f / (rate * 0.5f)); + return NiPoint3(q[0] * scale, q[1] * scale, q[2] * scale); +} + +// 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 +// object here, without a serialization of its physics, as a mover's. +void MovingPlatformComponent::RunRotator(const float deltaTime) { + auto* const rotator = static_cast(GetMoverSubComponent()); + if (!rotator || m_Path == nullptr || m_Path->pathWaypoints.empty()) return; + + auto rotation = m_Parent->GetRotation(); + bool moved = false; + if (!rotator->mPlaced) { + // LWOPlatform::ProcessStateChange 0x00cce380 from SetupPath: the object is put at its waypoint + rotator->mPlaced = true; + rotation = m_Path->pathWaypoints[rotator->mCurrentWaypointIndex].rotation; + moved = true; + } + if (UpdateActive(*rotator) && (rotator->mState & ePlatformStateFlag::Travelling) != 0) { + const auto velocity = rotator->GetAngularVelocity(); + const float speed = velocity.Length(); + if (speed > 0.0f && std::isfinite(speed)) { + const glm::vec3 axis(velocity.x / speed, velocity.y / speed, velocity.z / speed); + rotation = glm::normalize(glm::angleAxis(speed * deltaTime, axis) * rotation); + moved = true; + } + } + if (!moved) return; + if (auto* const simplePhysics = m_Parent->GetComponent()) { + simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(rotator->mPosition, rotation); + } + if (auto* const phantomPhysics = m_Parent->GetComponent()) { + phantomPhysics->SetPositionAndRotationIgnoringDirtyFlags(rotator->mPosition, rotation); + } +} + +// LWOPlatform::Run 0x00c87080 and RunPlatform 0x00c86e10. The client's platform is moved by its physics, at the +// velocity SetLinearVelocity 0x00be9200 gives it; the server moves it here by the same speed. +void MovingPlatformComponent::Run(const float deltaTime) { + if (m_MoverSubComponentType == eMoverSubComponentType::rotater) { + RunRotator(deltaTime); + return; + } + if (!IsMover()) return; + auto* mover = GetMoverSubComponent(); + UpdateActive(*mover); if (!mover->mActive || mover->mState == 0) return; const bool travelling = (mover->mState & ePlatformStateFlag::Travelling) != 0; diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index 59d5b9b5c..ed18d446f 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -133,13 +133,35 @@ public: }; /** - * A rotator (6), as the client's LWOPlatformRotator: it only spins (rotX/rotY/rotZ at rate) while travelling; it sends - * the LWOPlatform data a mover sends. + * The angular velocity of a rotator, as the client's LWOPlatformRotator::SetAngularVelocity 0x00bfa380 works it out: + * the rotation NiMatrix3::FromEulerAnglesXYZ 0x0116c3a0 makes of the three angles (radians; the matrix about x times + * the one about y times the one about z, each turning the other way round than the usual right-handed one), as a + * quaternion (NiQuaternion::FromRotation), and the velocity that turns from no rotation to it (from it to none in + * reverse) in half the rate, by the shortest way (hkpKeyFrameUtility::KeyFrameInfo::setUsingPositionOrientationPair + * 0x012a4050: the axis times the angle, at most half a turn, over the time). Angles that add up to whole turns give + * no velocity. The client divides by a rate of 0; here that gives none. + */ +NiPoint3 CalcRotatorAngularVelocity(float rotX, float rotY, float rotZ, float rate, bool reverse); + +/** + * A rotator (6), as the client's LWOPlatformRotator: on its path it does not move, it turns at its angular velocity + * while travelling (rotX/rotY/rotZ in half turns, at rate), and never waits or arrives (IncrementWaitingTime + * 0x004a0390, HandleStoppedLogic 0x00bdc660); it sends the LWOPlatform data a mover sends. */ class RotatorSubComponent final : public PlatformSubComponent { public: RotatorSubComponent(const NiPoint3& startPos) { mPosition = startPos; } eMoverSubComponentType GetType() const override { return eMoverSubComponentType::rotater; } + + // LWOPlatformRotator::LoadConfigData 0x00c33e10: rotX, rotY and rotZ times pi, and rate (seconds) + float mRotationX = 0.0f; + float mRotationY = 0.0f; + float mRotationZ = 0.0f; + float mRate = 0.0f; + // Whether its object was put at its waypoint (LWOPlatform::ProcessStateChange 0x00cce380, from SetupPath) + bool mPlaced = false; + + NiPoint3 GetAngularVelocity() const { return CalcRotatorAngularVelocity(mRotationX, mRotationY, mRotationZ, mRate, mInReverse); } }; /** @@ -343,6 +365,12 @@ private: void SetupMoverPath(); // Puts the object where the mover is, without a serialization of its physics void MoveObject(); + // 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 + void RunRotator(float deltaTime); + // LWOPlatform::SetupPath and SendMessage (RebuildNotifyState): whether the platform of a quick build runs yet + bool UpdateActive(PlatformSubComponent& platform); // LWOPlatform::SetWaitingTime: starts a wait of at least 0.1 s void MoverSetWaitingTime(float waitTime, float idleTimeElapsed); // LWOPlatform::ResumeTravelling: stopped between two waypoints it travels on; at a waypoint it first waits there diff --git a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp index ec0938172..8bdb504c7 100644 --- a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp @@ -434,3 +434,86 @@ TEST_F(MovingPlatformComponentTest, BuiltSimpleMoverSetsOffLikeLive) { platform->Serialize(arrived, false); EXPECT_EQ(Bits(arrived), "00"); } + +// LWOPlatformRotator::SetAngularVelocity 0x00bfa380: the velocity that turns from no rotation to the configured one in +// half the rate. The client's matrices about an axis turn the other way round than the usual ones, so a quarter turn +// about x in 1 s is -pi/2 about x; in reverse it is the other way. Whole turns have no axis and give no velocity. Half +// a turn can go either way round: the float pi is a little over pi, so the shortest way is the usual direction. +TEST_F(MovingPlatformComponentTest, RotatorAngularVelocityAsTheClient) { + constexpr float pi = 3.14159265358979323846f; + const auto quarterX = CalcRotatorAngularVelocity(0.5f * pi, 0.0f, 0.0f, 2.0f, false); + EXPECT_NEAR(quarterX.x, -pi / 2.0f, 0.0001f); + EXPECT_NEAR(quarterX.y, 0.0f, 0.0001f); + EXPECT_NEAR(quarterX.z, 0.0f, 0.0001f); + const auto quarterXReverse = CalcRotatorAngularVelocity(0.5f * pi, 0.0f, 0.0f, 2.0f, true); + EXPECT_NEAR(quarterXReverse.x, pi / 2.0f, 0.0001f); + + const auto eighthY = CalcRotatorAngularVelocity(0.0f, 0.25f * pi, 0.0f, 4.0f, false); + EXPECT_NEAR(eighthY.y, -pi / 8.0f, 0.0001f); + const auto quarterZ = CalcRotatorAngularVelocity(0.0f, 0.0f, 0.5f * pi, 1.0f, false); + EXPECT_NEAR(quarterZ.z, -pi, 0.0001f); + + // x * (y * z): a quarter turn about x and one about y is a third of a turn about (-1, -1, 1) + const auto both = CalcRotatorAngularVelocity(0.5f * pi, 0.5f * pi, 0.0f, 2.0f, false); + const float third = 2.0f * pi / 3.0f / std::sqrt(3.0f); + EXPECT_NEAR(both.x, -third, 0.0001f); + EXPECT_NEAR(both.y, -third, 0.0001f); + EXPECT_NEAR(both.z, third, 0.0001f); + + EXPECT_EQ(CalcRotatorAngularVelocity(2.0f * pi, 0.0f, 0.0f, 2.0f, false), NiPoint3Constant::ZERO); + EXPECT_EQ(CalcRotatorAngularVelocity(0.0f, 0.0f, 0.0f, 2.0f, false), NiPoint3Constant::ZERO); + EXPECT_EQ(CalcRotatorAngularVelocity(0.5f * pi, 0.0f, 0.0f, 0.0f, false), NiPoint3Constant::ZERO); + + const auto halfX = CalcRotatorAngularVelocity(pi, 0.0f, 0.0f, 2.0f, false); + EXPECT_NEAR(halfX.x, pi, 0.0001f); +} + +// A rotator on a path stays at its waypoint and turns its object at its angular velocity while it travels (rotY half a +// half turn at rate 2: a quarter turn the client's way about y each second), without a serialization of its physics; +// stopped it turns no more, started again it does +TEST_F(MovingPlatformComponentTest, RotatorTurnsItsObject) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestRotatorPath"; + path.pathBehavior = PathBehavior::Loop; + PathWaypoint first; + first.position = NiPoint3(1.0f, 2.0f, 3.0f); + PathWaypoint second = first; + second.position = NiPoint3(10.0f, 2.0f, 3.0f); + path.pathWaypoints = { first, second }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + info.pos = NiPoint3(1.0f, 2.0f, 3.0f); + info.rot = QuatUtils::IDENTITY; + info.settings.Insert(u"platformIsRotater", true); + info.settings.Insert(u"rotY", 0.5f); + info.settings.Insert(u"rate", 2.0f); + Entity entity(288300744895900113, info); + info.settings.values.clear(); + auto* const simplePhysics = entity.AddComponent(-1); + auto* const platform = entity.AddComponent(-1, "TestRotatorPath"); + ASSERT_EQ(platform->GetMoverSubComponentType(), eMoverSubComponentType::rotater); + auto* const rotator = platform->GetMoverSubComponent(); + EXPECT_EQ(rotator->mState, 2u); + + RakNet::BitStream before; + simplePhysics->Serialize(before, false); + platform->Run(1.0f); + EXPECT_EQ(entity.GetPosition(), NiPoint3(1.0f, 2.0f, 3.0f)); + EXPECT_NEAR(entity.GetRotation().w, 0.7071f, 0.001f); + EXPECT_NEAR(entity.GetRotation().y, -0.7071f, 0.001f); + RakNet::BitStream moved; + simplePhysics->Serialize(moved, false); + EXPECT_LT(moved.GetNumberOfBitsUsed(), 7u * 32u); + + platform->StopPathing(); + EXPECT_EQ(rotator->mState & 7u, 4u); + platform->Run(1.0f); + EXPECT_NEAR(entity.GetRotation().y, -0.7071f, 0.001f); + + platform->StartPathing(); + EXPECT_EQ(rotator->mState, 2u); + platform->Run(1.0f); + EXPECT_NEAR(std::abs(entity.GetRotation().y), 1.0f, 0.001f); + EXPECT_EQ(entity.GetPosition(), NiPoint3(1.0f, 2.0f, 3.0f)); +}