From ede4bcc718a4d15c02756bd375a7bc2b9ea7893c Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:57:02 -0700 Subject: [PATCH] fix(spawners): make SpawnerDto.ToSpawner() self-cleaning on failure CreateEmpty() registers the Item in the world before population. If ApplyDto or subtype-field application throws, the import catch cannot reach the (unassigned) spawner local, re-opening the orphan leak Approach B prevents. Wrap population in try/catch in the base ToSpawner() and all three overrides: on failure Delete() the single Item and rethrow, so the import path only counts the failure. Co-Authored-By: Claude Sonnet 4.6 --- .../Engines/Spawners/Json/SpawnerDto.cs | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs b/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs index ef956c513..725d7c236 100644 --- a/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs +++ b/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs @@ -51,8 +51,18 @@ public abstract record SpawnerDto public virtual BaseSpawner ToSpawner() { var spawner = CreateEmpty(); - spawner.ApplyDto(this); - return spawner; + try + { + spawner.ApplyDto(this); + return spawner; + } + catch + { + // CreateEmpty already registered the Item in the world; if population throws, + // delete it here so no orphan can escape (the import catch can't reach it). + spawner.Delete(); + throw; + } } } @@ -66,12 +76,20 @@ public sealed record SpawnerDataDto : SpawnerDto public override BaseSpawner ToSpawner() { var spawner = (Spawner)base.ToSpawner(); - if (SpawnBounds is { } bounds && bounds != default) + try { - spawner.SpawnBounds = bounds; - } + if (SpawnBounds is { } bounds && bounds != default) + { + spawner.SpawnBounds = bounds; + } - return spawner; + return spawner; + } + catch + { + spawner.Delete(); + throw; + } } } @@ -85,8 +103,16 @@ public sealed record RegionSpawnerDto : SpawnerDto public override BaseSpawner ToSpawner() { var spawner = (RegionSpawner)base.ToSpawner(); - spawner.SpawnRegion = Server.Region.Find(Region, Map) as BaseRegion; - return spawner; + try + { + spawner.SpawnRegion = Server.Region.Find(Region, Map) as BaseRegion; + return spawner; + } + catch + { + spawner.Delete(); + throw; + } } } @@ -103,14 +129,22 @@ public sealed record ProximitySpawnerDto : SpawnerDto public override BaseSpawner ToSpawner() { var spawner = (ProximitySpawner)base.ToSpawner(); - if (SpawnBounds is { } bounds && bounds != default) + try { - spawner.SpawnBounds = bounds; - } + if (SpawnBounds is { } bounds && bounds != default) + { + spawner.SpawnBounds = bounds; + } - spawner.TriggerRange = TriggerRange; - spawner.SpawnMessage = SpawnMessage; - spawner.InstantFlag = Instant; - return spawner; + spawner.TriggerRange = TriggerRange; + spawner.SpawnMessage = SpawnMessage; + spawner.InstantFlag = Instant; + return spawner; + } + catch + { + spawner.Delete(); + throw; + } } }