feat: deletion restrictions

Deleting an item now follows the item's DeletionRestrictions row (ItemComponent.delResIndex), as the client's LWOInventoryComponent_Common::CanRemoveFromInventory (0x00ce0d20) and CheckDeletionRestrictionIndex (0x00c94c20) do: an item the player is driving or riding can't be deleted, an operator can delete anything, and otherwise the row decides:
- LOTS_INCLUDED: only while the player has another item of any of the LOTs (the last rocket can't be deleted)
- LOTS_EXCLUDED: only while the player has other items of all of the LOTs
- ANY_RESTRICTION / ALL_RESTRICTIONS: any / all of the listed rows allow it (a row that names itself again allows it)
- ZONE: only in the listed zones
- ALWAYS_RESTRICTED: never
A missing, unrestricted or bad row (no valid ids, unknown check type) allows it. The client checks first and shows the row's failure reason; the server checks again and ignores a client's forceDeletion.

Refs issue 960

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-16 21:05:47 -05:00
parent fc6a7fbd52
commit 221dd36d80
11 changed files with 256 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
set(DCOMPONENTS_TESTS
"DeletionRestrictionTests.cpp"
"DestroyableComponentTests.cpp"
"PetComponentTests.cpp"
"SimplePhysicsComponentTests.cpp"

View File

@@ -0,0 +1,84 @@
#include "GameDependencies.h"
#include "CDClientManager.h"
#include "CDComponentsRegistryTable.h"
#include "CDDeletionRestrictionsTable.h"
#include "Entity.h"
#include "eDeletionRestrictionsCheckType.h"
#include "InventoryComponent.h"
#include <gtest/gtest.h>
// DeletionRestrictions rows as the client's LWOInventoryComponent_Common::CheckDeletionRestrictionIndex (0x00c94c20)
// reads them. The test zone is map 1.
class DeletionRestrictionTests : public GameDependenciesTest {
protected:
std::unique_ptr<Entity> entity;
InventoryComponent* inventory{};
void SetUp() override {
SetUpDependencies();
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);
using enum eDeletionRestrictionsCheckType;
Row(1, true, ALWAYS_RESTRICTED, {});
Row(2, false, ALWAYS_RESTRICTED, {});
Row(3, true, ZONE, { 1, 1100 });
Row(4, true, ZONE, { 1000 });
Row(5, true, ANY_RESTRICTION, { 1, 3 });
Row(6, true, ALL_RESTRICTIONS, { 1, 3 });
Row(7, true, ALL_RESTRICTIONS, { 3, 7 });
Row(8, true, ZONE, {});
Row(9, true, LOTS_INCLUDED, { 6416 });
Row(10, true, MAX, { 1 });
}
void TearDown() override {
CDClientManager::GetEntriesMutable<CDDeletionRestrictionsTable>().clear();
entity.reset();
TearDownDependencies();
}
static void Row(const uint32_t id, const bool restricted, const eDeletionRestrictionsCheckType checkType, const std::vector<uint32_t>& ids) {
CDClientManager::GetEntriesMutable<CDDeletionRestrictionsTable>().insert_or_assign(id, CDDeletionRestriction{ id, restricted, ids, checkType });
}
bool Allows(const uint32_t row) const {
std::set<uint32_t> checked;
return inventory->CheckDeletionRestriction(1234, row, checked);
}
};
TEST_F(DeletionRestrictionTests, AlwaysRestrictedOnlyWhenRestricted) {
EXPECT_FALSE(Allows(1));
EXPECT_TRUE(Allows(2));
}
TEST_F(DeletionRestrictionTests, ZoneAllowsOnlyInTheListedZones) {
EXPECT_TRUE(Allows(3));
EXPECT_FALSE(Allows(4));
}
TEST_F(DeletionRestrictionTests, AnyAndAllCombineOtherRows) {
EXPECT_TRUE(Allows(5)); // always restricted OR in zone 1
EXPECT_FALSE(Allows(6)); // always restricted AND in zone 1
}
TEST_F(DeletionRestrictionTests, ARowThatNamesItselfDoesNotLoop) {
EXPECT_TRUE(Allows(7));
}
TEST_F(DeletionRestrictionTests, MissingRowsAndBadRowsAllowIt) {
EXPECT_TRUE(Allows(99));
EXPECT_TRUE(Allows(8)); // no valid ids
EXPECT_TRUE(Allows(10)); // unknown check type
}
TEST_F(DeletionRestrictionTests, LotsIncludedNeedsAnotherItemOfThoseLots) {
// The player has no other rocket
EXPECT_FALSE(Allows(9));
EXPECT_FALSE(inventory->HasOtherItemsOfLots({ 6416 }, 1234, true));
EXPECT_FALSE(inventory->HasOtherItemsOfLots({ 6416, 4713 }, 1234, false));
}