feat(inventory): enforce DeletionRestrictions when deleting items

Deleting an item now follows its ItemComponent delResIndex row in the
DeletionRestrictions table, the way the client's shared inventory code
decides it (LWOInventoryComponent_Common::CanRemoveFromInventory @
0x00ce0d20, CheckDeletionRestrictionIndex @ 0x00c94c20):
- missing, unrestricted, unknown-type or empty rows allow it;
- LOTS_INCLUDED: another item of any listed LOT must remain;
- LOTS_EXCLUDED: other items of every listed LOT must remain;
- ANY_RESTRICTION / ALL_RESTRICTIONS: any / all listed rows allow it;
- ZONE: only in the listed maps; ALWAYS_RESTRICTED: never.
Operators (GM level 9) may delete anything, as in the client. A refused
delete is logged and the item stays.

ItemComponent.minNumRequired is not used: the client never reads it, so
its meaning can't be verified.

Issue 960: the rocket (6416, row 8) and the classic rocket parts (rows 1-3)
have rows that keep at least one rocket or part, so the last rocket can
no longer be deleted and strand the player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 08:49:03 -05:00
parent 4399daad4c
commit 02108055e7
9 changed files with 276 additions and 1 deletions

View File

@@ -36,6 +36,7 @@
#include "CDMissionEmailTable.h"
#include "CDRewardsTable.h"
#include "CDPropertyEntranceComponentTable.h"
#include "CDDeletionRestrictionsTable.h"
#include "CDPropertyTemplateTable.h"
#include "CDModularBuildComponentTable.h"
#include "CDFeatureGatingTable.h"
@@ -93,6 +94,7 @@ DEFINE_TABLE_STORAGE(CDPackageComponentTable);
DEFINE_TABLE_STORAGE(CDPetComponentTable);
DEFINE_TABLE_STORAGE(CDProximityMonitorComponentTable);
DEFINE_TABLE_STORAGE(CDPropertyEntranceComponentTable);
DEFINE_TABLE_STORAGE(CDDeletionRestrictionsTable);
DEFINE_TABLE_STORAGE(CDPropertyTemplateTable);
DEFINE_TABLE_STORAGE(CDModularBuildComponentTable);
DEFINE_TABLE_STORAGE(CDRailActivatorComponentTable);
@@ -118,6 +120,7 @@ void CDClientManager::LoadValuesFromDatabase() {
CDBrickIDTableTable::Instance().LoadValuesFromDatabase();
CDCLIENT_DONT_CACHE_TABLE(CDComponentsRegistryTable::Instance().LoadValuesFromDatabase());
CDCurrencyTableTable::Instance().LoadValuesFromDatabase();
CDDeletionRestrictionsTable::Instance().LoadValuesFromDatabase();
CDDestructibleComponentTable::Instance().LoadValuesFromDatabase();
CDEmoteTableTable::Instance().LoadValuesFromDatabase();
CDFeatureGatingTable::Instance().LoadValuesFromDatabase();

View File

@@ -0,0 +1,22 @@
#include "CDDeletionRestrictionsTable.h"
void CDDeletionRestrictionsTable::LoadValuesFromDatabase() {
auto& entries = GetEntriesMutable();
auto tableData = CDClientDatabase::ExecuteQuery("SELECT id, restricted, ids, checkType FROM DeletionRestrictions;");
while (!tableData.eof()) {
CDDeletionRestriction entry;
entry.restricted = tableData.getIntField("restricted", 0) != 0;
entry.ids = tableData.getStringField("ids", "");
entry.checkType = tableData.getIntField("checkType", 0);
entries.insert_or_assign(tableData.getIntField("id", -1), std::move(entry));
tableData.nextRow();
}
tableData.finalize();
}
const CDDeletionRestriction* CDDeletionRestrictionsTable::Get(const int32_t id) const {
const auto& entries = GetEntries();
const auto it = entries.find(id);
return it == entries.end() ? nullptr : &it->second;
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include "CDTable.h"
#include <cstdint>
#include <map>
#include <string>
// How a DeletionRestrictions row decides whether an item may be deleted (eDeletionRestrictionCheckType in the client)
enum class eDeletionRestrictionCheckType : int32_t {
LOTS_INCLUDED = 0, // allowed if another item of one of the LOTs is owned
LOTS_EXCLUDED = 1, // allowed if other items of every one of the LOTs are owned
ANY_RESTRICTION = 2, // allowed if any of the listed rows allows it
ALL_RESTRICTIONS = 3, // allowed if all of the listed rows allow it
ZONE = 4, // allowed in the listed map IDs
ALWAYS_RESTRICTED = 5, // never allowed
};
struct CDDeletionRestriction {
bool restricted{};
std::string ids; // comma separated LOTs, row IDs or map IDs, depending on checkType
int32_t checkType{};
};
class CDDeletionRestrictionsTable : public CDTable<CDDeletionRestrictionsTable, std::map<int32_t, CDDeletionRestriction>> {
public:
void LoadValuesFromDatabase();
// The row with this ID, or nullptr
const CDDeletionRestriction* Get(int32_t id) const;
};

View File

@@ -33,7 +33,7 @@ struct CDItemComponent {
uint32_t itemRating; //!< ???
bool isTwoHanded; //!< Whether or not the item is double handed
uint32_t minNumRequired; //!< Maybe the minimum number required for a mission, or to own this object?
uint32_t delResIndex; //!< ???
uint32_t delResIndex; //!< Row of DeletionRestrictions, -1 (as unsigned) for none
uint32_t currencyLOT; //!< ???
uint32_t altCurrencyCost; //!< ???
std::string subItems; //!< A comma seperate string of sub items (maybe for multi-itemed things like faction test gear set)

View File

@@ -28,6 +28,7 @@ set(DDATABASE_CDCLIENTDATABASE_CDCLIENTTABLES_SOURCES "CDActivitiesTable.cpp"
"CDPlayerFlagsTable.cpp"
"CDPhysicsComponentTable.cpp"
"CDPropertyEntranceComponentTable.cpp"
"CDDeletionRestrictionsTable.cpp"
"CDPropertyTemplateTable.cpp"
"CDModularBuildComponentTable.cpp"
"CDProximityMonitorComponentTable.cpp"