From b035f60e7232aeeec1c25596fba7c7512ab7023b Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 04:41:44 -0500 Subject: [PATCH] fix(replica): send each object's real age as time_since_created_on_server Live sent the milliseconds since the object was created on the server in every construction header (nonzero in 142,258 of 145,677 captured constructions; the same object constructed again later carries a larger value by the time elapsed; the local player's own construction on load is 0). DLU always wrote 0. The client stores it in the object's config (LWOGameObject::time_since_created_on_server). Entities now remember when they were created (steady clock) and write their age. Test: ReplicaConstructionTest.TimeSinceCreatedOnServerIsTheObjectsAge. Check in game: zone in and walk around several worlds; objects, NPCs, enemies and other players appear and animate as before (no objects popping, stuck or misplaced animations on platforms/spinners). Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 4 +- dGame/Entity.h | 9 ++ .../dComponentsTests/CMakeLists.txt | 1 + .../ReplicaConstructionTests.cpp | 86 +++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 111afd67c..0faccd1d6 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -997,7 +997,9 @@ void Entity::WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacke outBitStream.Write(name.size()); outBitStream.Write(name); - outBitStream.Write(0); //Time since created on server + // Live sent the object's age on this server in milliseconds: 0 for an object constructed as it is created + // (a player's own construction on load), the time since load for zone objects. + outBitStream.Write(GetTimeSinceCreatedMs()); const auto& syncLDF = GetVar>(u"syncLDF"); diff --git a/dGame/Entity.h b/dGame/Entity.h index 35b187274..cb297441f 100644 --- a/dGame/Entity.h +++ b/dGame/Entity.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -218,6 +219,11 @@ public: std::unordered_map& GetComponents() { return m_Components; } // TODO: Remove void WriteBaseReplicaData(RakNet::BitStream& outBitStream, eReplicaPacketType packetType); + + // Milliseconds since this object was created on this server (the construction's time_since_created_on_server). + uint32_t GetTimeSinceCreatedMs() const { + return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - m_CreationTime).count()); + } void WriteComponents(RakNet::BitStream& outBitStream, eReplicaPacketType packetType) const; void UpdateXMLDoc(tinyxml2::XMLDocument& doc); void Update(float deltaTime); @@ -433,6 +439,9 @@ private: bool m_IsParentChildDirty = true; + // When this object was created on this server. Construction sends its age from this, as live did. + std::chrono::steady_clock::time_point m_CreationTime = std::chrono::steady_clock::now(); + bool m_IsSleeping = false; /* diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 4e2ada537..97ccf063b 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -15,6 +15,7 @@ set(DCOMPONENTS_TESTS "ReplicaComponentOrderTests.cpp" "QuickBuildCompleteTests.cpp" "RocketLaunchTests.cpp" + "ReplicaConstructionTests.cpp" ) # Get the folder name and prepend it to the files above diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp new file mode 100644 index 000000000..74a66e9df --- /dev/null +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -0,0 +1,86 @@ +// Object construction data as live sent it. The expected bits are built field by field in the order the client reads +// them (lu_packets replica structs), with the values of live constructions from the 2011/2012 captures. +#include "GameDependencies.h" +#include + +#include +#include + +#include "BitStream.h" +#include "CDClientDatabase.h" +#include "CDComponentsRegistryTable.h" +#include "CDInventoryComponentTable.h" +#include "CDItemComponentTable.h" +#include "Character.h" +#include "CharacterComponent.h" +#include "ControllablePhysicsComponent.h" +#include "DestroyableComponent.h" +#include "Entity.h" +#include "InventoryComponent.h" +#include "ItemComponent.h" +#include "Item.h" +#include "ModelComponent.h" +#include "MovingPlatformComponent.h" +#include "SimplePhysicsComponent.h" +#include "User.h" +#include "eReplicaComponentType.h" +#include "eReplicaPacketType.h" + +namespace { + // Copies the bits of `stream` from bit `from` on into `tail` + void Tail(RakNet::BitStream& stream, const uint32_t from, RakNet::BitStream& tail) { + stream.SetReadOffset(from); + const auto bits = stream.GetNumberOfBitsUsed() - from; + std::vector buffer(BITS_TO_BYTES(bits) + 1); + stream.ReadBits(buffer.data(), bits, false); + tail.WriteBits(buffer.data(), bits, false); + } + + // The stream's bits as '0'/'1' characters, so a failure shows where they differ + std::string Bits(const RakNet::BitStream& stream) { + std::string bits; + for (uint32_t i = 0; i < stream.GetNumberOfBitsUsed(); ++i) bits += (stream.GetData()[i / 8] & (0x80 >> (i % 8))) ? '1' : '0'; + return bits; + } + + void ExpectSameBits(const RakNet::BitStream& actual, const RakNet::BitStream& expected) { + EXPECT_EQ(Bits(actual), Bits(expected)); + } +} + +class ReplicaConstructionTest : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +// time_since_created_on_server: live sent each object's age on the server in milliseconds (0 for the local player's own +// construction right after it was made, the zone's uptime for objects loaded with the zone). DLU sent 0. +TEST_F(ReplicaConstructionTest, TimeSinceCreatedOnServerIsTheObjectsAge) { + info.lot = 12266; + Entity entity(288300744895979394, info); + + const auto readTime = [&entity]() { + RakNet::BitStream stream; + entity.WriteBaseReplicaData(stream, eReplicaPacketType::CONSTRUCTION); + LWOOBJID objectID{}; + LOT lot{}; + uint8_t nameLength{}; + uint32_t time{}; + EXPECT_TRUE(stream.Read(objectID)); + EXPECT_TRUE(stream.Read(lot)); + EXPECT_TRUE(stream.Read(nameLength)); + EXPECT_EQ(nameLength, 0); + EXPECT_TRUE(stream.Read(time)); + EXPECT_EQ(objectID, 288300744895979394); + EXPECT_EQ(lot, 12266); + return time; + }; + + EXPECT_LT(readTime(), 1000u); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + const auto later = readTime(); + EXPECT_GE(later, 30u); + EXPECT_LT(later, 60000u); + EXPECT_GE(entity.GetTimeSinceCreatedMs(), later); +}