fix(inventory): a removed item is freed by its inventory, not by itself (issue 1568)

Item::RemoveFromInventory ended with `delete this`, so every caller that still used the item afterwards (SetCount(0)
returning into its caller, loops that remove several items, proxies purged while their parent is handled) touched
freed memory. The inventory now takes the removed item and frees it at the inventory component's next update, or
with the inventory.

Check in game: sell, drop, delete, trade, mail and use up stacks (including the last of a stack); unequip and
remove an item set piece and a proxy-bearing item (rocket, modular car); donate items; nothing crashes and the
inventory shows the right counts after relogging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 10:52:22 -05:00
parent 1aa70c523b
commit 585e2a6ace
6 changed files with 127 additions and 1 deletions

View File

@@ -964,6 +964,11 @@ void InventoryComponent::Update(float deltaTime) {
for (auto* set : m_Itemsets) {
set->Update(deltaTime);
}
// Items removed during the last frame are no longer in use
for (auto* const inventory : m_Inventories | std::views::values) {
inventory->FreeRetiredItems();
}
}
void InventoryComponent::UpdateSlot(const std::string& location, const EquippedItem& item, bool keepCurrent) {

View File

@@ -236,6 +236,14 @@ void Inventory::RemoveManagedItem(Item* item) {
free++;
}
void Inventory::RetireItem(Item* item) {
m_RetiredItems.emplace_back(item);
}
void Inventory::FreeRetiredItems() {
m_RetiredItems.clear();
}
eInventoryType Inventory::FindInventoryTypeForLot(const LOT lot) {
auto itemComponent = FindItemComponent(lot);

View File

@@ -4,6 +4,7 @@
#define INVENTORY_H
#include <map>
#include <memory>
#include <vector>
#include "CDItemComponentTable.h"
@@ -126,6 +127,23 @@ public:
*/
void RemoveManagedItem(Item* item);
/**
* Takes ownership of an item that was taken out of the inventory. Code still holding the item can use it until
* FreeRetiredItems runs (the inventory component's next update), so an item never frees itself.
* @param item the removed item
*/
void RetireItem(Item* item);
/**
* Frees the items retired since the last call
*/
void FreeRetiredItems();
/**
* @return whether removed items are waiting to be freed
*/
bool HasRetiredItems() const { return !m_RetiredItems.empty(); }
/**
* Returns the inventory type an item of the specified lot should be placed in
* @param lot the lot to find the inventory type for
@@ -183,6 +201,11 @@ private:
*/
std::map<LWOOBJID, Item*> items;
/**
* Items taken out of this inventory, freed by FreeRetiredItems
*/
std::vector<std::unique_ptr<Item>> m_RetiredItems;
/**
* The inventory component this inventory belongs to
*/

View File

@@ -580,7 +580,9 @@ void Item::RemoveFromInventory() {
inventory->RemoveManagedItem(this);
delete this;
// The inventory frees the item at its next update, so callers still holding it (SetCount(0) and the loops that
// remove several items) are not left with a freed item
inventory->RetireItem(this);
}
Item::~Item() {