## 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.
338 lines
8.8 KiB
JSON
338 lines
8.8 KiB
JSON
[
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "0c48edf9-a88e-45f1-9ba3-cd8c919aefbc",
|
|
"name": "Spawner",
|
|
"location": [823, 2274, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCabbage", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Cabbage Field in Skara Brae 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "0c6481ab-3341-4113-ac1a-6523c19c881d",
|
|
"name": "Spawner",
|
|
"location": [1216, 1604, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableTurnip", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Turnip Field in Britain 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "2bf378f0-77ed-44cd-8dfb-181d068f401e",
|
|
"name": "Spawner",
|
|
"location": [843, 2352, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Skara Brae 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "3d7925bf-2642-4185-bc03-fa314fb75a7f",
|
|
"name": "Spawner",
|
|
"location": [675, 1179, 0],
|
|
"map": "Felucca",
|
|
"count": 10,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 10, "probability": 100 }
|
|
],
|
|
"region": "A Field of Sheep in Yew 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "425ac125-1597-41b4-9e6d-72b91801230b",
|
|
"name": "Spawner",
|
|
"location": [675, 939, 0],
|
|
"map": "Felucca",
|
|
"count": 10,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 10, "probability": 100 }
|
|
],
|
|
"region": "A Field of Sheep in Yew 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "427451dd-93fa-43c9-85c6-01536066c8cc",
|
|
"name": "Spawner",
|
|
"location": [823, 2254, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCarrot", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Carrot Field in Skara Brae"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "4b4e3973-d836-4f7c-b9f7-5fb7d468da28",
|
|
"name": "Spawner",
|
|
"location": [4567, 1475, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCotton", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Cotton Field in Moonglow"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "57da6436-6b16-4697-add5-f4b45776ccbc",
|
|
"name": "Spawner",
|
|
"location": [813, 2163, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Skara Brae 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "57f955bb-49e1-4a66-a214-4fbddf8d572b",
|
|
"name": "Spawner",
|
|
"location": [1231, 1723, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableOnion", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "An Onion Field in Britain 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "58b14343-2032-4507-a913-6b536507c2e5",
|
|
"name": "Spawner",
|
|
"location": [1199, 1823, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Britain 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "640fe3f8-9917-4dfa-986d-038923163add",
|
|
"name": "Spawner",
|
|
"location": [823, 2264, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableOnion", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "An Onion Field in Skara Brae"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "7b09d4b5-fe63-4f95-8525-3c970c513adc",
|
|
"name": "Spawner",
|
|
"location": [1232, 1604, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCarrot", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Carrot Field in Britain 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "8536c19c-2036-495a-a3c5-e82c9f32f5ad",
|
|
"name": "Spawner",
|
|
"location": [1183, 1683, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCabbage", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Cabbage Field in Britain 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "9805ef58-355f-4352-bfc4-43c6710ff409",
|
|
"name": "Spawner",
|
|
"location": [1215, 1723, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCarrot", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Carrot Field in Britain 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "aed692cc-2dac-4c80-931b-35c73ca8d092",
|
|
"name": "Spawner",
|
|
"location": [571, 1099, 0],
|
|
"map": "Felucca",
|
|
"count": 10,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 10, "probability": 100 }
|
|
],
|
|
"region": "A Farm in Yew"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "b29b54ab-5ae3-4ce3-93ac-326f324a34f6",
|
|
"name": "Spawner",
|
|
"location": [1135, 1791, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Britain 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "bba64d97-11eb-4a87-b326-e41e3c9b355d",
|
|
"name": "Spawner",
|
|
"location": [375, 1191, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Yew 2"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "d8a2e54e-4951-468f-93c5-376460070cba",
|
|
"name": "Spawner",
|
|
"location": [1199, 1683, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableTurnip", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Turnip Field in Britain 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "e37b4d74-c87e-4102-8c87-8f80c728fe0c",
|
|
"name": "Spawner",
|
|
"location": [567, 1239, 0],
|
|
"map": "Felucca",
|
|
"count": 5,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 5, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Yew 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "e416e9fa-5a9a-426b-962e-50cdce64ca7f",
|
|
"name": "Spawner",
|
|
"location": [1152, 1576, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Britain 5"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "e6a4aede-5a76-473d-8331-4bb73701ea1a",
|
|
"name": "Spawner",
|
|
"location": [1120, 1624, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Britain 4"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "e7b34039-ec1f-43c8-b64f-d7462ce7ae9d",
|
|
"name": "Spawner",
|
|
"location": [823, 2355, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCotton", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Cotton Field in Skara Brae"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "f4bccdc2-c663-4ccd-b7df-31656fa2ae09",
|
|
"name": "Spawner",
|
|
"location": [823, 2264, 0],
|
|
"map": "Felucca",
|
|
"count": 6,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableCabbage", "maxCount": 6, "probability": 100 }
|
|
],
|
|
"region": "A Cabbage Field in Skara Brae 1"
|
|
},
|
|
{
|
|
"$type": "RegionSpawner",
|
|
"guid": "f68b6bc8-f822-4c7a-9d01-37fad25bc95b",
|
|
"name": "Spawner",
|
|
"location": [1231, 1887, 0],
|
|
"map": "Felucca",
|
|
"count": 8,
|
|
"minDelay": "00:00:10",
|
|
"maxDelay": "00:00:30",
|
|
"entries": [
|
|
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
|
|
],
|
|
"region": "A Wheatfield in Britain 3"
|
|
}
|
|
]
|