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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 05:39:10 -05:00
parent 545db47a40
commit bdc9170754
4 changed files with 37 additions and 8 deletions

View File

@@ -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,
};
}();

View File

@@ -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>(LWOOBJID_EMPTY); // ug_id
outBitStream.Write<uint32_t>(0); // ug_moderation_status NoStatus
outBitStream.Write0(); // ug_description
}
}

View File

@@ -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;

View File

@@ -242,6 +242,25 @@ TEST_F(ReplicaConstructionTest, TemplateFactionMinusOneIsReplicated) {
EXPECT_EQ(smashableDestroyable->GetFactionIDs(), std::vector<int32_t>{ 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<ItemComponent>(-1);
RakNet::BitStream construction;
component->Serialize(construction, true);
RakNet::BitStream expected;
expected.Write1();
expected.Write<LWOOBJID>(0);
expected.Write<uint32_t>(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)