ModernUO/tools/spawner-json-migrate/migrate.mjs
Kamron Batman 2f8a7afa57 chore(spawners): migrate spawn data to $type discriminator
- Rename "type" -> "$type" (first key, required for STJ polymorphism) in
  all 109 spawn JSON files (5,612 records).
- Convert "homeRange" + "location" -> "spawnBounds" object, dropping homeRange.
  Runtime keeps reading homeRange so un-migrated/external files still load.
- Add BoundsEquivalenceTests (hr=0/1/3/7) validating that the script's toBounds
  formula produces the same Rectangle3D as the runtime homeRange path.
- Add MigratedDataLoadTests loading a real migrated file as List<SpawnerDto>
  and round-tripping each DTO through ToSpawner().
- Add tools/spawner-json-migrate/migrate.mjs (offline Node script).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:17:35 -07:00

81 lines
2.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// One-time migration: rename "type" -> "$type" (kept first), and convert legacy
// homeRange (+location) -> spawnBounds, dropping homeRange. Run once, then delete this tool.
//
// Usage: node tools/spawner-json-migrate/migrate.mjs Distribution/Data/Spawns
import { readdirSync, readFileSync, writeFileSync, statSync } from "node:fs";
import { join } from "node:path";
function* walk(dir) {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) {
yield* walk(p);
} else if (name.endsWith(".json")) {
yield p;
}
}
}
// Produce a spawnBounds object that deserializes to the SAME Rectangle3D as the
// runtime homeRange path in BaseSpawner.ApplyDto.
//
// Runtime formula:
// homeRange == 0 -> Rectangle3D(x, y, z, 1, 1, 0)
// -> _start=(x,y,z), _end=(x+1,y+1,z)
// homeRange > 0 -> Rectangle3D(x-hr, y-hr, -128, hr*2+1, hr*2+1, 256)
// -> _start=(x-hr,y-hr,-128), _end=(x+hr+1,y+hr+1,128)
//
// Rectangle3DConverter x1/y1/z1/x2/y2/z2 form creates:
// Rectangle3D(Point3D(x1,y1,z1), Point3D(x2,y2,z2))
// where _start=(x1,y1,z1) and _end=(x2,y2,z2).
// Rectangle3D.End is EXCLUSIVE (contains: x >= start.X && x < end.X).
function toBounds(loc, hr) {
const [x, y, z] = loc;
if (hr === 0) {
// Single-tile bounds: _start=(x,y,z), _end=(x+1,y+1,z)
return { x1: x, y1: y, z1: z, x2: x + 1, y2: y + 1, z2: z };
}
// hr*2+1 wide/tall: _start=(x-hr,y-hr,-128), _end=(x+hr+1,y+hr+1,128)
return { x1: x - hr, y1: y - hr, z1: -128, x2: x + hr + 1, y2: y + hr + 1, z2: 128 };
}
function migrateOne(obj) {
const out = {};
// $type must be the first key (STJ polymorphism requires discriminator first).
if ("type" in obj) {
out["$type"] = obj["type"];
} else if ("$type" in obj) {
out["$type"] = obj["$type"];
}
for (const [k, v] of Object.entries(obj)) {
if (k === "type" || k === "$type") {
continue;
}
if (k === "homeRange") {
// Convert homeRange -> spawnBounds (skip if spawnBounds already present).
if (obj.spawnBounds === undefined && Array.isArray(obj.location)) {
out.spawnBounds = toBounds(obj.location, v);
}
continue; // drop homeRange from output
}
out[k] = v;
}
return out;
}
let files = 0;
let records = 0;
for (const file of walk(process.argv[2])) {
// Strip UTF-8 BOM if present (some editors write it, JSON.parse chokes on it).
const raw = readFileSync(file, "utf8").replace(/^/, "");
const data = JSON.parse(raw);
const arr = Array.isArray(data) ? data : [data];
const migrated = arr.map((o) => {
records++;
return migrateOne(o);
});
const result = Array.isArray(data) ? migrated : migrated[0];
writeFileSync(file, JSON.stringify(result, null, 2) + "\n", "utf8");
files++;
}
console.log(`Migrated ${records} record(s) across ${files} file(s).`);