refactor(spawners): replace DynamicJson with typed SpawnerDto records (#2505)

## Summary

Removes the dated `DynamicJson` JSON helper and migrates spawner JSON (de)serialization to a polymorphic `record SpawnerDto` hierarchy. `DynamicJson` was the last remaining consumer (regions moved off it in #1400).

The key correctness improvement: **System.Text.Json deserializes plain DTO records, never a live `Item`.** Previously, deserializing directly into an `Item` meant STJ constructed world-registered objects *before* the data was validated — a malformed/hand-edited spawn file could leave orphaned spawner Items in the world save. Now a parse failure is GC-only; `dto.ToSpawner()` constructs the spawner only from a fully-validated DTO and self-cleans on failure.

## What changed

- **New:** `SpawnerDto` (abstract) + `SpawnerDataDto` / `RegionSpawnerDto` / `ProximitySpawnerDto`, each marked with a reusable `[JsonDiscoverableType]` opt-in attribute. Auto-discovered at the `Configure` phase — no manual registration list (avoids the regions `Register<T>()` footgun), open to custom spawner subtypes.
- **Symmetric mapping:** `BaseSpawner.ToDto()` (export) ⇄ `SpawnerDto.ToSpawner()` (import). `ToSpawner()` deletes-and-rethrows on any failure, so the importer can never orphan an Item.
- **`SpawnerJsonSerializer`** wires `$type` polymorphism on the `SpawnerDto` root with loud collision/constructibility validation.
- **Import/export commands** rewired to the typed DTO path (reflection `FindTypeByName`/`CreateInstance` removed).
- **Data migration:** the 109 `Distribution/Data/Spawns/**` files moved from `"type"`→`"$type"` and legacy `homeRange`→`spawnBounds`. The runtime still *reads* legacy `homeRange` for external files. The `homeRange→spawnBounds` formula is proven equivalent to the runtime conversion (`BoundsEquivalenceTests`, hr=0/1/3/7).
- **Deleted:** `Projects/Server/Json/DynamicJson.cs`.

Sparse export output matches the legacy `ToJson` (nullable DTO properties + `WhenWritingNull`). Binary world-save serialization is untouched.

## Tests

- DTO round-trip per spawner type; sparse-default omission; legacy `homeRange` read; export/import file round-trip.
- `Import_MalformedFile_LeaksNoWorldItems` — proves a mid-array parse failure constructs zero world Items.
- `AllSpawnFilesLoadTests` — every migrated spawn file deserializes and builds.
- Duplicate-discriminator validation.
- UOContent.Tests 485/485, Server.Tests 710/710, build clean.

## Follow-up (not in this PR)

`Rectangle3DConverter.Write` (in `Projects/Server/`) omits `z1/z2` when `Start.Z == -128`, so a future server-side export of a `homeRange`-style spawner round-trips depth 256→255. Pre-existing and out of scope here (Server change); the migrated data reads correctly. Worth a separate small converter PR.
This commit is contained in:
Kamron Batman 2026-06-25 23:24:45 -07:00 • committed by GitHub
parent a26219c837
commit d8a64f3316
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
135 changed files with 7654 additions and 6079 deletions

View file

@ -1,6 +1,6 @@
[
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "05d2885b-2b4d-4c4e-907e-2fdbe4d54e69",
"name": "Spawner (204)",
"location": [5212, 674, -20],
@ -19,7 +19,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0b6e0a59-f918-4f77-8bc8-f5b5d944d3a5",
"name": "Spawner (204)",
"location": [5219, 573, 0],
@ -38,7 +38,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0be3858f-bd81-4a27-9a8d-3846c0df0603",
"name": "Spawner (204)",
"location": [5192, 695, 0],
@ -55,7 +55,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "19d96c26-d7f8-4209-9071-00e0117ed1d8",
"name": "Spawner (204)",
"location": [5163, 567, 0],
@ -73,7 +73,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1cbfae9c-7cc7-45cc-842f-0e75a158cd8a",
"name": "Spawner (204)",
"location": [5307, 546, 0],
@ -92,7 +92,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "25b2761d-dc06-443e-b979-049c7f50abf6",
"name": "Spawner (204)",
"location": [5267, 690, 0],
@ -110,7 +110,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2a7f9024-7d0f-43a0-8721-52b6a3c27c8f",
"name": "Spawner (204)",
"location": [5307, 675, -20],
@ -127,7 +127,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "350cfc3c-1c89-463d-a93a-f2ab22149d19",
"name": "Spawner (204)",
"location": [5323, 571, 0],
@ -144,7 +144,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "35acdddb-eef4-4687-b6d7-6dea19535b71",
"name": "Spawner (204)",
"location": [5314, 748, -20],
@ -164,7 +164,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3d25517a-5b90-4468-9e23-4041f88774e2",
"name": "Spawner (204)",
"location": [5299, 603, 0],
@ -183,7 +183,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "56939d4e-8a5d-47da-a117-e7f56ff6671b",
"name": "Spawner (204)",
"location": [5151, 718, 0],
@ -202,7 +202,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "684234a4-21ea-4f94-897b-e92d88a17ced",
"name": "Spawner (204)",
"location": [5219, 552, 0],
@ -221,7 +221,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7016863d-26dc-4d2e-beda-68031a966ebe",
"name": "Spawner (204)",
"location": [5318, 578, 0],
@ -241,7 +241,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "750ebeff-4327-4bf0-b285-8c6749530985",
"name": "Spawner (204)",
"location": [5190, 655, 0],
@ -257,7 +257,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "79dfd73d-ede7-44e2-8651-94cfe5f77314",
"name": "Spawner (204)",
"location": [5342, 582, 0],
@ -274,7 +274,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "818d1f8c-a5ba-4ef1-a823-2b04bca2f2e2",
"name": "Spawner (204)",
"location": [5320, 708, 0],
@ -290,7 +290,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8507dbe4-8220-4439-adbe-5ef6ebb0fec3",
"name": "Spawner (204)",
"location": [5186, 575, 0],
@ -306,7 +306,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "88b5fff8-7124-4a19-83ad-465e8aaf072b",
"name": "Spawner (204)",
"location": [5150, 742, 0],
@ -324,7 +324,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9862d38e-e12f-4e04-932f-0020f3121d9a",
"name": "Spawner (204)",
"location": [5145, 553, -40],
@ -341,7 +341,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9b00a63e-cf2d-495b-8bd9-61cef122f168",
"name": "Spawner (204)",
"location": [5286, 542, 0],
@ -360,7 +360,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a2d6e670-8282-4b60-bd5f-933e45d3d7a8",
"name": "Spawner (204)",
"location": [5170, 678, 0],
@ -379,7 +379,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ab35ec53-2a16-4cdd-a2a1-f1b84922e36c",
"name": "Spawner (204)",
"location": [5147, 598, -50],
@ -396,7 +396,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c69344fe-718b-4e69-83b8-d12141250707",
"name": "Spawner (204)",
"location": [5219, 734, -20],
@ -412,7 +412,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "cacb456a-f52d-4168-9d59-8df80779a1d6",
"name": "Spawner (204)",
"location": [5183, 727, 0],
@ -429,7 +429,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "cb79321c-1a80-4881-8961-1997174d0458",
"name": "Spawner (204)",
"location": [5212, 531, 0],
@ -446,7 +446,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d4857128-1876-473c-951f-418107fad15c",
"name": "Spawner (204)",
"location": [5327, 542, 0],
@ -465,7 +465,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d8ba7cc3-c1f6-4701-b684-850203cb9e37",
"name": "Spawner (204)",
"location": [5147, 579, -50],
@ -482,7 +482,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d9f4d863-626f-480c-968a-9630e6a9a83d",
"name": "Spawner (204)",
"location": [5145, 622, -50],
@ -499,7 +499,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dbb8562b-7557-4347-9faa-a5facf1ba1d4",
"name": "Spawner (204)",
"location": [5280, 675, 8],
@ -515,7 +515,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "edd28bb3-5aeb-487a-8571-9e4e4aea0783",
"name": "Spawner (204)",
"location": [5187, 603, 0],
@ -533,7 +533,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "efc2564d-a6df-40a6-ad5e-6e6e81d066e9",
"name": "Spawner (204)",
"location": [5322, 626, 0],
@ -553,7 +553,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f2fc19bd-75c9-4f68-b59d-6efc65381167",
"name": "Spawner (204)",
"location": [5283, 583, 0],
@ -570,7 +570,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f5e81dcf-fa17-4d36-8aa1-e73496aad012",
"name": "Spawner (204)",
"location": [5139, 661, 0],
@ -589,7 +589,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f6b68da6-e7f9-4cb1-870c-079ebff4787b",
"name": "Spawner (204)",
"location": [5145, 618, -50],
@ -606,7 +606,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fa668f86-9786-48bc-a676-5a879e77bee2",
"name": "Spawner (204)",
"location": [5295, 623, -5],
@ -622,7 +622,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fb409b68-fe6b-422f-bf2d-95c4a92f5e21",
"name": "Spawner (204)",
"location": [5187, 545, 0],