From 3956df88777f5a180145c6a375baab4002a477ae Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 08:39:23 -0500 Subject: [PATCH] fix(moving platform): end the subcomponent list with a 0 bit The client reads a moving platform's subcomponents while a 1 bit comes before one (LWOMovingPlatformComponent::Deserialize), as live wrote them. DLU wrote the mover without the closing 0 bit, so the client took the next component's first bit for it. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/MovingPlatformComponent.cpp | 3 +++ .../ReplicaConstructionTests.cpp | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/dGame/dComponents/MovingPlatformComponent.cpp b/dGame/dComponents/MovingPlatformComponent.cpp index 186944337..4737af31d 100644 --- a/dGame/dComponents/MovingPlatformComponent.cpp +++ b/dGame/dComponents/MovingPlatformComponent.cpp @@ -118,6 +118,9 @@ void MovingPlatformComponent::Serialize(RakNet::BitStream& outBitStream, bool bI } else { mover->Serialize(outBitStream, bIsInitialUpdate); } + + // The client reads subcomponents while a 1 bit comes before one: a 0 bit ends the list + outBitStream.Write0(); } } diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index d5559e476..e869e1cd5 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -258,6 +258,33 @@ TEST_F(ReplicaConstructionTest, SimplePhysicsConstructionLikeLive) { info.settings.values.clear(); } +// The client reads a moving platform's subcomponents while a 1 bit comes before one (as live wrote them): the list +// ends with a 0 bit, which DLU left out, so the client took the next component's first bit for it +TEST_F(ReplicaConstructionTest, MovingPlatformEndsItsSubcomponentList) { + info.lot = 2349; + Entity platform(288300744895900005, info); + auto* const component = platform.AddComponent(-1, ""); + component->SetSerialized(true); + RakNet::BitStream stream; + component->Serialize(stream, true); + + bool bit{}; + ASSERT_TRUE(stream.Read(bit)); + EXPECT_TRUE(bit); // has subcomponents + ASSERT_TRUE(stream.Read(bit)); + EXPECT_FALSE(bit); // no path + ASSERT_TRUE(stream.Read(bit)); + EXPECT_TRUE(bit); // a subcomponent follows + uint32_t type{}; + ASSERT_TRUE(stream.Read(type)); + EXPECT_EQ(type, 4u); // mover + // The mover: a 1 bit, state, desired waypoint, 2 flags, percent, position, current and next waypoint, 2 times + stream.IgnoreBits(1 + 32 + 32 + 2 + 32 + 96 + 32 + 32 + 32 + 32); + ASSERT_TRUE(stream.Read(bit)); + EXPECT_FALSE(bit); // no more subcomponents + EXPECT_EQ(stream.GetNumberOfUnreadBits(), 0u); +} + // Live replicated the DestructibleComponent factionList, so a list of -1 as [-1] (12,829 constructions: vendors, // quickbuilds, bouncers; DLU dropped it and sent []), and a row with no faction but factionList 6 as [6]. TEST_F(ReplicaConstructionTest, TemplateFactionMinusOneIsReplicated) {