`spawnAtlasSource.js` gains a second backend behind its existing interface
(docs/link/v8.md 10). Where a shard is linked and enabled the tree arrives over
the sidecar; where there is none, a local ServUO tree is read exactly as before.
An explicit --servuo path is an instruction and overrules both.
The parsers do not move. spawnAtlasParse.js is still pure, still fs-free and
still CI-covered without a ServUO tree anywhere near it; `buildFromFiles` is now
where the parse starts, and both readers feed it the same shape.
treeBridge.js walks the manifest and then the chunks. Three of its checks are
not decoration -- each is a way this ends in a tree that LOOKS imported, and XML
is forgiving enough that a mis-assembled spawn file parses cleanly and simply
has fewer spawns in it:
- every chunk re-declares its address and carries the hash of its own
uncompressed bytes, and chunks are placed by declared index rather than
arrival order
- the whole file is hashed after reassembly against its manifest row
- the catalog must not move mid-walk, or the import is refused rather than
stitched out of two trees
Boot does not call the shard. The same answer 17.7 gave the cliloc table, and
the same reasoning: a local tree hashes in ~120 ms and skips, while a round trip
in the boot sequence would answer "no" on every restart that did not follow a
map edit. Editing spawn files is an operator action, so importing is one --
Admin -> Spawn Atlas -> Import. What that costs is real and is said out loud in
the panel, the CLI and the log: an install on the bridge has NO automatic
refresh at all.
Two things the live walk found that the unit tests could not:
- PARSER_VERSION 4 -> 5. The parse is order-sensitive in one place -- the
decoration index keeps the FIRST item id it sees for a type -- and the two
readers agreed on a stock tree by coincidence, since the filesystem reader
walks each directory with localeCompare while the shard sorts whole relative
paths. buildFromFiles now sorts by label, ordinally, once, whatever order
the files arrived in. Identical input, a different answer for a handful of
types: exactly what the version number exists to push through the hash gate.
The parity test asserted deepEqual, which ignores key order; it now asserts
serialised equality too.
- The source fingerprint is taken over RAW BYTES at both ends. Hashing decoded
text hashes a UTF-8 re-encoding -- identical for valid UTF-8, different for a
file that is not, because an undecodable byte becomes U+FFFD and never comes
back. One Latin-1 character in a creature name would have made the drift gate
report a change on every import, forever, with the tree untouched.
A 200 from assets.sources also stopped meaning "the client files are on offer":
a shard may now serve its configuration tree while declining to serve its UO
client. Both client-file readers check `assetsEnabled` and say DISABLED, instead
of reading an empty file list as "your client has no cliloc.enu" and sending an
operator to their client install for a setting that lives on their shard.
Measured end to end against a live shard and the real sidecar: 141 files,
11.9 MB, 158 chunks, 3 pages, 1.33 MB on the wire, 512 ms; every file
byte-identical to disk; and the atlas built over the bridge identical to the one
built off it -- 6,455 points, 800 creatures, 387 regions, 558 landmarks,
25 champions, 309 decoration types.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
841 lines
49 KiB
JavaScript
841 lines
49 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: which source the tree comes from, whether it is readable, whether it has drifted from what is loaded, and any refresh staged for review.',
|
||
properties: {
|
||
configured: { type: 'boolean', example: true },
|
||
source: {
|
||
type: 'string',
|
||
enum: ['bridge', 'fs'],
|
||
description: '`bridge`: the shard serves its own configuration files over uo-link (protocol 8 phase 7, the normal case once a shard is linked). `fs`: a ServUO tree the website can read directly — development and same-host installs, and the only source where boot re-imports by itself.',
|
||
example: 'bridge',
|
||
},
|
||
path: { type: 'string', description: 'The local tree path, or `the shard bridge` when that is the source.', example: 'the shard bridge' },
|
||
treeReadable: { type: 'boolean', example: true },
|
||
drift: { type: 'boolean', nullable: true, description: 'True when the source file hashes differ from the loaded atlas. NULL when the source could not be read. On the bridge this is answered from the shard\'s file MANIFEST — hashes only, no file bytes.', 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 },
|
||
source: {
|
||
type: 'string',
|
||
enum: ['bridge', 'fs'],
|
||
nullable: true,
|
||
description: 'Which end this attempt read from. Absent only on `skipped`, where there was no source at all.',
|
||
example: 'bridge',
|
||
},
|
||
path: { type: 'string', nullable: true, description: 'The local tree path, or `the shard bridge`.' },
|
||
code: { type: 'string', nullable: true, description: 'On `unavailable`: NO_PATH, NOT_FOUND, NO_REGIONS or NO_SPAWNS from a local tree; DISABLED, SOURCE_CHANGED, INCOMPLETE, MALFORMED, BUSY, SHARD_DOWN or TOO_LARGE from the bridge.' },
|
||
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 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|