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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-01 06:27:36 -05:00
parent b2192e158c
commit cc337a298d
4 changed files with 33 additions and 18 deletions

View File

@@ -262,6 +262,8 @@ void Entity::Initialize() {
AddComponent<SimplePhysicsComponent>(simplePhysicsComponentID);
AddComponent<ItemComponent>(compRegistryTable->GetByIDAndType(m_TemplateID, eReplicaComponentType::ITEM));
AddComponent<ModelComponent>(-1)->LoadBehaviors();
AddComponent<RenderComponent>(-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<ItemComponent>(itemID);
}
break;

View File

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

View File

@@ -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<LWOOBJID>(m_userModelID != LWOOBJID_EMPTY ? m_userModelID : m_Parent->GetObjectID());
outBitStream.Write<int>(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

View File

@@ -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<SimplePhysicsComponent>(-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<ItemComponent>(-1);
model.AddComponent<ModelComponent>(-1)->LoadBehaviors();
auto* const destroyable = model.AddComponent<DestroyableComponent>(-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<LWOOBJID>(u"userModelID", 1152921507179669081);
Entity model(288300744895900201, info);
info.settings.values.clear();
model.AddComponent<SimplePhysicsComponent>(-1);
model.AddComponent<ItemComponent>(-1);
model.AddComponent<ModelComponent>(-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<bool>(u"propertyObjectID", true);
Entity model(288300744895900210, info);
model.AddComponent<SimplePhysicsComponent>(-1);
model.AddComponent<ItemComponent>(-1);
model.AddComponent<ModelComponent>(-1)->LoadBehaviors();
ReplicaDecoder::Session session(table);
const auto constructed = session.Decode(Construction(model, 4), 1);