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": "b37c3d38-2d53-4d2d-a8aa-a2e4926a29c6",
"name": "Spawner (402)",
"location": [1119, 530, -90],
@ -19,7 +19,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e1ba3ada-d1fd-44fb-83b8-82e0e1fcf94f",
"name": "Spawner (402)",
"location": [1674, 659, -84],
@ -40,7 +40,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "099efd1a-520c-4e6b-bb39-552def69e2df",
"name": "Spawner (402)",
"location": [1434, 607, -89],
@ -61,7 +61,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c9c80a39-40db-4cb9-a3c7-243957ecdd39",
"name": "Spawner (402)",
"location": [1194, 638, -84],
@ -82,7 +82,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0335054c-d38b-4c5a-9fc6-c04c42ba9de1",
"name": "Spawner (402)",
"location": [920, 356, -85],

View file

@ -1,6 +1,6 @@
[
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "27bdb175-d7c0-49c9-b389-b26fe028e58a",
"name": "Spawner (404)",
"location": [1920, 1124, -90],
@ -23,7 +23,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "56c3146a-f634-4ee8-9dd8-65e7fb98f8e2",
"name": "Spawner (404)",
"location": [1920, 1124, -90],
@ -52,7 +52,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5286c49e-21e5-407a-a363-8ecf2ff5eafe",
"name": "Spawner (404)",
"location": [2266, 1218, -85],
@ -74,7 +74,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "695dcc16-4e4d-4b93-be25-05cf6ddf199c",
"name": "Spawner (404)",
"location": [2267, 1218, -85],
@ -103,7 +103,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bb4e40ab-96e4-4061-85e9-8011b42ed020",
"name": "Spawner (404)",
"location": [2303, 1258, -86],
@ -125,7 +125,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3e41feda-8300-4fa4-ba72-6752d37486cd",
"name": "Spawner (404)",
"location": [2304, 1258, -86],
@ -146,7 +146,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4b59baa2-7ce0-40a6-9c68-c6f2210f0869",
"name": "Spawner (404)",
"location": [1641, 1797, -110],
@ -165,7 +165,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9aa39353-19ab-480a-9e96-6a7ba1485251",
"name": "Spawner (404)",
"location": [1899, 1609, -110],
@ -184,7 +184,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6216b5d7-8c6f-420b-b615-54edf74cb173",
"name": "Spawner (404)",
"location": [1741, 1487, -110],
@ -203,7 +203,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3f641570-9b47-44a3-859a-e9ab15dd4da8",
"name": "Spawner (404)",
"location": [1837, 1803, -110],
@ -222,7 +222,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "18a04411-aa44-46c4-9476-a77365090afa",
"name": "Spawner (404)",
"location": [1823, 1799, -90],
@ -231,7 +231,10 @@
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": { "start": { "x": 1819, "y": 1795, "z": -90 }, "end": { "x": 1827, "y": 1803, "z": -75 } },
"spawnBounds": {
"start": { "x": 1819, "y": 1795, "z": -90 },
"end": { "x": 1827, "y": 1803, "z": -75 }
},
"walkingRange": 15,
"entries": [
{ "name": "SkeletalKnight", "maxCount": 2, "probability": 100 },
@ -243,7 +246,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4a2b88ce-b146-439d-917c-3f056775012e",
"name": "Spawner (404)",
"location": [1821, 1798, -110],
@ -252,7 +255,10 @@
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": { "start": { "x": 1817, "y": 1794, "z": -110 }, "end": { "x": 1825, "y": 1802, "z": -95 } },
"spawnBounds": {
"start": { "x": 1817, "y": 1794, "z": -110 },
"end": { "x": 1825, "y": 1802, "z": -95 }
},
"walkingRange": 15,
"entries": [
{ "name": "GiantRat", "maxCount": 4, "probability": 100 },
@ -262,7 +268,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d91c089c-551a-4333-adbb-4ff82e82908f",
"name": "Spawner (404)",
"location": [1466, 1418, -90],
@ -282,7 +288,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "01e68ebc-8521-44bd-ada3-a96cf6dc7b8e",
"name": "Spawner (404)",
"location": [1477, 1241, -85],
@ -302,7 +308,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7a14417b-5b9c-4c46-978d-4f9aaf4ab2d8",
"name": "Spawner (404)",
"location": [1347, 1739, -110],
@ -322,7 +328,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c5036af7-93ad-41b3-a29b-48b1e60393fe",
"name": "Spawner (404)",
"location": [1327, 1146, -90],
@ -341,7 +347,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6d49d6f6-4054-4b02-a319-8be526aaefcf",
"name": "Spawner (404)",
"location": [1256, 1221, -90],
@ -360,7 +366,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "aa9e1655-d9ed-4fdc-99c9-ef04a9234b9b",
"name": "Spawner (404)",
"location": [1345, 1314, -85],
@ -379,7 +385,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fad094bd-915f-49d1-a0c6-486029d25506",
"name": "Spawner (404)",
"location": [943, 1150, -90],
@ -395,7 +401,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c2cdce00-aaf5-40cc-b1f5-28c0d1364eff",
"name": "Spawner (404)",
"location": [900, 1494, -90],
@ -414,7 +420,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c5657dd4-3bbc-4557-a5bb-9a111c7e6763",
"name": "Spawner (404)",
"location": [829, 1474, -85],
@ -433,7 +439,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eb36fa8a-c49d-42b2-ac5a-246391bdb716",
"name": "Spawner (404)",
"location": [989, 1419, -82],
@ -452,7 +458,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e9f2158f-8b22-46cd-94ce-924a22b6dd65",
"name": "Spawner (404)",
"location": [913, 1335, -90],
@ -471,7 +477,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "20e95aca-18d5-4862-a0cd-72e65f188dc4",
"name": "Spawner (404)",
"location": [881, 927, -90],
@ -487,7 +493,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "51a28e73-74fd-454b-8c02-5aeacd7f041b",
"name": "Spawner (404)",
"location": [882, 1000, -90],
@ -503,7 +509,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "83ded31b-46a4-4d33-b340-abb593f01ea9",
"name": "Spawner (404)",
"location": [989, 1420, -82],
@ -519,7 +525,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0109ed70-dc93-4c76-9647-ef3565315a1b",
"name": "Spawner (404)",
"location": [1455, 1445, -90],
@ -535,7 +541,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c3fa83a1-72e9-440b-9ddf-d8733e54e59f",
"name": "Spawner (404)",
"location": [1429, 1549, -110],
@ -551,7 +557,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f18bf6f0-7dde-43f7-81f5-ac26393e48c0",
"name": "Spawner (404)",
"location": [1305, 1095, -90],
@ -567,7 +573,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b49f301b-9378-489a-b999-51785a3307e9",
"name": "Spawner (404)",
"location": [790, 948, -90],
@ -586,7 +592,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "74e793f9-ab88-4c51-b713-203fe3ec3828",
"name": "Spawner (404)",
"location": [766, 1364, -90],
@ -605,7 +611,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d5aa4b2a-02bd-4fab-ad78-6e0a2cd7c575",
"name": "Spawner (404)",
"location": [656, 1431, -90],
@ -624,7 +630,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ff590783-b876-429c-aaad-e6096a86ea7e",
"name": "Spawner (404)",
"location": [805, 1232, -90],
@ -643,7 +649,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eeaacf1f-a83f-4ba0-86f3-1f39aff6a3ec",
"name": "Spawner (404)",
"location": [672, 1237, -90],
@ -662,7 +668,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d0fe5086-d802-43d1-b62f-a81dbc603ca1",
"name": "Spawner (404)",
"location": [684, 1109, -90],
@ -681,7 +687,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c30d468f-b35f-4aa5-a13f-773518810151",
"name": "Spawner (404)",
"location": [1079, 1440, -90],
@ -700,7 +706,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "cf2df3ea-792b-4d9e-9723-b094b5654dcf",
"name": "Spawner (404)",
"location": [966, 1213, -90],
@ -719,7 +725,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6651b803-1285-427b-93a5-c127957915bc",
"name": "Spawner (404)",
"location": [925, 1105, -90],
@ -738,7 +744,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bfcc0baf-f539-440f-96b6-911679afc732",
"name": "Spawner (404)",
"location": [1049, 1332, -90],
@ -757,7 +763,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "445e307b-72d4-4d2e-9284-14938e4dab15",
"name": "Spawner (404)",
"location": [1086, 1067, -90],
@ -776,7 +782,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "375e8bd1-6823-442a-a125-2e2acf53edd9",
"name": "Spawner (404)",
"location": [1062, 988, -90],

View file

@ -1,6 +1,6 @@
[
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "befc0201-8bec-432e-8ced-3ee5ceb57d09",
"name": "Spawner (405)",
"location": [1050, 1434, -85],
@ -18,7 +18,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d8e03ce8-cd76-433e-af76-0c0767290705",
"name": "Spawner (405)",
"location": [1042, 1444, -90],