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>
133 lines
8.8 KiB
JavaScript
133 lines
8.8 KiB
JavaScript
// Public · Atlas — the spawn atlas / bestiary. Static shard CONTENT derived from
|
|
// the shard's own ServUO tree, not live shard state.
|
|
//
|
|
// Mounted at /api/v1/public/atlas by public/index.js. Two deliberate differences
|
|
// from the /public/shard routes next door (docs/link/v3.md §6):
|
|
//
|
|
// • **Not under /shard.** Nothing here round-trips the sidecar, and the pages
|
|
// stay fully populated while the shard is down. Mounting it under /shard
|
|
// would imply a dependency it does not have.
|
|
// • **siteMode-gated, like /posts and /wiki.** The shard routes are exempt
|
|
// because shard status is wanted *during* maintenance; a bestiary is site
|
|
// content and follows site content's rules.
|
|
//
|
|
// Every route also carries `requireFeature('atlas')` — 404 when an admin has
|
|
// disabled the feature, 403 when the caller sits below its configured audience.
|
|
// The default audience is `anonymous`, so these gates are inert until an admin
|
|
// changes something.
|
|
|
|
// express and express-validator come from core, never from a require here: this
|
|
// file lives outside server/, so Node's resolver would not find them, and a
|
|
// second express in the process would be a second Router prototype
|
|
// (docs/website/MODULE_API.md §2.3).
|
|
const core = require('../core')
|
|
const atlas = require('./atlas.controller')
|
|
const { requireFeature } = require('../utils/visibility')
|
|
|
|
const { express, validator, middleware } = core
|
|
const { param, query } = validator
|
|
const { siteMode, validate } = middleware
|
|
|
|
const atlasRouter = express.Router()
|
|
|
|
// Facet names come from the shard's own files and are never validated against a
|
|
// list — nothing in the codebase names a facet (§6.1 R2). Only the length is
|
|
// bounded, and the query matches exactly, so an unknown name returns an empty
|
|
// result rather than an error.
|
|
const facetParam = query('facet').optional({ values: 'falsy' }).isString().isLength({ max: 40 })
|
|
|
|
atlasRouter.get(
|
|
'/creatures',
|
|
requireFeature('atlas'),
|
|
// #swagger.tags = ['Public · Atlas']
|
|
// #swagger.summary = 'Search the bestiary (paginated)'
|
|
// #swagger.description = 'Every creature the shard spawns, most numerous first. `total` is how many can be alive at once across all spawners; `points` is how many spawners mention it; `facets` maps facet name to that creature\'s share on it. Static content parsed from the shard\'s ServUO tree — unaffected by the shard being offline.'
|
|
// #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the creature name (max 60 chars).' }
|
|
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to creatures spawning on this facet. Facet names come from the shard\'s own files; an unknown one returns an empty page.' }
|
|
// #swagger.parameters['limit'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Page size, 1..100 (default 50).' }
|
|
// #swagger.parameters['offset'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Rows to skip (default 0).' }
|
|
/* #swagger.responses[200] = { description: 'A page of creatures plus the unpaginated total', content: { "application/json": { schema: { $ref: "#/components/schemas/AtlasCreaturePage" } } } } */
|
|
/* #swagger.responses[403] = { description: 'The atlas feature is gated above this caller', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
/* #swagger.responses[404] = { description: 'The atlas feature is disabled', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }),
|
|
facetParam,
|
|
query('limit').optional().isInt({ min: 1, max: 100 }),
|
|
query('offset').optional().isInt({ min: 0, max: 100000 }),
|
|
validate,
|
|
siteMode,
|
|
atlas.getCreatures,
|
|
)
|
|
atlasRouter.get(
|
|
'/creatures/:slug',
|
|
requireFeature('atlas'),
|
|
// #swagger.tags = ['Public · Atlas']
|
|
// #swagger.summary = 'One creature: where it spawns, and what spawns with it'
|
|
// #swagger.description = 'The answer the atlas exists to give. `places` is the aggregate — "lizardman → Shrines, Isamu-Jima, Yew" — resolved by point-in-rect against the shard\'s own region rectangles, falling back to the nearest landmark, else "Wilderness". `spawners` lists the individual spawn points (bounded; `spawnersTruncated` says when the list was cut), and `alsoHere` is what shares those spawners.'
|
|
// #swagger.parameters['slug'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Creature slug, e.g. lizardman.' }
|
|
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Restrict places and spawners to one facet.' }
|
|
// #swagger.parameters['points'] = { in: 'query', required: false, schema: { type: 'integer' }, description: 'Max spawners to return, 1..1000 (default 200).' }
|
|
/* #swagger.responses[200] = { description: 'The creature', content: { "application/json": { schema: { $ref: "#/components/schemas/AtlasCreature" } } } } */
|
|
/* #swagger.responses[404] = { description: 'No such creature in this atlas (or the feature is disabled)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */
|
|
param('slug').isString().isLength({ min: 1, max: 120 }),
|
|
facetParam,
|
|
query('points').optional().isInt({ min: 1, max: 1000 }),
|
|
validate,
|
|
siteMode,
|
|
atlas.getCreature,
|
|
)
|
|
atlasRouter.get(
|
|
'/regions',
|
|
requireFeature('atlas'),
|
|
// #swagger.tags = ['Public · Atlas']
|
|
// #swagger.summary = 'Named regions and their rectangles'
|
|
// #swagger.description = 'Flattened out of the shard\'s nested Regions.xml. `priority` and the rectangles are what placed each spawn point, kept so the placement can be re-derived rather than taken on trust.'
|
|
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet.' }
|
|
// #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the region name.' }
|
|
/* #swagger.responses[200] = { description: 'Regions, by facet then name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AtlasRegion" } } } } } */
|
|
facetParam,
|
|
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }),
|
|
validate,
|
|
siteMode,
|
|
atlas.getRegions,
|
|
)
|
|
atlasRouter.get(
|
|
'/landmarks',
|
|
requireFeature('atlas'),
|
|
// #swagger.tags = ['Public · Atlas']
|
|
// #swagger.summary = 'Points of interest (dungeon levels, town markers)'
|
|
// #swagger.description = 'From the shard\'s Data/Locations files. `group` is the innermost enclosing parent ("Covetous"), which is the label worth showing over the individual marker ("Level 1").'
|
|
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet.' }
|
|
// #swagger.parameters['q'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Substring match on the landmark name or its group.' }
|
|
/* #swagger.responses[200] = { description: 'Landmarks, by facet then group', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AtlasLandmark" } } } } } */
|
|
facetParam,
|
|
query('q').optional({ values: 'falsy' }).isString().isLength({ max: 60 }),
|
|
validate,
|
|
siteMode,
|
|
atlas.getLandmarks,
|
|
)
|
|
atlasRouter.get(
|
|
'/champions',
|
|
requireFeature('atlas'),
|
|
// #swagger.tags = ['Public · Atlas']
|
|
// #swagger.summary = 'Configured champion altars (the roster, not the live board)'
|
|
// #swagger.description = 'Where the altars are and what each one summons — "there is an Unholy Terror altar in Deceit". `randomType` marks altars whose champion is drawn at activation. Do not conflate this with GET /public/shard/champs, which is the live sidecar-fed board ("it is on level 3 right now").'
|
|
// #swagger.parameters['facet'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Limit to one facet.' }
|
|
/* #swagger.responses[200] = { description: 'Altars, by facet then name', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/AtlasChampion" } } } } } */
|
|
facetParam,
|
|
validate,
|
|
siteMode,
|
|
atlas.getChampions,
|
|
)
|
|
atlasRouter.get(
|
|
'/meta',
|
|
requireFeature('atlas'),
|
|
// #swagger.tags = ['Public · Atlas']
|
|
// #swagger.summary = 'What atlas is loaded: facets, counts, when it was imported'
|
|
// #swagger.description = 'Drives the facet filter and the "parsed from the shard\'s own files on <date>" line. Reports the game world only — the ServUO path, the per-file hashes and any pending refresh are operator detail and live on the admin status route.'
|
|
/* #swagger.responses[200] = { description: 'Atlas metadata', content: { "application/json": { schema: { $ref: "#/components/schemas/AtlasMeta" } } } } */
|
|
siteMode,
|
|
atlas.getMeta,
|
|
)
|
|
|
|
module.exports = atlasRouter
|