Merge exp/platforms-3: moving platforms rounds 3-4

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 17:11:47 -06:00
26 changed files with 1339 additions and 230 deletions

View File

@@ -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;

View File

@@ -16,6 +16,7 @@
#include "eReplicaComponentType.h"
#include "Zone.h"
#include "dpKnockback.h"
#include <functional>
#include <optional>
#include <vector>
@@ -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<void(uint32_t)>& 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<std::function<void(uint32_t)>> m_WaypointReachedCallbacks;
LWOOBJID m_FollowedTarget{ LWOOBJID_EMPTY };
/**

View File

@@ -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<bool>(true);
@@ -154,6 +157,18 @@ void MovingPlatformComponent::LoadConfigData() {
if (m_Parent->HasVar(u"platformMoveTime")) simpleMover->mMoveTime = m_Parent->GetVar<float>(u"platformMoveTime");
}
simpleMover->mCurrentWaypointIndex = simpleMover->mStartAtEnd ? 1 : 0;
// As a mover on its path (LWOPlatform::ResumeTravelling 0x00c86b90 and SetWaitingTime 0x00bdc860): unless
// startPathingOnLoad is false it waits at its waypoint (9), for the shortest wait as its waypoints have none.
// Live's scripts for the NJ spinners, which are simple movers that only move when used, stop them when they
// start up (L_NJ_SPINNER_SERVER: StopPathing), and the simple movers that wait for an event without such a script
// have startPathingOnLoad 0.
const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar<bool>(u"startPathingOnLoad");
if (startOnLoad) {
simpleMover->mState = ePlatformStateFlag::Waiting | ePlatformStateFlag::ReachedDesiredWaypoint;
simpleMover->mTimeToWait = 0.1f;
simpleMover->mWaitSet = true;
}
}
const auto pathName = GeneralUtils::UTF16ToWTF8(m_PathName);
@@ -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<int32_t>(u"attached_path_start") : 0;
m_StartWaypoint = start >= 0 && static_cast<size_t>(start) < m_Path->pathWaypoints.size() ? start : 0;
MoverProcessStateChange(m_StartWaypoint);
const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar<bool>(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<bool>(u"startPathingOnLoad");
GetMoverSubComponent()->mState = startOnLoad ? ePlatformStateFlag::Travelling : ePlatformStateFlag::Stopped;
const auto start = m_Parent->HasVar(u"attached_path_start") ? m_Parent->GetVar<int32_t>(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<bool>(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<RotatorSubComponent*>(GetMoverSubComponent());
// LWOPlatformRotator::LoadConfigData 0x00c33e10
constexpr float pi = 3.14159265358979323846f;
if (m_Parent->HasVar(u"rotX")) rotator->mRotationX = m_Parent->GetVarAs<float>(u"rotX") * pi;
if (m_Parent->HasVar(u"rotY")) rotator->mRotationY = m_Parent->GetVarAs<float>(u"rotY") * pi;
if (m_Parent->HasVar(u"rotZ")) rotator->mRotationZ = m_Parent->GetVarAs<float>(u"rotZ") * pi;
rotator->mRate = m_Parent->GetVarAs<float>(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<size_t>(m_StartWaypoint) >= m_Path->pathWaypoints.size()) m_StartWaypoint = 0;
MoverProcessStateChange(m_StartWaypoint);
const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar<bool>(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<MoverSubComponent>(m_Parent->GetPosition());
else if (subComponent->GetType() == eMoverSubComponentType::rotater) subComponent = std::make_unique<RotatorSubComponent>(m_Parent->GetPosition());
}
const bool mover = IsMover();
if (mover) {
m_StartWaypoint = static_cast<size_t>(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<bool>(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<uint16_t>(c);
}
outBitStream.Write<uint32_t>(m_StartWaypoint);
outBitStream.Write<bool>(false);
outBitStream.Write<bool>(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<int32_t>(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<uint32_t>(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<uint32_t>(m_Path->pathWaypoints.size() - 1);
const bool atEnd = arrived == last;
const bool atDesired = mover->mDesiredWaypointIndex == static_cast<int32_t>(arrived);
bool stop = false;
if (atDesired) {
stop = mover->mShouldStopAtDesiredWaypoint;
const bool atLastWaypoint = arrived == static_cast<uint32_t>(m_Path->pathWaypoints.size() - 1);
bool atDesiredWaypoint = false;
bool stopAtDesiredWaypoint = false;
bool turnedAtEnd = false;
mover->mState = ePlatformStateFlag::ReachedDesiredWaypoint;
if (mover->mDesiredWaypointIndex == static_cast<int32_t>(arrived)) {
atDesiredWaypoint = true;
stopAtDesiredWaypoint = mover->mShouldStopAtDesiredWaypoint;
mover->mShouldStopAtDesiredWaypoint = false;
mover->mDesiredWaypointIndex = -1;
}
if ((arrived == 0 || atEnd) && static_cast<PathBehavior>(m_Path->pathBehavior) == PathBehavior::Once) {
stop = true;
mover->mInReverse = !mover->mInReverse;
if (arrived == 0 || atLastWaypoint) {
mover->mState |= ePlatformStateFlag::ReachedFinalDestination;
if (static_cast<PathBehavior>(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<SimplePhysicsComponent>()) {
simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(mover->mPosition, rotation);
}
if (auto* const phantomPhysics = m_Parent->GetComponent<PhantomPhysicsComponent>()) {
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<QuickBuildComponent>();
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<float>(std::sin(static_cast<double>(rotX))), cx = static_cast<float>(std::cos(static_cast<double>(rotX)));
const float sy = static_cast<float>(std::sin(static_cast<double>(rotY))), cy = static_cast<float>(std::cos(static_cast<double>(rotY)));
const float sz = static_cast<float>(std::sin(static_cast<double>(rotZ))), cz = static_cast<float>(std::cos(static_cast<double>(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<float>::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<RotatorSubComponent*>(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<SimplePhysicsComponent>()) {
simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(rotator->mPosition, rotation);
}
if (auto* const phantomPhysics = m_Parent->GetComponent<PhantomPhysicsComponent>()) {
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<uint32_t>(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<SimplePhysicsComponent>()) {
simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(position, simpleMover->mStartRotation);
}
if (auto* const phantomPhysics = m_Parent->GetComponent<PhantomPhysicsComponent>()) {
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<void()>& callback) {
m_AtLastWaypointCallbacks.push_back(callback);
}
void MovingPlatformComponent::Allocator(Entity& entity, ObjectLoadState& state) {

View File

@@ -6,6 +6,7 @@
#ifndef MOVINGPLATFORMCOMPONENT_H
#define MOVINGPLATFORMCOMPONENT_H
#include <functional>
#include <memory>
#include <vector>
@@ -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<void()>& 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<std::unique_ptr<PlatformSubComponent>> m_SubComponents;
std::vector<std::function<void()>> 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;

View File

@@ -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; }

View File

@@ -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");

View File

@@ -8,6 +8,7 @@
#include "QuickBuildComponent.h"
#include "BouncerComponent.h"
#include <algorithm>
#include <functional>
#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<void(bool)>& callback) { m_ActivationCallbacks.push_back(callback); }
const std::vector<int32_t>& GetFactionsToRespondTo() const {
return m_FactionsToRespondTo;
}
@@ -113,6 +120,8 @@ private:
BouncerComponent* m_PetBouncer = nullptr;
std::vector<int32_t> m_FactionsToRespondTo{};
std::vector<std::function<void(bool)>> m_ActivationCallbacks;
};
#endif // SWITCHCOMPONENT_H

View File

@@ -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<MovingPlatformComponent>();
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 {

View File

@@ -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;

View File

@@ -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"

View File

@@ -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<SwitchComponent>();
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<MovingPlatformComponent>()) 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);
}

View File

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

View File

@@ -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<MovingPlatformComponent>()) platform->GotoWaypoint(1);
}

View File

@@ -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;
};

View File

@@ -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"

View File

@@ -84,19 +84,16 @@ void CavePrisonCage::SpawnCounterweight(Entity* self, Spawner* spawner) {
});
quickBuildComponent->AddQuickBuildCompleteCallback([this, self](Entity* user) {
auto* counterweight = Game::entityManager->GetEntity(self->GetVar<LWOOBJID>(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<MovingPlatformComponent>();
if (platform) platform->GotoWaypoint(1);
// Save the userID as Builder
self->SetVar<LWOOBJID>(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<MovingPlatformComponent>();
if (platform) {
platform->AddPlatformAtLastWaypointCallback([this, self]() {
// Get the button and make sure it still exists
auto* button = Game::entityManager->GetEntity(self->GetVar<LWOOBJID>(u"Button"));

View File

@@ -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<std::u16string>(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<MovingPlatformComponent>()) platform->GotoWaypoint(0);
}

View File

@@ -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;
};

View File

@@ -0,0 +1,53 @@
#include "FireTransRocksManager.h"
#include <algorithm>
#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<std::u16string>(u"RockGroup", u"FireTransRocks");
if (!self->HasVar(u"NumberOfRocks")) self->SetVar<int32_t>(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<int32_t>(rocks.size()) < self->GetVarAs<int32_t>(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<std::vector<LWOOBJID>>(u"PlatTable");
if (std::ranges::find(down, sender->GetObjectID()) != down.end()) return;
down.push_back(sender->GetObjectID());
if (static_cast<int32_t>(down.size()) < self->GetVarAs<int32_t>(u"NumberOfRocks")) {
self->SetVar<std::vector<LWOOBJID>>(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<MovingPlatformComponent>()) 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<std::vector<LWOOBJID>>(u"PlatTable", {});
SendRocksUp(self);
}
}

View File

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

View File

@@ -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<LWOOBJID>(LedgeFrakjawVariable));
if (frakjaw == nullptr) {
GameMessages::NotifyClientObject(self->GetObjectID(), u"LedgeFrakjawDead", 0, 0, LWOOBJID_EMPTY, "").Send(UNASSIGNED_SYSTEM_ADDRESS);
return;
}
auto* skillComponent = frakjaw->GetComponent<SkillComponent>();
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<uint32_t>(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<MovingPlatformComponent>();
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<LWOOBJID>(LedgeFrakjawVariable));
if (frakjaw == nullptr) {
GameMessages::NotifyClientObject(self->GetObjectID(), u"LedgeFrakjawDead", 0, 0, LWOOBJID_EMPTY, "").Send(UNASSIGNED_SYSTEM_ADDRESS);
return;
}
auto* skillComponent = frakjaw->GetComponent<SkillComponent>();
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<uint32_t>(WaveNumberVariable) == 3) {
LowerFrakjaw(self, frakjaw);
return;
}
ActivityTimerStart(self, SpawnNextWaveTimer, 2.0f, 2.0f);
});
});
}
void NjMonastryBossInstance::HandleLowerFrakjawSpawned(Entity* self, Entity* lowerFrakjaw) {

View File

@@ -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
};
};

View File

@@ -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<SGEnemy>(u"SpawnData", toSpawn);
info.settings.Insert<std::string>(u"custom_script_server", "scripts/ai/ACT/SG_TARGET.lua"); // this script is never loaded;
info.settings.Insert<std::string>(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<std::string>(u"custom_script_client", "scripts/client/ai/SG_TARGET_CLIENT.lua");
info.settings.Insert<std::string>(u"attached_path", path->pathName);
info.settings.Insert<uint32_t>(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<MovementAIComponent>(-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<MovingPlatformComponent>() : nullptr;
MovementAIComponent* movementAI = nullptr;
if (movingPlatform) {
movingPlatform->SetCurrentPath(path->pathName, 0);
} else {
movementAI = enemy->AddComponent<MovementAIComponent>(-1, MovementAIInfo{});
}
auto* simplePhysicsComponent = enemy->GetComponent<SimplePhysicsComponent>();
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<bool>(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<uint32_t>(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<std::vector<LWOOBJID>&>(self->GetVar<std::vector<LWOOBJID>>(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<std::vector<LWOOBJID>>(u"CannonBallKills", {});
}
void SGCannon::CheckSpawn(Entity* self, Entity* target, const std::string& timerName) {
if (!self->GetVar<bool>(GameStartedVariable)) return;
const auto& spawnInfo = target->GetVar<SGEnemy>(u"SpawnData");
if (spawnInfo.respawns && target->GetVar<uint32_t>(u"wave") == self->GetVar<uint32_t>(ThisWaveVariable)) {
const auto respawnTime = GeneralUtils::GenerateRandomNumber<float_t>(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<bool>& silentDeath) {
// Once: a platform sent on would tell again
if (target->GetVar<bool>(u"FinishedPath")) return;
target->SetVar<bool>(u"FinishedPath", true);
CheckSpawn(self, target, timerName);
const auto& spawnInfo = target->GetVar<SGEnemy>(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<bool>(GameStartedVariable)) return;
@@ -680,11 +740,7 @@ void SGCannon::RegisterHit(Entity* self, Entity* target, const std::string& time
self->SetVar<std::vector<LWOOBJID>>(u"CannonBallKills", cannonBallKills);
const auto& spawnInfo = target->GetVar<SGEnemy>(u"SpawnData");
if (spawnInfo.respawns && target->GetVar<uint32_t>(u"wave") == self->GetVar<uint32_t>(ThisWaveVariable)) {
const auto respawnTime = GeneralUtils::GenerateRandomNumber<float_t>(spawnInfo.minRespawnTime, spawnInfo.maxRespawnTime);
ActivityTimerStart(self, timerName, respawnTime, respawnTime);
}
CheckSpawn(self, target, timerName);
int32_t score = spawnInfo.score;
@@ -1104,22 +1160,14 @@ std::vector<std::vector<SGEnemy>> 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<std::string> { "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<std::string> { "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<std::string> { "Wave_3_FShip_1", "Wave_3_FShip_2" },

View File

@@ -1,4 +1,5 @@
#pragma once
#include <memory>
#include <regex>
#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<bool>& silentDeath);
void UpdateStreak(Entity* self);
float_t GetCurrentBonus(Entity* self);

View File

@@ -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<MovingPlatformComponent>(-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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
info.settings.Insert<bool>(u"startPathingOnLoad", false);
Entity entity(288300744895900109, info);
info.settings.values.clear();
auto* const platform = entity.AddComponent<MovingPlatformComponent>(-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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
info.pos = NiPoint3(0.0f, 0.0f, 0.0f);
Entity entity(288300744895900110, info);
auto* const simplePhysics = entity.AddComponent<SimplePhysicsComponent>(-1);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(-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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
Entity entity(288300744895900111, info);
entity.AddComponent<SimplePhysicsComponent>(-1);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(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<SimplePhysicsComponent>(-1);
auto* const platform = counterweight.AddComponent<MovingPlatformComponent>(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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
info.pos = NiPoint3(1.0f, 2.0f, 3.0f);
info.rot = QuatUtils::IDENTITY;
info.settings.Insert<bool>(u"platformIsRotater", true);
info.settings.Insert<float>(u"rotY", 0.5f);
info.settings.Insert<float>(u"rate", 2.0f);
Entity entity(288300744895900113, info);
info.settings.values.clear();
auto* const simplePhysics = entity.AddComponent<SimplePhysicsComponent>(-1);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(-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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
info.pos = NiPoint3(0.0f, 0.0f, 0.0f);
Entity entity(288300744895900114, info);
entity.AddComponent<SimplePhysicsComponent>(-1);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(-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<SimplePhysicsComponent>(-1);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(41, "");
auto* const mover = platform->GetSimpleMoverSubComponent();
EXPECT_EQ(mover->mState, 9u);
entity.Update(0.05f);
EXPECT_EQ(mover->mState, 9u);
entity.Update(0.06f);
EXPECT_EQ(mover->mState, 2u);
entity.Update(0.0f);
entity.Update(1.1f);
EXPECT_EQ(mover->mState, 28u);
EXPECT_EQ(mover->mCurrentWaypointIndex, 1u);
// Stopped at its end it does not set off again by itself
entity.Update(1.0f);
EXPECT_EQ(mover->mState, 28u);
}
{
Entity entity(288300744895900116, info);
auto* const platform = entity.AddComponent<MovingPlatformComponent>(41, "");
auto* const mover = platform->GetSimpleMoverSubComponent();
platform->StopPathing();
EXPECT_EQ(mover->mState, 12u);
entity.Update(1.0f);
EXPECT_EQ(mover->mState, 12u);
EXPECT_EQ(mover->mCurrentWaypointIndex, 0u);
}
{
info.settings.Insert<bool>(u"startPathingOnLoad", false);
Entity entity(288300744895900117, info);
info.settings.values.clear();
auto* const platform = entity.AddComponent<MovingPlatformComponent>(41, "");
auto* const mover = platform->GetSimpleMoverSubComponent();
EXPECT_EQ(mover->mState, 12u);
entity.Update(1.0f);
EXPECT_EQ(mover->mState, 12u);
}
}
// 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<bool>(u"startPathingOnLoad", false);
info.settings.Insert<std::u16string>(u"name_deactivated_event", u"blades_off");
Entity entity(288300744895900118, info);
info.settings.values.clear();
auto* const platform = entity.AddComponent<MovingPlatformComponent>(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<Zone*>(Game::zoneManager->GetZone())->AddPath(path);
info.settings.Insert<bool>(u"startPathingOnLoad", false);
Entity entity(288300744895900119, info);
info.settings.values.clear();
auto* const platform = entity.AddComponent<MovingPlatformComponent>(-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);
}

View File

@@ -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);