diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index 792139eec..44f904396 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -541,6 +541,9 @@ void MovementAIComponent::SetMaxSpeed(const float value) { void MovementAIComponent::RunWaypointCommands(uint32_t waypointNum) { m_Parent->GetScript()->OnWaypointReached(m_Parent, waypointNum); + // A callback may add another + const auto callbacks = m_WaypointReachedCallbacks; + for (const auto& callback : callbacks) callback(waypointNum); if (!m_Path || waypointNum >= m_Path->pathWaypoints.size()) return; const auto& commands = m_Path->pathWaypoints[waypointNum].commands; diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 91fce3184..d4a508a70 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -16,6 +16,7 @@ #include "eReplicaComponentType.h" #include "Zone.h" #include "dpKnockback.h" +#include #include #include @@ -188,6 +189,12 @@ public: */ bool AtFinalWaypoint() const { return m_AtFinalWaypoint; } + /** + * Calls back at each waypoint of the path the entity follows, with the waypoint's number (what the Arrived game + * message tells an object's script) + */ + void AddWaypointReachedCallback(const std::function& callback) { m_WaypointReachedCallbacks.push_back(callback); } + /** * Renders the entity stationary */ @@ -380,6 +387,8 @@ private: // The number of waypoints that were on the path in the call to SetPath uint32_t m_CurrentPathWaypointCount{ 0 }; + std::vector> m_WaypointReachedCallbacks; + LWOOBJID m_FollowedTarget{ LWOOBJID_EMPTY }; /** diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index f4b7e36c3..74f836288 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -20,6 +20,9 @@ #include "Entity.h" #include "ObjectLoadState.h" #include "MovementAIComponent.h" +#include "QuickBuildComponent.h" +#include "PhantomPhysicsComponent.h" +#include "eQuickBuildState.h" void PlatformSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { outBitStream.Write(true); @@ -154,6 +157,18 @@ void MovingPlatformComponent::LoadConfigData() { if (m_Parent->HasVar(u"platformMoveTime")) simpleMover->mMoveTime = m_Parent->GetVar(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(u"startPathingOnLoad"); + if (startOnLoad) { + simpleMover->mState = ePlatformStateFlag::Waiting | ePlatformStateFlag::ReachedDesiredWaypoint; + simpleMover->mTimeToWait = 0.1f; + simpleMover->mWaitSet = true; + } } const auto pathName = GeneralUtils::UTF16ToWTF8(m_PathName); @@ -163,21 +178,14 @@ void MovingPlatformComponent::LoadConfigData() { 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(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"); - MoverStopAtWaypoint(startOnLoad); + 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 - const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar(u"startPathingOnLoad"); - GetMoverSubComponent()->mState = startOnLoad ? ePlatformStateFlag::Travelling : ePlatformStateFlag::Stopped; + 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 @@ -185,6 +193,71 @@ 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(); +} + +// 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->GetVarAs(u"rotX") * pi; + if (m_Parent->HasVar(u"rotY")) rotator->mRotationY = m_Parent->GetVarAs(u"rotY") * pi; + if (m_Parent->HasVar(u"rotZ")) rotator->mRotationZ = m_Parent->GetVarAs(u"rotZ") * pi; + rotator->mRate = m_Parent->GetVarAs(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 +// 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()); + else if (m_MoverSubComponentType == eMoverSubComponentType::rotater) SetupRotator(); + } + Game::entityManager->SerializeEntity(m_Parent); + return mover; +} + MovingPlatformComponent::~MovingPlatformComponent() = default; void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { @@ -207,11 +280,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) { @@ -220,7 +295,7 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI outBitStream.Write(c); } outBitStream.Write(m_StartWaypoint); - outBitStream.Write(false); + outBitStream.Write(m_PathReverse); } } @@ -246,6 +321,11 @@ void MovingPlatformComponent::OnQuickBuildInitilized() { void MovingPlatformComponent::OnCompleteQuickBuild() { if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + // Live (the NJ counterweights, LOT 16141): the built simple mover is serialized waiting at its waypoint (9) and + // 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): RunSimpleMover. GetSimpleMoverSubComponent()->mDirtyState = true; return; } @@ -269,7 +349,9 @@ void MovingPlatformComponent::SetMovementState(eMovementPlatformState 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 +// Sends the mover to a waypoint, stopping there when stopAtWaypoint: it turns round if the waypoint is behind it. Live +// (the NJ falling tiles): a mover at a waypoint first waits there (9, or 25 at an end, with the desired waypoint), then +// leaves (2). void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) { if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { SimpleMoverGotoWaypoint(static_cast(index)); @@ -282,7 +364,10 @@ void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) // Already there mover->mDesiredWaypointIndex = -1; mover->mShouldStopAtDesiredWaypoint = false; - if (stopAtWaypoint) MoverStopAtWaypoint(false); + if (stopAtWaypoint) { + mover->mState = (mover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Travelling)) | ePlatformStateFlag::Stopped; + mover->mVelocity = NiPoint3Constant::ZERO; + } FireArrivalHooks(index, true, index == m_Path->pathWaypoints.size() - 1); return; } @@ -307,11 +392,14 @@ void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) mover->mNextWaypointIndex = mover->mCurrentWaypointIndex; mover->mCurrentWaypointIndex = heading; mover->mPercentBetweenPoints = 1.0f - std::min(percent, 1.0f); + mover->mSegmentLength = Vector3::Distance(m_Path->pathWaypoints[mover->mCurrentWaypointIndex].position, m_Path->pathWaypoints[mover->mNextWaypointIndex].position); + mover->mWaitTime = m_Path->pathWaypoints[mover->mCurrentWaypointIndex].movingPlatform.wait; if (mover->mTimeBased) mover->mMoveTimeElapsed = mover->mCurrentSpeed * mover->mPercentBetweenPoints; else std::swap(mover->mCurrentSpeed, mover->mNextSpeed); } } - MoverDepart(); + MoverResumeTravelling(); + Game::entityManager->SerializeEntity(m_Parent); } void MovingPlatformComponent::StartPathing() { @@ -321,7 +409,8 @@ void MovingPlatformComponent::StartPathing() { return; } if (m_MoverSubComponentType == eMoverSubComponentType::rotater) { - // LWOPlatformRotator::StartLinearMovement: spinning again + // LWOPlatformRotator::StartLinearMovement: spinning again (only on a path, as above) + if (m_Path == nullptr) return; GetMoverSubComponent()->mState = ePlatformStateFlag::Travelling; Game::entityManager->SerializeEntity(m_Parent); return; @@ -332,36 +421,34 @@ void MovingPlatformComponent::StartPathing() { if ((mover->mState & ePlatformStateFlag::Travelling) != 0) return; // LWOPlatform::ResumeTravelling: stopped between two waypoints it carries on; at a waypoint it first waits there // for the waypoint's wait (issue 964: the AG monument elevator waited about 2 s before rising) - if ((mover->mState & ePlatformStateFlag::ReachedDesiredWaypoint) != 0) { - MoverStopAtWaypoint(true); - Game::entityManager->SerializeEntity(m_Parent); - return; - } - MoverDepart(); + MoverResumeTravelling(); + Game::entityManager->SerializeEntity(m_Parent); } void MovingPlatformComponent::StopPathing() { if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { - // LWOPlatformSimpleMover::StopPlatform: it stops where it is, no longer travelling + // LWOPlatformSimpleMover::StopPlatform 0x00cce1e0: at an end it is stopped; on its way it goes on to the end it + // was heading for, where its path (run once) stops it anyway auto* const simpleMover = GetSimpleMoverSubComponent(); - m_MoveGeneration++; - simpleMover->mState = ePlatformStateFlag::Stopped; + if ((simpleMover->mState & ePlatformStateFlag::ReachedDesiredWaypoint) == 0) return; + simpleMover->mState = (simpleMover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Travelling)) | ePlatformStateFlag::Stopped; simpleMover->mDirtyState = true; Game::entityManager->SerializeEntity(m_Parent); return; } if (m_MoverSubComponentType == eMoverSubComponentType::rotater) { // LWOPlatformRotator::StopPlatform: no longer spinning - GetMoverSubComponent()->mState = ePlatformStateFlag::Stopped; + auto* const rotator = GetMoverSubComponent(); + rotator->mState = (rotator->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Travelling)) | ePlatformStateFlag::Stopped; Game::entityManager->SerializeEntity(m_Parent); return; } if (!IsMover()) return; m_PathingStopped = true; 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; + // LWOPlatform::StopPlatform 0x00c86b70: stopped where it is (the client stops on the stopped bit, 4) + mover->mState = (mover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Travelling)) | ePlatformStateFlag::Stopped; + mover->mVelocity = NiPoint3Constant::ZERO; mover->mDesiredWaypointIndex = -1; mover->mShouldStopAtDesiredWaypoint = false; Game::entityManager->SerializeEntity(m_Parent); @@ -404,6 +491,7 @@ void MovingPlatformComponent::MoverProcessStateChange(const uint32_t index) { mover->mCurrentWaypointIndex = current; mover->mNextWaypointIndex = next; + mover->mWaitTime = waypoints[current].movingPlatform.wait; mover->mPercentBetweenPoints = 0.0f; mover->mMoveTimeElapsed = 0.0f; mover->mPosition = waypoints[current].position; @@ -419,75 +507,312 @@ void MovingPlatformComponent::MoverProcessStateChange(const uint32_t index) { } } -void MovingPlatformComponent::MoverStopAtWaypoint(const bool depart) { +void MovingPlatformComponent::MoverSetWaitingTime(const float waitTime, const float idleTimeElapsed) { auto* const mover = GetMoverSubComponent(); - const auto last = static_cast(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; + mover->mTimeToWait = std::max(0.1f, waitTime); + mover->mIdleTimeElapsed = idleTimeElapsed; + mover->mWaitSet = true; + // Only at an end of the path, or stopped, does the object lose its velocities here. Waiting at a waypoint in between + // it keeps them: live's platforms went on as they came for the wait (the GF shooting gallery subs 2 units past each + // waypoint, the FV dragons 5.33; a dragon met 2.2 s into a 5 s wait was 88 past its waypoint). + if ((mover->mState & (ePlatformStateFlag::ReachedFinalDestination | ePlatformStateFlag::Stopped)) != 0) mover->mVelocity = NiPoint3Constant::ZERO; } -void MovingPlatformComponent::MoverDepart() { +void MovingPlatformComponent::MoverUpdateVelocities() { auto* const mover = GetMoverSubComponent(); - mover->mState = ePlatformStateFlag::Travelling; - mover->mIdleTimeElapsed = 0.0f; - mover->mDepartAfterWait = false; - Game::entityManager->SerializeEntity(m_Parent); + auto direction = m_Path->pathWaypoints[mover->mNextWaypointIndex].position - m_Path->pathWaypoints[mover->mCurrentWaypointIndex].position; + const float distance = direction.Length(); + if (distance > 0.0f) direction = direction / distance; + // LWOPlatform::GetSpeed 0x00bd40e0: a time based path's mCurrentSpeed is the seconds to the next waypoint; else from + // the waypoint's speed to the next one's along the way + float speed{}; + if (mover->mTimeBased) speed = mover->mCurrentSpeed > 0.0f ? distance / mover->mCurrentSpeed : 0.0f; + else speed = mover->mCurrentSpeed + (mover->mNextSpeed - mover->mCurrentSpeed) * mover->mPercentBetweenPoints; + mover->mVelocity = direction * speed; + mover->mState &= ePlatformStateFlag::MovementMask; } -// 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() { +void MovingPlatformComponent::MoverResumeTravelling() { auto* const mover = GetMoverSubComponent(); + if ((mover->mState & ePlatformStateFlag::Stopped) != 0 && (mover->mState & ePlatformStateFlag::ReachedDesiredWaypoint) == 0) { + // LWOPlatform::StartTravelling 0x0058b170 + mover->mState = (mover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Stopped)) | ePlatformStateFlag::Travelling; + } + if ((mover->mState & ePlatformStateFlag::Travelling) != 0) { + MoverUpdateVelocities(); + } else { + mover->mState = (mover->mState & ~(ePlatformStateFlag::Travelling | ePlatformStateFlag::Stopped)) | ePlatformStateFlag::Waiting; + MoverSetWaitingTime(mover->mWaitTime, 0.0f); + } +} + +void MovingPlatformComponent::MoverArrivedAtWaypoint() { + auto* const mover = GetMoverSubComponent(); + // A platform with nowhere to go (one waypoint, or at the end of a path run once) is not arriving + if (mover->mCurrentWaypointIndex == mover->mNextWaypointIndex) return; + + const auto& next = m_Path->pathWaypoints[mover->mNextWaypointIndex].position; + bool closeEnough{}; + if (mover->mTimeBased) { + // LWOPlatform::CheckEnoughTimeElapsedAtWaypoint 0x00bdc720 + closeEnough = std::abs(mover->mCurrentSpeed - mover->mMoveTimeElapsed) < 0.02f; + } else { + // LWOPlatform::CloseToNextWaypoint 0x00bd4140: within 0.01 of it, or past it + const auto offset = next - mover->mPosition; + const auto direction = next - m_Path->pathWaypoints[mover->mCurrentWaypointIndex].position; + closeEnough = offset.SquaredLength() <= 0.0001f || direction.DotProduct(offset) <= 0.0f; + } + if (!closeEnough) return; + const auto arrived = mover->mNextWaypointIndex; - const auto last = static_cast(m_Path->pathWaypoints.size() - 1); - const bool atEnd = arrived == last; - const bool atDesired = mover->mDesiredWaypointIndex == static_cast(arrived); - bool stop = false; - if (atDesired) { - stop = mover->mShouldStopAtDesiredWaypoint; + const bool atLastWaypoint = arrived == static_cast(m_Path->pathWaypoints.size() - 1); + bool atDesiredWaypoint = false; + bool stopAtDesiredWaypoint = false; + bool turnedAtEnd = false; + + mover->mState = ePlatformStateFlag::ReachedDesiredWaypoint; + if (mover->mDesiredWaypointIndex == static_cast(arrived)) { + atDesiredWaypoint = true; + stopAtDesiredWaypoint = mover->mShouldStopAtDesiredWaypoint; mover->mShouldStopAtDesiredWaypoint = false; mover->mDesiredWaypointIndex = -1; } - if ((arrived == 0 || atEnd) && static_cast(m_Path->pathBehavior) == PathBehavior::Once) { - stop = true; - mover->mInReverse = !mover->mInReverse; + if (arrived == 0 || atLastWaypoint) { + mover->mState |= ePlatformStateFlag::ReachedFinalDestination; + if (static_cast(m_Path->pathBehavior) == PathBehavior::Once) { + turnedAtEnd = true; + mover->mInReverse = !mover->mInReverse; + } + } + if (stopAtDesiredWaypoint || turnedAtEnd) { + mover->mState = (mover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Travelling)) | ePlatformStateFlag::Stopped; + mover->mVelocity = NiPoint3Constant::ZERO; + } else { + mover->mState = (mover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Stopped)) | ePlatformStateFlag::Travelling; } MoverProcessStateChange(arrived); - MoverStopAtWaypoint(!stop); - Game::entityManager->SerializeEntity(m_Parent); - FireArrivalHooks(arrived, atDesired, atEnd); + MoverHandleWaypointArrived(arrived, atDesiredWaypoint, atLastWaypoint); } -void MovingPlatformComponent::Run(const float deltaTime) { - if (!IsMover()) return; +void MovingPlatformComponent::MoverHandleWaypointArrived(const uint32_t index, const bool atDesiredWaypoint, const bool atLastWaypoint) { + FireArrivalHooks(index, atDesiredWaypoint, atLastWaypoint); + + // The scripts may have taken the platform or its object away auto* const mover = GetMoverSubComponent(); + if (!mover || !IsMover()) return; 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; + mover->mState = (mover->mState & ~(ePlatformStateFlag::Travelling | ePlatformStateFlag::Stopped)) | ePlatformStateFlag::Waiting; + MoverSetWaitingTime(mover->mWaitTime, 0.0f); } + // Live serialized a platform that waits at the waypoint it arrived at (it leaves again), not one that stopped there + if ((mover->mState & ePlatformStateFlag::Stopped) == 0) Game::entityManager->SerializeEntity(m_Parent); +} + +// LWOPlatform moves its object with SetPosition and SetRotation ignoring the dirty flags (ProcessStateChange 0x00cce380, +// Resync 0x00be9310), and its physics carry it between waypoints: live did not serialize a mover's physics for it, each +// client moves its own. The server's object goes with the mover, turned between the two waypoints' rotations. +void MovingPlatformComponent::MoveObject() { + const auto* const mover = GetMoverSubComponent(); + const auto& from = m_Path->pathWaypoints[mover->mCurrentWaypointIndex].rotation; + const auto& to = m_Path->pathWaypoints[mover->mNextWaypointIndex].rotation; + const auto rotation = from == to ? from : glm::slerp(from, to, std::min(mover->mPercentBetweenPoints, 1.0f)); + if (auto* const simplePhysics = m_Parent->GetComponent()) { + simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(mover->mPosition, rotation); + } + if (auto* const phantomPhysics = m_Parent->GetComponent()) { + phantomPhysics->SetPositionAndRotationIgnoringDirtyFlags(mover->mPosition, rotation); + } +} + +// 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) platform.mActive = false; + } + if (m_QuickBuild) { + const auto quickBuildState = m_QuickBuild->GetState(); + 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 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 +// 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 (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + RunSimpleMover(deltaTime); + return; + } + if (!IsMover()) return; + auto* mover = GetMoverSubComponent(); + UpdateActive(*mover); + if (!mover->mActive) { + // RebuildNotifyState: an open quick build's platform loses its velocities + mover->mVelocity = NiPoint3Constant::ZERO; + return; + } + if (mover->mState == 0) return; + + const bool travelling = (mover->mState & ePlatformStateFlag::Travelling) != 0; + if (mover->mTimeBased && travelling) mover->mMoveTimeElapsed += deltaTime; + // The object's physics: it goes at its velocity, travelling or not + mover->mPosition = mover->mPosition + mover->mVelocity * deltaTime; + + if (mover->mTimeBased) { + // LWOPlatform::TimeBasedCalculatePercentToNextWaypoint 0x00bd4090: mCurrentSpeed is the seconds to the next waypoint + mover->mMoveTimeElapsed = std::min(mover->mMoveTimeElapsed, mover->mCurrentSpeed); + mover->mPercentBetweenPoints = mover->mCurrentSpeed > 0.0f ? mover->mMoveTimeElapsed / mover->mCurrentSpeed : 1.0f; + } else { + // LWOPlatform::CalculatePercentTravelledToWaypoint 0x00be9050: how far the object is from the current waypoint, + // over the way to the next (live sent 1.219 for a dragon that far past its waypoint) + const auto& from = m_Path->pathWaypoints[mover->mCurrentWaypointIndex].position; + mover->mPercentBetweenPoints = mover->mSegmentLength > 0.0f ? Vector3::Distance(from, mover->mPosition) / mover->mSegmentLength : 0.0f; + } + + MoveObject(); + + const bool stopped = (mover->mState & ePlatformStateFlag::Stopped) != 0; + // LWOPlatform::IncrementWaitingTime 0x00bd4370 + bool doneWaiting = false; + if ((mover->mState & ePlatformStateFlag::Waiting) != 0 && mover->mWaitSet) { + mover->mIdleTimeElapsed += deltaTime; + if (mover->mIdleTimeElapsed >= mover->mTimeToWait) { + mover->mIdleTimeElapsed = 0.0f; + doneWaiting = true; + } + } + if (doneWaiting) { + mover->mState = (mover->mState & ~(ePlatformStateFlag::Waiting | ePlatformStateFlag::Stopped)) | ePlatformStateFlag::Travelling; + } else if (!stopped && (travelling || mover->mSegmentLength > 0.0f)) { + // LWOPlatform::HandleStoppedLogic 0x00bd16a0: a platform that has not stopped looks whether it arrived, also while + // it waits. Live's FV dragons, going on as they came through the 5 s wait at their waypoint 4, were at waypoint 5 + // (waiting there, then going on in the direction they came to 4 in) without ever having left 4. Where two + // waypoints are in one place the client arrives every frame of the wait; here only a travelling one arrives there. + MoverArrivedAtWaypoint(); + // A script told of the arrival may have put the platform on another path + mover = GetMoverSubComponent(); + if (!IsMover()) return; + } + + if ((mover->mState & ePlatformStateFlag::Travelling) != 0 && (doneWaiting || mover->mCurrentSpeed != mover->mNextSpeed)) { + MoverUpdateVelocities(); + // Live serialized a platform leaving its waypoint, where it then was: as far past the waypoint as the wait took it + if (doneWaiting) Game::entityManager->SerializeEntity(m_Parent); + } + if (IsMover()) MoveObject(); } void MovingPlatformComponent::FillResync(GameMessages::PlatformResync& resync) const { @@ -535,7 +860,10 @@ void MovingPlatformComponent::WarpToWaypoint(size_t index) { if (!IsMover() || index >= m_Path->pathWaypoints.size()) return; const auto& waypoint = m_Path->pathWaypoints[index]; MoverProcessStateChange(static_cast(index)); - MoverStopAtWaypoint(false); + auto* const mover = GetMoverSubComponent(); + const bool atEnd = index == 0 || index == m_Path->pathWaypoints.size() - 1; + mover->mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint | (atEnd ? ePlatformStateFlag::ReachedFinalDestination : 0); + mover->mVelocity = NiPoint3Constant::ZERO; m_Parent->SetPosition(waypoint.position); m_Parent->SetRotation(waypoint.rotation); @@ -582,13 +910,19 @@ void MovingPlatformComponent::OnSimpleMoverArrived(const int32_t index) { auto* const simpleMover = GetSimpleMoverSubComponent(); if (!simpleMover) return; - // LWOPlatform::ArrivedAtWaypoint: both ends are the path's ends, and a path run once stops there + // LWOPlatform::ArrivedAtWaypoint: both ends are the path's ends, and a path run once stops there (28) and turns + // round. Live did not serialize it (each client's simple mover arrives by itself), and the object is moved ignoring + // the dirty flags (ProcessStateChange 0x00cce380). simpleMover->mCurrentWaypointIndex = index; simpleMover->mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint | ePlatformStateFlag::ReachedFinalDestination; simpleMover->mInReverse = index == 1; - simpleMover->mDirtyState = true; - m_Parent->SetPosition(simpleMover->GetWaypointPosition(index)); - Game::entityManager->SerializeEntity(m_Parent); + const auto position = simpleMover->GetWaypointPosition(index); + if (auto* const simplePhysics = m_Parent->GetComponent()) { + simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(position, simpleMover->mStartRotation); + } + if (auto* const phantomPhysics = m_Parent->GetComponent()) { + phantomPhysics->SetPositionAndRotationIgnoringDirtyFlags(position, simpleMover->mStartRotation); + } FireArrivalHooks(index, true, index == 1); } @@ -599,7 +933,16 @@ void MovingPlatformComponent::FireArrivalHooks(const uint32_t index, const bool auto* const script = m_Parent->GetScript(); script->OnWaypointReached(m_Parent, index); if (atDesiredWaypoint) script->OnArrivedAtDesiredWaypoint(m_Parent, index); - if (atLastWaypoint) script->OnPlatformAtLastWaypoint(m_Parent); + if (atLastWaypoint) { + script->OnPlatformAtLastWaypoint(m_Parent); + // A callback may add another + const auto callbacks = m_AtLastWaypointCallbacks; + for (const auto& callback : callbacks) callback(); + } +} + +void MovingPlatformComponent::AddPlatformAtLastWaypointCallback(const std::function& callback) { + m_AtLastWaypointCallbacks.push_back(callback); } void MovingPlatformComponent::Allocator(Entity& entity, ObjectLoadState& state) { diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index f732b9bea..6eb4e364e 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -6,6 +6,7 @@ #ifndef MOVINGPLATFORMCOMPONENT_H #define MOVINGPLATFORMCOMPONENT_H +#include #include #include @@ -48,14 +49,18 @@ enum class eMoverSubComponentType : uint32_t { }; /** - * The client's platform state flags (LWOPlatform) + * The client's platform state flags (LWOPlatform), as live sent them: a platform that was never started is stopped at + * its waypoint (12), one waiting out a waypoint's wait is 9 (25 at either end of its path), one on its way is 2 and + * one that stopped at an end of a path run once is 28. */ namespace ePlatformStateFlag { - constexpr uint32_t Stopped = 1; + constexpr uint32_t Waiting = 1; constexpr uint32_t Travelling = 2; - constexpr uint32_t Waiting = 4; + constexpr uint32_t Stopped = 4; constexpr uint32_t ReachedDesiredWaypoint = 8; constexpr uint32_t ReachedFinalDestination = 16; + // What LWOPlatform::UpdateVelocities 0x00bd4330 keeps when the platform sets off (it drops the two "reached" bits) + constexpr uint32_t MovementMask = Waiting | Travelling | Stopped; }; /** @@ -90,15 +95,24 @@ public: bool mShouldStopAtDesiredWaypoint = false; // From the current waypoint (0) to the next (1) float mPercentBetweenPoints = 0; - // Where it is now (current waypoint + percent towards the next) + // Where it is now: its object's position, which its linear velocity moves NiPoint3 mPosition{}; + // The linear velocity LWOPlatform::SetLinearVelocity 0x00be9200 gave its object when it last set off (or last looked, + // between waypoints of different speeds). It is only taken away where the platform stops and at either end of the + // path (SetWaitingTime 0x00bdc860), so through a wait at a waypoint in between the object goes on as it came. + NiPoint3 mVelocity{}; 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) + // Time spent at the waypoint, and how long it waits there (LWOPlatform::SetWaitingTime 0x00bdc860: the waypoint's + // wait, at least 0.1 s); the wait only counts once it was set float mIdleTimeElapsed = 0; float mTimeToWait = 0; - // Whether it leaves when the wait is over (false: stopped until told otherwise) - bool mDepartAfterWait = false; + bool mWaitSet = false; + // The current waypoint's wait + float mWaitTime = 0; + // Whether it runs: a quick build's platform does not until it is built, nor while the quick build is open again + // (LWOPlatform::SendMessage 0x00bdc930, RebuildNotifyState) + bool mActive = true; // 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 @@ -111,11 +125,10 @@ public: /** * A mover along its path (4), as the client's LWOPlatformMover, 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). + * LWOPlatform logic (RunPlatform 0x00c86e10, ArrivedAtWaypoint 0x00c86c30, HandleWaypointArrived 0x00c17bc0, + * ResumeTravelling 0x00c86b90, ProcessStateChange 0x00cce380, GetNextWaypoint 0x00bd4490 / GetNextReversedWaypoint + * 0x00bd4420, the speeds of GetWaypointSpeed 0x00be90d0). Live serialized it when it began waiting at a waypoint and + * when it left one, not when it stopped (the client stops by itself). */ class MoverSubComponent final : public PlatformSubComponent { public: @@ -124,19 +137,43 @@ 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); } }; /** * 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. Live constructed simple movers stopped at their desired waypoint. + * 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: @@ -207,6 +244,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 */ @@ -259,6 +302,12 @@ public: eMoverSubComponentType GetMoverSubComponentType() const { return m_MoverSubComponentType; } + /** + * Calls back whenever the platform is at the last waypoint of its path (the PlatformAtLastWaypoint game message, + * which live's scripts ask to be notified of on the NJ counterweights) + */ + void AddPlatformAtLastWaypointCallback(const std::function& callback); + /** * Called when a simple mover gets to the waypoint it was sent to: the client's LWOPlatform::ArrivedAtWaypoint * and HandleWaypointArrived (Arrived, then ArrivedAtDesiredWaypoint and PlatformAtLastWaypoint) @@ -284,6 +333,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) @@ -295,6 +348,12 @@ private: */ std::vector> m_SubComponents; + std::vector> m_AtLastWaypointCallbacks; + + // The object's quick build, looked up when the platform first runs + class QuickBuildComponent* m_QuickBuild = nullptr; + bool m_CheckedQuickBuild = false; + /** * Counts sends so an arrival timer from an older send does nothing */ @@ -304,13 +363,30 @@ private: 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 + // LWOPlatform::ProcessStateChange: index becomes the current waypoint (the next from the path's behavior and direction) 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(); + // 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(); + // 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 + 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::UpdateVelocities 0x00bd4330: the velocity towards the next waypoint at the speed for where it is + void MoverUpdateVelocities(); + // LWOPlatform::ResumeTravelling: stopped between two waypoints it travels on; at a waypoint it first waits there + void MoverResumeTravelling(); + // LWOPlatform::ArrivedAtWaypoint: once it is at its next waypoint + void MoverArrivedAtWaypoint(); + // LWOPlatform::HandleWaypointArrived: tells the scripts, then a platform that travels on waits at the waypoint + void MoverHandleWaypointArrived(uint32_t index, bool atDesiredWaypoint, bool atLastWaypoint); public: void Run(float deltaTime) override; diff --git a/dGame/dComponents/Physics/PhysicsComponent.h b/dGame/dComponents/Physics/PhysicsComponent.h index da10e3146..df7a5ceeb 100644 --- a/dGame/dComponents/Physics/PhysicsComponent.h +++ b/dGame/dComponents/Physics/PhysicsComponent.h @@ -37,6 +37,14 @@ public: const NiQuaternion& GetRotation() const { return m_Rotation; } virtual void SetRotation(const NiQuaternion& rot) { if (m_Rotation == rot) return; m_Rotation = rot; m_DirtyPosition = true; } + // As the client's SetPosition and SetRotation with bIgnoreDirtyFlags: moved without being serialized for it + void SetPositionAndRotationIgnoringDirtyFlags(const NiPoint3& pos, const NiQuaternion& rot) { + const bool dirty = m_DirtyPosition; + SetPosition(pos); + SetRotation(rot); + m_DirtyPosition = dirty; + } + int32_t GetCollisionGroup() const noexcept { return m_CollisionGroup; } void SetCollisionGroup(int32_t group) noexcept { m_CollisionGroup = group; } diff --git a/dGame/dComponents/SwitchComponent.cpp b/dGame/dComponents/SwitchComponent.cpp index 7c38fba11..205491cfc 100644 --- a/dGame/dComponents/SwitchComponent.cpp +++ b/dGame/dComponents/SwitchComponent.cpp @@ -77,6 +77,7 @@ void SwitchComponent::EntityEnter(Entity* entity) { m_Active = true; if (!m_Parent) return; m_Parent->TriggerEvent(eTriggerEventType::ACTIVATED, entity); + for (const auto& callback : std::vector(m_ActivationCallbacks)) callback(true); const auto grpName = m_Parent->GetVarAsString(u"grp_name"); @@ -114,6 +115,7 @@ void SwitchComponent::Run(float deltaTime) { m_Active = false; if (!m_Parent) return; m_Parent->TriggerEvent(eTriggerEventType::DEACTIVATED, m_Parent); + for (const auto& callback : std::vector(m_ActivationCallbacks)) callback(false); const auto grpName = m_Parent->GetVarAsString(u"grp_name"); diff --git a/dGame/dComponents/SwitchComponent.h b/dGame/dComponents/SwitchComponent.h index 88763647e..b7d9aa0cb 100644 --- a/dGame/dComponents/SwitchComponent.h +++ b/dGame/dComponents/SwitchComponent.h @@ -8,6 +8,7 @@ #include "QuickBuildComponent.h" #include "BouncerComponent.h" #include +#include #include "Component.h" #include "eReplicaComponentType.h" @@ -72,6 +73,12 @@ public: */ static SwitchComponent* GetClosestSwitch(NiPoint3 position); + /** + * Calls back when the switch is activated (true) and when it is deactivated (false): what an object's script asks + * a switch for with a notification request for ObjectActivated and ObjectDeactivated + */ + void AddActivationCallback(const std::function& callback) { m_ActivationCallbacks.push_back(callback); } + const std::vector& GetFactionsToRespondTo() const { return m_FactionsToRespondTo; } @@ -113,6 +120,8 @@ private: BouncerComponent* m_PetBouncer = nullptr; std::vector m_FactionsToRespondTo{}; + + std::vector> m_ActivationCallbacks; }; #endif // SWITCHCOMPONENT_H diff --git a/dGame/dGameMessages/MovementMessages.cpp b/dGame/dGameMessages/MovementMessages.cpp index cede9c002..2056430fd 100644 --- a/dGame/dGameMessages/MovementMessages.cpp +++ b/dGame/dGameMessages/MovementMessages.cpp @@ -50,26 +50,6 @@ namespace GameMessages { return true; } - PlatformResync::PlatformResync(const Entity& entity, bool bStopAtDesiredWaypoint, int32_t iIndex, int32_t iDesiredWaypointIndex, - int32_t nextIndex, eMovementPlatformState movementState, bool special) : PlatformResync() { - const auto lot = entity.GetLOT(); - - if (lot == 12341 || lot == 5027 || lot == 5028 || lot == 14335 || lot == 14447 || lot == 14449 || lot == 11306 || lot == 11308 || lot == 9483) { - iDesiredWaypointIndex = (lot == 11306 || lot == 11308) ? 1 : 0; - iIndex = lot == 9483 ? 1 : 0; - nextIndex = lot == 9483 && !special ? 1 : 0; - bStopAtDesiredWaypoint = true; - movementState = lot == 9483 && !special ? eMovementPlatformState::Stopped : eMovementPlatformState::Stationary; - } - - target = entity.GetObjectID(); - this->bStopAtDesiredWaypoint = bStopAtDesiredWaypoint; - this->iIndex = iIndex; - this->iDesiredWaypointIndex = iDesiredWaypointIndex; - this->iNextIndex = nextIndex; - this->eState = movementState; - } - void PlatformResync::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(bReverse); bitStream.Write(bStopAtDesiredWaypoint); @@ -121,21 +101,18 @@ 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) + // A mover answers with where the server has it (live: travelling 2 with its percent and position, or waiting at + // a waypoint with the time waited) auto* const platform = entity.GetComponent(); - if (platform && platform->IsMover()) { - PlatformResync resync; - resync.target = entity.GetObjectID(); - platform->FillResync(resync); - resync.Send(sysAddr); + if (!platform || !platform->IsMover()) { + // Nothing else has waypoints to resync to: the client's simple mover reads them from its own two point path + // (LWOPlatformSimpleMover::Resync 0x00bf9ec0, a crash without one) and would set off on a travelling state return; } - // A platform without a path has no waypoints to resync to; the client's simple mover reads them anyway and - // crashes (LWOPlatformSimpleMover::Resync 0x00bf9ec0 -> Path::GetWaypointAtIndex on a null path) - if (platform && !platform->HasPath()) return; - if (entity.GetLOT() == 6267 || entity.GetLOT() == 16141) return; - PlatformResync(entity).Send(sysAddr); + PlatformResync resync; + resync.target = entity.GetObjectID(); + platform->FillResync(resync); + resync.Send(sysAddr); } void OrientToAngle::Serialize(RakNet::BitStream& bitStream) const { diff --git a/dGame/dGameMessages/MovementMessages.h b/dGame/dGameMessages/MovementMessages.h index a1a8524e4..11ad03097 100644 --- a/dGame/dGameMessages/MovementMessages.h +++ b/dGame/dGameMessages/MovementMessages.h @@ -49,11 +49,6 @@ namespace GameMessages { // Server -> client, to one client (UNASSIGNED broadcasts). struct PlatformResync : public NetGameMsg { PlatformResync() : NetGameMsg(MessageType::Game::PLATFORM_RESYNC) {} - // What DLU sends for a platform entity: the arguments, overridden for a few LOTs whose paths only have a - // start and an end (special is for the FV tree platform). - PlatformResync(const Entity& entity, bool bStopAtDesiredWaypoint = false, - int32_t iIndex = 0, int32_t iDesiredWaypointIndex = 1, int32_t nextIndex = 1, - eMovementPlatformState movementState = eMovementPlatformState::Moving, bool special = false); void Serialize(RakNet::BitStream& bitStream) const override; bool Deserialize(RakNet::BitStream& bitStream) override; diff --git a/dScripts/02_server/Map/General/CMakeLists.txt b/dScripts/02_server/Map/General/CMakeLists.txt index dc32d53c5..cb07588fe 100644 --- a/dScripts/02_server/Map/General/CMakeLists.txt +++ b/dScripts/02_server/Map/General/CMakeLists.txt @@ -15,6 +15,8 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_GENERAL "PropertyPlatform.cpp" "QbEnemyStunner.cpp" "QbSpawner.cpp" + "SimpleMoverSwitch.cpp" + "SimpleOnceOnlyMoverSpinnerEvent.cpp" "StoryBoxInteractServer.cpp" "TokenConsoleServer.cpp" "TouchMissionUpdateServer.cpp" diff --git a/dScripts/02_server/Map/General/SimpleMoverSwitch.cpp b/dScripts/02_server/Map/General/SimpleMoverSwitch.cpp new file mode 100644 index 000000000..868993735 --- /dev/null +++ b/dScripts/02_server/Map/General/SimpleMoverSwitch.cpp @@ -0,0 +1,37 @@ +#include "SimpleMoverSwitch.h" + +#include "Entity.h" +#include "EntityManager.h" +#include "MovingPlatformComponent.h" +#include "SwitchComponent.h" + +void SimpleMoverSwitch::OnStartup(Entity* self) { + RequestSwitches(self); +} + +// switchStartup: every switch in the group is asked for its ObjectActivated (and from then on, in turn, its +// ObjectDeactivated and ObjectActivated). A switch loaded after the platform tells its group so in live +// (objectTypeLoaded) and is asked then; here the platform looks again until it has found one. +void SimpleMoverSwitch::RequestSwitches(Entity* self) { + const auto group = self->GetVarAsString(u"switchGroup"); + if (group.empty()) return; + + bool found = false; + for (auto* const object : Game::entityManager->GetEntitiesInGroup(group)) { + if (object == self) continue; + auto* const switchComponent = object->GetComponent(); + if (!switchComponent) continue; + found = true; + switchComponent->AddActivationCallback([platformID = self->GetObjectID()](const bool active) { + auto* const platformObject = Game::entityManager->GetEntity(platformID); + if (!platformObject) return; + // notifyObjectActivated: GoToWaypoint{ iPathIndex = 1 }; notifyObjectDeactivated: GoToWaypoint{ iPathIndex = 0 } + if (auto* const platform = platformObject->GetComponent()) platform->GotoWaypoint(active ? 1 : 0); + }); + } + if (!found) self->AddTimer("FindSwitch", 1.0f); +} + +void SimpleMoverSwitch::OnTimerDone(Entity* self, std::string timerName) { + if (timerName == "FindSwitch") RequestSwitches(self); +} diff --git a/dScripts/02_server/Map/General/SimpleMoverSwitch.h b/dScripts/02_server/Map/General/SimpleMoverSwitch.h new file mode 100644 index 000000000..7ac01bf44 --- /dev/null +++ b/dScripts/02_server/Map/General/SimpleMoverSwitch.h @@ -0,0 +1,13 @@ +#pragma once +#include "CppScripts.h" + +// L_SIMPLE_MOVER_SWITCH.lua with L_BASE_SWITCH_ACTIVATED_SERVER.lua: a moving platform that a switch sends to and fro +// (the NJ monastery's ice and lightning attic puzzles). It asks the switches in its switchGroup to tell it when they +// are activated and deactivated, and goes to waypoint 1 on the one and back to waypoint 0 on the other. +class SimpleMoverSwitch : public CppScripts::Script { +public: + void OnStartup(Entity* self) override; + void OnTimerDone(Entity* self, std::string timerName) override; +private: + void RequestSwitches(Entity* self); +}; diff --git a/dScripts/02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.cpp b/dScripts/02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.cpp new file mode 100644 index 000000000..81e0a232d --- /dev/null +++ b/dScripts/02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.cpp @@ -0,0 +1,10 @@ +#include "SimpleOnceOnlyMoverSpinnerEvent.h" + +#include "Entity.h" +#include "MovingPlatformComponent.h" + +void SimpleOnceOnlyMoverSpinnerEvent::OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) { + const auto event = self->GetVarAsString(u"name_deactivated_event"); + if (event.empty() || args != event) return; + if (auto* const platform = self->GetComponent()) platform->GotoWaypoint(1); +} diff --git a/dScripts/02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.h b/dScripts/02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.h new file mode 100644 index 000000000..dfd39aa5f --- /dev/null +++ b/dScripts/02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.h @@ -0,0 +1,10 @@ +#pragma once +#include "CppScripts.h" + +// L_SIMPLE_ONCE_ONLY_MOVER_SPINNER_EVENT.lua (02_server/Map/firetemple): a simple mover (or a mover on a two point +// path) that a spinner sends to its other end. It is put in the spinner's group_deactivated_event group with the +// spinner's name_deactivated_event, and goes to waypoint 1 when the spinner fires that event. +class SimpleOnceOnlyMoverSpinnerEvent : public CppScripts::Script { +public: + void OnFireEventServerSide(Entity* self, Entity* sender, std::string args, int32_t param1, int32_t param2, int32_t param3) override; +}; diff --git a/dScripts/02_server/Map/njhub/CMakeLists.txt b/dScripts/02_server/Map/njhub/CMakeLists.txt index b2d0ba7f8..4c13fcb82 100644 --- a/dScripts/02_server/Map/njhub/CMakeLists.txt +++ b/dScripts/02_server/Map/njhub/CMakeLists.txt @@ -5,6 +5,8 @@ set(DSCRIPTS_SOURCES_02_SERVER_MAP_NJHUB "CavePrisonCage.cpp" "EnemySkeletonSpawner.cpp" "FallingTile.cpp" + "FireTransRocks.cpp" + "FireTransRocksManager.cpp" "FlameJetServer.cpp" "LightningOrbServer.cpp" "ImaginationShrineServer.cpp" diff --git a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp index 6ce4e0868..04b4d6690 100644 --- a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp +++ b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp @@ -84,19 +84,16 @@ void CavePrisonCage::SpawnCounterweight(Entity* self, Spawner* spawner) { }); quickBuildComponent->AddQuickBuildCompleteCallback([this, self](Entity* user) { - auto* counterweight = Game::entityManager->GetEntity(self->GetVar(u"Counterweight")); - - if (counterweight == nullptr) { - return; - } - - // The counterweight is a simple mover (MovingPlatforms row 41): send it down to its end - auto* const platform = counterweight->GetComponent(); - if (platform) platform->GotoWaypoint(1); - // Save the userID as Builder self->SetVar(u"Builder", user->GetObjectID()); + }); + } + // L_CAVE_PRISON_CAGE (notifyPlatformAtLastWaypoint): the built counterweight, a simple mover (MovingPlatforms row + // 41) that sets off by itself when its quick build completes, has gone down to the end of its path + auto* const platform = counterweight->GetComponent(); + if (platform) { + platform->AddPlatformAtLastWaypointCallback([this, self]() { // Get the button and make sure it still exists auto* button = Game::entityManager->GetEntity(self->GetVar(u"Button")); diff --git a/dScripts/02_server/Map/njhub/FireTransRocks.cpp b/dScripts/02_server/Map/njhub/FireTransRocks.cpp new file mode 100644 index 000000000..ad9147e52 --- /dev/null +++ b/dScripts/02_server/Map/njhub/FireTransRocks.cpp @@ -0,0 +1,24 @@ +#include "FireTransRocks.h" + +#include "Entity.h" +#include "EntityManager.h" +#include "MovingPlatformComponent.h" + +void FireTransRocks::OnStartup(Entity* self) { + if (!self->HasVar(u"RockManagerGroup")) self->SetVar(u"RockManagerGroup", u"FireTransRocksManager"); +} + +void FireTransRocks::OnArrivedAtDesiredWaypoint(Entity* self, const uint32_t waypointIndex) { + if (waypointIndex == 1) { + self->AddTimer("PlatHold", 3.5f); + return; + } + // The first manager there is hears that the rock is down + const auto managers = Game::entityManager->GetEntitiesInGroup(self->GetVarAsString(u"RockManagerGroup")); + if (!managers.empty()) managers.front()->GetScript()->OnNotifyObject(managers.front(), self, "PlatformDown"); +} + +void FireTransRocks::OnTimerDone(Entity* self, std::string timerName) { + if (timerName != "PlatHold") return; + if (auto* const platform = self->GetComponent()) platform->GotoWaypoint(0); +} diff --git a/dScripts/02_server/Map/njhub/FireTransRocks.h b/dScripts/02_server/Map/njhub/FireTransRocks.h new file mode 100644 index 000000000..ffcc0107d --- /dev/null +++ b/dScripts/02_server/Map/njhub/FireTransRocks.h @@ -0,0 +1,11 @@ +#pragma once +#include "CppScripts.h" + +// L_FIRE_TRANS_ROCKS.lua: a rock in the NJ monastery's fire transition, a mover between its bottom (waypoint 0) and its +// top (waypoint 1). At the top it holds 3.5 s and goes back down; at the bottom it tells the rocks' manager. +class FireTransRocks : public CppScripts::Script { +public: + void OnStartup(Entity* self) override; + void OnArrivedAtDesiredWaypoint(Entity* self, uint32_t waypointIndex) override; + void OnTimerDone(Entity* self, std::string timerName) override; +}; diff --git a/dScripts/02_server/Map/njhub/FireTransRocksManager.cpp b/dScripts/02_server/Map/njhub/FireTransRocksManager.cpp new file mode 100644 index 000000000..97407e9ff --- /dev/null +++ b/dScripts/02_server/Map/njhub/FireTransRocksManager.cpp @@ -0,0 +1,53 @@ +#include "FireTransRocksManager.h" + +#include + +#include "Entity.h" +#include "EntityManager.h" +#include "MovingPlatformComponent.h" + +void FireTransRocksManager::OnStartup(Entity* self) { + self->AddTimer("check", 5.0f); + if (!self->HasVar(u"RockGroup")) self->SetVar(u"RockGroup", u"FireTransRocks"); + if (!self->HasVar(u"NumberOfRocks")) self->SetVar(u"NumberOfRocks", 3); +} + +void FireTransRocksManager::CheckForRocks(Entity* self) { + const auto rocks = Game::entityManager->GetEntitiesInGroup(self->GetVarAsString(u"RockGroup")); + // Not all there yet: look again in a second + if (static_cast(rocks.size()) < self->GetVarAs(u"NumberOfRocks")) { + self->AddTimer("check", 1.0f); + return; + } + self->AddTimer("StartPlatforms", 5.0f); +} + +void FireTransRocksManager::OnNotifyObject(Entity* self, Entity* sender, const std::string& name, int32_t param1, int32_t param2) { + if (name != "PlatformDown" || !sender) return; + auto down = self->GetVar>(u"PlatTable"); + if (std::ranges::find(down, sender->GetObjectID()) != down.end()) return; + down.push_back(sender->GetObjectID()); + if (static_cast(down.size()) < self->GetVarAs(u"NumberOfRocks")) { + self->SetVar>(u"PlatTable", down); + return; + } + // All of them are down + self->AddTimer("restartPlatforms", 1.0f); +} + +void FireTransRocksManager::SendRocksUp(Entity* self) { + for (auto* const rock : Game::entityManager->GetEntitiesInGroup(self->GetVarAsString(u"RockGroup"))) { + if (auto* const platform = rock->GetComponent()) platform->GotoWaypoint(1); + } +} + +void FireTransRocksManager::OnTimerDone(Entity* self, std::string timerName) { + if (timerName == "check") { + CheckForRocks(self); + } else if (timerName == "StartPlatforms") { + SendRocksUp(self); + } else if (timerName == "restartPlatforms") { + self->SetVar>(u"PlatTable", {}); + SendRocksUp(self); + } +} diff --git a/dScripts/02_server/Map/njhub/FireTransRocksManager.h b/dScripts/02_server/Map/njhub/FireTransRocksManager.h new file mode 100644 index 000000000..4d942f551 --- /dev/null +++ b/dScripts/02_server/Map/njhub/FireTransRocksManager.h @@ -0,0 +1,15 @@ +#pragma once +#include "CppScripts.h" + +// L_FIRE_TRANS_ROCKS_MANAGER.lua: the manager of the rocks in the NJ monastery's fire transition. Once all the rocks of +// its RockGroup are there (NumberOfRocks, 3) it sends them up 5 s later, and each time all of them are back down it +// sends them up again after 1 s. +class FireTransRocksManager : public CppScripts::Script { +public: + void OnStartup(Entity* self) override; + void OnNotifyObject(Entity* self, Entity* sender, const std::string& name, int32_t param1 = 0, int32_t param2 = 0) override; + void OnTimerDone(Entity* self, std::string timerName) override; +private: + void CheckForRocks(Entity* self); + void SendRocksUp(Entity* self); +}; diff --git a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp index 965662b75..2b2b01253 100644 --- a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp +++ b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp @@ -1,5 +1,6 @@ #include "NjMonastryBossInstance.h" #include "QuickBuildComponent.h" +#include "MovingPlatformComponent.h" #include "DestroyableComponent.h" #include "EntityManager.h" #include "dZoneManager.h" @@ -237,44 +238,49 @@ void NjMonastryBossInstance::HandleCounterWeightSpawned(Entity* self, Entity* co case eQuickBuildState::RESETTING: ActivityTimerStart(self, SpawnCounterWeightTimer, 0.0f, 0.0f); return; - case eQuickBuildState::COMPLETED: { - // TODO: Move the platform? - - // The counterweight is actually a moving platform and we should listen to the last waypoint event here - // 0.5f is a rough estimate of that path, though, and results in less needed logic - self->AddCallbackTimer(0.5f, [this, self, counterWeight]() { - if (counterWeight != nullptr) { - counterWeight->Kill(); - } - - auto* frakjaw = Game::entityManager->GetEntity(self->GetVar(LedgeFrakjawVariable)); - if (frakjaw == nullptr) { - GameMessages::NotifyClientObject(self->GetObjectID(), u"LedgeFrakjawDead", 0, 0, LWOOBJID_EMPTY, "").Send(UNASSIGNED_SYSTEM_ADDRESS); - return; - } - - auto* skillComponent = frakjaw->GetComponent(); - if (skillComponent != nullptr) { - skillComponent->CalculateBehavior(1635, 39097, frakjaw->GetObjectID(), true, false); - } - - RenderComponent::PlayAnimation(frakjaw, StunnedAnimation); - GameMessages::PlayNDAudioEmitter(frakjaw->GetObjectID(), CounterSmashAudio).Send(UNASSIGNED_SYSTEM_ADDRESS); - - // Before wave 4 we should lower frakjaw from the ledge - if (self->GetVar(WaveNumberVariable) == 3) { - LowerFrakjaw(self, frakjaw); - return; - } - - ActivityTimerStart(self, SpawnNextWaveTimer, 2.0f, 2.0f); - }); - } default: return; } }); } + + // L_MONASTERY_BOSS_INSTANCE_SERVER (notifyPlatformAtLastWaypoint): the built counterweight has gone down to the end + // of its path. It is a simple mover that sets off by itself when its quick build completes: live sent it + // travelling about 0.1 s after the build, and its trip takes the second of its MovingPlatforms row. + auto* const platform = counterWeight->GetComponent(); + if (platform == nullptr) return; + const auto counterWeightID = counterWeight->GetObjectID(); + platform->AddPlatformAtLastWaypointCallback([this, self, counterWeightID]() { + // Not from inside the counterweight's own arrival + self->AddCallbackTimer(0.0f, [this, self, counterWeightID]() { + auto* const counterWeight = Game::entityManager->GetEntity(counterWeightID); + if (counterWeight != nullptr) { + counterWeight->Kill(); + } + + auto* frakjaw = Game::entityManager->GetEntity(self->GetVar(LedgeFrakjawVariable)); + if (frakjaw == nullptr) { + GameMessages::NotifyClientObject(self->GetObjectID(), u"LedgeFrakjawDead", 0, 0, LWOOBJID_EMPTY, "").Send(UNASSIGNED_SYSTEM_ADDRESS); + return; + } + + auto* skillComponent = frakjaw->GetComponent(); + if (skillComponent != nullptr) { + skillComponent->CalculateBehavior(1635, 39097, frakjaw->GetObjectID(), true, false); + } + + RenderComponent::PlayAnimation(frakjaw, StunnedAnimation); + GameMessages::PlayNDAudioEmitter(frakjaw->GetObjectID(), CounterSmashAudio).Send(UNASSIGNED_SYSTEM_ADDRESS); + + // Before wave 4 we should lower frakjaw from the ledge + if (self->GetVar(WaveNumberVariable) == 3) { + LowerFrakjaw(self, frakjaw); + return; + } + + ActivityTimerStart(self, SpawnNextWaveTimer, 2.0f, 2.0f); + }); + }); } void NjMonastryBossInstance::HandleLowerFrakjawSpawned(Entity* self, Entity* lowerFrakjaw) { diff --git a/dScripts/CppScripts.cpp b/dScripts/CppScripts.cpp index c84075099..0c2a30b4e 100644 --- a/dScripts/CppScripts.cpp +++ b/dScripts/CppScripts.cpp @@ -272,6 +272,10 @@ #include "NjNPCMissionSpinjitzuServer.h" #include "NjJayMissionItems.h" #include "FallingTile.h" +#include "FireTransRocks.h" +#include "FireTransRocksManager.h" +#include "SimpleMoverSwitch.h" +#include "SimpleOnceOnlyMoverSpinnerEvent.h" #include "ImaginationShrineServer.h" #include "Lieutenant.h" #include "RainOfArrows.h" @@ -679,6 +683,10 @@ namespace { {"scripts\\02_server\\Map\\General\\Ninjago\\L_RAIL_POST_SERVER.lua", []() {return new NjRailPostServer();}}, {"scripts\\02_server\\Map\\General\\Ninjago\\L_ICE_RAIL_ACTIVATOR_SERVER.lua", []() {return new NjIceRailActivator();}}, {"scripts\\02_server\\Map\\njhub\\L_FALLING_TILE.lua", []() {return new FallingTile();}}, + {"scripts\\02_server\\Map\\njhub\\L_FIRE_TRANS_ROCKS.lua", []() {return new FireTransRocks();}}, + {"scripts\\02_server\\Map\\njhub\\L_FIRE_TRANS_ROCKS_MANAGER.lua", []() {return new FireTransRocksManager();}}, + {"scripts\\02_server\\Map\\General\\L_SIMPLE_MOVER_SWITCH.lua", []() {return new SimpleMoverSwitch();}}, + {"scripts\\02_server\\Map\\firetemple\\L_SIMPLE_ONCE_ONLY_MOVER_SPINNER_EVENT.lua", []() {return new SimpleOnceOnlyMoverSpinnerEvent();}}, {"scripts\\02_server\\Enemy\\General\\L_ENEMY_NJ_BUFF_STUN_IMMUNITY.lua", []() {return new EnemyNjBuff();}}, {"scripts\\02_server\\Map\\njhub\\L_IMAGINATION_SHRINE_SERVER.lua", []() {return new ImaginationShrineServer();}}, {"scripts\\02_server\\Map\\njhub\\L_LIEUTENANT.lua", []() {return new Lieutenant();}}, @@ -812,7 +820,6 @@ namespace { "scripts\\ai\\GENERAL\\L_NPC_GENERIC_WANDER_SMALL.lua", // Really old alpha script "scripts\\zone\\LUPs\\Moonbase Intro\\WBL_Enemy_Grabbler.lua", // Really old alpha script, only sets wander variables "scripts\\ai\\NP\\L_NPC_NP_OLD_MAN_SHERLAND.lua", // This NPC doesn't even exist in modern crux, the only place this is used... - "scripts\\02_server\\Map\\General\\L_SIMPLE_MOVER_SWITCH.lua", // This platform does not exist even when moved manually on a client }; }; diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp index 2a889eb86..4b6262b08 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.cpp @@ -11,6 +11,7 @@ #include "CharacterComponent.h" #include "SimplePhysicsComponent.h" #include "MovementAIComponent.h" +#include "MovingPlatformComponent.h" #include "MissionComponent.h" #include "Loot.h" #include "InventoryComponent.h" @@ -286,7 +287,7 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { info.pos = path->pathWaypoints[0].position; info.settings.Insert(u"SpawnData", toSpawn); - info.settings.Insert(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // this script is never loaded; + info.settings.Insert(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // never loaded (the targets have a script row): TargetFinishedPath does its part info.settings.Insert(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua"); info.settings.Insert(u"attached_path", path->pathName); info.settings.Insert(u"attached_path_start", 0); @@ -295,7 +296,17 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { auto* enemy = Game::entityManager->CreateEntity(info, nullptr, self); - auto* movementAI = enemy->AddComponent(-1, MovementAIInfo{}); + // sg_cannon.lua (onChildLoaded): a moving platform target gets SetPathingSpeed, which nothing on a moving platform + // reads, and SetCurrentPath{ pathName, startPoint = 0 }: its mover runs the path at the waypoints' speeds, as the + // client's does. Live constructed these targets with the path and the mover waiting at waypoint 0 (9), and + // serialized the mover at each waypoint. The other targets follow their waypoints. + auto* const movingPlatform = toSpawn.isMovingPlatform ? enemy->GetComponent() : nullptr; + MovementAIComponent* movementAI = nullptr; + if (movingPlatform) { + movingPlatform->SetCurrentPath(path->pathName, 0); + } else { + movementAI = enemy->AddComponent(-1, MovementAIInfo{}); + } auto* simplePhysicsComponent = enemy->GetComponent(); if (simplePhysicsComponent) { simplePhysicsComponent->SetPhysicsMotionState(4); @@ -303,20 +314,38 @@ void SGCannon::DoSpawnTimerFunc(Entity* self, const std::string& name) { Game::entityManager->ConstructEntity(enemy); - movementAI->SetMaxSpeed(toSpawn.initialSpeed); - movementAI->SetCurrentSpeed(toSpawn.initialSpeed); - movementAI->SetHaltDistance(0.0f); + if (movementAI) { + movementAI->SetMaxSpeed(toSpawn.initialSpeed); + movementAI->SetCurrentSpeed(toSpawn.initialSpeed); + movementAI->SetHaltDistance(0.0f); - movementAI->SetPath(path->pathWaypoints); + movementAI->SetPath(path->pathWaypoints); + } - enemy->AddDieCallback([this, self, enemy, name]() { - RegisterHit(self, enemy, name); + // A hit target scores (live's cannon ball tells the cannon of its kills). One that got to the end of its path + // tells the cannon and dies silently a moment later (SG_TARGET.lua), which is no hit. + const auto silentDeath = std::make_shared(false); + enemy->AddDieCallback([this, self, enemy, name, silentDeath]() { + if (!*silentDeath) RegisterHit(self, enemy, name); }); - // Save the enemy and tell it to start pathing + // SG_TARGET.lua (onArrived): the Arrived message says whether the waypoint is the path's last one, for a moving + // platform as for an object following waypoints (the script reads isLastPoint, which the client's PushToLua + // 0x00b1a910 sets from the path and the waypoint) + const auto lastWaypoint = static_cast(path->pathWaypoints.size() - 1); + if (movingPlatform) { + movingPlatform->AddPlatformAtLastWaypointCallback([this, self, enemy, name, silentDeath]() { + TargetFinishedPath(self, enemy, name, silentDeath); + }); + } else if (movementAI) { + movementAI->AddWaypointReachedCallback([this, self, enemy, name, silentDeath, lastWaypoint](const uint32_t waypoint) { + if (waypoint == lastWaypoint) TargetFinishedPath(self, enemy, name, silentDeath); + }); + } + + // Save the enemy if (enemy != nullptr) { const_cast&>(self->GetVar>(SpawnedObjects)).push_back(enemy->GetObjectID()); - GameMessages::PlatformResync(*enemy).Send(UNASSIGNED_SYSTEM_ADDRESS); } } } @@ -672,6 +701,37 @@ void SGCannon::OnActivityNotify(Entity* self, GameMessages::ActivityNotify& noti self->SetVar>(u"CannonBallKills", {}); } +void SGCannon::CheckSpawn(Entity* self, Entity* target, const std::string& timerName) { + if (!self->GetVar(GameStartedVariable)) return; + const auto& spawnInfo = target->GetVar(u"SpawnData"); + + if (spawnInfo.respawns && target->GetVar(u"wave") == self->GetVar(ThisWaveVariable)) { + const auto respawnTime = GeneralUtils::GenerateRandomNumber(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime); + + ActivityTimerStart(self, timerName, respawnTime, respawnTime); + } +} + +// SG_TARGET.lua: at the last waypoint of its path a target notifies the cannon (FinishedPath: sg_cannon.lua spawns the +// next one as after a hit, without a score) and, when it is to despawn there, dies silently 1 s later, a moving +// platform after 2 s (so the clients' platforms get to the end of the path first). +void SGCannon::TargetFinishedPath(Entity* self, Entity* target, const std::string& timerName, const std::shared_ptr& silentDeath) { + // Once: a platform sent on would tell again + if (target->GetVar(u"FinishedPath")) return; + target->SetVar(u"FinishedPath", true); + + CheckSpawn(self, target, timerName); + + const auto& spawnInfo = target->GetVar(u"SpawnData"); + if (!spawnInfo.despawnOnLastWaypoint) return; + target->AddCallbackTimer(spawnInfo.isMovingPlatform ? 2.0f : 1.0f, [target, silentDeath]() { + // Hit in the meantime + if (target->GetIsDead()) return; + *silentDeath = true; + target->Smash(target->GetObjectID(), eKillType::SILENT); + }); +} + void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& timerName) { if (!self->GetVar(GameStartedVariable)) return; @@ -680,11 +740,7 @@ void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& time self->SetVar>(u"CannonBallKills", cannonBallKills); const auto& spawnInfo = target->GetVar(u"SpawnData"); - if (spawnInfo.respawns && target->GetVar(u"wave") == self->GetVar(ThisWaveVariable)) { - const auto respawnTime = GeneralUtils::GenerateRandomNumber(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime); - - ActivityTimerStart(self, timerName, respawnTime, respawnTime); - } + CheckSpawn(self, target, timerName); int32_t score = spawnInfo.score; @@ -1104,22 +1160,14 @@ std::vector> SGCannon::GetWaves() { 1.0, false, true }, - // Ness temp fix for dlu where speeds are set to 7 to match a speed closer to live while we work on movingplatform components. + // Ness { std::vector { "Wave_1_Ness_1", "Wave_1_Ness_2", "Wave_2_Ness_1" }, 2565, 10.0, 15.0, true, 10.0, 15.0, - 7.0, 10000, false, 0.0, 7.0, - 7.0, true, true + 2.0, 10000, false, 0.0, 1.0, + 1.0, true, true }, - // // Ness - // { - // std::vector { "Wave_1_Ness_1", "Wave_1_Ness_2", "Wave_2_Ness_1" }, - // 2565, 10.0, 15.0, true, 10.0, 15.0, - // 2.0, 10000, false, 0.0, 1.0, - // 1.0, true, true - // }, - // Friendly 1 { std::vector { "Wave_3_FShip_1", "Wave_3_FShip_2" }, diff --git a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h index 3c9ce83bc..1d810a562 100644 --- a/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h +++ b/dScripts/ai/MINIGAME/SG_GF/SERVER/SGCannon.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include "ActivityManager.h" @@ -89,6 +90,10 @@ private: void PauseChargeCannon(Entity* self); void StopGame(Entity* self, bool cancel = false); void RegisterHit(Entity* self, Entity* target, const std::string& timerName); + // sg_cannon.lua (checkSpawn): a target that respawns gets its next spawn, while its wave runs + void CheckSpawn(Entity* self, Entity* target, const std::string& timerName); + // SG_TARGET.lua (onArrived at the last waypoint): the cannon spawns the next one, and the target is gone a moment later + void TargetFinishedPath(Entity* self, Entity* target, const std::string& timerName, const std::shared_ptr& silentDeath); void UpdateStreak(Entity* self); float_t GetCurrentBonus(Entity* self); diff --git a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp index 04b8fcd89..00b92846d 100644 --- a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp @@ -8,9 +8,12 @@ #include "BitStream.h" #include "CDClientDatabase.h" #include "Entity.h" +#include "GameMessages.h" +#include "02_server/Map/njhub/FireTransRocks.h" #include "LDFFormat.h" #include "MovementMessages.h" #include "MovingPlatformComponent.h" +#include "02_server/Map/General/SimpleOnceOnlyMoverSpinnerEvent.h" #include "SimplePhysicsComponent.h" #include "Zone.h" #include "dZoneManager.h" @@ -89,7 +92,7 @@ TEST_F(MovingPlatformComponentTest, OneSubcomponentPerSetting) { } // Live: 1 (subcomponents), 1 (dirty), 0 (no path), 1, type 5, the starting point (the object's position and -// rotation, w first), state 9 (stopped at its desired waypoint), waypoint 0, not reversing, then the ending 0 bit +// rotation, w first), state 9 (waiting at its desired waypoint), waypoint 0, not reversing, then the ending 0 bit TEST_F(MovingPlatformComponentTest, SimpleMoverConstructionMatchesLive) { info.lot = 16141; info.pos = NiPoint3(Float(0xc2e02255), Float(0x436d1992), Float(0xc32347e8)); @@ -158,6 +161,10 @@ TEST_F(MovingPlatformComponentTest, RotaterIsWrittenAsTypeSix) { ASSERT_EQ(platform->GetMoverSubComponentType(), eMoverSubComponentType::rotater); ASSERT_NE(platform->GetMoverSubComponent(), nullptr); platform->SetSerialized(true); + // Without a path it does not run: it stays stopped at its waypoint, also when started + EXPECT_EQ(platform->GetMoverSubComponent()->mState, 12u); + platform->StartPathing(); + EXPECT_EQ(platform->GetMoverSubComponent()->mState, 12u); RakNet::BitStream stream; platform->Serialize(stream, true); @@ -167,9 +174,9 @@ TEST_F(MovingPlatformComponentTest, RotaterIsWrittenAsTypeSix) { 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 +// A mover runs on the server as live ran it: it waits at its first waypoint (9: waiting at a waypoint, as the GF +// shooting gallery targets were constructed), travels after the waypoint's wait (2), waits at the other end (25, turned +// round on a bounce path); sent to a waypoint to stop it waits with the desired waypoint, leaves and stops there (28) TEST_F(MovingPlatformComponentTest, MoverRunsLikeLive) { Path path; path.pathType = PathType::MovingPlatform; @@ -189,12 +196,12 @@ TEST_F(MovingPlatformComponentTest, MoverRunsLikeLive) { auto* const platform = entity.AddComponent(-1, "TestMoverPath"); ASSERT_TRUE(platform->IsMover()); auto* const mover = platform->GetMoverSubComponent(); - EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mState, 9u); EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); EXPECT_EQ(mover->mNextWaypointIndex, 1u); platform->Run(1.9f); - EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mState, 9u); EXPECT_FLOAT_EQ(mover->mIdleTimeElapsed, 1.9f); platform->Run(0.2f); EXPECT_EQ(mover->mState, 2u); @@ -217,28 +224,461 @@ TEST_F(MovingPlatformComponentTest, MoverRunsLikeLive) { EXPECT_TRUE(mover->mInReverse); EXPECT_EQ(mover->mPercentBetweenPoints, 0.0f); - // Sent back to the start to stop there: it leaves at once and stays + // Sent back to the start to stop there: it waits out the waypoint's wait, leaves and stays platform->GotoWaypoint(0, true); + EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mDesiredWaypointIndex, 0); + EXPECT_TRUE(mover->mShouldStopAtDesiredWaypoint); + platform->Run(1.1f); EXPECT_EQ(mover->mState, 2u); EXPECT_EQ(mover->mDesiredWaypointIndex, 0); platform->Run(2.5f); - EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mState, 28u); EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); EXPECT_EQ(mover->mDesiredWaypointIndex, -1); platform->Run(10.0f); - EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mState, 28u); - // Started at a waypoint it first waits there (issue 964), then stopped mid way and started again + // Started at a waypoint it first waits there (issue 964), then stopped mid way (4, the bit the client stops on) and + // started again platform->StartPathing(); EXPECT_EQ(mover->mState, 25u); platform->Run(2.1f); EXPECT_EQ(mover->mState, 2u); platform->Run(1.0f); platform->StopPathing(); - EXPECT_EQ(mover->mState & 1u, 1u); + EXPECT_EQ(mover->mState, 4u); const auto percent = mover->mPercentBetweenPoints; platform->Run(1.0f); EXPECT_EQ(mover->mPercentBetweenPoints, percent); platform->StartPathing(); EXPECT_EQ(mover->mState, 2u); } + +// The NJ falling tiles in the live captures (LOT 14335, a two waypoint path run once, not started on load): 12 until +// stepped on, sent to waypoint 1 it waits (9) and leaves (2); there it has stopped and turned round (28), and sent back +// it waits (25, reversed, towards 0) and leaves (2), to stop at the start as live constructed used tiles (28, forwards) +TEST_F(MovingPlatformComponentTest, MoverOnceLikeLiveFallingTile) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestDropPath"; + path.pathBehavior = PathBehavior::Once; + PathWaypoint first; + first.position = NiPoint3(0.0f, 10.0f, 0.0f); + first.speed = 5.0f; + PathWaypoint second = first; + second.position = NiPoint3(0.0f, 0.0f, 0.0f); + path.pathWaypoints = { first, second }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + info.settings.Insert(u"startPathingOnLoad", false); + Entity entity(288300744895900109, info); + info.settings.values.clear(); + auto* const platform = entity.AddComponent(-1, "TestDropPath"); + auto* const mover = platform->GetMoverSubComponent(); + EXPECT_EQ(mover->mState, 12u); + platform->Run(5.0f); + EXPECT_EQ(mover->mState, 12u); + + platform->GotoWaypoint(1); + EXPECT_EQ(mover->mState, 9u); + EXPECT_EQ(mover->mDesiredWaypointIndex, 1); + EXPECT_TRUE(mover->mShouldStopAtDesiredWaypoint); + platform->Run(0.11f); + EXPECT_EQ(mover->mState, 2u); + platform->Run(1.0f); + EXPECT_EQ(mover->mState, 2u); + platform->Run(1.1f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + EXPECT_EQ(mover->mNextWaypointIndex, 0u); + EXPECT_TRUE(mover->mInReverse); + EXPECT_EQ(mover->mDesiredWaypointIndex, -1); + + platform->GotoWaypoint(0); + EXPECT_EQ(mover->mState, 25u); + EXPECT_EQ(mover->mDesiredWaypointIndex, 0); + platform->Run(0.11f); + EXPECT_EQ(mover->mState, 2u); + platform->Run(2.1f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); + EXPECT_EQ(mover->mNextWaypointIndex, 1u); + EXPECT_FALSE(mover->mInReverse); +} + +// The object goes with its mover on the server (LWOPlatform moves it ignoring the dirty flags), turned between the +// waypoints' rotations, and its physics are not serialized for it: live sent a moving target's mover data only +TEST_F(MovingPlatformComponentTest, MoverCarriesItsObject) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestCarryPath"; + path.pathBehavior = PathBehavior::Once; + PathWaypoint first; + first.position = NiPoint3(0.0f, 0.0f, 0.0f); + first.speed = 5.0f; + PathWaypoint second = first; + second.position = NiPoint3(10.0f, 0.0f, 0.0f); + second.rotation = NiQuaternion(0.0f, 0.0f, 1.0f, 0.0f); + path.pathWaypoints = { first, second }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + info.pos = NiPoint3(0.0f, 0.0f, 0.0f); + Entity entity(288300744895900110, info); + auto* const simplePhysics = entity.AddComponent(-1); + auto* const platform = entity.AddComponent(-1, "TestCarryPath"); + + RakNet::BitStream before; + simplePhysics->Serialize(before, false); + platform->Run(0.11f); + platform->Run(1.0f); + EXPECT_NEAR(entity.GetPosition().x, 5.0f, 0.01f); + EXPECT_NEAR(std::abs(entity.GetRotation().y), 0.7071f, 0.001f); + + RakNet::BitStream moved; + simplePhysics->Serialize(moved, false); + // No position and rotation (seven floats) in it + EXPECT_LT(moved.GetNumberOfBitsUsed(), 7u * 32u); + + platform->Run(1.1f); + 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); +} + +// The NJ counterweights in the live captures (LOT 16141, the Frakjaw battle): when the quick build completes the simple +// mover is serialized waiting at waypoint 0 (9), then travelling from it (2); it stops at its end without a +// serialization, where PlatformAtLastWaypoint tells the scripts +TEST_F(MovingPlatformComponentTest, BuiltSimpleMoverSetsOffLikeLive) { + info.lot = 16141; + info.pos = NiPoint3(10.0f, 20.0f, 30.0f); + info.rot = QuatUtils::IDENTITY; + Entity counterweight(288300744895900112, info); + counterweight.AddComponent(-1); + auto* const platform = counterweight.AddComponent(41, ""); + auto* const mover = platform->GetSimpleMoverSubComponent(); + uint32_t atLastWaypoint = 0; + platform->AddPlatformAtLastWaypointCallback([&atLastWaypoint] { atLastWaypoint++; }); + + platform->OnCompleteQuickBuild(); + RakNet::BitStream built; + platform->Serialize(built, false); + EXPECT_EQ(Bits(built), "101" "00000101000000000000000000000000" "0" "1" "00001001000000000000000000000000" "00000000000000000000000000000000" "0" "0"); + + counterweight.Update(0.0f); // timers added now start on the next update + counterweight.Update(0.11f); + EXPECT_EQ(mover->mState, 2u); + RakNet::BitStream travelling; + platform->Serialize(travelling, false); + EXPECT_EQ(Bits(travelling), "101" "00000101000000000000000000000000" "0" "1" "00000010000000000000000000000000" "00000000000000000000000000000000" "0" "0"); + + counterweight.Update(0.0f); + counterweight.Update(0.5f); + EXPECT_EQ(mover->mState, 2u); + EXPECT_EQ(atLastWaypoint, 0u); + counterweight.Update(0.6f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + EXPECT_EQ(atLastWaypoint, 1u); + EXPECT_EQ(counterweight.GetPosition(), NiPoint3(10.0f, 17.5f, 30.0f)); + RakNet::BitStream arrived; + 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)); +} + +// Live's movers at a waypoint between the ends of their path: the object is put at the waypoint and serialized waiting +// there (9), keeps the velocity it came with for the wait (LWOPlatform::SetWaitingTime 0x00bdc860 only takes it away at +// an end or when stopped), and is serialized leaving (2) from where that took it, with the part of the next stretch it +// is from the waypoint: the GF shooting gallery subs (speed 15, no wait, so the shortest, 0.1 s: two of live's frames) +// left 2 units past each waypoint. From there it goes the next stretch's direction and arrives as it passes the +// waypoint. At the end of the path it stops where the waypoint is. +TEST_F(MovingPlatformComponentTest, MoverKeepsItsVelocityThroughAWaitLikeLive) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestCornerPath"; + path.pathBehavior = PathBehavior::Once; + PathWaypoint first; + first.position = NiPoint3(0.0f, 0.0f, 0.0f); + first.speed = 15.0f; + PathWaypoint second = first; + second.position = NiPoint3(30.0f, 0.0f, 0.0f); + PathWaypoint third = first; + third.position = NiPoint3(30.0f, 0.0f, 30.0f); + path.pathWaypoints = { first, second, third }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + info.pos = NiPoint3(0.0f, 0.0f, 0.0f); + Entity entity(288300744895900114, info); + entity.AddComponent(-1); + auto* const platform = entity.AddComponent(-1, "TestCornerPath"); + auto* const mover = platform->GetMoverSubComponent(); + platform->Run(0.11f); + EXPECT_EQ(mover->mState, 2u); + EXPECT_EQ(mover->mPercentBetweenPoints, 0.0f); + + platform->Run(2.0f); + EXPECT_EQ(mover->mState, 9u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + EXPECT_EQ(mover->mPosition, NiPoint3(30.0f, 0.0f, 0.0f)); + EXPECT_EQ(mover->mPercentBetweenPoints, 0.0f); + + platform->Run(1.0f / 15.0f); + EXPECT_EQ(mover->mState, 9u); + EXPECT_NEAR(mover->mPosition.x, 31.0f, 0.001f); + platform->Run(1.0f / 15.0f); + EXPECT_EQ(mover->mState, 2u); + EXPECT_NEAR(mover->mPosition.x, 32.0f, 0.001f); + EXPECT_NEAR(mover->mPosition.z, 0.0f, 0.001f); + EXPECT_NEAR(mover->mPercentBetweenPoints, 2.0f / 30.0f, 0.001f); + EXPECT_NEAR(entity.GetPosition().x, 32.0f, 0.001f); + + platform->Run(1.0f); + EXPECT_NEAR(mover->mPosition.x, 32.0f, 0.001f); + EXPECT_NEAR(mover->mPosition.z, 15.0f, 0.001f); + platform->Run(1.0f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 2u); + EXPECT_EQ(mover->mPosition, NiPoint3(30.0f, 0.0f, 30.0f)); + platform->Run(1.0f); + 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(-1); + auto* const platform = entity.AddComponent(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(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(u"startPathingOnLoad", false); + Entity entity(288300744895900117, info); + info.settings.values.clear(); + auto* const platform = entity.AddComponent(41, ""); + auto* const mover = platform->GetSimpleMoverSubComponent(); + EXPECT_EQ(mover->mState, 12u); + entity.Update(1.0f); + EXPECT_EQ(mover->mState, 12u); + } +} + +// L_SIMPLE_ONCE_ONLY_MOVER_SPINNER_EVENT: the platform goes to its waypoint 1 when the event named in its +// name_deactivated_event is fired at it, and at no other +TEST_F(MovingPlatformComponentTest, OnceOnlyMoverGoesOnItsSpinnersEvent) { + info.lot = 16141; + info.settings.Insert(u"startPathingOnLoad", false); + info.settings.Insert(u"name_deactivated_event", u"blades_off"); + Entity entity(288300744895900118, info); + info.settings.values.clear(); + auto* const platform = entity.AddComponent(41, ""); + auto* const mover = platform->GetSimpleMoverSubComponent(); + SimpleOnceOnlyMoverSpinnerEvent script; + + script.OnFireEventServerSide(&entity, &entity, "blades_on", 0, 0, 0); + EXPECT_EQ(mover->mState, 12u); + script.OnFireEventServerSide(&entity, &entity, "blades_off", 0, 0, 0); + EXPECT_EQ(mover->mState, 2u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); +} + +// L_FIRE_TRANS_ROCKS: a rock that arrived at its top (waypoint 1) holds there (the PlatHold timer) and is then sent +// back down to waypoint 0 +TEST_F(MovingPlatformComponentTest, FireTransRockGoesBackDownAfterItsHold) { + Path path; + path.pathType = PathType::MovingPlatform; + path.pathName = "TestRockPath"; + path.pathBehavior = PathBehavior::Once; + PathWaypoint first; + first.position = NiPoint3(0.0f, 0.0f, 0.0f); + first.speed = 5.0f; + PathWaypoint second = first; + second.position = NiPoint3(0.0f, 10.0f, 0.0f); + path.pathWaypoints = { first, second }; + const_cast(Game::zoneManager->GetZone())->AddPath(path); + + info.settings.Insert(u"startPathingOnLoad", false); + Entity entity(288300744895900119, info); + info.settings.values.clear(); + auto* const platform = entity.AddComponent(-1, "TestRockPath"); + auto* const mover = platform->GetMoverSubComponent(); + FireTransRocks script; + script.OnStartup(&entity); + EXPECT_EQ(entity.GetVarAsString(u"RockManagerGroup"), "FireTransRocksManager"); + + platform->GotoWaypoint(1); + platform->Run(0.11f); + platform->Run(2.1f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + + script.OnTimerDone(&entity, "PlatHold"); + EXPECT_EQ(mover->mDesiredWaypointIndex, 0); + platform->Run(0.11f); + platform->Run(2.1f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 0u); +} diff --git a/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp index accbdf551..307adb6c9 100644 --- a/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/RemainingMessagesTests.cpp @@ -89,14 +89,21 @@ TEST_F(RemainingMessagesTests, TeleportMatchesLegacy) { } TEST_F(RemainingMessagesTests, StartPathingAndPlatformResyncMatchLegacy) { - for (const LOT lot : { 999, 12341, 9483, 11306, 6267 }) { + // The legacy sender overrode its arguments for a few LOTs; the message has no such cases, so only others compare + for (const LOT lot : { 999, 6267 }) { info.lot = lot; Entity entity(0x1000000000000001LL, info); ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendStartPathing(&entity); }, [&] { GameMessages::StartPathing msg; msg.target = entity.GetObjectID(); return msg; }()); for (uint32_t mask = 0; mask < 4; mask++) { for (const auto state : { eMovementPlatformState::Moving, eMovementPlatformState::Stationary, eMovementPlatformState::Stopped }) { for (const int32_t index : { 0, 1, 5 }) { - GameMessages::PlatformResync msg(entity, Bit(mask, 0), index, index + 1, index + 2, state, Bit(mask, 1)); + GameMessages::PlatformResync msg; + msg.target = entity.GetObjectID(); + msg.bStopAtDesiredWaypoint = Bit(mask, 0); + msg.iIndex = index; + msg.iDesiredWaypointIndex = index + 1; + msg.iNextIndex = index + 2; + msg.eState = state; ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendPlatformResync(&entity, address, Bit(mask, 0), index, index + 1, index + 2, state, Bit(mask, 1)); }, msg);