diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index da24fa3fa..60f6ddbf6 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -98,6 +98,12 @@ void ModelComponent::LoadBehaviors() { // add behavior at the back LoadBehavior(behaviorId.value(), m_Behaviors.size(), false); } + + // Live constructed models with behaviors as dynamic and models without as keyframed (655 of 666 model constructions) + auto* const simplePhysics = m_Parent->GetComponent(); + if (simplePhysics && !m_Parent->HasVar(u"motionType")) { + simplePhysics->SetPhysicsMotionState(m_Behaviors.empty() ? SimplePhysicsComponent::MOTION_TYPE_KEYFRAMED : SimplePhysicsComponent::MOTION_TYPE_DYNAMIC); + } } void ModelComponent::LoadBehavior(const LWOOBJID behaviorID, const size_t index, const bool isIndexed) { diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 18911fc19..186944337 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -65,6 +65,10 @@ MovingPlatformComponent::MovingPlatformComponent(Entity* parent, const int32_t c if (m_Path == nullptr) { LOG("Path not found: %s", pathName.c_str()); } + + // Live constructed every moving platform with simple physics as keyframed + auto* const simplePhysics = m_Parent->GetComponent(); + if (simplePhysics && !m_Parent->HasVar(u"motionType")) simplePhysics->SetPhysicsMotionState(SimplePhysicsComponent::MOTION_TYPE_KEYFRAMED); } MovingPlatformComponent::~MovingPlatformComponent() { diff --git a/dGame/dComponents/SimplePhysicsComponent.cpp b/dGame/dComponents/SimplePhysicsComponent.cpp index cb888459b..95e58735e 100644 --- a/dGame/dComponents/SimplePhysicsComponent.cpp +++ b/dGame/dComponents/SimplePhysicsComponent.cpp @@ -31,7 +31,8 @@ SimplePhysicsComponent::SimplePhysicsComponent(Entity* parent, const int32_t com } else { SetClimbableType(eClimbableType::CLIMBABLE_TYPE_NOT); } - m_PhysicsMotionState = m_Parent->GetVarAs(u"motionType"); + // Without a motionType the object is fixed (live: Fixed on 38,970 of 40,786 constructions, never the invalid 0) + if (m_Parent->HasVar(u"motionType")) m_PhysicsMotionState = m_Parent->GetVarAs(u"motionType"); } SimplePhysicsComponent::~SimplePhysicsComponent() { @@ -50,13 +51,16 @@ void SimplePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bool bIs outBitStream.Write(m_ClimbableType); } - outBitStream.Write(m_DirtyVelocity || bIsInitialUpdate); - if (m_DirtyVelocity || bIsInitialUpdate) { + // Live left the velocity out of a fixed object's construction and wrote it for keyframed and dynamic ones. + const bool writeVelocity = bIsInitialUpdate + ? m_PhysicsMotionState != MOTION_TYPE_FIXED || m_Velocity != NiPoint3Constant::ZERO || m_AngularVelocity != NiPoint3Constant::ZERO + : m_DirtyVelocity; + outBitStream.Write(writeVelocity); + if (writeVelocity) { outBitStream.Write(m_Velocity); outBitStream.Write(m_AngularVelocity); - - m_DirtyVelocity = false; } + if (writeVelocity || bIsInitialUpdate) m_DirtyVelocity = false; // Physics motion state outBitStream.Write(m_DirtyPhysicsMotionState || bIsInitialUpdate); diff --git a/dGame/dComponents/SimplePhysicsComponent.h b/dGame/dComponents/SimplePhysicsComponent.h index 402689221..cf1639d78 100644 --- a/dGame/dComponents/SimplePhysicsComponent.h +++ b/dGame/dComponents/SimplePhysicsComponent.h @@ -30,6 +30,11 @@ class SimplePhysicsComponent : public PhysicsComponent { public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::SIMPLE_PHYSICS; + // Havok motion types the client reads as the physics motion state + static constexpr uint32_t MOTION_TYPE_DYNAMIC = 1; + static constexpr uint32_t MOTION_TYPE_KEYFRAMED = 4; + static constexpr uint32_t MOTION_TYPE_FIXED = 5; + SimplePhysicsComponent(Entity* parent, const int32_t componentID); ~SimplePhysicsComponent() override; @@ -106,7 +111,7 @@ private: /** * The current physics motion state */ - uint32_t m_PhysicsMotionState = 5; + uint32_t m_PhysicsMotionState = MOTION_TYPE_FIXED; bool m_DirtyPhysicsMotionState = true; diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 1824cf2e6..46c0d215e 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -159,6 +159,65 @@ TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionWritesCheatsOnlyW EXPECT_FLOAT_EQ(cheats.second, 1.05f); } +// Simple physics: live sent motion type Fixed for objects without a motionType (never the invalid 0 DLU sent when the +// level gave none) and no velocity for fixed objects (38,970 Fixed constructions, all without velocity); moving +// platforms were Keyframed with a velocity (1,059 of 1,059). +TEST_F(ReplicaConstructionTest, SimplePhysicsConstructionLikeLive) { + info.lot = 12266; + Entity smashable(288300744895979394, info); + auto* const physics = smashable.AddComponent(-1); + physics->SetPosition(NiPoint3(-259.89438f, 77.147575f, 485.42264f)); + NiQuaternion rotation = QuatUtils::IDENTITY; + rotation.x = 0.51204455f; + rotation.y = 0.0f; + rotation.z = 0.0f; + rotation.w = 0.8589589f; + physics->SetRotation(rotation); + + RakNet::BitStream construction; + physics->Serialize(construction, true); + RakNet::BitStream expected; + expected.Write0(); // is_climbable + expected.Write(0); // climbing_property + expected.Write0(); // velocity_info None + expected.Write1(); // motion_type Some + expected.Write(5); // Fixed + expected.Write1(); // position_rotation_info + expected.Write(-259.89438f); + expected.Write(77.147575f); + expected.Write(485.42264f); + expected.Write(0.51204455f); // x, y, z, w + expected.Write(0.0f); + expected.Write(0.0f); + expected.Write(0.8589589f); + ExpectSameBits(construction, expected); + + info.lot = 11950; + Entity platform(288300744895900003, info); + auto* const platformPhysics = platform.AddComponent(-1); + platform.AddComponent(-1, ""); + EXPECT_EQ(platformPhysics->GetPhysicsMotionState(), SimplePhysicsComponent::MOTION_TYPE_KEYFRAMED); + RakNet::BitStream platformConstruction; + platformPhysics->Serialize(platformConstruction, true); + platformConstruction.IgnoreBits(1 + 32); + bool hasVelocity{}; + EXPECT_TRUE(platformConstruction.Read(hasVelocity)); + EXPECT_TRUE(hasVelocity); + + // A property model without behaviors is keyframed + info.lot = 14; + Entity model(288300744895908946, info); + auto* const modelPhysics = model.AddComponent(-1); + model.AddComponent(-1)->LoadBehaviors(); + EXPECT_EQ(modelPhysics->GetPhysicsMotionState(), SimplePhysicsComponent::MOTION_TYPE_KEYFRAMED); + + // A level-set motion type wins + info.settings.Insert(u"motionType", 1); + Entity dynamic(288300744895900004, info); + EXPECT_EQ(dynamic.AddComponent(-1)->GetPhysicsMotionState(), SimplePhysicsComponent::MOTION_TYPE_DYNAMIC); + info.settings.values.clear(); +} + class InventoryConstructionTest : public GameDependenciesTest { protected: // The items of the live sample below (CDClient 1.10.64 values)