Until now the only way a creature got a picture on this site was for an
operator to open UOFiddler on a desktop, export sprites by hand, copy them to
the web host and write a spawnAtlas.art.json naming each one. Almost nobody
did, so shard_spawn_creatures.art was NULL on every install.
The shard has had those files the whole time. Admin -> Shard -> Import now
walks its asset manifest, fetches only the sprites whose hash changed, writes
them under uploads/atlas/, asks the shard for a body id per atlas creature
(§8: it CONSTRUCTS the creature and reads Body.BodyID, which is the only thing
that is right for a shard's own custom creatures) and points each creature at
its picture. On a stock client that is 787 portraits, about a megabyte.
**The one thing v8.md §12 got wrong, and it is not cosmetic.** It says
`shard_spawn_creatures.art` "starts being filled by the import". That table is
emptied and refilled by replaceAtlas on EVERY atlas refresh, and a refresh runs
on every boot -- so a filename stored there would be destroyed by an ordinary
re-parse of the ServUO tree, with the next Update finding the client files
unchanged, reporting "nothing to do", and never restoring it. Nothing would
report a fault; the pictures would just be gone.
So the assets and the body map live in their own tables outside that blast
radius, and applyAtlas re-derives `art` on the way past as
`{ ...derived, ...operatorMap }` -- which is also the one place "the operator's
own artwork wins" is enforced, on every rebuild rather than only at import.
Smaller decisions worth not rediscovering:
- The derivation joins on the catalogue KEY, not on the body id. The simpler
join is correct today and stops being correct the moment phase 6 adds
body/400/a2/f0, at which point one slug matches dozens of rows.
- Filenames are content-addressed. A stable name overwritten in place leaves
every browser and CDN serving the previous client's sprite, with the database
row perfectly correct.
- An unchanged key whose FILE is missing is fetched again. The row and the disk
can disagree (a wiped uploads volume, a restore from a dump), and a broken
image on a creature page is worse than one re-fetched sprite.
- A key the shard cannot render is not a failure. Two thirds of the playable
ghost and gargoyle bodies have no art on a stock client, and an import that
reported eight failures every time would teach an operator to ignore the panel.
- A key that VANISHED from the manifest needs review before anything changes:
an unmounted client volume and a deliberate downgrade look identical here.
23 new tests; 674 server and 42 client tests pass. The SQL was also run against
a real MariaDB, which is what proved the CONCAT join and the singleton CHECK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
776 lines
44 KiB
JavaScript
776 lines
44 KiB
JavaScript
// ── module-uo's OpenAPI fragment: the shared half ──────────────────────────
|
||
//
|
||
// The tags and component schemas every `#swagger.*` annotation under
|
||
// `server/router/**` refers to. `scripts/swaggerFragment.js` feeds this to
|
||
// swagger-autogen; the per-endpoint detail lives beside each route, exactly as
|
||
// it does in core.
|
||
//
|
||
// These 31 schemas were core's until phase 3 — they sat in
|
||
// `website/server/swagger/swagger.js` describing routes core no longer serves,
|
||
// which is what an extraction leaves behind if nobody looks (the inert-leaf
|
||
// class slice 4 found in `api/client.js`). They moved with the routes.
|
||
//
|
||
// **Two rules about names, and both are the merged document's, not this file's**
|
||
// (docs/website/MODULE_API.md §6.1a):
|
||
//
|
||
// • **What this module DEFINES is namespaced `Uo…`.** Core merges started
|
||
// modules' fragments into one `/api/docs.json`, and core wins every key
|
||
// collision — so an un-namespaced `ShardStatus` from a second game's module
|
||
// would silently lose to, or clobber, this one. The prefix is what makes two
|
||
// modules able to describe the same idea.
|
||
// • **What core defines is referenced by CORE's name.** The annotations point
|
||
// at `#/components/schemas/Error` and `ValidationError` and this file does
|
||
// not redefine them: they resolve in the merged spec, where core's
|
||
// definitions are. Shipping our own copy would be a collision core drops,
|
||
// which is the correct outcome arrived at the expensive way.
|
||
//
|
||
// Tag NAMES are core's originals (`Public · Shard`, not `Uo · Shard`). A tag is
|
||
// how the docs UI groups operations, and core stopped declaring these four in
|
||
// the same slice this file started — nothing collides, and renaming them would
|
||
// churn every reader's bookmark for no gain.
|
||
|
||
module.exports = {
|
||
tags: [
|
||
{ name: 'Public · Shard', description: 'Live shard data ingested from the uo-link sidecar (status, feed, economy, IDOC, characters)' },
|
||
{ name: 'Public · Atlas', description: 'Spawn atlas / bestiary — static shard content parsed from the shard\'s own ServUO tree, independent of the sidecar' },
|
||
{ name: 'Player · Shard', description: 'Link an in-game account and read its roster / vendors (uo-link)' },
|
||
{ name: 'Admin · Shard', description: 'uo-link sidecar connection config, live status and town crier (admin only)' },
|
||
],
|
||
components: {
|
||
schemas: {
|
||
// ── uo-link shard data ──────────────────────────────────────────────
|
||
UoShardStatus: {
|
||
type: 'object',
|
||
description: 'Public shard status (GET /public/shard/status).',
|
||
properties: {
|
||
enabled: { type: 'boolean', example: true },
|
||
status: { type: 'string', example: 'connected', description: 'connected | reconnecting | disconnected | error' },
|
||
pluginConnected: { type: 'boolean', description: 'Is the shard link up right now?', example: true },
|
||
lastEventAt: { type: 'string', format: 'date-time', nullable: true },
|
||
onlineCount: { type: 'integer', example: 12 },
|
||
economy: { $ref: '#/components/schemas/UoShardEconomyPoint' },
|
||
},
|
||
},
|
||
UoShardEvent: {
|
||
type: 'object',
|
||
description: 'A logged shard event.',
|
||
properties: {
|
||
id: { type: 'integer', example: 4821 },
|
||
kind: { type: 'string', example: 'vendor.sale' },
|
||
t: { type: 'integer', description: 'Event time, epoch ms.', example: 1783720195626 },
|
||
bootId: { type: 'string', nullable: true, example: 'boot-abc123' },
|
||
payload: { type: 'object', additionalProperties: true, description: 'The full event object.' },
|
||
createdAt: { type: 'string', format: 'date-time' },
|
||
},
|
||
},
|
||
UoShardEconomyPoint: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'One gold-supply sample.',
|
||
properties: {
|
||
accounts: { type: 'integer', nullable: true, example: 240 },
|
||
gold: { type: 'integer', nullable: true, example: 1028983421 },
|
||
t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 },
|
||
},
|
||
},
|
||
UoShardOnlinePlayer: {
|
||
type: 'object',
|
||
description: 'A LINKED player online now (only accounts linked to a website user are listed).',
|
||
properties: {
|
||
serial: { type: 'string', example: '0x24C' },
|
||
name: { type: 'string', example: 'Darrow' },
|
||
map: { type: 'string', nullable: true, example: 'Trammel' },
|
||
x: { type: 'integer', nullable: true, example: 1402 },
|
||
y: { type: 'integer', nullable: true, example: 1604 },
|
||
z: { type: 'integer', nullable: true, example: 0 },
|
||
},
|
||
},
|
||
UoShardVendorSale: {
|
||
type: 'object',
|
||
description: 'A player-vendor sale (visible only to the linked owner).',
|
||
properties: {
|
||
t: { type: 'integer', description: 'Sale time, epoch ms.', example: 1783720195626 },
|
||
itemType: { type: 'string', example: 'Longsword' },
|
||
amount: { type: 'integer', example: 1 },
|
||
price: { type: 'integer', example: 100 },
|
||
commission: { type: 'integer', nullable: true, example: 5 },
|
||
ownerAcct: { type: 'string', example: 'whitlocktech' },
|
||
},
|
||
},
|
||
UoShardHouse: {
|
||
type: 'object',
|
||
description: 'A house at its current decay stage.',
|
||
properties: {
|
||
serial: { type: 'string', example: '0x4004705F' },
|
||
stage: { type: 'string', example: 'IDOC' },
|
||
map: { type: 'string', nullable: true, example: 'Trammel' },
|
||
x: { type: 'integer', nullable: true },
|
||
y: { type: 'integer', nullable: true },
|
||
z: { type: 'integer', nullable: true },
|
||
region: { type: 'string', nullable: true },
|
||
name: { type: 'string', nullable: true, example: 'An Unnamed House' },
|
||
ownerSerial: { type: 'string', nullable: true },
|
||
ownerAcct: { type: 'string', nullable: true },
|
||
builtOn: { type: 'string', format: 'date-time', nullable: true },
|
||
lastRefreshed: { type: 'string', format: 'date-time', nullable: true },
|
||
isIdoc: { type: 'boolean', example: true },
|
||
updatedAt: { type: 'string', format: 'date-time' },
|
||
},
|
||
},
|
||
UoShardPointsBoard: {
|
||
type: 'object',
|
||
description:
|
||
"One point system's leaderboard (Protocol 3.0 points.board). The shard carries ~25 separate point currencies; each publishes its own board. The display name may arrive as a literal string, a cliloc id, or both — resolve clilocs client-side.",
|
||
properties: {
|
||
system: { type: 'string', example: 'QueensLoyalty', description: "The shard's PointsType name; the board's stable key." },
|
||
nameString: { type: 'string', nullable: true, example: "Queen's Loyalty" },
|
||
nameNumber: { type: 'integer', nullable: true, example: 1114938, description: 'Cliloc id, 0 when the name is a literal.' },
|
||
maxPoints: { type: 'integer', nullable: true, example: 30000 },
|
||
players: { type: 'integer', nullable: true, example: 842, description: 'Players actually holding points in this system.' },
|
||
showOnGump: { type: 'boolean', example: true, description: "The shard's own 'is this player-facing?' flag." },
|
||
top: {
|
||
type: 'array',
|
||
description: 'The ranked players, best first. Capped by the shard (10 by default). Empty when nobody has scored yet.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
rank: { type: 'integer', example: 1 },
|
||
serial: { type: 'string', example: '0x1A2B' },
|
||
name: { type: 'string', example: 'Darrow', description: 'Omitted when the leaderboards `name` field is gated above the caller.' },
|
||
points: { type: 'integer', example: 29500 },
|
||
},
|
||
},
|
||
},
|
||
t: { type: 'integer', nullable: true, description: 'Frame time, epoch ms.' },
|
||
updatedAt: { type: 'string', format: 'date-time' },
|
||
},
|
||
},
|
||
UoShardMarketLocation: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description:
|
||
"Where a vendor is standing. ONE nested object rather than flat map/x/y/region because it is one admin-configurable field (`market.location`) — the whole object is omitted when that field is gated above the caller.",
|
||
properties: {
|
||
map: { type: 'string', nullable: true, example: 'Trammel' },
|
||
x: { type: 'integer', nullable: true, example: 1421 },
|
||
y: { type: 'integer', nullable: true, example: 1699 },
|
||
z: { type: 'integer', nullable: true, example: 0 },
|
||
region: { type: 'string', nullable: true, example: 'Britain' },
|
||
house: { type: 'string', nullable: true, example: "Darrow's Villa", description: "The house SIGN's name, not the house type. Null for a vendor standing outside one." },
|
||
},
|
||
},
|
||
UoShardMarketListing: {
|
||
type: 'object',
|
||
description:
|
||
'One priced listing on a player vendor, carrying enough of its shop to be actionable without a second request.',
|
||
properties: {
|
||
serial: { type: 'string', example: '0x40012ABC' },
|
||
itemId: { type: 'integer', example: 3922, description: 'ItemID (the art/graphic id).' },
|
||
hue: { type: 'integer', example: 0 },
|
||
amount: { type: 'integer', example: 1 },
|
||
price: { type: 'integer', example: 25000 },
|
||
name: { type: 'string', nullable: true, description: "The item's own literal name, set by a player. Null for most items." },
|
||
cliloc: { type: 'integer', nullable: true, example: 1023721, description: "The item's LabelNumber." },
|
||
displayName: {
|
||
type: 'string',
|
||
nullable: true,
|
||
example: 'quarter staff',
|
||
description: 'Resolved server-side from `name` (preferred, being player-set and more specific) else `cliloc`. Null on a shard with no cliloc table configured — render the item id.',
|
||
},
|
||
child: { type: 'boolean', example: false, description: 'Priced by an enclosing container rather than itself, exactly as the in-game Vendor Search reports it.' },
|
||
vendor: {
|
||
type: 'object',
|
||
properties: {
|
||
serial: { type: 'string', example: '0x40001234' },
|
||
shopName: { type: 'string', nullable: true, example: "Darrow's Bargains" },
|
||
ownerSerial: { type: 'string', nullable: true, example: '0x1A2B', description: 'Omitted when the market `ownerSerial` field is gated above the caller.' },
|
||
ownerName: { type: 'string', nullable: true, example: 'Darrow', description: 'Omitted when the market `ownerName` field is gated above the caller.' },
|
||
location: { $ref: '#/components/schemas/UoShardMarketLocation' },
|
||
updatedAt: { type: 'string', format: 'date-time', description: 'When the shard last published this shop.' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
UoShardMarketPage: {
|
||
type: 'object',
|
||
description: 'A page of marketplace listings plus the unpaginated total and the staleness stamp.',
|
||
properties: {
|
||
listings: { type: 'array', items: { $ref: '#/components/schemas/UoShardMarketListing' } },
|
||
total: { type: 'integer', example: 1284, description: 'Matching listings, ignoring paging.' },
|
||
limit: { type: 'integer', example: 50 },
|
||
offset: { type: 'integer', example: 0 },
|
||
vendors: { type: 'integer', example: 137, description: 'Vendors in the whole index.' },
|
||
staleAt: {
|
||
type: 'string',
|
||
format: 'date-time',
|
||
nullable: true,
|
||
description: 'The OLDEST vendor row. The shard sweeps vendors round-robin, so the index can be a full cycle behind and a client must say so rather than implying live prices.',
|
||
},
|
||
},
|
||
},
|
||
UoShardMarketVendor: {
|
||
type: 'object',
|
||
description: 'One player vendor and its listings.',
|
||
properties: {
|
||
serial: { type: 'string', example: '0x40001234' },
|
||
shopName: { type: 'string', nullable: true, example: "Darrow's Bargains" },
|
||
ownerSerial: { type: 'string', nullable: true },
|
||
ownerName: { type: 'string', nullable: true, example: 'Darrow' },
|
||
location: { $ref: '#/components/schemas/UoShardMarketLocation' },
|
||
count: { type: 'integer', example: 250, description: 'Listings the shard published for this shop.' },
|
||
total: { type: 'integer', example: 3104, description: 'Listings the shop actually holds.' },
|
||
truncated: { type: 'boolean', example: true, description: '`total` exceeds `count` — the shop holds more than the shard publishes per frame.' },
|
||
updatedAt: { type: 'string', format: 'date-time' },
|
||
items: { type: 'array', items: { $ref: '#/components/schemas/UoShardMarketListing' } },
|
||
},
|
||
},
|
||
UoShardMarketMeta: {
|
||
type: 'object',
|
||
description: 'Marketplace size, staleness and the filter options a client needs to build its UI.',
|
||
properties: {
|
||
vendors: { type: 'integer', example: 137 },
|
||
items: { type: 'integer', example: 18422 },
|
||
staleAt: { type: 'string', format: 'date-time', nullable: true },
|
||
freshAt: { type: 'string', format: 'date-time', nullable: true },
|
||
maps: { type: 'array', items: { type: 'string' }, example: ['Felucca', 'Trammel'], description: "Facets that actually hold vendors. From the shard's own data — never a hardcoded list." },
|
||
regions: { type: 'array', items: { type: 'string' }, example: ['Britain', 'Luna'] },
|
||
},
|
||
},
|
||
UoShardFeatures: {
|
||
type: 'object',
|
||
description:
|
||
"The shard features the caller may reach, plus the audience rung they resolved to. Drives client nav so it never renders a link that would 403.",
|
||
properties: {
|
||
level: {
|
||
type: 'string',
|
||
enum: ['anonymous', 'logged_in', 'player', 'staff', 'admin'],
|
||
example: 'anonymous',
|
||
},
|
||
features: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
example: ['status', 'activity', 'champs', 'guilds', 'governors', 'houses', 'presence'],
|
||
},
|
||
},
|
||
},
|
||
UoShardFeatureVisibility: {
|
||
type: 'object',
|
||
description: 'Visibility settings for one shard feature.',
|
||
properties: {
|
||
enabled: { type: 'boolean', example: true },
|
||
audience: {
|
||
type: 'string',
|
||
enum: ['anonymous', 'logged_in', 'player', 'staff', 'admin'],
|
||
description: 'Minimum rung that may reach this feature. Each rung implies the ones below it.',
|
||
example: 'anonymous',
|
||
},
|
||
stream: {
|
||
type: 'boolean',
|
||
description: "Whether this feature's event kinds fan out over SSE at all.",
|
||
example: true,
|
||
},
|
||
fieldRules: {
|
||
type: 'object',
|
||
additionalProperties: { type: 'string' },
|
||
description:
|
||
'Per-field rung overrides for the sensitive fields this feature exposes. acct / webId are admin-only always and are rejected here.',
|
||
example: { location: 'staff' },
|
||
},
|
||
},
|
||
},
|
||
UoShardVisibilityConfig: {
|
||
type: 'object',
|
||
properties: {
|
||
ladder: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
example: ['anonymous', 'logged_in', 'player', 'staff', 'admin'],
|
||
},
|
||
lockedFields: { type: 'array', items: { type: 'string' }, example: ['acct', 'webId'] },
|
||
defaults: {
|
||
type: 'object',
|
||
additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' },
|
||
},
|
||
features: {
|
||
type: 'object',
|
||
additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' },
|
||
},
|
||
},
|
||
},
|
||
UoShardVisibilityUpdate: {
|
||
type: 'object',
|
||
required: ['features'],
|
||
properties: {
|
||
features: {
|
||
type: 'object',
|
||
additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' },
|
||
example: { market: { enabled: true, audience: 'player', stream: false, fieldRules: { ownerName: 'player' } } },
|
||
},
|
||
},
|
||
},
|
||
// ── Spawn atlas (Protocol 3.0 Part C) ────────────────────────────────
|
||
// Static shard content, parsed from the shard's own ServUO tree. Nothing
|
||
// here comes from the sidecar, so it stays populated while the shard is
|
||
// down. Facet names are whatever the shard's files declare — the examples
|
||
// below are stock ServUO, not a fixed list.
|
||
UoAtlasCreature: {
|
||
type: 'object',
|
||
description: 'A creature in the bestiary. `places`/`points`/`alsoHere` are present only on the single-creature route.',
|
||
properties: {
|
||
slug: { type: 'string', example: 'lizardman' },
|
||
name: { type: 'string', example: 'Lizardman' },
|
||
total: { type: 'integer', description: 'How many can be alive at once, summed across every spawner.', example: 214 },
|
||
points: { type: 'integer', description: 'How many spawners mention this creature.', example: 62 },
|
||
facets: {
|
||
type: 'object',
|
||
additionalProperties: { type: 'integer' },
|
||
description: "This creature's share per facet.",
|
||
example: { Felucca: 96, Trammel: 88, Tokuno: 30 },
|
||
},
|
||
art: { type: 'string', nullable: true, description: 'Operator-supplied art under uploads/atlas/. NULL on a fresh import — the repo ships no creature art.' },
|
||
places: {
|
||
type: 'array',
|
||
description: 'Where it spawns, aggregated by resolved place. The answer the atlas exists to give.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
facet: { type: 'string', example: 'Trammel' },
|
||
label: { type: 'string', description: 'Resolved region, else nearest landmark group, else "Wilderness".', example: 'Shrines' },
|
||
spawners: { type: 'integer', example: 7 },
|
||
maxAlive: { type: 'integer', example: 21 },
|
||
},
|
||
},
|
||
},
|
||
spawners: {
|
||
type: 'array',
|
||
description: 'The individual spawners. Named separately from `points` (the count) so one key never means two things.',
|
||
items: { $ref: '#/components/schemas/UoAtlasSpawner' },
|
||
},
|
||
spawnersTruncated: { type: 'boolean', description: 'True when the spawner list was cut at the requested bound.', example: false },
|
||
alsoHere: {
|
||
type: 'array',
|
||
description: 'Creatures sharing a spawner with this one.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
slug: { type: 'string', example: 'lizardman-warrior' },
|
||
name: { type: 'string', example: 'Lizardman Warrior' },
|
||
shared: { type: 'integer', example: 12 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
UoAtlasSpawner: {
|
||
type: 'object',
|
||
description: 'One ServUO spawner, with the place its coordinates resolved to.',
|
||
properties: {
|
||
id: { type: 'integer' },
|
||
facet: { type: 'string', example: 'Felucca' },
|
||
name: { type: 'string', nullable: true, description: "The spawner's own name in the ServUO file." },
|
||
x: { type: 'integer', example: 5411 },
|
||
y: { type: 'integer', example: 1234 },
|
||
width: { type: 'integer' },
|
||
height: { type: 'integer' },
|
||
range: { type: 'integer', description: 'Spawn radius.' },
|
||
maxCount: { type: 'integer', description: 'How many of THIS creature this spawner keeps alive.', example: 3 },
|
||
minDelay: { type: 'integer', description: 'Respawn window, in SECONDS. Normalised at parse time — the source stores minutes or seconds per record, decided by its own DelayInSec flag.', example: 300 },
|
||
maxDelay: { type: 'integer', example: 600 },
|
||
todStart: { type: 'integer', description: 'Meaningless unless todMode is non-zero.' },
|
||
todEnd: { type: 'integer' },
|
||
todMode: { type: 'integer' },
|
||
region: { type: 'string', nullable: true, example: 'Despise' },
|
||
landmark: { type: 'string', nullable: true, example: 'Covetous' },
|
||
label: { type: 'string', description: 'Region, else landmark group, else "Wilderness".', example: 'Despise' },
|
||
},
|
||
},
|
||
UoAtlasCreaturePage: {
|
||
type: 'object',
|
||
properties: {
|
||
total: { type: 'integer', description: 'Matching creatures before pagination.', example: 800 },
|
||
limit: { type: 'integer', example: 50 },
|
||
offset: { type: 'integer', example: 0 },
|
||
creatures: { type: 'array', items: { $ref: '#/components/schemas/UoAtlasCreature' } },
|
||
},
|
||
},
|
||
UoAtlasRegion: {
|
||
type: 'object',
|
||
description: 'A named region, flattened out of the shard\'s nested Regions.xml.',
|
||
properties: {
|
||
facet: { type: 'string', example: 'Felucca' },
|
||
name: { type: 'string', example: 'Despise' },
|
||
type: { type: 'string', nullable: true, description: 'ServUO region class.', example: 'DungeonRegion' },
|
||
priority: { type: 'integer', example: 50 },
|
||
parent: { type: 'string', nullable: true, example: 'Britain' },
|
||
rects: {
|
||
type: 'array',
|
||
description: 'The rectangles that placed each spawn point.',
|
||
items: { type: 'object', additionalProperties: true },
|
||
},
|
||
},
|
||
},
|
||
UoAtlasLandmark: {
|
||
type: 'object',
|
||
properties: {
|
||
facet: { type: 'string', example: 'Trammel' },
|
||
name: { type: 'string', example: 'Level 1' },
|
||
group: { type: 'string', nullable: true, description: 'Innermost enclosing parent — the label worth showing.', example: 'Covetous' },
|
||
x: { type: 'integer', example: 5411 },
|
||
y: { type: 'integer', example: 1234 },
|
||
z: { type: 'integer', example: 0 },
|
||
},
|
||
},
|
||
UoAtlasChampion: {
|
||
type: 'object',
|
||
description: 'A CONFIGURED champion altar. Not the live board — see GET /public/shard/champs for that.',
|
||
properties: {
|
||
slug: { type: 'string', example: 'felucca-deceit' },
|
||
name: { type: 'string', example: 'Deceit' },
|
||
group: { type: 'string', nullable: true, description: 'Spawn group; one altar active per group.', example: 'Dungeons' },
|
||
type: { type: 'string', nullable: true, description: 'NULL when the champion is drawn at activation.', example: 'UnholyTerror' },
|
||
randomType: { type: 'boolean', example: false },
|
||
facet: { type: 'string', example: 'Felucca' },
|
||
x: { type: 'integer' },
|
||
y: { type: 'integer' },
|
||
z: { type: 'integer' },
|
||
radius: { type: 'integer', example: 60 },
|
||
label: { type: 'string', nullable: true, example: 'Deceit' },
|
||
},
|
||
},
|
||
UoAtlasMeta: {
|
||
type: 'object',
|
||
description: 'What atlas is loaded. Game-world facts only: the ServUO path, source hashes and any pending refresh are operator detail and live on the admin status route.',
|
||
properties: {
|
||
importedAt: { type: 'string', format: 'date-time', nullable: true },
|
||
generatedAt: { type: 'string', format: 'date-time', nullable: true },
|
||
counts: {
|
||
type: 'object',
|
||
nullable: true,
|
||
additionalProperties: true,
|
||
example: { facets: 6, points: 6455, creatures: 800, regions: 387, landmarks: 558, champions: 25, unresolvedPoints: 1086 },
|
||
},
|
||
facets: { type: 'array', items: { type: 'string' }, example: ['Felucca', 'Ilshenar', 'Malas', 'TerMur', 'Tokuno', 'Trammel'] },
|
||
},
|
||
},
|
||
UoAtlasStatus: {
|
||
type: 'object',
|
||
description: 'Admin view of atlas state: where the tree is, whether it is readable, whether it has drifted from what is loaded, and any refresh staged for review.',
|
||
properties: {
|
||
configured: { type: 'boolean', example: true },
|
||
path: { type: 'string', example: '/srv/servuo' },
|
||
treeReadable: { type: 'boolean', example: true },
|
||
drift: { type: 'boolean', nullable: true, description: 'True when the tree\'s source hashes differ from the loaded atlas. NULL when the tree could not be read.', example: false },
|
||
facets: { type: 'array', items: { type: 'string' } },
|
||
importedAt: { type: 'string', format: 'date-time', nullable: true },
|
||
counts: { type: 'object', nullable: true, additionalProperties: true },
|
||
pending: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'A refresh that was parsed but NOT applied because it would remove a facet. `status` is pending or rejected.',
|
||
additionalProperties: true,
|
||
},
|
||
},
|
||
},
|
||
UoAtlasRefreshResult: {
|
||
type: 'object',
|
||
description: 'Outcome of a refresh. Reported rather than thrown, so an unreadable tree is an answer and not a 500.',
|
||
properties: {
|
||
status: {
|
||
type: 'string',
|
||
enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed', 'rejected', 'none'],
|
||
example: 'imported',
|
||
},
|
||
reason: { type: 'string', nullable: true },
|
||
path: { type: 'string', nullable: true },
|
||
counts: { type: 'object', nullable: true, additionalProperties: true },
|
||
addedFacets: { type: 'array', items: { type: 'string' } },
|
||
removedFacets: { type: 'array', items: { type: 'string' } },
|
||
},
|
||
},
|
||
UoClilocStatus: {
|
||
type: 'object',
|
||
description:
|
||
'Admin view of cliloc state: which source the base table comes from, whether it can be read, how many entries are loaded, and whether anything has drifted from them. Nothing configured at all is a supported state — item names then render as ids.',
|
||
properties: {
|
||
source: {
|
||
type: 'string',
|
||
enum: ['bridge', 'file'],
|
||
description: '`bridge`: the shard reads its own UO client (protocol 8, the normal case). `file`: a converted file on disk — the pre-protocol-8 pipeline, deprecated, kept for installs with no shard link.',
|
||
example: 'bridge',
|
||
},
|
||
configured: { type: 'boolean', example: true },
|
||
path: { type: 'string', description: 'On the bridge: where `custom/` overlays are read from. On a file source: the base path too.', example: '/srv/uo-client' },
|
||
file: { type: 'string', nullable: true, description: 'The base file in use — the shard’s own `cliloc.enu` on the bridge, the resolved local file otherwise.', example: 'cliloc.enu' },
|
||
fileReadable: { type: 'boolean', example: true },
|
||
problem: { type: 'string', nullable: true, description: 'Why the base cannot be used, when it cannot: a shard that is down or has assets switched off, or (on a file source) a missing or still-compressed file.', example: null },
|
||
code: { type: 'string', nullable: true, description: 'Machine-readable cause of `problem`.', enum: ['NO_PATH', 'NOT_FOUND', 'NO_FILE', 'UNREADABLE', 'COMPRESSED', 'DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'UNAVAILABLE'] },
|
||
shard: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'Present on the bridge: the shard’s own cliloc file as it is right now. `hashing: true` with a null `sha256` means the hash has not been computed yet — “ask again”, not “changed”.',
|
||
properties: {
|
||
size: { type: 'integer', example: 4989921 },
|
||
mtime: { type: 'integer', description: 'Unix milliseconds.', example: 1757462400000 },
|
||
sha256: { type: 'string', nullable: true },
|
||
extractorVersion: { type: 'integer', description: 'The version of the shard’s extraction code. A bump makes everything derived from it drift.', example: 1 },
|
||
hashing: { type: 'boolean', example: false },
|
||
complete: { type: 'boolean', description: 'Every client file has a hash.', example: true },
|
||
},
|
||
},
|
||
drift: { type: 'boolean', nullable: true, description: 'True when any source hash differs from the loaded table. NULL when the sources could not be read or are not usable.', example: false },
|
||
count: { type: 'integer', description: 'Entries currently loaded.', example: 67496 },
|
||
sources: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
description: 'Every source found now, root-relative, base first then overlays in merge order.',
|
||
example: ['custom/uomysticmoon.tsv'],
|
||
},
|
||
loadedSources: {
|
||
type: 'array',
|
||
nullable: true,
|
||
description: 'What each source contributed at the last import.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
label: { type: 'string', example: 'custom/uomysticmoon.tsv' },
|
||
kind: { type: 'string', enum: ['shard', 'base', 'custom'], description: '`shard` is the table read over the bridge; `base` a converted file on disk.', example: 'custom' },
|
||
entries: { type: 'integer', example: 37 },
|
||
added: { type: 'integer', description: 'Ids this source introduced.', example: 25 },
|
||
overrode: { type: 'integer', description: 'Ids it replaced from an earlier source.', example: 12 },
|
||
},
|
||
},
|
||
},
|
||
missingSources: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
description: 'Sources loaded previously and now absent. An import refuses these without `approve`.',
|
||
example: [],
|
||
},
|
||
importedAt: { type: 'string', format: 'date-time', nullable: true },
|
||
sourceBytes: { type: 'integer', nullable: true, example: 4973525 },
|
||
},
|
||
},
|
||
UoClilocRefreshResult: {
|
||
type: 'object',
|
||
description:
|
||
'Outcome of a cliloc refresh. Reported rather than thrown, so a missing or compressed file is an answer and not a 500.',
|
||
properties: {
|
||
status: {
|
||
type: 'string',
|
||
enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed'],
|
||
description: '`needsReview` means a previously-loaded source has vanished and nothing was applied; re-run with `approve` to accept it.',
|
||
example: 'imported',
|
||
},
|
||
reason: { type: 'string', nullable: true },
|
||
source: { type: 'string', nullable: true, enum: ['bridge', 'file'], description: 'Which source this refresh read.', example: 'bridge' },
|
||
code: {
|
||
type: 'string',
|
||
nullable: true,
|
||
description: 'Machine-readable cause. Bridge codes describe the shard (`DISABLED`: the operator switched the asset plane off; `NO_SOURCE`: its client has no cliloc file; `SHARD_DOWN`; `SOURCE_CHANGED`: the client was patched mid-import, so nothing was applied). File codes describe the path — `COMPRESSED` means the client\'s own Cliloc.enu was supplied instead of a converted one.',
|
||
enum: ['NO_PATH', 'NOT_FOUND', 'NO_FILE', 'UNREADABLE', 'COMPRESSED', 'TRUNCATED', 'EMPTY', 'NOT_BUFFER', 'DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'UNAVAILABLE', 'SOURCE_CHANGED', 'INCOMPLETE', 'STUCK', 'MALFORMED', 'TOO_LARGE'],
|
||
},
|
||
path: { type: 'string', nullable: true },
|
||
file: { type: 'string', nullable: true },
|
||
count: { type: 'integer', nullable: true, description: 'Entries stored (blank strings are dropped).', example: 67496 },
|
||
parsed: { type: 'integer', nullable: true, description: 'Entries read across every source before blanks were dropped.', example: 123527 },
|
||
blank: { type: 'integer', nullable: true, example: 0 },
|
||
pages: { type: 'integer', nullable: true, description: 'Bridge only: how many pages the table arrived in (a stock English table is about eleven).', example: 11 },
|
||
reported: { type: 'integer', nullable: true, description: 'Bridge only: how many rows the shard said it holds.', example: 67496 },
|
||
received: { type: 'integer', nullable: true, description: 'Bridge only: how many arrived. Disagreeing with `reported` means the walk is wrong.', example: 67496 },
|
||
overlayProblem: { type: 'string', nullable: true, description: 'The base imported, but the overlay directory could not be read. Reported rather than fatal.' },
|
||
sources: {
|
||
type: 'array',
|
||
nullable: true,
|
||
description: 'Per-source breakdown: what each file contributed and how much of it overrode an earlier source.',
|
||
items: {
|
||
type: 'object',
|
||
properties: {
|
||
label: { type: 'string' },
|
||
kind: { type: 'string', enum: ['shard', 'base', 'custom'] },
|
||
entries: { type: 'integer' },
|
||
added: { type: 'integer' },
|
||
overrode: { type: 'integer' },
|
||
},
|
||
},
|
||
},
|
||
missingSources: {
|
||
type: 'array',
|
||
nullable: true,
|
||
items: { type: 'string' },
|
||
description: 'On `needsReview`: the sources that vanished. Nothing was applied.',
|
||
},
|
||
acceptedMissing: {
|
||
type: 'array',
|
||
nullable: true,
|
||
items: { type: 'string' },
|
||
description: 'On `imported` with `approve`: the vanished sources the admin accepted.',
|
||
},
|
||
},
|
||
},
|
||
UoAssetStatus: {
|
||
type: 'object',
|
||
description:
|
||
'Admin view of the client-asset import (docs/link/v8.md §6, §8). What the site holds beside what the shard’s UO client currently is. Holding nothing at all is a supported state — creature pages simply render without pictures, which is what every install did before this pipeline existed.',
|
||
properties: {
|
||
loaded: {
|
||
type: 'object',
|
||
description: 'What this site currently holds.',
|
||
properties: {
|
||
assets: { type: 'integer', description: 'Rows in the imported catalogue.', example: 787 },
|
||
stored: { type: 'integer', description: 'How many of those have a picture on disk. Lower than `assets` when the shard listed a key it could not render.', example: 787 },
|
||
creatures: { type: 'integer', description: 'Atlas creatures the shard has answered a body question about, resolved or not.', example: 812 },
|
||
resolved: { type: 'integer', description: 'How many of those resolved to a body id. The rest are types this shard’s scripts do not define, or spawn entries naming an item rather than a creature.', example: 780 },
|
||
catalog: { type: 'string', nullable: true, description: 'The shard’s catalogue id at the last import — derived from its client files, so it changes exactly when they do.', example: 'a3f9c21d4b8e0771' },
|
||
extractorVersion: { type: 'integer', nullable: true, description: 'The version of the shard’s extraction code. A bump makes every derived byte drift even though the client files did not move.', example: 1 },
|
||
importedAt: { type: 'string', format: 'date-time', nullable: true },
|
||
},
|
||
},
|
||
shard: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'The shard’s own client files right now. NULL when there is no shard link or it could not be reached — see `reason`.',
|
||
properties: {
|
||
files: { type: 'integer', description: 'How many of the animation/definition files this catalogue reads the shard actually has. Few clients carry all five anim files.', example: 9 },
|
||
extractorVersion: { type: 'integer', example: 1 },
|
||
hashing: { type: 'boolean', description: 'A hash is being computed in the background. A null `sha256` while this is true means “not yet”, never “changed”.', example: false },
|
||
complete: { type: 'boolean', description: 'Every client file has a content hash.', example: true },
|
||
imaging: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'Whether the shard host can render an image at all. `ok: false` is the named NO_IMAGING state: ServUO under Mono needs libgdiplus, and without it a Linux shard cannot decode a sprite. Cliloc and atlas import are unaffected.',
|
||
properties: {
|
||
ok: { type: 'boolean', example: true },
|
||
code: { type: 'string', nullable: true, example: null },
|
||
reason: { type: 'string', nullable: true },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
drift: {
|
||
type: 'boolean',
|
||
nullable: true,
|
||
description: 'True when the shard’s client files no longer match what was imported — press Import. NULL when they could not be read.',
|
||
example: false,
|
||
},
|
||
reason: { type: 'string', nullable: true, description: 'Why the shard could not be asked, when it could not.' },
|
||
code: {
|
||
type: 'string',
|
||
nullable: true,
|
||
description: 'Machine-readable cause of `reason`.',
|
||
enum: ['DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'NO_IMAGING', 'SOURCE_CHANGED', 'UNAVAILABLE'],
|
||
},
|
||
},
|
||
},
|
||
UoAssetImportResult: {
|
||
type: 'object',
|
||
description:
|
||
'Outcome of an asset import. Reported rather than thrown, so a shard that is down or a host that cannot render images is an answer and not a 500.',
|
||
properties: {
|
||
status: {
|
||
type: 'string',
|
||
enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed'],
|
||
description: '`skipped`: no shard is configured. `unchanged`: the client files match what was imported and nothing was fetched. `needsReview`: assets this site holds are no longer offered by the shard, and nothing was changed — re-run with `approve` to accept it.',
|
||
example: 'imported',
|
||
},
|
||
reason: { type: 'string', nullable: true },
|
||
code: {
|
||
type: 'string',
|
||
nullable: true,
|
||
description: 'Machine-readable cause. `NO_IMAGING` is a shard host with no libgdiplus; `SOURCE_CHANGED` is a client patched partway through the walk, in which case nothing was applied.',
|
||
enum: ['DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'NO_IMAGING', 'SOURCE_CHANGED', 'INCOMPLETE', 'STUCK', 'MALFORMED', 'TOO_LARGE', 'UNAVAILABLE'],
|
||
},
|
||
catalog: { type: 'string', nullable: true, example: 'a3f9c21d4b8e0771' },
|
||
extractorVersion: { type: 'integer', nullable: true, example: 1 },
|
||
assets: { type: 'integer', nullable: true, description: 'Catalogue rows after the import.', example: 787 },
|
||
fetched: { type: 'integer', nullable: true, description: 'How many sprites actually crossed the wire. On an Update after a client patch this is far smaller than `assets`, which is the point of the manifest.', example: 12 },
|
||
written: { type: 'integer', nullable: true, description: 'How many were written to disk.', example: 12 },
|
||
absent: {
|
||
type: 'integer',
|
||
nullable: true,
|
||
description: 'Keys the shard listed but could not render. NOT a failure: this client has no art at that key, which is the expected answer for two thirds of the playable ghost and gargoyle bodies.',
|
||
example: 0,
|
||
},
|
||
unsupported: { type: 'integer', nullable: true, description: 'Keys the shard does not serve at all. Unlike `absent` this indicates a bug on the site’s side, not a gap in the client.', example: 0 },
|
||
removed: { type: 'integer', nullable: true, description: 'Assets deleted because the shard no longer offers them (only with `approve`).', example: 0 },
|
||
scanned: { type: 'integer', nullable: true, description: 'Body ids the shard walked. Far larger than `assets` — most of the addressable range has no art.', example: 2047 },
|
||
pages: { type: 'integer', nullable: true, description: 'Manifest pages. This family pages on the shard’s scan budget rather than on bytes, so several is normal.', example: 4 },
|
||
playerBodies: {
|
||
type: 'array',
|
||
nullable: true,
|
||
items: { type: 'integer' },
|
||
description: 'The body ids the shard reports as player-character bodies — every registered race’s male, female and ghost bodies, asked of the shard rather than hardcoded. These render head-on; everything else renders three-quarter.',
|
||
example: [400, 401, 402, 403, 605, 606, 607, 608, 666, 667, 694, 695],
|
||
},
|
||
vanished: { type: 'array', nullable: true, items: { type: 'string' }, description: 'On `needsReview`: up to fifty of the keys that disappeared.' },
|
||
vanishedCount: { type: 'integer', nullable: true },
|
||
bodies: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'The slug → body id pass (§8). The shard constructs each creature and reads its body id, which is the only thing correct for a shard’s own custom creatures.',
|
||
properties: {
|
||
asked: { type: 'integer', example: 812 },
|
||
answered: { type: 'integer', example: 812 },
|
||
resolved: { type: 'integer', example: 780 },
|
||
tally: {
|
||
type: 'object',
|
||
description: 'Per-outcome counts. `unknown` is real drift worth acting on — a spawn file naming a type this shard’s scripts do not define. `notCreature` is a spawn entry for an item or decoration and is permanent.',
|
||
properties: {
|
||
ok: { type: 'integer', example: 780 },
|
||
unknown: { type: 'integer', example: 20 },
|
||
notCreature: { type: 'integer', example: 12 },
|
||
failed: { type: 'integer', example: 0 },
|
||
},
|
||
},
|
||
reason: { type: 'string', nullable: true },
|
||
},
|
||
},
|
||
art: {
|
||
type: 'object',
|
||
nullable: true,
|
||
description: 'The derivation onto `shard_spawn_creatures.art`. An operator-supplied `spawnAtlas.art.json` always wins over an imported sprite.',
|
||
properties: {
|
||
applied: { type: 'integer', description: 'Creatures now pointing at a picture.', example: 763 },
|
||
derived: { type: 'integer', description: 'From the import.', example: 763 },
|
||
operator: { type: 'integer', description: 'From the operator’s own map.', example: 0 },
|
||
error: { type: 'string', nullable: true },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
UoShardLinkRequest: {
|
||
type: 'object',
|
||
required: ['code'],
|
||
properties: {
|
||
code: { type: 'string', description: 'The one-time code shown by [link in game.', example: 'AB12CD' },
|
||
},
|
||
},
|
||
UoShardLinkResult: {
|
||
type: 'object',
|
||
properties: {
|
||
linked: { type: 'boolean', example: true },
|
||
account: { type: 'string', example: 'whitlocktech' },
|
||
},
|
||
},
|
||
UoShardLink: {
|
||
type: 'object',
|
||
description: 'A linked in-game account (GET /player/shard/accounts).',
|
||
properties: {
|
||
account: { type: 'string', example: 'whitlocktech' },
|
||
userId: { type: 'integer', example: 42 },
|
||
charName: { type: 'string', nullable: true, example: 'Darrow' },
|
||
linkedAt: { type: 'string', format: 'date-time' },
|
||
},
|
||
},
|
||
UoTownCrierRequest: {
|
||
type: 'object',
|
||
required: ['id', 'lines'],
|
||
properties: {
|
||
id: { type: 'string', maxLength: 64, description: 'Re-posting the same id replaces the prior entry.', example: 'news-42' },
|
||
lines: { type: 'array', items: { type: 'string', maxLength: 200 }, example: ['Hear ye!', 'Market tax is now 5%.'] },
|
||
durationSec: { type: 'integer', minimum: 1, maximum: 86400, example: 3600 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|