Files
Module-uo/server/swagger/doc.js
wtclaude a194ec68e0
All checks were successful
PR Checks / client-build (pull_request) Successful in 18s
PR Checks / server-tests (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 49s
feat(assets): creature artwork from the shard's own client (Phase 3)
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
2026-09-10 18:40:46 -05:00

776 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── 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 shards 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 shards 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 shards 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 shards 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 shards scripts do not define, or spawn entries naming an item rather than a creature.', example: 780 },
catalog: { type: 'string', nullable: true, description: 'The shards 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 shards 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 shards 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 shards 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 sites 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 shards 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 races 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 shards 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 shards 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 operators 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 },
},
},
},
},
}