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": "0d9c6323-7da9-4e12-861a-76fd30b3e3dc",
"name": "Spawner (405)",
"location": [950, 520, -55],
@ -17,7 +17,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0db7d5e9-beaa-43a2-ac46-2802ac962fcb",
"name": "Spawner (405)",
"location": [2068, 1372, -75],
@ -34,7 +34,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3d301c77-1ade-4823-b2ff-f1d4e0ed2bf0",
"name": "Spawner (405)",
"location": [1029, 520, -55],
@ -51,7 +51,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3d69c950-70d4-48c5-8729-be8848330c1a",
"name": "Spawner (405)",
"location": [2045, 1397, -90],
@ -67,7 +67,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3dcbbc92-14ac-4798-8753-41aa3b6b2450",
"name": "Spawner (405)",
"location": [2023, 1379, -80],
@ -85,7 +85,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "54deea77-bc1e-446b-8274-ea23bddb369e",
"name": "Spawner (405)",
"location": [989, 527, -50],
@ -101,7 +101,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "55fab330-6d2a-4823-a666-3dfa33dceeb6",
"name": "Spawner (405)",
"location": [1977, 1365, -80],
@ -118,7 +118,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "72a7f47d-d4cb-4530-9ba6-3878482279b2",
"name": "Spawner (405)",
"location": [2078, 1327, -80],
@ -135,7 +135,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "79aa68b8-d2bd-4a4d-ad84-422588aeaaff",
"name": "Spawner (405)",
"location": [1016, 514, -70],
@ -151,7 +151,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7a10d380-53e3-48c9-9c5b-07a3c215032a",
"name": "Spawner (405)",
"location": [1027, 494, -70],
@ -167,7 +167,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7bd799ab-c7fb-446e-b6cb-580d1fe2f7fb",
"name": "Spawner (405)",
"location": [976, 512, -50],
@ -184,7 +184,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "81e4af4e-3310-4864-8517-a032d487716f",
"name": "Spawner (405)",
"location": [1992, 1315, -90],
@ -200,7 +200,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8ed338dc-51c1-40b3-a91a-7ebd8d91131b",
"name": "Spawner (405)",
"location": [2060, 1283, -80],
@ -218,7 +218,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9347985d-23a4-4044-9a5c-79939c9f7df0",
"name": "Spawner (405)",
"location": [2037, 1311, -85],
@ -234,7 +234,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "99cde945-5b9c-4cff-924f-a59142bcbdb9",
"name": "Spawner (405)",
"location": [989, 520, -50],
@ -251,7 +251,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9ad9f47f-17bc-4790-9a84-1e9aee66478f",
"name": "Spawner (405)",
"location": [2011, 1326, -80],
@ -268,7 +268,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a0c83d13-2354-494f-9535-d4e587cc7a6b",
"name": "Spawner (405)",
"location": [2025, 1387, -80],
@ -286,7 +286,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a263ccfc-65e1-4181-93b6-6e1c7914f5b8",
"name": "Spawner (405)",
"location": [2048, 1343, -85],
@ -303,7 +303,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a4200c02-8035-48bc-b79d-8b45225ffd4f",
"name": "Spawner (405)",
"location": [1004, 527, -50],
@ -322,7 +322,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b4f71d7e-8c76-47db-a1c1-b81c4484741b",
"name": "Spawner (405)",
"location": [2027, 1353, -90],
@ -341,7 +341,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b7226aea-9fd0-4079-8e21-70a7a443d0b2",
"name": "Spawner (405)",
"location": [1056, 1434, -85],
@ -357,7 +357,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c1bda852-fee4-45ee-aa79-630980bc44e9",
"name": "Spawner (405)",
"location": [1003, 512, -50],
@ -377,7 +377,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d6de9f30-1ee9-4914-853c-be075bd119ce",
"name": "Spawner (405)",
"location": [2083, 1322, -80],
@ -395,7 +395,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "df531ac9-d2b0-48ae-bc19-dc043d6af3de",
"name": "Spawner (405)",
"location": [993, 511, -50],
@ -413,7 +413,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e08eb38c-0b25-4387-8999-f9d8a0a877aa",
"name": "Spawner (405)",
"location": [961, 517, -70],
@ -429,7 +429,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eb50b13f-dd98-42c1-bfa0-2599bdc85de3",
"name": "Spawner (405)",
"location": [2066, 1282, -80],
@ -446,7 +446,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f62df843-74a7-4bb0-a101-53d1dc4cb806",
"name": "Spawner (405)",
"location": [976, 527, -50],
@ -466,7 +466,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fa9dbd50-e0ca-4206-a197-0fe85a349900",
"name": "Spawner (405)",
"location": [2017, 1356, -90],