Nineteen `#swagger` descriptions carried a `\'` inside a single-quoted string. That is correct JavaScript and wrong here: swagger-autogen does not evaluate the annotation as JS, so the backslash survives into the spec and Swagger UI renders "the shard\'s published ruleset" to a reader. Replaced with a typographic apostrophe, which the same files already use elsewhere. Found by opening /api/docs in a browser against a real core with this module installed — the fragment was valid JSON, the paths were right, every test passed, and it was still wrong on screen. Nothing that reads the artifact can see this; only reading the rendered page can. Also documents the four environment variables this module reads (UOLINK_BASE_URL / _WS_URL / _PROTOCOL, TOWNCRIER_DURATION_SEC). Core's .env.example is dropping them in the paired website PR: they were never core's, and a half-copy in two repos goes stale silently. Co-Authored-By: Claude <noreply@anthropic.com>
130 lines
8.6 KiB
JavaScript
130 lines
8.6 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.
|
||
|
||
const core = require('../../core')
|
||
|
||
const express = core.express
|
||
const { param, query } = core.validator
|
||
|
||
const atlas = require('./atlas.controller')
|
||
const { siteMode, validate } = core.middleware
|
||
const { requireFeature } = require('../../utils/shardVisibility')
|
||
|
||
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/UoAtlasCreaturePage" } } } } */
|
||
/* #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/UoAtlasCreature" } } } } */
|
||
/* #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/UoAtlasRegion" } } } } } */
|
||
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/UoAtlasLandmark" } } } } } */
|
||
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/UoAtlasChampion" } } } } } */
|
||
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/UoAtlasMeta" } } } } */
|
||
siteMode,
|
||
atlas.getMeta,
|
||
)
|
||
|
||
module.exports = atlasRouter
|