fix(inventory): a save in build mode keeps the equipment from before it

Entering build mode pushes the equipped items (PushEquippedItems); the
thinking hat and a model picked up off the property are then equipped
until PopEquippedItems puts the old equipment back. A character save in
between (the world's periodic save, a disconnect, a zone change) wrote
what was equipped at that moment, so a carried brick built model (LOT
6662) was saved in MODELS with eq="true" and equipped again on every
load. While the equipment is pushed, the save now writes the pushed
equipment as equipped instead. Nothing is dropped from the save.

In game: on a property, pick up a brick built model and carry it, wait
for a save (or log out) while carrying it, log back in: the model is in
the Models bag, not equipped, and your normal gear is worn. Characters
already saved with an equipped model keep it until it is put away or
the save is cleaned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 23:33:58 -05:00
parent b5e92ce97c
commit 107437e72f
4 changed files with 112 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
set(DCOMPONENTS_TESTS
"DeletionRestrictionTests.cpp"
"InventorySaveTests.cpp"
"VendorBuybackTests.cpp"
"RacingWrongWayTests.cpp"
"DestroyableComponentTests.cpp"

View File

@@ -0,0 +1,95 @@
#include "GameDependencies.h"
#include "CDClientManager.h"
#include "CDComponentsRegistryTable.h"
#include "CDItemComponentTable.h"
#include "Entity.h"
#include "InventoryComponent.h"
#include "Item.h"
#include "eReplicaComponentType.h"
#include "tinyxml2.h"
#include <gtest/gtest.h>
// What the character save writes as equipped ("eq"), in and out of a pushed equipment state (build mode).
class InventorySaveTests : public GameDependenciesTest {
protected:
static constexpr LOT HAT = 1000;
static constexpr LOT MODEL = 6662; // a brick built model item, carried while building on a property
std::unique_ptr<Entity> entity;
InventoryComponent* inventory{};
tinyxml2::XMLDocument doc;
LWOOBJID nextId = 0x7000;
void SetUp() override {
SetUpDependencies();
RegisterLot(HAT, "hair");
RegisterLot(MODEL, "Extra_1");
// The entity's own LOT has no components, so InventoryComponent doesn't look it up in a database
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);
doc.Parse("<obj><inv><bag/><items/></inv></obj>");
}
void TearDown() override {
entity.reset();
TearDownDependencies();
}
static void RegisterLot(const LOT lot, const std::string& equipLocation) {
const auto componentID = static_cast<uint32_t>(96000 + lot);
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
registry.insert_or_assign(static_cast<uint64_t>(lot), componentID);
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::ITEM) << 32 | static_cast<uint64_t>(lot), componentID);
CDItemComponent component{};
component.id = componentID;
component.equipLocation = equipLocation;
CDClientManager::GetEntriesMutable<CDItemComponentTable>().insert_or_assign(componentID, component);
}
// The inventory owns the item; equipped the way loading a save equips it
Item* GiveEquipped(const LOT lot, const eInventoryType type) {
auto* const bag = inventory->GetInventory(type);
auto* const item = new Item(nextId++, lot, bag, static_cast<uint32_t>(bag->GetItems().size()), 1, false, {}, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE);
inventory->UpdateSlot(item->GetInfo().equipLocation, { item->GetId(), item->GetLot(), item->GetCount(), item->GetSlot() });
return item;
}
// The "eq" attribute the save wrote for an item
bool SavedEquipped(const LWOOBJID id) {
inventory->UpdateXml(doc);
for (auto* bag = doc.FirstChildElement("obj")->FirstChildElement("inv")->FirstChildElement("items")->FirstChildElement("in"); bag; bag = bag->NextSiblingElement("in")) {
for (auto* element = bag->FirstChildElement("i"); element; element = element->NextSiblingElement("i")) {
if (element->Int64Attribute("id") == id) return element->BoolAttribute("eq");
}
}
ADD_FAILURE() << "item " << id << " was not saved";
return false;
}
};
TEST_F(InventorySaveTests, EquippedItemsAreSavedEquipped) {
const auto* const hat = GiveEquipped(HAT, eInventoryType::ITEMS);
EXPECT_TRUE(SavedEquipped(hat->GetId()));
}
TEST_F(InventorySaveTests, ModelCarriedInBuildModeIsNotSavedEquipped) {
const auto* const hat = GiveEquipped(HAT, eInventoryType::ITEMS);
inventory->PushEquippedItems(); // entering build mode
const auto* const model = GiveEquipped(MODEL, eInventoryType::MODELS); // picked up off the property
EXPECT_FALSE(SavedEquipped(model->GetId()));
EXPECT_TRUE(SavedEquipped(hat->GetId()));
}
TEST_F(InventorySaveTests, ItemTakenOffInBuildModeIsStillSavedEquipped) {
const auto* const hat = GiveEquipped(HAT, eInventoryType::ITEMS);
inventory->PushEquippedItems();
inventory->RemoveSlot("hair");
EXPECT_TRUE(SavedEquipped(hat->GetId()));
}