diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 76e6f46c8..3a6a41784 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -787,7 +787,8 @@ void Entity::Initialize() { if (path) { // if we have a moving platform path, then we need a moving platform component if (path->pathType == PathType::MovingPlatform) { - AddComponent(-1, pathName); + // The registry component ID picks the subcomponent (a simple mover when it has one), path or not + AddComponent(compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::MOVING_PLATFORM, -1), pathName); } else if (path->pathType == PathType::Movement) { auto* const movementAIcomponent = GetComponent(); if (movementAIcomponent && combatAiID == 0) { diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 4737af31d..345020d4b 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -13,6 +13,9 @@ #include "CppScripts.h" #include "SimplePhysicsComponent.h" #include "Zone.h" +#include "CDClientDatabase.h" + +#include MoverSubComponent::MoverSubComponent(const NiPoint3& startPos) { mPosition = {}; @@ -53,11 +56,93 @@ void MoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsIniti outBitStream.Write(0.0f); // Move time elapsed } +//------------- SimpleMoverSubComponent below -------------- + +// LWOPlatformSimpleMover::Deserialize: a dirty bit, then whether there is a starting point with its position and +// rotation (w, x, y, z); a dirty bit, then the state, the current waypoint and whether it is reversing +void SimpleMoverSubComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { + outBitStream.Write(bIsInitialUpdate || mDirtyStartingPoint); + if (bIsInitialUpdate || mDirtyStartingPoint) { + outBitStream.Write1(); + outBitStream.Write(mStartPosition.x); + outBitStream.Write(mStartPosition.y); + outBitStream.Write(mStartPosition.z); + outBitStream.Write(mStartRotation.w); + outBitStream.Write(mStartRotation.x); + outBitStream.Write(mStartRotation.y); + outBitStream.Write(mStartRotation.z); + if (!bIsInitialUpdate) mDirtyStartingPoint = false; + } + + outBitStream.Write(bIsInitialUpdate || mDirtyState); + if (bIsInitialUpdate || mDirtyState) { + outBitStream.Write(mState); + outBitStream.Write(mCurrentWaypointIndex); + outBitStream.Write(mInReverse); + if (!bIsInitialUpdate) mDirtyState = false; + } +} + +NiPoint3 SimpleMoverSubComponent::GetWaypointPosition(const int32_t index) const { + if (index <= 0) return mStartPosition; + const glm::vec3 moved = mStartRotation * glm::vec3(mMove.x, mMove.y, mMove.z); + return mStartPosition + NiPoint3(moved.x, moved.y, moved.z); +} + +eMoverSubComponentType ChooseMoverSubComponentType(const int32_t componentID, const Entity& entity) { + const bool setRotater = entity.HasVar(u"platformIsRotater"); + const bool setMover = entity.HasVar(u"platformIsMover"); + const bool setSimpleMover = entity.HasVar(u"platformIsSimpleMover"); + + if (!setRotater && !setMover && !setSimpleMover) { + return componentID < 1 ? eMoverSubComponentType::mover : eMoverSubComponentType::simpleMover; + } + + if (setMover && entity.GetVar(u"platformIsMover")) return eMoverSubComponentType::mover; + if (setSimpleMover && entity.GetVar(u"platformIsSimpleMover")) return eMoverSubComponentType::simpleMover; + if (setRotater && entity.GetVar(u"platformIsRotater")) return eMoverSubComponentType::rotater; + return eMoverSubComponentType::none; +} + //------------- MovingPlatformComponent below -------------- MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t componentID, const std::string& pathName) : Component(parent, componentID) { - m_MoverSubComponentType = eMoverSubComponentType::mover; - m_MoverSubComponent = new MoverSubComponent(m_Parent->GetDefaultPosition()); + m_MoverSubComponentType = ChooseMoverSubComponentType(componentID, *m_Parent); + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + auto* const simpleMover = new SimpleMoverSubComponent(); + m_MoverSubComponent = simpleMover; + simpleMover->mStartPosition = m_Parent->GetDefaultPosition(); + simpleMover->mStartRotation = m_Parent->GetDefaultRotation(); + + // LWOPlatformSimpleMover::LoadTemplateData: the MovingPlatforms row, unless the object sets dbonly false + const bool dbOnly = m_Parent->GetVar(u"dbonly"); + if (componentID > 0 && (!m_Parent->HasVar(u"dbonly") || dbOnly)) { + auto query = CDClientDatabase::CreatePreppedStmt( + "SELECT platformMoveX, platformMoveY, platformMoveZ, platformMoveTime, platformStartAtEnd FROM MovingPlatforms WHERE id = ?;"); + query.bind(1, componentID); + auto result = query.execQuery(); + if (!result.eof()) { + simpleMover->mMove = NiPoint3(result.getFloatField(0), result.getFloatField(1), result.getFloatField(2)); + simpleMover->mMoveTime = result.getFloatField(3); + simpleMover->mStartAtEnd = result.getIntField(4) != 0; + } else { + LOG("LOT %i couldn't find simple mover %i in MovingPlatforms", m_Parent->GetLOT(), componentID); + } + } + + // LWOPlatformSimpleMover::LoadConfigData: the object's settings over the table's, unless it is db only + if (m_Parent->HasVar(u"attached_path_start")) simpleMover->mStartAtEnd = m_Parent->GetVar(u"attached_path_start") != 0; + if (m_Parent->HasVar(u"platformStartAtEnd")) simpleMover->mStartAtEnd = m_Parent->GetVar(u"platformStartAtEnd"); + if (!dbOnly) { + if (m_Parent->HasVar(u"platformMoveX")) simpleMover->mMove.x = m_Parent->GetVar(u"platformMoveX"); + if (m_Parent->HasVar(u"platformMoveY")) simpleMover->mMove.y = m_Parent->GetVar(u"platformMoveY"); + if (m_Parent->HasVar(u"platformMoveZ")) simpleMover->mMove.z = m_Parent->GetVar(u"platformMoveZ"); + if (m_Parent->HasVar(u"platformMoveTime")) simpleMover->mMoveTime = m_Parent->GetVar(u"platformMoveTime"); + } + simpleMover->mCurrentWaypointIndex = simpleMover->mStartAtEnd ? 1 : 0; + } else { + m_MoverSubComponent = new MoverSubComponent(m_Parent->GetDefaultPosition()); + } m_PathName = GeneralUtils::ASCIIToUTF16(pathName); m_Path = Game::zoneManager->GetZone()->GetPath(pathName); m_NoAutoStart = false; @@ -72,10 +157,28 @@ MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t c } MovingPlatformComponent::~MovingPlatformComponent() { - delete static_cast(m_MoverSubComponent); + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) delete static_cast(m_MoverSubComponent); + else delete static_cast(m_MoverSubComponent); } void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { + // Live always constructed a simple mover with its starting point (the client builds its path from it), and + // serialized it only when its state changed: no path, then the one subcomponent and the 0 bit ending the list + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + auto* const simpleMover = GetSimpleMoverSubComponent(); + const bool dirty = bIsInitialUpdate || simpleMover->mDirtyStartingPoint || simpleMover->mDirtyState; + outBitStream.Write(dirty); + outBitStream.Write(bIsInitialUpdate); + if (bIsInitialUpdate) outBitStream.Write0(); + if (dirty) { + outBitStream.Write1(); + outBitStream.Write(m_MoverSubComponentType); + simpleMover->Serialize(outBitStream, bIsInitialUpdate); + outBitStream.Write0(); + } + return; + } + // Here we don't serialize the moving platform to let the client simulate the movement if (!m_Serialize) { @@ -106,18 +209,15 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI outBitStream.Write(false); } - const auto hasPlatform = m_MoverSubComponent != nullptr; + // A platform the client made no subcomponent for must not get one: it could not read it + const auto hasPlatform = m_MoverSubComponentType != eMoverSubComponentType::none; outBitStream.Write(hasPlatform); if (hasPlatform) { + // A rotater's data is a mover's (both read by LWOPlatform::Deserialize) auto* mover = static_cast(m_MoverSubComponent); outBitStream.Write(m_MoverSubComponentType); - - if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { - // TODO - } else { - mover->Serialize(outBitStream, bIsInitialUpdate); - } + mover->Serialize(outBitStream, bIsInitialUpdate); // The client reads subcomponents while a 1 bit comes before one: a 0 bit ends the list outBitStream.Write0(); @@ -125,10 +225,21 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI } void MovingPlatformComponent::OnQuickBuildInitilized() { + // Live left a quick built simple mover (the NJ counterweights) where it was, only resending its state: scripts + // send it on + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + GetSimpleMoverSubComponent()->mDirtyState = true; + return; + } StopPathing(); } void MovingPlatformComponent::OnCompleteQuickBuild() { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + GetSimpleMoverSubComponent()->mDirtyState = true; + return; + } + if (m_NoAutoStart) return; @@ -136,6 +247,14 @@ void MovingPlatformComponent::OnCompleteQuickBuild() { } void MovingPlatformComponent::SetMovementState(eMovementPlatformState value) { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + auto* const simpleMover = GetSimpleMoverSubComponent(); + simpleMover->mState = static_cast(value); + simpleMover->mDirtyState = true; + Game::entityManager->SerializeEntity(m_Parent); + return; + } + auto* subComponent = static_cast(m_MoverSubComponent); subComponent->mState = value; @@ -144,6 +263,11 @@ void MovingPlatformComponent::SetMovementState(eMovementPlatformState value) { } void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + SimpleMoverGotoWaypoint(static_cast(index)); + return; + } + auto* subComponent = static_cast(m_MoverSubComponent); subComponent->mDesiredWaypointIndex = index; @@ -154,6 +278,11 @@ void MovingPlatformComponent::GotoWaypoint(uint32_t index, bool stopAtWaypoint) } void MovingPlatformComponent::StartPathing() { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + // A simple mover's path is start to end: starting sends it to the other end + SimpleMoverGotoWaypoint(GetSimpleMoverSubComponent()->mCurrentWaypointIndex == 0 ? 1 : 0); + return; + } //GameMessages::SendStartPathing(m_Parent); m_PathingStopped = false; @@ -190,7 +319,7 @@ void MovingPlatformComponent::StartPathing() { const auto travelNext = subComponent->mWaitTime + travelTime; m_Parent->AddCallbackTimer(travelTime, [subComponent, this] { - this->m_Parent->GetScript()->OnWaypointReached(m_Parent, subComponent->mNextWaypointIndex); + FireArrivalHooks(subComponent->mNextWaypointIndex, subComponent->mNextWaypointIndex == static_cast(subComponent->mDesiredWaypointIndex)); }); m_Parent->AddCallbackTimer(travelNext, [this] { @@ -203,6 +332,7 @@ void MovingPlatformComponent::StartPathing() { } void MovingPlatformComponent::ContinuePathing() { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) return; auto* subComponent = static_cast(m_MoverSubComponent); subComponent->mState = eMovementPlatformState::Stationary; @@ -300,7 +430,7 @@ void MovingPlatformComponent::ContinuePathing() { const auto travelNext = subComponent->mWaitTime + travelTime; m_Parent->AddCallbackTimer(travelTime, [subComponent, this] { - this->m_Parent->GetScript()->OnWaypointReached(m_Parent, subComponent->mNextWaypointIndex); + FireArrivalHooks(subComponent->mNextWaypointIndex, subComponent->mNextWaypointIndex == static_cast(subComponent->mDesiredWaypointIndex)); }); m_Parent->AddCallbackTimer(travelNext, [this] { @@ -311,6 +441,15 @@ void MovingPlatformComponent::ContinuePathing() { } void MovingPlatformComponent::StopPathing() { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + // LWOPlatformSimpleMover::StopPlatform: it stops where it is, no longer travelling + auto* const simpleMover = GetSimpleMoverSubComponent(); + m_MoveGeneration++; + simpleMover->mState = ePlatformStateFlag::Stopped; + simpleMover->mDirtyState = true; + Game::entityManager->SerializeEntity(m_Parent); + return; + } //m_Parent->CancelCallbackTimers(); auto* subComponent = static_cast(m_MoverSubComponent); @@ -339,6 +478,17 @@ void MovingPlatformComponent::SetNoAutoStart(const bool value) { } void MovingPlatformComponent::WarpToWaypoint(size_t index) { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) { + auto* const simpleMover = GetSimpleMoverSubComponent(); + m_MoveGeneration++; + simpleMover->mCurrentWaypointIndex = index == 0 ? 0 : 1; + simpleMover->mInReverse = simpleMover->mCurrentWaypointIndex == 1; + simpleMover->mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint | ePlatformStateFlag::ReachedFinalDestination; + simpleMover->mDirtyState = true; + m_Parent->SetPosition(simpleMover->GetWaypointPosition(simpleMover->mCurrentWaypointIndex)); + Game::entityManager->SerializeEntity(m_Parent); + return; + } const auto& waypoint = m_Path->pathWaypoints[index]; m_Parent->SetPosition(waypoint.position); @@ -348,9 +498,61 @@ void MovingPlatformComponent::WarpToWaypoint(size_t index) { } size_t MovingPlatformComponent::GetLastWaypointIndex() const { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) return 1; return m_Path->pathWaypoints.size() - 1; } MoverSubComponent* MovingPlatformComponent::GetMoverSubComponent() const { + if (m_MoverSubComponentType == eMoverSubComponentType::simpleMover) return nullptr; return static_cast(m_MoverSubComponent); } + +SimpleMoverSubComponent* MovingPlatformComponent::GetSimpleMoverSubComponent() const { + if (m_MoverSubComponentType != eMoverSubComponentType::simpleMover) return nullptr; + return static_cast(m_MoverSubComponent); +} + +// The client runs the two point path at |platformMove| / platformMoveTime, so it takes platformMoveTime seconds from +// either end. Sending it travelling from its current waypoint towards the other one is all the client needs. +void MovingPlatformComponent::SimpleMoverGotoWaypoint(int32_t index) { + auto* const simpleMover = GetSimpleMoverSubComponent(); + index = index <= 0 ? 0 : 1; + const bool travelling = (simpleMover->mState & ePlatformStateFlag::Travelling) != 0; + if (!travelling && index == simpleMover->mCurrentWaypointIndex) return; + + simpleMover->mCurrentWaypointIndex = index == 1 ? 0 : 1; + simpleMover->mInReverse = index == 0; + simpleMover->mState = ePlatformStateFlag::Travelling; + simpleMover->mDirtyState = true; + Game::entityManager->SerializeEntity(m_Parent); + + const auto generation = ++m_MoveGeneration; + m_Parent->AddCallbackTimer(simpleMover->mMoveTime, [this, generation, index] { + if (generation == m_MoveGeneration) OnSimpleMoverArrived(index); + }); +} + +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 + 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); + + FireArrivalHooks(index, true); +} + +// LWOPlatform::HandleWaypointArrived: Arrived, then ArrivedAtDesiredWaypoint at the waypoint it was sent to, then +// PlatformAtLastWaypoint at either end of the path +void MovingPlatformComponent::FireArrivalHooks(const uint32_t index, const bool atDesiredWaypoint) { + auto* const script = m_Parent->GetScript(); + script->OnWaypointReached(m_Parent, index); + if (atDesiredWaypoint) script->OnArrivedAtDesiredWaypoint(m_Parent, index); + const size_t last = m_MoverSubComponentType == eMoverSubComponentType::simpleMover ? 1 : (m_Path ? m_Path->pathWaypoints.size() - 1 : 0); + if (index == 0 || index == last) script->OnPlatformAtLastWaypoint(m_Parent); +} diff --git a/dGame/dComponents/MovingPlatformComponent.h b/dGame/dComponents/MovingPlatformComponent.h index 065a27867..c84f60f72 100644 --- a/dGame/dComponents/MovingPlatformComponent.h +++ b/dGame/dComponents/MovingPlatformComponent.h @@ -8,6 +8,7 @@ #include "RakNetTypes.h" #include "NiPoint3.h" +#include "NiQuaternion.h" #include #include "dCommonVars.h" @@ -25,9 +26,69 @@ enum class eMoverSubComponentType : uint32_t { mover = 4, /** - * Used in NJ + * A two point mover set up from the MovingPlatforms table or the platformMove* settings (used in NJ) */ - simpleMover = 5, + simpleMover = 5, + + /** + * A mover that turns between rotations along its path (same data as a mover) + */ + rotater = 6, + + /** + * The level config set the platform flags all false: the client makes no subcomponent + */ + none = 0, +}; + +/** + * The client's platform state flags (LWOPlatform) + */ +namespace ePlatformStateFlag { + constexpr uint32_t Stopped = 1; + constexpr uint32_t Travelling = 2; + constexpr uint32_t Waiting = 4; + constexpr uint32_t ReachedDesiredWaypoint = 8; + constexpr uint32_t ReachedFinalDestination = 16; +}; + +/** + * How the client picks the subcomponent (LWOMovingPlatformComponent::LoadConfigData): the platformIsRotater, + * platformIsMover and platformIsSimpleMover settings, and when none is set a mover for a platform with no registry + * component (set up from its attached path) or a simple mover for one with a registry component. + * The client makes one subcomponent per flag set (mover, simple mover, rotater, LWOMovingPlatformComponent::SetupPlatform); + * DLU keeps one, the first in that order. + */ +eMoverSubComponentType ChooseMoverSubComponentType(int32_t componentID, const Entity& entity); + +/** + * A mover between a start point and that point moved by platformMove (in the start rotation), taking + * platformMoveTime seconds, as LWOPlatformSimpleMover::GenerateSimpleMoverPath builds its two waypoint path. + */ +class SimpleMoverSubComponent { +public: + void Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate); + + NiPoint3 mStartPosition{}; + NiQuaternion mStartRotation{}; + NiPoint3 mMove{}; + float mMoveTime = 0.0f; + bool mStartAtEnd = false; + + /** + * ePlatformStateFlag bits. Live constructed simple movers stopped at their desired waypoint. + */ + uint32_t mState = ePlatformStateFlag::Stopped | ePlatformStateFlag::ReachedDesiredWaypoint; + int32_t mCurrentWaypointIndex = 0; + bool mInReverse = false; + + bool mDirtyStartingPoint = false; + bool mDirtyState = false; + + /** + * The world position of a waypoint (0 the start, 1 the end) + */ + NiPoint3 GetWaypointPosition(int32_t index) const; }; /** @@ -187,6 +248,19 @@ public: */ MoverSubComponent* GetMoverSubComponent() const; + /** + * Returns the simple mover sub component, if this platform is a simple mover + */ + SimpleMoverSubComponent* GetSimpleMoverSubComponent() const; + + eMoverSubComponentType GetMoverSubComponentType() const { return m_MoverSubComponentType; } + + /** + * 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) + */ + void OnSimpleMoverArrived(int32_t index); + private: /** @@ -214,6 +288,15 @@ private: */ void* m_MoverSubComponent; + /** + * Counts sends so an arrival timer from an older send does nothing + */ + uint32_t m_MoveGeneration = 0; + + void SimpleMoverGotoWaypoint(int32_t index); + + void FireArrivalHooks(uint32_t index, bool atDesiredWaypoint); + /** * Whether the platform shouldn't auto start */