## 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.
522 lines
14 KiB
JSON
522 lines
14 KiB
JSON
[
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "03f1632a-1fc5-4132-92a7-288b4baf9ae9",
|
|
"name": "Spawner (503)",
|
|
"location": [744, 1347, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "217af4ba-df3f-4134-b360-8f8f921e8a01",
|
|
"name": "Spawner (503)",
|
|
"location": [721, 1274, 25],
|
|
"map": "Tokuno",
|
|
"count": 5,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "366108fc-b366-4620-b8bb-4d04014d40b7",
|
|
"name": "Spawner (503)",
|
|
"location": [625, 1317, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Crane", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Goat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rabbit", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "3cc46920-b82d-4c3b-b622-611f7b416b66",
|
|
"name": "Spawner (503)",
|
|
"location": [803, 1338, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Rat", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "3eced8bf-e4d6-484b-ae5f-6519e827a787",
|
|
"name": "Spawner (503)",
|
|
"location": [679, 1224, 25],
|
|
"map": "Tokuno",
|
|
"count": 2,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "40d3a681-f9b5-4866-b6c2-93aa8d211660",
|
|
"name": "Spawner (503)",
|
|
"location": [834, 1351, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Crane", "maxCount": 4, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "40d40ab7-a774-4443-8225-790b81878d3a",
|
|
"name": "Spawner (503)",
|
|
"location": [768, 1192, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "42f392a2-faec-48fe-b8fa-44c394ce4f31",
|
|
"name": "Spawner (503)",
|
|
"location": [844, 1318, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Crane", "maxCount": 1, "probability": 100 },
|
|
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Pig", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "4400a7f9-33b7-4f12-993e-6370e8422577",
|
|
"name": "Spawner (503)",
|
|
"location": [812, 1222, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "59135bf6-67a7-42db-ae76-2d9d53ee8d6b",
|
|
"name": "Spawner (503)",
|
|
"location": [628, 1310, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 4, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "5a5e3558-be1a-49cc-a5cc-201c160e6c7b",
|
|
"name": "Spawner (503)",
|
|
"location": [711, 1322, 25],
|
|
"map": "Tokuno",
|
|
"count": 8,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Dog", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Crane", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Sheep", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "63828858-74cb-4daa-a2e3-d79928cd539d",
|
|
"name": "Spawner (503)",
|
|
"location": [630, 1225, 25],
|
|
"map": "Tokuno",
|
|
"count": 8,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 3, "probability": 100 },
|
|
{ "name": "Crane", "maxCount": 3, "probability": 100 },
|
|
{ "name": "Rabbit", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "660e512a-833e-42d2-b13d-8d9aee2b8e68",
|
|
"name": "Spawner (503)",
|
|
"location": [667, 1343, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Rabbit", "maxCount": 4, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "787cabab-33ea-4938-a32d-5f3443af22c4",
|
|
"name": "Spawner (503)",
|
|
"location": [712, 1238, 25],
|
|
"map": "Tokuno",
|
|
"count": 1,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Cat", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "7c22ce54-d85a-48c0-8388-25f26d1c7904",
|
|
"name": "Spawner (503)",
|
|
"location": [756, 1236, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "8065872f-41df-44a9-b382-d5d16e9f31da",
|
|
"name": "Spawner (503)",
|
|
"location": [683, 1231, 25],
|
|
"map": "Tokuno",
|
|
"count": 1,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "8ca277a3-a47d-4c33-8470-60d67d1fb2c7",
|
|
"name": "Spawner (503)",
|
|
"location": [628, 1272, 25],
|
|
"map": "Tokuno",
|
|
"count": 8,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 3, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 3, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "962520ff-e7ec-4187-b385-5550af5428cb",
|
|
"name": "Spawner (503)",
|
|
"location": [844, 1198, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rabbit", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Horse", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Pig", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "9780bf47-bc15-4bdd-8dc6-0a5ec2abddea",
|
|
"name": "Spawner (503)",
|
|
"location": [740, 1284, 25],
|
|
"map": "Tokuno",
|
|
"count": 2,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "9bf1fbe7-640a-44fe-89c5-b8771032d64b",
|
|
"name": "Spawner (503)",
|
|
"location": [807, 1287, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "ae3ea0e1-e260-4f24-a0f2-7a617cbe7bbe",
|
|
"name": "Spawner (503)",
|
|
"location": [713, 1351, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"spawnBounds": {
|
|
"start": { "x": 693, "y": 1331, "z": -128 },
|
|
"end": { "x": 733, "y": 1371, "z": 40 }
|
|
},
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Crane", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "c7bce2da-6a6c-4d0f-9c79-70c5a2895962",
|
|
"name": "Spawner (503)",
|
|
"location": [853, 1287, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "cb8699ff-ca4c-44b1-9dd8-422e6e31fbbf",
|
|
"name": "Spawner (503)",
|
|
"location": [636, 1331, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Rabbit", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Pig", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Crane", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "d9dd4287-6d11-4e50-b4b1-834c431ee60b",
|
|
"name": "Spawner (503)",
|
|
"location": [710, 1350, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"spawnBounds": {
|
|
"start": { "x": 690, "y": 1330, "z": -128 },
|
|
"end": { "x": 730, "y": 1370, "z": 40 }
|
|
},
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 4, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "dbffb36b-2e05-4968-96f6-c8f53ddeb50a",
|
|
"name": "Spawner (503)",
|
|
"location": [706, 1208, 25],
|
|
"map": "Tokuno",
|
|
"count": 2,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "df568bbb-9e8c-4594-a32b-85dd93540f10",
|
|
"name": "Spawner (503)",
|
|
"location": [770, 1344, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Goat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Pig", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Horse", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "e828c6c6-2867-4d26-a77a-ab581c4a7dcc",
|
|
"name": "Spawner (503)",
|
|
"location": [843, 1247, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Pig", "maxCount": 2, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "f3688f98-8e8b-4100-8952-4f90020ab8ae",
|
|
"name": "Spawner (503)",
|
|
"location": [857, 1166, 25],
|
|
"map": "Tokuno",
|
|
"count": 4,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Bird", "maxCount": 1, "probability": 100 },
|
|
{ "name": "Dog", "maxCount": 1, "probability": 100 }
|
|
]
|
|
},
|
|
{
|
|
"$type": "Spawner",
|
|
"guid": "f4d88eea-fd00-4029-bf05-960a4a03a082",
|
|
"name": "Spawner (503)",
|
|
"location": [690, 1288, 25],
|
|
"map": "Tokuno",
|
|
"count": 6,
|
|
"minDelay": "00:05:00",
|
|
"maxDelay": "00:10:00",
|
|
"team": 0,
|
|
"homeRange": 20,
|
|
"walkingRange": 20,
|
|
"entries": [
|
|
{ "name": "Dog", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Rat", "maxCount": 2, "probability": 100 },
|
|
{ "name": "Cat", "maxCount": 2, "probability": 100 }
|
|
]
|
|
}
|
|
]
|