THROWAWAY BRANCH — evidence for the Phase 1 contract, never merged. See
modules/uo/SPIKE.md and docs/website/MODULE_API.md Part 7.
The six public spawn-atlas routes now live in modules/uo/, reached only through
the ctx/register surface, with the client half loading as a prebuilt ESM chunk.
All three exit criteria met:
• zero internal-file imports from the module into core; the built chunk has
zero bare import specifiers and bundles no React
• routes.manifest.json AND routes.guards.json are byte-identical
• /uo/atlas renders from /modules/uo/entry.js under script-src 'self' with
zero CSP violation reports
729 core tests and 81 module tests pass. Verified end to end against the real
database: the schema fragment replays after core's, onBoot runs the atlas
refresh, and the six API URLs answer unchanged.
Two things the spike changed in the contract:
• ctx.express / ctx.validator. A module lives outside server/, so Node never
reaches server/node_modules and require('express') fails outright — the
server-side twin of the one-React rule, which §2.6 had only for the client.
• window.__rg.jsxRuntime, so a module can build with the automatic JSX
runtime its tooling already assumes rather than being forced to classic.
And it confirmed §6.1 empirically: regenerating the OpenAPI spec silently
deleted all 361 lines of the atlas paths with "Swagger-autogen: Success", while
the route manifest kept all six in the same run. That is exactly the
static-analysis-vs-runtime split the fragment merge exists to prevent.
Co-Authored-By: Claude <noreply@anthropic.com>
135 lines
5.0 KiB
JavaScript
135 lines
5.0 KiB
JavaScript
// ── Public: the spawn atlas ────────────────────────────────────────────────
|
|
//
|
|
// A browsable catalogue of what the shard CONTAINS — which creatures spawn,
|
|
// where, how many, and which champion altars are configured. Everything here is
|
|
// a plain indexed read of the tables the boot-time import fills from the shard's
|
|
// own ServUO tree (docs/website/SPAWN_ATLAS.md).
|
|
//
|
|
// Two properties separate this from /public/shard/*:
|
|
//
|
|
// • **Nothing touches the sidecar.** The atlas is static shard content, not
|
|
// live shard state, so these pages stay fully populated while the shard is
|
|
// down. That is why the routes are mounted at /public/atlas and are
|
|
// siteMode-gated like /posts and /wiki, rather than under /shard.
|
|
// • **The live champion feed is a different thing.** `/atlas/champions` is the
|
|
// configured roster ("there is an Unholy Terror altar in Deceit");
|
|
// `/shard/champs` is the running state ("it is on level 3 right now").
|
|
//
|
|
// Every response is still passed through `projectFeature` for the `atlas`
|
|
// feature. It declares no sensitive fields today, so the projection is a
|
|
// no-op — but v3.md §3.6.1's rule is that a read path returning shard data and
|
|
// not projecting is a bug, and the cost of honouring it is one call per handler
|
|
// rather than a retrofit the first time a field needs gating.
|
|
|
|
const atlas = require('../model/shardAtlas/shardAtlas.model')
|
|
const visibility = require('../utils/visibility')
|
|
|
|
const log = require('../core').logger('public-atlas')
|
|
|
|
const FEATURE = 'atlas'
|
|
|
|
// Query params arrive as strings; express-validator has already bounded them.
|
|
const int = (value, fallback) => {
|
|
const n = Number.parseInt(value, 10)
|
|
return Number.isFinite(n) ? n : fallback
|
|
}
|
|
|
|
const str = (value) => (typeof value === 'string' ? value.trim() : '')
|
|
|
|
// GET /public/atlas/creatures?q=&facet=&limit=&offset=
|
|
async function getCreatures(req, res) {
|
|
try {
|
|
const page = await atlas.searchCreatures({
|
|
q: str(req.query.q),
|
|
facet: str(req.query.facet),
|
|
limit: int(req.query.limit, 50),
|
|
offset: int(req.query.offset, 0),
|
|
})
|
|
return res.json(await visibility.project(FEATURE, page, req))
|
|
} catch (err) {
|
|
log.error('atlas.getCreatures', err)
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
}
|
|
|
|
// GET /public/atlas/creatures/:slug — one creature, with the places it spawns.
|
|
//
|
|
// 404 means "no such creature in this atlas", which also covers "the atlas has
|
|
// never been imported" — an empty atlas has no slugs, and there is nothing more
|
|
// specific to say to an anonymous caller.
|
|
async function getCreature(req, res) {
|
|
try {
|
|
const creature = await atlas.getCreature(req.params.slug, {
|
|
facet: str(req.query.facet),
|
|
points: int(req.query.points, 200),
|
|
})
|
|
if (!creature) return res.status(404).json({ message: 'Not Found' })
|
|
return res.json(await visibility.project(FEATURE, creature, req))
|
|
} catch (err) {
|
|
log.error('atlas.getCreature', err)
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
}
|
|
|
|
// GET /public/atlas/regions?facet=&q=
|
|
async function getRegions(req, res) {
|
|
try {
|
|
const regions = await atlas.listRegions({
|
|
facet: str(req.query.facet),
|
|
q: str(req.query.q),
|
|
})
|
|
return res.json(await visibility.project(FEATURE, regions, req))
|
|
} catch (err) {
|
|
log.error('atlas.getRegions', err)
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
}
|
|
|
|
// GET /public/atlas/landmarks?facet=&q=
|
|
async function getLandmarks(req, res) {
|
|
try {
|
|
const landmarks = await atlas.listLandmarks({
|
|
facet: str(req.query.facet),
|
|
q: str(req.query.q),
|
|
})
|
|
return res.json(await visibility.project(FEATURE, landmarks, req))
|
|
} catch (err) {
|
|
log.error('atlas.getLandmarks', err)
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
}
|
|
|
|
// GET /public/atlas/champions?facet= — the CONFIGURED altar roster.
|
|
async function getChampions(req, res) {
|
|
try {
|
|
const champions = await atlas.listChampions({ facet: str(req.query.facet) })
|
|
return res.json(await visibility.project(FEATURE, champions, req))
|
|
} catch (err) {
|
|
log.error('atlas.getChampions', err)
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
}
|
|
|
|
// GET /public/atlas/meta — what is loaded: facets, counts, when it was imported.
|
|
//
|
|
// Public-safe by construction: the model omits the ServUO path, the per-file
|
|
// hashes and the pending-refresh state, all of which describe the operator's
|
|
// filesystem rather than the game world. The admin status route carries those.
|
|
async function getMeta(req, res) {
|
|
try {
|
|
return res.json(await visibility.project(FEATURE, await atlas.publicMeta(), req))
|
|
} catch (err) {
|
|
log.error('atlas.getMeta', err)
|
|
return res.status(500).json({ message: 'Internal Server Error' })
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getCreatures,
|
|
getCreature,
|
|
getRegions,
|
|
getLandmarks,
|
|
getChampions,
|
|
getMeta,
|
|
}
|