mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
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:
@@ -826,6 +826,13 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) {
|
||||
|
||||
items->DeleteChildren();
|
||||
|
||||
// Saved while the equipped items are pushed (build mode, possibly carrying a model): the character's equipment is
|
||||
// the pushed state, which PopEquippedItems puts back, not the temporary items equipped since.
|
||||
const auto& savedEquipment = m_HasPushed ? m_Pushed : m_Equipped;
|
||||
const auto isSavedEquipped = [&savedEquipment](const LWOOBJID id) {
|
||||
return std::ranges::any_of(savedEquipment, [id](const auto& slot) { return slot.second.id == id; });
|
||||
};
|
||||
|
||||
for (auto* inventory : inventoriesToSave) {
|
||||
if (inventory->GetSize() == 0) {
|
||||
continue;
|
||||
@@ -845,7 +852,7 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) {
|
||||
itemElement->SetAttribute("s", static_cast<unsigned int>(item->GetSlot()));
|
||||
itemElement->SetAttribute("c", static_cast<unsigned int>(item->GetCount()));
|
||||
itemElement->SetAttribute("b", item->GetBound());
|
||||
itemElement->SetAttribute("eq", item->IsEquipped());
|
||||
itemElement->SetAttribute("eq", isSavedEquipped(item->GetId()));
|
||||
itemElement->SetAttribute("sk", item->GetSubKey());
|
||||
|
||||
// Begin custom xml
|
||||
@@ -1197,6 +1204,7 @@ void InventoryComponent::RemoveBuff(Item* item) const {
|
||||
|
||||
void InventoryComponent::PushEquippedItems() {
|
||||
m_Pushed = m_Equipped;
|
||||
m_HasPushed = true;
|
||||
|
||||
m_Dirty = true;
|
||||
}
|
||||
@@ -1225,6 +1233,7 @@ void InventoryComponent::PopEquippedItems() {
|
||||
}
|
||||
|
||||
m_Pushed.clear();
|
||||
m_HasPushed = false;
|
||||
|
||||
auto destroyableComponent = m_Parent->GetComponent<DestroyableComponent>();
|
||||
|
||||
|
||||
@@ -517,6 +517,12 @@ private:
|
||||
*/
|
||||
EquipmentMap m_Pushed;
|
||||
|
||||
/**
|
||||
* Whether the equipped items are pushed (between PushEquippedItems and PopEquippedItems, e.g. in build mode).
|
||||
* What is equipped then is temporary (the thinking hat, a carried model) and m_Pushed is what gets saved.
|
||||
*/
|
||||
bool m_HasPushed{};
|
||||
|
||||
/**
|
||||
* If the inventory has changed
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
set(DCOMPONENTS_TESTS
|
||||
"DeletionRestrictionTests.cpp"
|
||||
"InventorySaveTests.cpp"
|
||||
"VendorBuybackTests.cpp"
|
||||
"RacingWrongWayTests.cpp"
|
||||
"DestroyableComponentTests.cpp"
|
||||
|
||||
95
tests/dGameTests/dComponentsTests/InventorySaveTests.cpp
Normal file
95
tests/dGameTests/dComponentsTests/InventorySaveTests.cpp
Normal 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()));
|
||||
}
|
||||
Reference in New Issue
Block a user