diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 11c9cb8b7..e1639db37 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -285,7 +285,21 @@ void MovingPlatformComponent::OnQuickBuildInitilized() { void MovingPlatformComponent::OnCompleteQuickBuild() { if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { - GetSimpleMoverSubComponent()->mDirtyState = true; + // 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). + auto* const simpleMover = GetSimpleMoverSubComponent(); + simpleMover->mDirtyState = true; + const bool startOnLoad = !m_Parent->HasVar(u"startPathingOnLoad") || m_Parent->GetVar(u"startPathingOnLoad"); + if (!startOnLoad || (simpleMover->mState & ePlatformStateFlag::Travelling) != 0) return; + const auto generation = ++m_MoveGeneration; + m_Parent->AddCallbackTimer(0.1f, [this, generation] { + if (generation != m_MoveGeneration) return; + auto* const simpleMover = GetSimpleMoverSubComponent(); + if (simpleMover) SimpleMoverGotoWaypoint(simpleMover->mCurrentWaypointIndex == 0 ? 1 : 0); + }); return; } @@ -728,13 +742,19 @@ void MovingPlatformComponent::OnSimpleMoverArrived(const int32_t index) { auto* const simpleMover = GetSimpleMoverSubComponent(); if (!simpleMover) return; - // LWOPlatform::ArrivedAtWaypoint: both ends are the path's ends, and a path run once stops there + // LWOPlatform::ArrivedAtWaypoint: both ends are the path's ends, and a path run once stops there (28) and turns + // round. Live did not serialize it (each client's simple mover arrives by itself), and the object is moved ignoring + // the dirty flags (ProcessStateChange 0x00cce380). simpleMover->mCurrentWaypointIndex = index; simpleMover->mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint | ePlatformStateFlag::ReachedFinalDestination; simpleMover->mInReverse = index == 1; - simpleMover->mDirtyState = true; - m_Parent->SetPosition(simpleMover->GetWaypointPosition(index)); - Game::entityManager->SerializeEntity(m_Parent); + const auto position = simpleMover->GetWaypointPosition(index); + if (auto* const simplePhysics = m_Parent->GetComponent()) { + simplePhysics->SetPositionAndRotationIgnoringDirtyFlags(position, simpleMover->mStartRotation); + } + if (auto* const phantomPhysics = m_Parent->GetComponent()) { + phantomPhysics->SetPositionAndRotationIgnoringDirtyFlags(position, simpleMover->mStartRotation); + } FireArrivalHooks(index, true, index == 1); } @@ -745,7 +765,16 @@ void MovingPlatformComponent::FireArrivalHooks(const uint32_t index, const bool auto* const script = m_Parent->GetScript(); script->OnWaypointReached(m_Parent, index); if (atDesiredWaypoint) script->OnArrivedAtDesiredWaypoint(m_Parent, index); - if (atLastWaypoint) script->OnPlatformAtLastWaypoint(m_Parent); + if (atLastWaypoint) { + script->OnPlatformAtLastWaypoint(m_Parent); + // A callback may add another + const auto callbacks = m_AtLastWaypointCallbacks; + for (const auto& callback : callbacks) callback(); + } +} + +void MovingPlatformComponent::AddPlatformAtLastWaypointCallback(const std::function& callback) { + m_AtLastWaypointCallbacks.push_back(callback); } void MovingPlatformComponent::Allocator(Entity& entity, ObjectLoadState& state) { diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index 25536a2a1..59d5b9b5c 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -6,6 +6,7 @@ #ifndef MOVINGPLATFORMCOMPONENT_H #define MOVINGPLATFORMCOMPONENT_H +#include #include #include @@ -275,6 +276,12 @@ public: eMoverSubComponentType GetMoverSubComponentType() const { return m_MoverSubComponentType; } + /** + * Calls back whenever the platform is at the last waypoint of its path (the PlatformAtLastWaypoint game message, + * which live's scripts ask to be notified of on the NJ counterweights) + */ + void AddPlatformAtLastWaypointCallback(const std::function& callback); + /** * Called when a simple mover gets to the waypoint it was sent to: the client's LWOPlatform::ArrivedAtWaypoint * and HandleWaypointArrived (Arrived, then ArrivedAtDesiredWaypoint and PlatformAtLastWaypoint) @@ -315,6 +322,8 @@ private: */ std::vector> m_SubComponents; + std::vector> m_AtLastWaypointCallbacks; + // The object's quick build, looked up when the platform first runs class QuickBuildComponent* m_QuickBuild = nullptr; bool m_CheckedQuickBuild = false; diff --git a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp index 6ce4e0868..04b4d6690 100644 --- a/dScripts/02_server/Map/njhub/CavePrisonCage.cpp +++ b/dScripts/02_server/Map/njhub/CavePrisonCage.cpp @@ -84,19 +84,16 @@ void CavePrisonCage::SpawnCounterweight(Entity* self, Spawner* spawner) { }); quickBuildComponent->AddQuickBuildCompleteCallback([this, self](Entity* user) { - auto* counterweight = Game::entityManager->GetEntity(self->GetVar(u"Counterweight")); - - if (counterweight == nullptr) { - return; - } - - // The counterweight is a simple mover (MovingPlatforms row 41): send it down to its end - auto* const platform = counterweight->GetComponent(); - if (platform) platform->GotoWaypoint(1); - // Save the userID as Builder self->SetVar(u"Builder", user->GetObjectID()); + }); + } + // L_CAVE_PRISON_CAGE (notifyPlatformAtLastWaypoint): the built counterweight, a simple mover (MovingPlatforms row + // 41) that sets off by itself when its quick build completes, has gone down to the end of its path + auto* const platform = counterweight->GetComponent(); + if (platform) { + platform->AddPlatformAtLastWaypointCallback([this, self]() { // Get the button and make sure it still exists auto* button = Game::entityManager->GetEntity(self->GetVar(u"Button")); diff --git a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp index 965662b75..2b2b01253 100644 --- a/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp +++ b/dScripts/02_server/Map/njhub/boss_instance/NjMonastryBossInstance.cpp @@ -1,5 +1,6 @@ #include "NjMonastryBossInstance.h" #include "QuickBuildComponent.h" +#include "MovingPlatformComponent.h" #include "DestroyableComponent.h" #include "EntityManager.h" #include "dZoneManager.h" @@ -237,44 +238,49 @@ void NjMonastryBossInstance::HandleCounterWeightSpawned(Entity* self, Entity* co case eQuickBuildState::RESETTING: ActivityTimerStart(self, SpawnCounterWeightTimer, 0.0f, 0.0f); return; - case eQuickBuildState::COMPLETED: { - // TODO: Move the platform? - - // The counterweight is actually a moving platform and we should listen to the last waypoint event here - // 0.5f is a rough estimate of that path, though, and results in less needed logic - self->AddCallbackTimer(0.5f, [this, self, counterWeight]() { - if (counterWeight != nullptr) { - counterWeight->Kill(); - } - - auto* frakjaw = Game::entityManager->GetEntity(self->GetVar(LedgeFrakjawVariable)); - if (frakjaw == nullptr) { - GameMessages::NotifyClientObject(self->GetObjectID(), u"LedgeFrakjawDead", 0, 0, LWOOBJID_EMPTY, "").Send(UNASSIGNED_SYSTEM_ADDRESS); - return; - } - - auto* skillComponent = frakjaw->GetComponent(); - if (skillComponent != nullptr) { - skillComponent->CalculateBehavior(1635, 39097, frakjaw->GetObjectID(), true, false); - } - - RenderComponent::PlayAnimation(frakjaw, StunnedAnimation); - GameMessages::PlayNDAudioEmitter(frakjaw->GetObjectID(), CounterSmashAudio).Send(UNASSIGNED_SYSTEM_ADDRESS); - - // Before wave 4 we should lower frakjaw from the ledge - if (self->GetVar(WaveNumberVariable) == 3) { - LowerFrakjaw(self, frakjaw); - return; - } - - ActivityTimerStart(self, SpawnNextWaveTimer, 2.0f, 2.0f); - }); - } default: return; } }); } + + // L_MONASTERY_BOSS_INSTANCE_SERVER (notifyPlatformAtLastWaypoint): the built counterweight has gone down to the end + // of its path. It is a simple mover that sets off by itself when its quick build completes: live sent it + // travelling about 0.1 s after the build, and its trip takes the second of its MovingPlatforms row. + auto* const platform = counterWeight->GetComponent(); + if (platform == nullptr) return; + const auto counterWeightID = counterWeight->GetObjectID(); + platform->AddPlatformAtLastWaypointCallback([this, self, counterWeightID]() { + // Not from inside the counterweight's own arrival + self->AddCallbackTimer(0.0f, [this, self, counterWeightID]() { + auto* const counterWeight = Game::entityManager->GetEntity(counterWeightID); + if (counterWeight != nullptr) { + counterWeight->Kill(); + } + + auto* frakjaw = Game::entityManager->GetEntity(self->GetVar(LedgeFrakjawVariable)); + if (frakjaw == nullptr) { + GameMessages::NotifyClientObject(self->GetObjectID(), u"LedgeFrakjawDead", 0, 0, LWOOBJID_EMPTY, "").Send(UNASSIGNED_SYSTEM_ADDRESS); + return; + } + + auto* skillComponent = frakjaw->GetComponent(); + if (skillComponent != nullptr) { + skillComponent->CalculateBehavior(1635, 39097, frakjaw->GetObjectID(), true, false); + } + + RenderComponent::PlayAnimation(frakjaw, StunnedAnimation); + GameMessages::PlayNDAudioEmitter(frakjaw->GetObjectID(), CounterSmashAudio).Send(UNASSIGNED_SYSTEM_ADDRESS); + + // Before wave 4 we should lower frakjaw from the ledge + if (self->GetVar(WaveNumberVariable) == 3) { + LowerFrakjaw(self, frakjaw); + return; + } + + ActivityTimerStart(self, SpawnNextWaveTimer, 2.0f, 2.0f); + }); + }); } void NjMonastryBossInstance::HandleLowerFrakjawSpawned(Entity* self, Entity* lowerFrakjaw) { diff --git a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp index d231f02b6..dbfc654fb 100644 --- a/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/MovingPlatformComponentTests.cpp @@ -390,3 +390,43 @@ TEST_F(MovingPlatformComponentTest, SetCurrentPathPutsTheMoverOnThePath) { EXPECT_EQ(mover->mState, 28u); EXPECT_EQ(mover->mCurrentWaypointIndex, 2u); } + +// The NJ counterweights in the live captures (LOT 16141, the Frakjaw battle): when the quick build completes the simple +// mover is serialized waiting at waypoint 0 (9), then travelling from it (2); it stops at its end without a +// serialization, where PlatformAtLastWaypoint tells the scripts +TEST_F(MovingPlatformComponentTest, BuiltSimpleMoverSetsOffLikeLive) { + info.lot = 16141; + info.pos = NiPoint3(10.0f, 20.0f, 30.0f); + info.rot = QuatUtils::IDENTITY; + Entity counterweight(288300744895900112, info); + counterweight.AddComponent(-1); + auto* const platform = counterweight.AddComponent(41, ""); + auto* const mover = platform->GetSimpleMoverSubComponent(); + uint32_t atLastWaypoint = 0; + platform->AddPlatformAtLastWaypointCallback([&atLastWaypoint] { atLastWaypoint++; }); + + platform->OnCompleteQuickBuild(); + RakNet::BitStream built; + platform->Serialize(built, false); + EXPECT_EQ(Bits(built), "101" "00000101000000000000000000000000" "0" "1" "00001001000000000000000000000000" "00000000000000000000000000000000" "0" "0"); + + counterweight.Update(0.0f); // timers added now start on the next update + counterweight.Update(0.11f); + EXPECT_EQ(mover->mState, 2u); + RakNet::BitStream travelling; + platform->Serialize(travelling, false); + EXPECT_EQ(Bits(travelling), "101" "00000101000000000000000000000000" "0" "1" "00000010000000000000000000000000" "00000000000000000000000000000000" "0" "0"); + + counterweight.Update(0.0f); + counterweight.Update(0.5f); + EXPECT_EQ(mover->mState, 2u); + EXPECT_EQ(atLastWaypoint, 0u); + counterweight.Update(0.6f); + EXPECT_EQ(mover->mState, 28u); + EXPECT_EQ(mover->mCurrentWaypointIndex, 1u); + EXPECT_EQ(atLastWaypoint, 1u); + EXPECT_EQ(counterweight.GetPosition(), NiPoint3(10.0f, 17.5f, 30.0f)); + RakNet::BitStream arrived; + platform->Serialize(arrived, false); + EXPECT_EQ(Bits(arrived), "00"); +}