// ── 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, }