fix: property model and brick by brick fixes (#2041)

* test: the server mock keeps a copy of the last bitstream sent

The bitstreams sent are usually on the sender's stack, so the pointer it kept was dangling once Send returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: PlaceModelResponse writes the model's rotation

The client's PlaceModelResponse::Deserialize (0x00dc0170) reads the rotation as an optional w, x, y, z quaternion, but the server wrote the 4-byte response after the rotation flag, so with any rotation other than identity the client ran out of data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: naming a model no longer renames the property

The model info window sends UpdatePropertyOrModelForFilterCheck with isProperty false (client 0x00cc42e0); only the property editor sends it true (0x00c2e710). A model's name and description aren't stored yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep a model item's blueprint across saves

A brick built model item's config has blueprintid (lowercase), which the saved item XML didn't keep (only blueprintID), so a picked up model lost its blueprint on the next load and could not be placed again. It is now saved as bp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: write model behaviors only for property and inventory models

The client makes the mutable model component, which reads the behaviors and editing info, only when the object's config has propertyObjectID or inInventory (ObjectLoader2::LoadModelBehaviorsComponent); every other model gets the plain one, which reads only the model block. Writing the behaviors for every model made the client read them as the next component's data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the model vault uses the models' inventory groups, as the client

LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and msgUpdateInventoryGroupContents (0x00ce1f50) look the inventory type up in m_VaultToPlayerInventory first, which maps VAULT_MODELS to MODELS. The server refused group updates for the model vault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-27 09:34:31 -05:00
committed by GitHub
parent 9bf2ff4d44
commit 0ff5d2bf87
10 changed files with 187 additions and 6 deletions

View File

@@ -1,5 +1,7 @@
set(DCOMPONENTS_TESTS
"DestroyableComponentTests.cpp"
"InventoryGroupTests.cpp"
"ModelComponentTests.cpp"
"PetComponentTests.cpp"
"SimplePhysicsComponentTests.cpp"
"SavingTests.cpp"

View File

@@ -0,0 +1,56 @@
#include "GameDependencies.h"
#include "CDClientManager.h"
#include "CDComponentsRegistryTable.h"
#include "Entity.h"
#include "InventoryComponent.h"
#include <gtest/gtest.h>
class InventoryGroupTests : public GameDependenciesTest {
protected:
std::unique_ptr<Entity> entity;
InventoryComponent* inventory{};
void SetUp() override {
SetUpDependencies();
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
entity = std::make_unique<Entity>(1, info);
inventory = entity->AddComponent<InventoryComponent>(-1);
}
void TearDown() override {
entity.reset();
TearDownDependencies();
}
void Update(const InventoryComponent::GroupUpdateCommand command, const std::string& id, const eInventoryType type, const LOT lot = LOT_NULL) {
InventoryComponent::GroupUpdate update{};
update.groupId = id;
update.groupName = "name";
update.lot = lot;
update.inventory = type;
update.command = command;
inventory->UpdateGroup(update);
}
};
// The client's LWOInventoryComponent_Common::msgUpdateInventoryGroup (0x00c955b0) and msgUpdateInventoryGroupContents
// (0x00ce1f50) map the model vault to the models inventory (m_VaultToPlayerInventory)
TEST_F(InventoryGroupTests, TheModelVaultUsesTheModelsGroups) {
Update(InventoryComponent::GroupUpdateCommand::ADD, "user_group1", eInventoryType::VAULT_MODELS);
Update(InventoryComponent::GroupUpdateCommand::ADD_LOT, "user_group1", eInventoryType::VAULT_MODELS, 3001);
const auto& groups = inventory->GetGroups();
ASSERT_EQ(groups.at(eInventoryType::MODELS).size(), 1u);
EXPECT_EQ(groups.at(eInventoryType::MODELS)[0].lots.size(), 1u);
EXPECT_FALSE(groups.contains(eInventoryType::VAULT_MODELS));
}
TEST_F(InventoryGroupTests, OnlyBricksAndModelsHaveGroups) {
Update(InventoryComponent::GroupUpdateCommand::ADD, "user_group1", eInventoryType::ITEMS);
EXPECT_FALSE(inventory->GetGroups().contains(eInventoryType::ITEMS));
Update(InventoryComponent::GroupUpdateCommand::ADD, "user_group1", eInventoryType::BRICKS);
EXPECT_EQ(inventory->GetGroups().at(eInventoryType::BRICKS).size(), 1u);
}

View File

@@ -0,0 +1,55 @@
#include "GameDependencies.h"
#include "BitStream.h"
#include "CDClientManager.h"
#include "CDComponentsRegistryTable.h"
#include "Entity.h"
#include "GameMessages.h"
#include "ModelComponent.h"
#include <gtest/gtest.h>
class ModelComponentTests : public GameDependenciesTest {
protected:
void SetUp() override {
SetUpDependencies();
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
}
void TearDown() override {
TearDownDependencies();
}
// The bits a model's serialization writes
static uint32_t SerializedBits(Entity& entity) {
auto* model = entity.AddComponent<ModelComponent>(-1);
RakNet::BitStream bitStream;
model->Serialize(bitStream, true);
return bitStream.GetNumberOfBitsUsed();
}
};
// Item block (1 + 64 + 32 + 1), model block (1 + 1 + 32 + 96 + 128)
constexpr uint32_t ModelBlockBits = 98 + 258;
// Behavior info: flag, count, paused, and on the initial update the editing info flag
constexpr uint32_t BehaviorBits = 1 + 32 + 1 + 1;
// The client reads the behaviors only for property and inventory models (ObjectLoader2::LoadModelBehaviorsComponent)
TEST_F(ModelComponentTests, OtherModelsHaveNoBehaviorInfo) {
Entity entity(1, info);
EXPECT_EQ(SerializedBits(entity), ModelBlockBits);
}
TEST_F(ModelComponentTests, PropertyModelsHaveBehaviorInfo) {
EntityInfo propertyInfo = info;
propertyInfo.settings.Insert<bool>(u"propertyObjectID", true);
Entity entity(1, propertyInfo);
EXPECT_EQ(SerializedBits(entity), ModelBlockBits + BehaviorBits);
}
TEST_F(ModelComponentTests, InventoryModelsHaveBehaviorInfo) {
EntityInfo inventoryInfo = info;
inventoryInfo.settings.Insert<bool>(u"inInventory", true);
Entity entity(1, inventoryInfo);
EXPECT_EQ(SerializedBits(entity), ModelBlockBits + BehaviorBits);
}