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": "949f3b33-3f72-4065-b212-a15495e305d6",
"name": "Spawner (2113)",
"location": [3619, 2814, 21],
@ -16,7 +16,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b4555e77-ee2f-44d0-b85b-d7d6f26612ef",
"name": "Spawner (2113)",
"location": [3619, 2814, 21],
@ -32,7 +32,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8cd18e64-2c1c-4853-b82e-8842b4b71dcd",
"name": "Spawner (2113)",
"location": [3485, 2595, 12],
@ -55,7 +55,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c08f91a3-eb9d-464c-96ce-d70f6c5a8fc4",
"name": "Spawner (2113)",
"location": [3446, 2741, 49],
@ -78,7 +78,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0111020b-4ccf-45e7-bfbe-8a4f4fbeedad",
"name": "Spawner (2113)",
"location": [3459, 2655, 9],
@ -101,7 +101,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ed918ae7-13cc-45da-a9ee-166d198bb83f",
"name": "Spawner (2113)",
"location": [3379, 2693, 35],
@ -124,7 +124,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "63c48500-e80c-4662-a58d-b32b2591f94d",
"name": "Spawner (2113)",
"location": [3411, 2592, 44],
@ -147,7 +147,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8dc26593-aecf-4d5c-97c6-d4d11e8411dc",
"name": "Spawner (2113)",
"location": [3464, 2503, 49],
@ -170,7 +170,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2733ca83-6c31-48d5-b360-eedf6f885beb",
"name": "Spawner (2113)",
"location": [3602, 2815, 28],

View file

@ -1,6 +1,6 @@
[
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1ddbb313-bf13-4c9e-9116-3e67c835fbb9",
"name": "Spawner (2122)",
"location": [3674, 2624, 0],
@ -19,7 +19,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "988e7381-2448-4d37-b763-714f1078540b",
"name": "Spawner (2122)",
"location": [3726, 2647, 0],
@ -35,7 +35,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b52ecefe-4f07-4bcd-9870-d01a0a1a41be",
"name": "Spawner (2122)",
"location": [3652, 2511, 0],

View file

@ -1,6 +1,6 @@
[
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "011fc0f3-9e98-44ed-ab77-e0669247aacf",
"name": "Spawner (2125)",
"location": [3645, 2617, 0],
@ -17,7 +17,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "19ecf802-ce51-4125-b13a-efb0afd02cbf",
"name": "Spawner (2125)",
"location": [3667, 2506, 0],
@ -34,7 +34,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "56140e6f-fc52-49b6-bf74-fe782454a95a",
"name": "Spawner (2125)",
"location": [3655, 2665, -2],
@ -50,7 +50,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "625f9122-d16c-4e4e-9d33-bfd367428140",
"name": "Spawner (2125)",
"location": [3615, 2476, 0],
@ -66,7 +66,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d6745c4f-710a-4fc4-b39a-bc54123feac8",
"name": "Spawner (2125)",
"location": [3693, 2509, 0],
@ -82,7 +82,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "43797a84-5e69-45b3-87b6-122e033083e1",
"name": "Spawner (2125)",
"location": [3665, 2581, 0],
@ -99,7 +99,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e022aca5-1733-4cbc-b444-2bc16d30cf13",
"name": "Spawner (2125)",
"location": [3673, 2595, 0],
@ -116,7 +116,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ca72a646-61b9-41a7-988f-8bcf55f63745",
"name": "Spawner (2125)",
"location": [3632, 2573, 0],
@ -134,7 +134,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6ed9b012-4871-4674-a39c-c71f94ca6bc0",
"name": "Spawner (2125)",
"location": [3633, 2635, 0],
@ -151,7 +151,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "64838953-eca4-4076-9c4a-6b8d53dc6da4",
"name": "Spawner (2125)",
"location": [3754, 2704, 40],
@ -167,7 +167,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4391a5e3-0060-49e3-bb90-6bdcbf56f863",
"name": "Spawner (2125)",
"location": [3714, 2651, 20],
@ -183,7 +183,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "85b91e45-a5ec-46df-9286-89b0f4a602a4",
"name": "Spawner (2125)",
"location": [3740, 2692, 40],
@ -199,7 +199,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5156ee52-c3e4-42b0-ac5e-8d84b49cacb8",
"name": "Spawner (2125)",
"location": [3620, 2617, 0],
@ -216,7 +216,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d952003f-99e5-4cf4-bea7-06265ffd2dfb",
"name": "Spawner (2125)",
"location": [3628, 2540, 0],
@ -232,7 +232,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "20e39658-8433-437e-b7fc-d2ea491189f6",
"name": "Spawner (2125)",
"location": [3687, 2477, 0],
@ -250,7 +250,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0ef8d67e-4e4c-4a53-ab5e-2474e573f096",
"name": "Spawner (2125)",
"location": [3668, 2652, 0],
@ -269,7 +269,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dfa29c8c-8888-4752-a063-f8161f01f569",
"name": "Spawner (2125)",
"location": [3661, 2531, 0],
@ -285,7 +285,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d753713d-6bf9-47fe-b4f1-58b6f304ad5b",
"name": "Spawner (2125)",
"location": [3594, 2595, 0],
@ -302,7 +302,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "98ef6eb6-72ea-43a5-b2ea-8c153f6135d3",
"name": "Spawner (2125)",
"location": [3671, 2615, 0],
@ -318,7 +318,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5294b511-5759-452f-8009-94157b248961",
"name": "Spawner (2125)",
"location": [3745, 2585, 40],
@ -334,7 +334,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bd1d8c55-7a8a-465a-a8ff-f47adad641c6",
"name": "Spawner (2125)",
"location": [3642, 2504, 0],

View file

@ -1,6 +1,6 @@
[
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "85bca9f2-2bf4-4400-ac33-e72e379d2bb0",
"name": "Spawner (2126)",
"location": [3607, 2417, 45],
@ -34,7 +34,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2828aaaa-15bc-48ca-8200-b30244c9e88f",
"name": "Spawner (2126)",
"location": [3555, 2525, 13],
@ -68,7 +68,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bf2cf539-4f86-4a2c-81a2-6d09d6a36dee",
"name": "Spawner (2126)",
"location": [3506, 2477, 28],
@ -102,7 +102,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "335130b1-2341-4f80-88b6-1a2b1261d158",
"name": "Spawner (2126)",
"location": [3452, 2563, 32],
@ -136,7 +136,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "21e1d3a4-1447-4e38-9b17-70a49c7b9093",
"name": "Spawner (2126)",
"location": [3429, 2650, 43],
@ -170,7 +170,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "37fab5a5-6dcf-4ea0-9bcf-cee7546a8788",
"name": "Spawner (2126)",
"location": [3449, 2737, 49],
@ -204,7 +204,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "85d1ae4d-d1dd-431f-a749-7ba9e19b7adc",
"name": "Spawner (2126)",
"location": [3736, 2512, 35],
@ -238,7 +238,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "03dcb299-f89d-4554-8700-c389b70b560e",
"name": "Spawner (2126)",
"location": [3718, 2736, 38],
@ -272,7 +272,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4415e8ab-b437-46d7-a245-8056c9611fa4",
"name": "Spawner (2126)",
"location": [3654, 2866, 61],
@ -306,7 +306,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "363b94f4-e1ba-474a-ab21-76e457b14abf",
"name": "Spawner (2126)",
"location": [3662, 2802, 51],
@ -340,7 +340,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8d4ae520-6345-40f2-b7cb-3bb2e98d2219",
"name": "Spawner (2126)",
"location": [3596, 2804, 51],
@ -374,7 +374,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1105d916-9527-4d49-8ca2-54e4ca61a342",
"name": "Spawner (2126)",
"location": [3546, 2812, 13],