mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
Merge exp/platforms-3: moving platforms rounds 3-4
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
37
dScripts/02_server/Map/General/SimpleMoverSwitch.cpp
Normal file
37
dScripts/02_server/Map/General/SimpleMoverSwitch.cpp
Normal 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);
|
||||
}
|
||||
13
dScripts/02_server/Map/General/SimpleMoverSwitch.h
Normal file
13
dScripts/02_server/Map/General/SimpleMoverSwitch.h
Normal 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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
24
dScripts/02_server/Map/njhub/FireTransRocks.cpp
Normal file
24
dScripts/02_server/Map/njhub/FireTransRocks.cpp
Normal 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);
|
||||
}
|
||||
11
dScripts/02_server/Map/njhub/FireTransRocks.h
Normal file
11
dScripts/02_server/Map/njhub/FireTransRocks.h
Normal 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;
|
||||
};
|
||||
53
dScripts/02_server/Map/njhub/FireTransRocksManager.cpp
Normal file
53
dScripts/02_server/Map/njhub/FireTransRocksManager.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
15
dScripts/02_server/Map/njhub/FireTransRocksManager.h
Normal file
15
dScripts/02_server/Map/njhub/FireTransRocksManager.h
Normal 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);
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user