The base cliloc table now comes over the bridge. `clilocBridge.js` walks
`GET /cliloc` page by page and the model merges the `custom/` overlays over it —
overlays stay on disk because ServUO has no server-side notion of a custom
cliloc, so there is nothing on the shard to ask for.
**The shard wins whenever uo-link is configured and enabled**, with no mode
setting: there is no version of "which source?" an operator benefits from
answering. A file on disk remains the source only where there is no shard link,
plus a one-off explicit `path` — deprecated, not removed, and unchanged.
**Boot no longer imports on the bridge.** The file path could hash 5 MB locally
and skip in 14 ms; a shard round trip in the boot sequence would be spent
answering "no" on every restart but the one after a client patch — and patching a
client is an operator action, so importing became one. Admin → Shard → Import.
Whatever table is loaded keeps serving until then.
Three checks in the walk, each for a way a shard can hand back a table that looks
complete:
* only `cut: 'end'` finishes it — a short page can equally be a spent budget,
and a truncated table renders some items named and some not, which is exactly
what NO table looks like;
* the cursor must advance, or the walk stops rather than spinning;
* every page echoes the source's size and mtime, so a client patched mid-import
is refused outright rather than stitched from two files.
**The base is exempt from the vanished-source rule**, which is an upgrade detail
rather than a preference: an install that used the file pipeline carries its base
file's label in the stored fingerprint, and on the bridge that label is *supposed*
to disappear. Counting it as vanished would demand an approval for a change the
upgrade itself made. Overlays keep the rule in full.
**The protocol pin moves 7 → 8** — the third declaration site, and the one
nothing enforces. Phase 1 moved the sidecar and the overlay together because the
installer refuses a mismatched bundle; this one has to be moved by hand, in the
phase that first calls a protocol-8 route. The schema block above it is the
record of what forgetting costs: two phases of every REST call answered 409.
Verified against a live shard, sidecar and site: 12 pages, 67,496 rows imported
in 1.68 s, the operator's three-row overlay overriding stock strings on top of
it, and the next import correctly `unchanged`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
646 lines
35 KiB
JavaScript
646 lines
35 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.',
|
||
},
|
||
},
|
||
},
|
||
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 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|