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:
parent
a26219c837
commit
d8a64f3316
135 changed files with 7654 additions and 6079 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[
|
||||
[
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "0c48edf9-a88e-45f1-9ba3-cd8c919aefbc",
|
||||
"name": "Spawner",
|
||||
"location": [823, 2274, 0],
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"region": "A Cabbage Field in Skara Brae 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "0c6481ab-3341-4113-ac1a-6523c19c881d",
|
||||
"name": "Spawner",
|
||||
"location": [1216, 1604, 0],
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"region": "A Turnip Field in Britain 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "2bf378f0-77ed-44cd-8dfb-181d068f401e",
|
||||
"name": "Spawner",
|
||||
"location": [843, 2352, 0],
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
"region": "A Wheatfield in Skara Brae 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "3d7925bf-2642-4185-bc03-fa314fb75a7f",
|
||||
"name": "Spawner",
|
||||
"location": [675, 1179, 0],
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
"region": "A Field of Sheep in Yew 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "425ac125-1597-41b4-9e6d-72b91801230b",
|
||||
"name": "Spawner",
|
||||
"location": [675, 939, 0],
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
"region": "A Field of Sheep in Yew 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "427451dd-93fa-43c9-85c6-01536066c8cc",
|
||||
"name": "Spawner",
|
||||
"location": [823, 2254, 0],
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
"region": "A Carrot Field in Skara Brae"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "4b4e3973-d836-4f7c-b9f7-5fb7d468da28",
|
||||
"name": "Spawner",
|
||||
"location": [4567, 1475, 0],
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
"region": "A Cotton Field in Moonglow"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "57da6436-6b16-4697-add5-f4b45776ccbc",
|
||||
"name": "Spawner",
|
||||
"location": [813, 2163, 0],
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
"region": "A Wheatfield in Skara Brae 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "57f955bb-49e1-4a66-a214-4fbddf8d572b",
|
||||
"name": "Spawner",
|
||||
"location": [1231, 1723, 0],
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
"region": "An Onion Field in Britain 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "58b14343-2032-4507-a913-6b536507c2e5",
|
||||
"name": "Spawner",
|
||||
"location": [1199, 1823, 0],
|
||||
|
|
@ -140,7 +140,7 @@
|
|||
"region": "A Wheatfield in Britain 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "640fe3f8-9917-4dfa-986d-038923163add",
|
||||
"name": "Spawner",
|
||||
"location": [823, 2264, 0],
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"region": "An Onion Field in Skara Brae"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "7b09d4b5-fe63-4f95-8525-3c970c513adc",
|
||||
"name": "Spawner",
|
||||
"location": [1232, 1604, 0],
|
||||
|
|
@ -168,7 +168,7 @@
|
|||
"region": "A Carrot Field in Britain 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "8536c19c-2036-495a-a3c5-e82c9f32f5ad",
|
||||
"name": "Spawner",
|
||||
"location": [1183, 1683, 0],
|
||||
|
|
@ -182,7 +182,7 @@
|
|||
"region": "A Cabbage Field in Britain 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "9805ef58-355f-4352-bfc4-43c6710ff409",
|
||||
"name": "Spawner",
|
||||
"location": [1215, 1723, 0],
|
||||
|
|
@ -196,7 +196,7 @@
|
|||
"region": "A Carrot Field in Britain 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "aed692cc-2dac-4c80-931b-35c73ca8d092",
|
||||
"name": "Spawner",
|
||||
"location": [571, 1099, 0],
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
"region": "A Farm in Yew"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "b29b54ab-5ae3-4ce3-93ac-326f324a34f6",
|
||||
"name": "Spawner",
|
||||
"location": [1135, 1791, 0],
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
"region": "A Wheatfield in Britain 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "bba64d97-11eb-4a87-b326-e41e3c9b355d",
|
||||
"name": "Spawner",
|
||||
"location": [375, 1191, 0],
|
||||
|
|
@ -238,7 +238,7 @@
|
|||
"region": "A Wheatfield in Yew 2"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "d8a2e54e-4951-468f-93c5-376460070cba",
|
||||
"name": "Spawner",
|
||||
"location": [1199, 1683, 0],
|
||||
|
|
@ -252,7 +252,7 @@
|
|||
"region": "A Turnip Field in Britain 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "e37b4d74-c87e-4102-8c87-8f80c728fe0c",
|
||||
"name": "Spawner",
|
||||
"location": [567, 1239, 0],
|
||||
|
|
@ -266,7 +266,7 @@
|
|||
"region": "A Wheatfield in Yew 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "e416e9fa-5a9a-426b-962e-50cdce64ca7f",
|
||||
"name": "Spawner",
|
||||
"location": [1152, 1576, 0],
|
||||
|
|
@ -280,7 +280,7 @@
|
|||
"region": "A Wheatfield in Britain 5"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "e6a4aede-5a76-473d-8331-4bb73701ea1a",
|
||||
"name": "Spawner",
|
||||
"location": [1120, 1624, 0],
|
||||
|
|
@ -294,7 +294,7 @@
|
|||
"region": "A Wheatfield in Britain 4"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "e7b34039-ec1f-43c8-b64f-d7462ce7ae9d",
|
||||
"name": "Spawner",
|
||||
"location": [823, 2355, 0],
|
||||
|
|
@ -308,7 +308,7 @@
|
|||
"region": "A Cotton Field in Skara Brae"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "f4bccdc2-c663-4ccd-b7df-31656fa2ae09",
|
||||
"name": "Spawner",
|
||||
"location": [823, 2264, 0],
|
||||
|
|
@ -322,7 +322,7 @@
|
|||
"region": "A Cabbage Field in Skara Brae 1"
|
||||
},
|
||||
{
|
||||
"type": "RegionSpawner",
|
||||
"$type": "RegionSpawner",
|
||||
"guid": "f68b6bc8-f822-4c7a-9d01-37fad25bc95b",
|
||||
"name": "Spawner",
|
||||
"location": [1231, 1887, 0],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue