Both places this site already knew an item's (ItemID, hue) and could only print it as text now show the picture, hued the way the client would draw it. The shard does the hueing: whether a hue repaints every pixel or only the grey ones is a flag in `tiledata.mul`, which a browser has no way to read. **Ingest warms; the route only serves** (org lead, 2026-09-11). A page never waits on the shard and never causes a fetch -- it renders what is stored and leaves out what is not, which is the state every install was in before this phase. Fetching happens behind that, on a timer, from the keys the site's own rows name. The alternative, fetching on first request, was rejected on one number: the shard's asset plane serves ONE request at a time, so a URL that fetched would let any visitor walk 49,152 ids times 3,000 hues through that slot and park an operator's own import behind it. The wanted set is DERIVED (`SELECT DISTINCT item_id, hue`) rather than queued, so it is self-healing: a restart loses nothing, and a key stops being wanted the moment the vendor row naming it is deleted. The in-memory hint set on top is only for the character sheet, which is fetched live from the shard and stored nowhere -- nothing on disk would ever name those keys. Staleness without a manifest (§7): every row records the shard's `catalog` id, a hash of the files that decide its bytes. A client patch changes it and a restart does not, so "is this out of date?" is a per-row question -- and pictures nobody looks at any more are simply never re-fetched, which is why this is lazy rather than a sweep. `shard_asset_meta` is deliberately NOT written here: it is the body catalogue's singleton, and a warm pass touching it would tell the body import that a client it never looked at is unchanged. A key the shard has no art for writes no row at all. An empty row would make the key held and it would never be asked again -- including after the operator patches in the graphic that was missing. `assets.sources` now reports which families an overlay serves, so an overlay older than phase 5 is one reported state with a sentence naming the fix, instead of a refusal per pass forever with no picture ever appearing. 688 server tests pass (14 new); client builds; the frozen manifest regenerates with one added route, all documented, no core URL moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
827 lines
48 KiB
JavaScript
827 lines
48 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 },
|
||
items: { type: 'integer', description: 'Item pictures held. Unlike the catalogue this has no total to compare against: item art is fetched because something on the site names it, so this is the working set rather than a fraction of one.', example: 1840 },
|
||
land: { type: 'integer', description: 'Land tile pictures held. Zero on every install until something asks for one.', example: 0 },
|
||
},
|
||
},
|
||
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 },
|
||
},
|
||
},
|
||
families: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
description: 'Which asset key families this shard’s plugin overlay serves. An overlay older than phase 5 answers `["body"]` only — it has the creature catalogue and no item art.',
|
||
example: ['body', 'land', 'static'],
|
||
},
|
||
},
|
||
},
|
||
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 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
UoItemArtWarmResult: {
|
||
type: 'object',
|
||
description:
|
||
'Outcome of one item-art warm pass (docs/link/v8.md §11, phase 5). Unlike the body catalogue there is no manifest and no set: the client addresses 49,152 item graphics times three thousand hues, so what gets fetched is defined by what this site’s own rows name — every distinct (ItemID, hue) on a player vendor, plus anything a character sheet has shown since the last pass. Reported rather than thrown, so a shard that is down is an answer and not a 500.',
|
||
properties: {
|
||
status: {
|
||
type: 'string',
|
||
enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'failed'],
|
||
description:
|
||
'`skipped`: no shard is configured. `unchanged`: every wanted picture is already held and current. `unavailable`: the shard could not be asked, or its plugin overlay is too old to serve item art.',
|
||
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. `UNSUPPORTED` is a plugin overlay that serves the creature catalogue but not item art — update the overlay.',
|
||
enum: ['DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'NO_IMAGING', 'UNSUPPORTED', 'INCOMPLETE', 'STUCK', 'MALFORMED', 'TOO_LARGE', 'UNAVAILABLE'],
|
||
},
|
||
catalog: {
|
||
type: 'string',
|
||
nullable: true,
|
||
description:
|
||
'The shard’s art catalogue id these pictures were fetched under — a hash of the files that decide their bytes. Stored per row, which is how staleness is answered without a manifest.',
|
||
example: '7c1e04b9aa2f3d58',
|
||
},
|
||
wanted: { type: 'integer', nullable: true, description: 'Distinct keys this site’s rows name right now.', example: 1840 },
|
||
held: { type: 'integer', nullable: true, description: 'How many of those are already stored and current.', example: 1440 },
|
||
asked: { type: 'integer', nullable: true, description: 'How many this pass actually requested. Bounded by `limit`.', example: 400 },
|
||
fetched: { type: 'integer', nullable: true, description: 'How many the shard returned a picture for.', example: 396 },
|
||
written: { type: 'integer', nullable: true, description: 'How many were written to disk.', example: 396 },
|
||
absent: {
|
||
type: 'integer',
|
||
nullable: true,
|
||
description:
|
||
'Keys the shard has no art for. NOT a failure — 9,963 of this client’s static ids have an empty index entry, and an item using one simply has no picture.',
|
||
example: 4,
|
||
},
|
||
unsupported: { type: 'integer', nullable: true, description: 'Keys the shard does not serve at all. A bug on the site’s side rather than a gap in the client.', example: 0 },
|
||
remaining: { type: 'integer', nullable: true, description: 'Wanted keys left for the next pass. Passes repeat on a timer, so a backlog drains without an operator.', example: 0 },
|
||
},
|
||
},
|
||
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 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|