From bdc917075425924c0a7c9311ebe95f1799fc79fe Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 05:39:10 -0500 Subject: [PATCH] fix(replica): write the item component's UGC info, after skill and combat AI Live wrote the item component's UGC info on every construction of an object with an item component (99 of 99 constructions of LOT 14535, a possessable with item, skill and controllable physics and no Objects row: ug_id 0, moderation status NoStatus, no description). DLU wrote a 0 bit. The item component is read after the skill and base-combat-AI components (ComponentOrderVector::Initialize 0x0101f8e0; live constructions of LOT 14535 have Skill then Item). DLU wrote it before the inventory, which did not show while it was a single 0 bit next to the skill's 0 bit, but would misalign every component after it now that it writes more. It now comes after BASE_COMBAT_AI. The ReplicaComponentOrderTests oracle moves the item the same way (intended change). Test: ReplicaConstructionTest.ItemConstructionWritesEmptyUgcInfo, ReplicaComponentOrderTest.EveryListedComponent (oracle updated). Check in game: with a second player watching, use a possessable / mount / vehicle item (e.g. a racing car in the race lobby); it appears, moves and is left normally for both. Rocket launches and property models look as before. Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 4 ++-- dGame/dComponents/ItemComponent.cpp | 9 ++++++++- .../ReplicaComponentOrderTests.cpp | 13 ++++++++----- .../ReplicaConstructionTests.cpp | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index eabd86caa..f6998d806 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -1074,8 +1074,8 @@ namespace { using enum eReplicaComponentType; return std::array{ POSSESSABLE, MODULE_ASSEMBLY, CONTROLLABLE_PHYSICS, SIMPLE_PHYSICS, RIGID_BODY_PHANTOM_PHYSICS, HAVOK_VEHICLE_PHYSICS, - PHANTOM_PHYSICS, SOUND_TRIGGER, RACING_SOUND_TRIGGER, BUFF, DESTROYABLE, COLLECTIBLE, PET, CHARACTER, ITEM, INVENTORY, - SCRIPT, SKILL, BASE_COMBAT_AI, QUICK_BUILD, MOVING_PLATFORM, SWITCH, VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, + PHANTOM_PHYSICS, SOUND_TRIGGER, RACING_SOUND_TRIGGER, BUFF, DESTROYABLE, COLLECTIBLE, PET, CHARACTER, INVENTORY, + SCRIPT, SKILL, BASE_COMBAT_AI, ITEM, QUICK_BUILD, MOVING_PLATFORM, SWITCH, VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, LUP_EXHIBIT, MODEL, RENDER, MINI_GAME_CONTROL, }; }(); diff --git a/dGame/dComponents/ItemComponent.cpp b/dGame/dComponents/ItemComponent.cpp index 799cd935d..6d4969bc4 100644 --- a/dGame/dComponents/ItemComponent.cpp +++ b/dGame/dComponents/ItemComponent.cpp @@ -1,5 +1,12 @@ #include "ItemComponent.h" void ItemComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) { - outBitStream.Write0(); + // Live wrote the user-generated-content info on every construction of an object with only an item component: + // no UGC id, moderation status NoStatus, no description. + outBitStream.Write(isConstruction); + if (isConstruction) { + outBitStream.Write(LWOOBJID_EMPTY); // ug_id + outBitStream.Write(0); // ug_moderation_status NoStatus + outBitStream.Write0(); // ug_description + } } diff --git a/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp index 1995aec34..d9fad33aa 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaComponentOrderTests.cpp @@ -172,11 +172,6 @@ namespace { characterComponent->Serialize(outBitStream, bIsInitialUpdate); } - ItemComponent* itemComponent; - if (TryGet(entity, itemComponent)) { - itemComponent->Serialize(outBitStream, bIsInitialUpdate); - } - InventoryComponent* inventoryComponent; if (TryGet(entity, inventoryComponent)) { inventoryComponent->Serialize(outBitStream, bIsInitialUpdate); @@ -197,6 +192,14 @@ namespace { baseCombatAiComponent->Serialize(outBitStream, bIsInitialUpdate); } + // The item comes after the skill and combat AI components, as the client reads them (ComponentOrderVector::Initialize + // 0x0101f8e0; live constructions of LOT 14535). It used to come before the inventory, which was harmless while + // it wrote a single 0 bit. + ItemComponent* itemComponent; + if (TryGet(entity, itemComponent)) { + itemComponent->Serialize(outBitStream, bIsInitialUpdate); + } + QuickBuildComponent* quickBuildComponent; if (TryGet(entity, quickBuildComponent)) { DestroyableComponent* destroyableComponent; diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 96a650939..9f0fa6e9d 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -242,6 +242,25 @@ TEST_F(ReplicaConstructionTest, TemplateFactionMinusOneIsReplicated) { EXPECT_EQ(smashableDestroyable->GetFactionIDs(), std::vector{ 6 }); } +// An object with an item component (and no model component): live wrote the UGC info on every construction (99 of 99 +// constructions of LOT 14535): ug_id 0, NoStatus, no description. DLU wrote none. +TEST_F(ReplicaConstructionTest, ItemConstructionWritesEmptyUgcInfo) { + Entity item(15, info); + auto* const component = item.AddComponent(-1); + RakNet::BitStream construction; + component->Serialize(construction, true); + RakNet::BitStream expected; + expected.Write1(); + expected.Write(0); + expected.Write(0); + expected.Write0(); + ExpectSameBits(construction, expected); + + RakNet::BitStream serialization; + component->Serialize(serialization, false); + EXPECT_EQ(serialization.GetNumberOfBitsUsed(), 1u); +} + class InventoryConstructionTest : public GameDependenciesTest { protected: // The items of the live sample below (CDClient 1.10.64 values)