From cc337a298da74865902f00103abec939a5b9481c Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Thu, 1 Oct 2026 06:27:36 -0500 Subject: [PATCH] fix(entity): models get their item component, as the client makes it The client makes the registry's item component for models too (only a pet takes it away); DLU skipped it and the model component wrote the item block itself, for the 30 model LOTs without an item row as well, and with the object id as ug_id. Live (Raven Bluff property capture): the item block carries the userModelID for brick built models (LOT 14) and 0 for reward models (LOT 8069). 1,455 LOTs gain the item component; nothing else changes (LoaderSnapshotTest). Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 8 ++++-- dGame/dComponents/ItemComponent.cpp | 6 ++-- dGame/dComponents/ModelComponent.cpp | 9 +----- .../dComponentsTests/ReplicaDecoderTests.cpp | 28 ++++++++++++++++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 54631b05f..3bef88b26 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -262,6 +262,8 @@ void Entity::Initialize() { AddComponent(simplePhysicsComponentID); + AddComponent(compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::ITEM)); + AddComponent(-1)->LoadBehaviors(); AddComponent(-1); @@ -646,11 +648,11 @@ void Entity::Initialize() { break; } case eReplicaComponentType::ITEM: { + // A pet takes the item (and model) component away (LWOPetComponent::Factory); models keep theirs (live: an + // item block before the model's) PetComponent* petComponent{}; const auto itemID = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::ITEM); - // The model (42) comes later in the client's order: a model without a pet has no item component - const bool hasModel = compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::MODEL, -1) > -1 && petComponentID <= 0; - if (itemID > 0 && !TryGetComponent(eReplicaComponentType::PET, petComponent) && !hasModel) { + if (itemID > 0 && !TryGetComponent(eReplicaComponentType::PET, petComponent)) { AddComponent(itemID); } break; diff --git a/dGame/dComponents/ItemComponent.cpp b/dGame/dComponents/ItemComponent.cpp index 6d4969bc4..16543bfbc 100644 --- a/dGame/dComponents/ItemComponent.cpp +++ b/dGame/dComponents/ItemComponent.cpp @@ -1,11 +1,11 @@ #include "ItemComponent.h" void ItemComponent::Serialize(RakNet::BitStream& outBitStream, bool isConstruction) { - // 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. + // Live wrote the user-generated-content info on every construction: the model's userModelID (a brick built model, + // LOT 14), else no UGC id (items, reward models such as LOT 8069), moderation status NoStatus, no description. outBitStream.Write(isConstruction); if (isConstruction) { - outBitStream.Write(LWOOBJID_EMPTY); // ug_id + outBitStream.Write(m_Parent->GetVarAs(u"userModelID")); // ug_id outBitStream.Write(0); // ug_moderation_status NoStatus outBitStream.Write0(); // ug_description } diff --git a/dGame/dComponents/ModelComponent.cpp b/dGame/dComponents/ModelComponent.cpp index d103eec88..40dbb9d4b 100644 --- a/dGame/dComponents/ModelComponent.cpp +++ b/dGame/dComponents/ModelComponent.cpp @@ -131,14 +131,7 @@ void ModelComponent::Resume() { } void ModelComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { - // ItemComponent Serialization. Pets do not get this serialization. - if (!m_Parent->HasComponent(eReplicaComponentType::PET)) { - outBitStream.Write1(); - outBitStream.Write(m_userModelID != LWOOBJID_EMPTY ? m_userModelID : m_Parent->GetObjectID()); - outBitStream.Write(0); - outBitStream.Write0(); - } - + // The item block in front of it is the ItemComponent's (the client makes one from the registry) //actual model component: outBitStream.Write1(); // Yes we are writing model info outBitStream.Write(m_NumListeningInteract > 0); // Is pickable diff --git a/tests/dGameTests/dComponentsTests/ReplicaDecoderTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaDecoderTests.cpp index 8ee46c2f7..6738d393e 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaDecoderTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaDecoderTests.cpp @@ -14,6 +14,7 @@ #include "InventoryComponent.h" #include "LevelProgressionComponent.h" #include "MessageIdentifiers.h" +#include "ItemComponent.h" #include "ModelComponent.h" #include "PlayerForcedMovementComponent.h" #include "PossessorComponent.h" @@ -141,14 +142,14 @@ TEST_F(ReplicaDecoderTest, PlayerConstructionReadsTheCharacterParts) { EXPECT_EQ((*FieldsOf(*constructed, "CHARACTER"))["statistics"].size(), 27u); } -// A model's destroyable is not in its registry rows: it is written after the other components. Its item component is -// in the rows but not made (Entity::Initialize): the model component writes the item info +// A model's destroyable is not in its registry rows: it is written after the other components. Its item component comes +// from the rows, in front of the model's block, with no UGC id when there is no userModelID (live: reward model LOT 8069) TEST_F(ReplicaDecoderTest, ModelWithTheDestroyableTheRegistryDoesNotList) { info.lot = 6000; Entity model(288300744895900200, info); auto* const physics = model.AddComponent(-1); physics->SetPosition(NiPoint3(-12.0f, 432.96f, -49.6f)); - // The model component writes the item's user-generated-content block itself, where the client reads the item + model.AddComponent(-1); model.AddComponent(-1)->LoadBehaviors(); auto* const destroyable = model.AddComponent(-1); destroyable->SetIsSmashable(true); @@ -161,7 +162,25 @@ TEST_F(ReplicaDecoderTest, ModelWithTheDestroyableTheRegistryDoesNotList) { EXPECT_EQ(Names(*constructed), "SIMPLE_PHYSICS ITEM MODEL DESTROYABLE "); EXPECT_EQ((*FieldsOf(*constructed, "DESTROYABLE"))["smashable"], true); EXPECT_EQ((*FieldsOf(*constructed, "SIMPLE_PHYSICS"))["position"], json::array({ -12.0f, 432.96f, -49.6f })); - EXPECT_EQ((*FieldsOf(*constructed, "ITEM"))["ugID"], "288300744895900200"); + EXPECT_EQ((*FieldsOf(*constructed, "ITEM"))["ugID"], "0"); +} + +// A brick built model's item block carries its userModelID (live: LOT 14 on a Raven Bluff property, ug_id +// 1152921507179669081, item block between the simple physics and the model) +TEST_F(ReplicaDecoderTest, BrickModelItemHasItsUserModelId) { + info.lot = 14; + info.settings.Insert(u"userModelID", 1152921507179669081); + Entity model(288300744895900201, info); + info.settings.values.clear(); + model.AddComponent(-1); + model.AddComponent(-1); + model.AddComponent(-1)->LoadBehaviors(); + const ReplicaDecoder::ComponentTable table{ { 14, { SIMPLE_PHYSICS, ITEM, MODEL } } }; + ReplicaDecoder::Session session(table); + const auto constructed = session.Decode(Construction(model, 5), 1); + ASSERT_TRUE(constructed); + EXPECT_FALSE(constructed->contains("(layout did not match)")) << constructed->dump(); + EXPECT_EQ((*FieldsOf(*constructed, "ITEM"))["ugID"], "1152921507179669081"); } // When no layout reads the packet exactly, what read is shown with the rest as bits, never a guess @@ -359,6 +378,7 @@ TEST_F(ReplicaDecoderTest, ModelBehaviorsOnlyOnPropertyModels) { if (onProperty) info.settings.Insert(u"propertyObjectID", true); Entity model(288300744895900210, info); model.AddComponent(-1); + model.AddComponent(-1); model.AddComponent(-1)->LoadBehaviors(); ReplicaDecoder::Session session(table); const auto constructed = session.Decode(Construction(model, 4), 1);