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

@@ -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;
}