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": "04236803-3dc7-426b-8a8d-d927b1bd0086",
"name": "Spawner (311)",
"location": [1279, 781, -80],
@ -16,7 +16,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "063240fa-7f32-4bad-b79e-bf9c8ca7b9b9",
"name": "Spawner (311)",
"location": [1108, 492, -80],
@ -35,7 +35,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "149dfca3-652c-4777-b005-a0cc95f30d9d",
"name": "Spawner (311)",
"location": [1517, 541, 11],
@ -51,7 +51,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "165f16c6-b66f-474a-aed6-0d6c2ee57123",
"name": "Spawner (311)",
"location": [1715, 228, 78],
@ -69,7 +69,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "17243bc5-68af-437f-bb95-963913a05306",
"name": "Spawner (311)",
"location": [1191, 696, -80],
@ -85,7 +85,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1b101011-42fa-41b1-b15c-f15fdcaaf6ec",
"name": "Spawner (311)",
"location": [1517, 558, 5],
@ -101,7 +101,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1e1dd613-72d1-4715-b899-1086679533ab",
"name": "Spawner (311)",
"location": [1665, 244, 93],
@ -118,7 +118,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1e2bfa6b-abe9-4dcd-b996-bc4dcdd17f9f",
"name": "Spawner (311)",
"location": [1234, 760, -80],
@ -134,7 +134,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1f0236b1-b26e-4572-b03a-d53d94ab1a6a",
"name": "Spawner (311)",
"location": [1110, 542, -77],
@ -153,7 +153,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "26a0cc2e-3dfc-49d7-9274-d993fc0371db",
"name": "Spawner (311)",
"location": [1292, 755, -80],
@ -169,7 +169,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "34ad74fe-d7eb-4427-8c7e-e7658bd63156",
"name": "Spawner (311)",
"location": [1363, 1046, -13],
@ -185,7 +185,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "38614de6-1ed9-41a0-b30b-59ae8191316f",
"name": "Spawner (311)",
"location": [1213, 719, -80],
@ -202,7 +202,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3bfd2dc7-d6d7-4604-858f-246ed9b25d35",
"name": "Spawner (311)",
"location": [1203, 762, -80],
@ -218,7 +218,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4befab4e-1b1a-4290-ae76-6c176b48a18a",
"name": "Spawner (311)",
"location": [1046, 517, -80],
@ -235,7 +235,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "56b2d766-8057-4400-913f-6c10a5155f98",
"name": "Spawner (311)",
"location": [1251, 703, -80],
@ -251,7 +251,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "59ae00b4-1b69-42de-8faa-670350014ad7",
"name": "Spawner (311)",
"location": [1116, 527, -80],
@ -268,7 +268,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5a5f79c6-3813-428e-a5fe-00893d247556",
"name": "Spawner (311)",
"location": [1161, 668, -80],
@ -284,7 +284,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5f1d23bd-e6c8-422e-b69e-a88d61fa61c0",
"name": "Spawner (311)",
"location": [1243, 732, -80],
@ -302,7 +302,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "64e33b76-ba88-4906-a91f-4b56dea52a81",
"name": "Spawner (311)",
"location": [1181, 691, -80],
@ -318,7 +318,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6ac6b235-2d7c-4058-bc8d-d7f73cd17d5a",
"name": "Spawner (311)",
"location": [817, 1065, -30],
@ -338,7 +338,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6c8f4381-39be-4530-bfc3-1478035c1aa5",
"name": "Spawner (311)",
"location": [1108, 492, -80],
@ -354,7 +354,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "72ce5891-f2dd-4a3c-8019-199928c49dcc",
"name": "Spawner (311)",
"location": [1231, 694, -80],
@ -371,7 +371,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7f13f3fa-2330-4dd5-9985-164d8e240659",
"name": "Spawner (311)",
"location": [1180, 1188, -25],
@ -387,7 +387,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "824d3710-eb67-4f27-8917-6debd7c8e2ca",
"name": "Spawner (311)",
"location": [1517, 552, 10],
@ -405,7 +405,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "86ff8b56-5d0c-4218-80d4-749414e3f568",
"name": "Spawner (311)",
"location": [1191, 696, -80],
@ -421,7 +421,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "88f2279d-e351-4611-8255-206a456394d6",
"name": "Spawner (311)",
"location": [1110, 542, -77],
@ -437,7 +437,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "917aa704-acc1-49ce-81c6-401f0604b260",
"name": "Spawner (311)",
"location": [1274, 728, -80],
@ -453,7 +453,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "93476fd0-4859-4358-90ab-4371f8c1ac40",
"name": "Spawner (311)",
"location": [1181, 1189, -25],
@ -469,7 +469,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9b71629a-ede8-4fba-bd86-63a895c4c422",
"name": "Spawner (311)",
"location": [1214, 744, -80],
@ -485,7 +485,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9d3ed0a0-e458-4fa9-bc94-008304706893",
"name": "Spawner (311)",
"location": [1516, 557, 4],
@ -502,7 +502,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9fce7b2d-3671-4d8e-bf22-19c10ec5709f",
"name": "Spawner (311)",
"location": [1280, 770, -80],
@ -518,7 +518,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c4c688bb-cf62-4b3d-84bd-2633333c0242",
"name": "Spawner (311)",
"location": [1517, 552, 10],
@ -537,7 +537,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c671596f-69b6-45df-a106-b8fce71aa156",
"name": "Spawner (311)",
"location": [1517, 541, 61],
@ -553,7 +553,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e0710f4e-9d44-4e1e-be0d-3f560d5f52c6",
"name": "Spawner (311)",
"location": [1643, 310, 48],
@ -574,7 +574,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eb0f7637-62d3-483f-835e-248e48ffdc44",
"name": "Spawner (311)",
"location": [1187, 685, -80],
@ -590,7 +590,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ed8774de-7f1f-4f40-bb03-0ff9e7d1ff31",
"name": "Spawner (311)",
"location": [1262, 758, -80],
@ -606,7 +606,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ef56cf38-1547-41ad-adc4-ba72aa156f87",
"name": "Spawner (311)",
"location": [1127, 1112, -34],
@ -623,7 +623,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f216ee7b-bb49-415a-aace-924f1778e3cc",
"name": "Spawner (311)",
"location": [1643, 310, 48],
@ -640,7 +640,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fb0b433c-9d26-4a0f-b9a0-4ddb16ea1904",
"name": "Spawner (311)",
"location": [1046, 517, -80],
@ -656,7 +656,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fb1de953-cfdd-46d6-aab9-68cbdfe60780",
"name": "Spawner (311)",
"location": [1203, 1138, -25],