// Spawn atlas — the filesystem layer over a ServUO tree. // // `spawnAtlasParse.js` holds the pure parsers; this module is the only thing // that touches a ServUO tree on disk, and it is shared by both callers: // // - the server, which refreshes the atlas on boot (`shardAtlas.model.js`) // - the CLI (`scripts/importSpawnAtlas.js`) // // The shard's own files are the single source of truth. Nothing is precomputed // and committed, because a shard's maps change over its lifetime — facets get // added, replaced or renamed — and a snapshot in the repo would silently go // stale against the world players actually see. // // Reading and hashing the whole tree costs ~120 ms and a full parse ~400 ms, so // the boot path hashes first and only parses when something actually changed. const crypto = require('crypto') const fs = require('fs') const path = require('path') const { parsePoints, parseRegions, parseLocations, parseChampions, buildPlacementIndex, buildFacetIndex, resolveFacetName, resolveRegion, facetKey, slugify, } = require('./spawnAtlasParse') const REGIONS_FILE = path.join('Data', 'Regions.xml') const LOCATIONS_DIR = path.join('Data', 'Locations') const SPAWNS_DIR = 'Spawns' const CHAMPIONS_FILE = path.join('Config', 'ChampionSpawns.xml') class AtlasSourceError extends Error { constructor(message, code) { super(message) this.name = 'AtlasSourceError' this.code = code } } // ── Reading ──────────────────────────────────────────────────────────────── function sha256(text) { return crypto.createHash('sha256').update(text, 'utf8').digest('hex') } function listXml(dir) { try { return fs .readdirSync(dir) .filter((name) => name.toLowerCase().endsWith('.xml')) .sort() } catch (err) { if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return [] throw err } } function readIfPresent(file) { try { return fs.readFileSync(file, 'utf8') } catch (err) { if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return null throw err } } /** * Read every atlas source file under `root`. * * Returns `{ files: [{ label, text, sha256, bytes }] }`, labels being * tree-relative and forward-slashed so a hash map compares equal across * platforms — the same tree read on Windows and Linux must produce the same * fingerprint or every boot would look like a change. */ function readSources(root) { if (!root || String(root).trim() === '') { throw new AtlasSourceError('No ServUO path configured', 'NO_PATH') } if (!fs.existsSync(root)) { throw new AtlasSourceError(`ServUO path does not exist: ${root}`, 'NOT_FOUND') } const files = [] const push = (label, file) => { const text = readIfPresent(file) if (text === null) return false files.push({ label, text, sha256: sha256(text), bytes: Buffer.byteLength(text, 'utf8') }) return true } if (!push('Data/Regions.xml', path.join(root, REGIONS_FILE))) { throw new AtlasSourceError(`Missing required file: ${REGIONS_FILE}`, 'NO_REGIONS') } for (const name of listXml(path.join(root, LOCATIONS_DIR))) { push(`Data/Locations/${name}`, path.join(root, LOCATIONS_DIR, name)) } const spawnFiles = listXml(path.join(root, SPAWNS_DIR)) if (spawnFiles.length === 0) { throw new AtlasSourceError(`No spawn files found in ${SPAWNS_DIR}`, 'NO_SPAWNS') } for (const name of spawnFiles) push(`Spawns/${name}`, path.join(root, SPAWNS_DIR, name)) push('Config/ChampionSpawns.xml', path.join(root, CHAMPIONS_FILE)) return { files } } /** * A fingerprint of the tree: `{ "