fix: Stop treasure chest guardian spawn farming via stack splits (#2568)
### Summary Players reported an exploit: decipher a treasure map, then run a ClassicUO/Razor organizer agent that pulls the gold out of the chest in small amounts. Each pull spawned more monsters, turning one chest into an unbounded farmable spawn generator. ### Root cause `TreasureMapChest.OnItemLifted` grants a 10% guardian spawn roll per first-time-lifted item, deduplicated by the instance-keyed `_lifted` set. But a partial lift goes through `Mobile.LiftItemDupe`, which re-adds the stack remainder to the chest as a **brand-new item instance** (engine-side `AddItem`, bypassing the `CheckHold` block on refilling). Every subsequent pull lifts an instance the `_lifted` set has never seen, so each one re-rolls the 10% spawn chance: - A level 4 chest holds 4,000 gold → pulled coin by coin, ~400 spawned creatures (plus more from reagent stacks), hands-free, per chest. - Spawns use `guardian: false`, so nothing tracks or caps them. - Legit full-stack looting yields roughly 5–8 bonus spawns per chest for comparison. The code is inherited from RunUO, so descendant shards likely share the hole. ### Fix Mark every item that enters the chest **after the initial fill** as already lifted, via an `OnItemAdded` override gated by a non-serialized `_filled` flag (set at the end of the constructor and in `[AfterDeserialization]`). Ordering makes this exact: `LiftItemDupe` re-adds the remainder *before* the chest's `OnItemLifted` runs, so the lifted original still gets its one legitimate roll while the remainder is pre-marked. This also covers packing items *into* the chest (e.g., merging gold back in to lift it out again) and bounce-backs — anything not part of the original loot can never grant a spawn roll. ### Tests - `PartialLift_MarksSplitRemainderAsLifted` — drives the real `Mobile.Lift` path with a 1-coin pull and asserts the split remainder is marked (failed before the fix). - `ItemAddedAfterFill_IsMarkedLifted` — post-fill additions are marked (failed before the fix). - `OriginalFillLoot_IsNotMarkedLifted` — original loot keeps spawn-roll eligibility. Full `UOContent.Tests` suite: 701 passed.
This commit is contained in:
parent
0628902644
commit
c1442aff3e
3 changed files with 219 additions and 13 deletions
57
Projects/UOContent/Migrations/Server.Items.TreasureMapChest.v3.json
generated
Normal file
57
Projects/UOContent/Migrations/Server.Items.TreasureMapChest.v3.json
generated
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"version": 3,
|
||||
"type": "Server.Items.TreasureMapChest",
|
||||
"properties": [
|
||||
{
|
||||
"name": "Guardians",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@Tidy",
|
||||
"@CanBeNull",
|
||||
"Server.Mobile",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Temporary",
|
||||
"type": "bool",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Owner",
|
||||
"type": "Server.Mobile",
|
||||
"rule": "SerializableInterfaceMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Level",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ExpireTimer",
|
||||
"type": "Server.Timer",
|
||||
"rule": "TimerMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@TimerDrift"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Lifted",
|
||||
"type": "System.Collections.Generic.HashSet\u003CServer.Item\u003E",
|
||||
"rule": "HashSetMigrationRule",
|
||||
"ruleArguments": [
|
||||
"@Tidy",
|
||||
"@CanBeNull",
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue