Files
Module-uo/server/swagger/doc.js
wtclaude 893a36618b
All checks were successful
PR Checks / client-build (pull_request) Successful in 34s
PR Checks / frozen-manifest (pull_request) Successful in 53s
PR Checks / server-tests (pull_request) Successful in 8m18s
feat(cliloc): import the table from the shard, not from a file someone converted (Phase 2)
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
2026-09-10 11:13:24 -05:00

646 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// ── module-uo's OpenAPI fragment: the shared half ──────────────────────────
//
// The tags and component schemas every `#swagger.*` annotation under
// `server/router/**` refers to. `scripts/swaggerFragment.js` feeds this to
// swagger-autogen; the per-endpoint detail lives beside each route, exactly as
// it does in core.
//
// These 31 schemas were core's until phase 3 — they sat in
// `website/server/swagger/swagger.js` describing routes core no longer serves,
// which is what an extraction leaves behind if nobody looks (the inert-leaf
// class slice 4 found in `api/client.js`). They moved with the routes.
//
// **Two rules about names, and both are the merged document's, not this file's**
// (docs/website/MODULE_API.md §6.1a):
//
// • **What this module DEFINES is namespaced `Uo…`.** Core merges started
// modules' fragments into one `/api/docs.json`, and core wins every key
// collision — so an un-namespaced `ShardStatus` from a second game's module
// would silently lose to, or clobber, this one. The prefix is what makes two
// modules able to describe the same idea.
// • **What core defines is referenced by CORE's name.** The annotations point
// at `#/components/schemas/Error` and `ValidationError` and this file does
// not redefine them: they resolve in the merged spec, where core's
// definitions are. Shipping our own copy would be a collision core drops,
// which is the correct outcome arrived at the expensive way.
//
// Tag NAMES are core's originals (`Public · Shard`, not `Uo · Shard`). A tag is
// how the docs UI groups operations, and core stopped declaring these four in
// the same slice this file started — nothing collides, and renaming them would
// churn every reader's bookmark for no gain.
module.exports = {
tags: [
{ name: 'Public · Shard', description: 'Live shard data ingested from the uo-link sidecar (status, feed, economy, IDOC, characters)' },
{ name: 'Public · Atlas', description: 'Spawn atlas / bestiary — static shard content parsed from the shard\'s own ServUO tree, independent of the sidecar' },
{ name: 'Player · Shard', description: 'Link an in-game account and read its roster / vendors (uo-link)' },
{ name: 'Admin · Shard', description: 'uo-link sidecar connection config, live status and town crier (admin only)' },
],
components: {
schemas: {
// ── uo-link shard data ──────────────────────────────────────────────
UoShardStatus: {
type: 'object',
description: 'Public shard status (GET /public/shard/status).',
properties: {
enabled: { type: 'boolean', example: true },
status: { type: 'string', example: 'connected', description: 'connected | reconnecting | disconnected | error' },
pluginConnected: { type: 'boolean', description: 'Is the shard link up right now?', example: true },
lastEventAt: { type: 'string', format: 'date-time', nullable: true },
onlineCount: { type: 'integer', example: 12 },
economy: { $ref: '#/components/schemas/UoShardEconomyPoint' },
},
},
UoShardEvent: {
type: 'object',
description: 'A logged shard event.',
properties: {
id: { type: 'integer', example: 4821 },
kind: { type: 'string', example: 'vendor.sale' },
t: { type: 'integer', description: 'Event time, epoch ms.', example: 1783720195626 },
bootId: { type: 'string', nullable: true, example: 'boot-abc123' },
payload: { type: 'object', additionalProperties: true, description: 'The full event object.' },
createdAt: { type: 'string', format: 'date-time' },
},
},
UoShardEconomyPoint: {
type: 'object',
nullable: true,
description: 'One gold-supply sample.',
properties: {
accounts: { type: 'integer', nullable: true, example: 240 },
gold: { type: 'integer', nullable: true, example: 1028983421 },
t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 },
},
},
UoShardOnlinePlayer: {
type: 'object',
description: 'A LINKED player online now (only accounts linked to a website user are listed).',
properties: {
serial: { type: 'string', example: '0x24C' },
name: { type: 'string', example: 'Darrow' },
map: { type: 'string', nullable: true, example: 'Trammel' },
x: { type: 'integer', nullable: true, example: 1402 },
y: { type: 'integer', nullable: true, example: 1604 },
z: { type: 'integer', nullable: true, example: 0 },
},
},
UoShardVendorSale: {
type: 'object',
description: 'A player-vendor sale (visible only to the linked owner).',
properties: {
t: { type: 'integer', description: 'Sale time, epoch ms.', example: 1783720195626 },
itemType: { type: 'string', example: 'Longsword' },
amount: { type: 'integer', example: 1 },
price: { type: 'integer', example: 100 },
commission: { type: 'integer', nullable: true, example: 5 },
ownerAcct: { type: 'string', example: 'whitlocktech' },
},
},
UoShardHouse: {
type: 'object',
description: 'A house at its current decay stage.',
properties: {
serial: { type: 'string', example: '0x4004705F' },
stage: { type: 'string', example: 'IDOC' },
map: { type: 'string', nullable: true, example: 'Trammel' },
x: { type: 'integer', nullable: true },
y: { type: 'integer', nullable: true },
z: { type: 'integer', nullable: true },
region: { type: 'string', nullable: true },
name: { type: 'string', nullable: true, example: 'An Unnamed House' },
ownerSerial: { type: 'string', nullable: true },
ownerAcct: { type: 'string', nullable: true },
builtOn: { type: 'string', format: 'date-time', nullable: true },
lastRefreshed: { type: 'string', format: 'date-time', nullable: true },
isIdoc: { type: 'boolean', example: true },
updatedAt: { type: 'string', format: 'date-time' },
},
},
UoShardPointsBoard: {
type: 'object',
description:
"One point system's leaderboard (Protocol 3.0 points.board). The shard carries ~25 separate point currencies; each publishes its own board. The display name may arrive as a literal string, a cliloc id, or both — resolve clilocs client-side.",
properties: {
system: { type: 'string', example: 'QueensLoyalty', description: "The shard's PointsType name; the board's stable key." },
nameString: { type: 'string', nullable: true, example: "Queen's Loyalty" },
nameNumber: { type: 'integer', nullable: true, example: 1114938, description: 'Cliloc id, 0 when the name is a literal.' },
maxPoints: { type: 'integer', nullable: true, example: 30000 },
players: { type: 'integer', nullable: true, example: 842, description: 'Players actually holding points in this system.' },
showOnGump: { type: 'boolean', example: true, description: "The shard's own 'is this player-facing?' flag." },
top: {
type: 'array',
description: 'The ranked players, best first. Capped by the shard (10 by default). Empty when nobody has scored yet.',
items: {
type: 'object',
properties: {
rank: { type: 'integer', example: 1 },
serial: { type: 'string', example: '0x1A2B' },
name: { type: 'string', example: 'Darrow', description: 'Omitted when the leaderboards `name` field is gated above the caller.' },
points: { type: 'integer', example: 29500 },
},
},
},
t: { type: 'integer', nullable: true, description: 'Frame time, epoch ms.' },
updatedAt: { type: 'string', format: 'date-time' },
},
},
UoShardMarketLocation: {
type: 'object',
nullable: true,
description:
"Where a vendor is standing. ONE nested object rather than flat map/x/y/region because it is one admin-configurable field (`market.location`) — the whole object is omitted when that field is gated above the caller.",
properties: {
map: { type: 'string', nullable: true, example: 'Trammel' },
x: { type: 'integer', nullable: true, example: 1421 },
y: { type: 'integer', nullable: true, example: 1699 },
z: { type: 'integer', nullable: true, example: 0 },
region: { type: 'string', nullable: true, example: 'Britain' },
house: { type: 'string', nullable: true, example: "Darrow's Villa", description: "The house SIGN's name, not the house type. Null for a vendor standing outside one." },
},
},
UoShardMarketListing: {
type: 'object',
description:
'One priced listing on a player vendor, carrying enough of its shop to be actionable without a second request.',
properties: {
serial: { type: 'string', example: '0x40012ABC' },
itemId: { type: 'integer', example: 3922, description: 'ItemID (the art/graphic id).' },
hue: { type: 'integer', example: 0 },
amount: { type: 'integer', example: 1 },
price: { type: 'integer', example: 25000 },
name: { type: 'string', nullable: true, description: "The item's own literal name, set by a player. Null for most items." },
cliloc: { type: 'integer', nullable: true, example: 1023721, description: "The item's LabelNumber." },
displayName: {
type: 'string',
nullable: true,
example: 'quarter staff',
description: 'Resolved server-side from `name` (preferred, being player-set and more specific) else `cliloc`. Null on a shard with no cliloc table configured — render the item id.',
},
child: { type: 'boolean', example: false, description: 'Priced by an enclosing container rather than itself, exactly as the in-game Vendor Search reports it.' },
vendor: {
type: 'object',
properties: {
serial: { type: 'string', example: '0x40001234' },
shopName: { type: 'string', nullable: true, example: "Darrow's Bargains" },
ownerSerial: { type: 'string', nullable: true, example: '0x1A2B', description: 'Omitted when the market `ownerSerial` field is gated above the caller.' },
ownerName: { type: 'string', nullable: true, example: 'Darrow', description: 'Omitted when the market `ownerName` field is gated above the caller.' },
location: { $ref: '#/components/schemas/UoShardMarketLocation' },
updatedAt: { type: 'string', format: 'date-time', description: 'When the shard last published this shop.' },
},
},
},
},
UoShardMarketPage: {
type: 'object',
description: 'A page of marketplace listings plus the unpaginated total and the staleness stamp.',
properties: {
listings: { type: 'array', items: { $ref: '#/components/schemas/UoShardMarketListing' } },
total: { type: 'integer', example: 1284, description: 'Matching listings, ignoring paging.' },
limit: { type: 'integer', example: 50 },
offset: { type: 'integer', example: 0 },
vendors: { type: 'integer', example: 137, description: 'Vendors in the whole index.' },
staleAt: {
type: 'string',
format: 'date-time',
nullable: true,
description: 'The OLDEST vendor row. The shard sweeps vendors round-robin, so the index can be a full cycle behind and a client must say so rather than implying live prices.',
},
},
},
UoShardMarketVendor: {
type: 'object',
description: 'One player vendor and its listings.',
properties: {
serial: { type: 'string', example: '0x40001234' },
shopName: { type: 'string', nullable: true, example: "Darrow's Bargains" },
ownerSerial: { type: 'string', nullable: true },
ownerName: { type: 'string', nullable: true, example: 'Darrow' },
location: { $ref: '#/components/schemas/UoShardMarketLocation' },
count: { type: 'integer', example: 250, description: 'Listings the shard published for this shop.' },
total: { type: 'integer', example: 3104, description: 'Listings the shop actually holds.' },
truncated: { type: 'boolean', example: true, description: '`total` exceeds `count` — the shop holds more than the shard publishes per frame.' },
updatedAt: { type: 'string', format: 'date-time' },
items: { type: 'array', items: { $ref: '#/components/schemas/UoShardMarketListing' } },
},
},
UoShardMarketMeta: {
type: 'object',
description: 'Marketplace size, staleness and the filter options a client needs to build its UI.',
properties: {
vendors: { type: 'integer', example: 137 },
items: { type: 'integer', example: 18422 },
staleAt: { type: 'string', format: 'date-time', nullable: true },
freshAt: { type: 'string', format: 'date-time', nullable: true },
maps: { type: 'array', items: { type: 'string' }, example: ['Felucca', 'Trammel'], description: "Facets that actually hold vendors. From the shard's own data — never a hardcoded list." },
regions: { type: 'array', items: { type: 'string' }, example: ['Britain', 'Luna'] },
},
},
UoShardFeatures: {
type: 'object',
description:
"The shard features the caller may reach, plus the audience rung they resolved to. Drives client nav so it never renders a link that would 403.",
properties: {
level: {
type: 'string',
enum: ['anonymous', 'logged_in', 'player', 'staff', 'admin'],
example: 'anonymous',
},
features: {
type: 'array',
items: { type: 'string' },
example: ['status', 'activity', 'champs', 'guilds', 'governors', 'houses', 'presence'],
},
},
},
UoShardFeatureVisibility: {
type: 'object',
description: 'Visibility settings for one shard feature.',
properties: {
enabled: { type: 'boolean', example: true },
audience: {
type: 'string',
enum: ['anonymous', 'logged_in', 'player', 'staff', 'admin'],
description: 'Minimum rung that may reach this feature. Each rung implies the ones below it.',
example: 'anonymous',
},
stream: {
type: 'boolean',
description: "Whether this feature's event kinds fan out over SSE at all.",
example: true,
},
fieldRules: {
type: 'object',
additionalProperties: { type: 'string' },
description:
'Per-field rung overrides for the sensitive fields this feature exposes. acct / webId are admin-only always and are rejected here.',
example: { location: 'staff' },
},
},
},
UoShardVisibilityConfig: {
type: 'object',
properties: {
ladder: {
type: 'array',
items: { type: 'string' },
example: ['anonymous', 'logged_in', 'player', 'staff', 'admin'],
},
lockedFields: { type: 'array', items: { type: 'string' }, example: ['acct', 'webId'] },
defaults: {
type: 'object',
additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' },
},
features: {
type: 'object',
additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' },
},
},
},
UoShardVisibilityUpdate: {
type: 'object',
required: ['features'],
properties: {
features: {
type: 'object',
additionalProperties: { $ref: '#/components/schemas/UoShardFeatureVisibility' },
example: { market: { enabled: true, audience: 'player', stream: false, fieldRules: { ownerName: 'player' } } },
},
},
},
// ── Spawn atlas (Protocol 3.0 Part C) ────────────────────────────────
// Static shard content, parsed from the shard's own ServUO tree. Nothing
// here comes from the sidecar, so it stays populated while the shard is
// down. Facet names are whatever the shard's files declare — the examples
// below are stock ServUO, not a fixed list.
UoAtlasCreature: {
type: 'object',
description: 'A creature in the bestiary. `places`/`points`/`alsoHere` are present only on the single-creature route.',
properties: {
slug: { type: 'string', example: 'lizardman' },
name: { type: 'string', example: 'Lizardman' },
total: { type: 'integer', description: 'How many can be alive at once, summed across every spawner.', example: 214 },
points: { type: 'integer', description: 'How many spawners mention this creature.', example: 62 },
facets: {
type: 'object',
additionalProperties: { type: 'integer' },
description: "This creature's share per facet.",
example: { Felucca: 96, Trammel: 88, Tokuno: 30 },
},
art: { type: 'string', nullable: true, description: 'Operator-supplied art under uploads/atlas/. NULL on a fresh import — the repo ships no creature art.' },
places: {
type: 'array',
description: 'Where it spawns, aggregated by resolved place. The answer the atlas exists to give.',
items: {
type: 'object',
properties: {
facet: { type: 'string', example: 'Trammel' },
label: { type: 'string', description: 'Resolved region, else nearest landmark group, else "Wilderness".', example: 'Shrines' },
spawners: { type: 'integer', example: 7 },
maxAlive: { type: 'integer', example: 21 },
},
},
},
spawners: {
type: 'array',
description: 'The individual spawners. Named separately from `points` (the count) so one key never means two things.',
items: { $ref: '#/components/schemas/UoAtlasSpawner' },
},
spawnersTruncated: { type: 'boolean', description: 'True when the spawner list was cut at the requested bound.', example: false },
alsoHere: {
type: 'array',
description: 'Creatures sharing a spawner with this one.',
items: {
type: 'object',
properties: {
slug: { type: 'string', example: 'lizardman-warrior' },
name: { type: 'string', example: 'Lizardman Warrior' },
shared: { type: 'integer', example: 12 },
},
},
},
},
},
UoAtlasSpawner: {
type: 'object',
description: 'One ServUO spawner, with the place its coordinates resolved to.',
properties: {
id: { type: 'integer' },
facet: { type: 'string', example: 'Felucca' },
name: { type: 'string', nullable: true, description: "The spawner's own name in the ServUO file." },
x: { type: 'integer', example: 5411 },
y: { type: 'integer', example: 1234 },
width: { type: 'integer' },
height: { type: 'integer' },
range: { type: 'integer', description: 'Spawn radius.' },
maxCount: { type: 'integer', description: 'How many of THIS creature this spawner keeps alive.', example: 3 },
minDelay: { type: 'integer', description: 'Respawn window, in SECONDS. Normalised at parse time — the source stores minutes or seconds per record, decided by its own DelayInSec flag.', example: 300 },
maxDelay: { type: 'integer', example: 600 },
todStart: { type: 'integer', description: 'Meaningless unless todMode is non-zero.' },
todEnd: { type: 'integer' },
todMode: { type: 'integer' },
region: { type: 'string', nullable: true, example: 'Despise' },
landmark: { type: 'string', nullable: true, example: 'Covetous' },
label: { type: 'string', description: 'Region, else landmark group, else "Wilderness".', example: 'Despise' },
},
},
UoAtlasCreaturePage: {
type: 'object',
properties: {
total: { type: 'integer', description: 'Matching creatures before pagination.', example: 800 },
limit: { type: 'integer', example: 50 },
offset: { type: 'integer', example: 0 },
creatures: { type: 'array', items: { $ref: '#/components/schemas/UoAtlasCreature' } },
},
},
UoAtlasRegion: {
type: 'object',
description: 'A named region, flattened out of the shard\'s nested Regions.xml.',
properties: {
facet: { type: 'string', example: 'Felucca' },
name: { type: 'string', example: 'Despise' },
type: { type: 'string', nullable: true, description: 'ServUO region class.', example: 'DungeonRegion' },
priority: { type: 'integer', example: 50 },
parent: { type: 'string', nullable: true, example: 'Britain' },
rects: {
type: 'array',
description: 'The rectangles that placed each spawn point.',
items: { type: 'object', additionalProperties: true },
},
},
},
UoAtlasLandmark: {
type: 'object',
properties: {
facet: { type: 'string', example: 'Trammel' },
name: { type: 'string', example: 'Level 1' },
group: { type: 'string', nullable: true, description: 'Innermost enclosing parent — the label worth showing.', example: 'Covetous' },
x: { type: 'integer', example: 5411 },
y: { type: 'integer', example: 1234 },
z: { type: 'integer', example: 0 },
},
},
UoAtlasChampion: {
type: 'object',
description: 'A CONFIGURED champion altar. Not the live board — see GET /public/shard/champs for that.',
properties: {
slug: { type: 'string', example: 'felucca-deceit' },
name: { type: 'string', example: 'Deceit' },
group: { type: 'string', nullable: true, description: 'Spawn group; one altar active per group.', example: 'Dungeons' },
type: { type: 'string', nullable: true, description: 'NULL when the champion is drawn at activation.', example: 'UnholyTerror' },
randomType: { type: 'boolean', example: false },
facet: { type: 'string', example: 'Felucca' },
x: { type: 'integer' },
y: { type: 'integer' },
z: { type: 'integer' },
radius: { type: 'integer', example: 60 },
label: { type: 'string', nullable: true, example: 'Deceit' },
},
},
UoAtlasMeta: {
type: 'object',
description: 'What atlas is loaded. Game-world facts only: the ServUO path, source hashes and any pending refresh are operator detail and live on the admin status route.',
properties: {
importedAt: { type: 'string', format: 'date-time', nullable: true },
generatedAt: { type: 'string', format: 'date-time', nullable: true },
counts: {
type: 'object',
nullable: true,
additionalProperties: true,
example: { facets: 6, points: 6455, creatures: 800, regions: 387, landmarks: 558, champions: 25, unresolvedPoints: 1086 },
},
facets: { type: 'array', items: { type: 'string' }, example: ['Felucca', 'Ilshenar', 'Malas', 'TerMur', 'Tokuno', 'Trammel'] },
},
},
UoAtlasStatus: {
type: 'object',
description: 'Admin view of atlas state: where the tree is, whether it is readable, whether it has drifted from what is loaded, and any refresh staged for review.',
properties: {
configured: { type: 'boolean', example: true },
path: { type: 'string', example: '/srv/servuo' },
treeReadable: { type: 'boolean', example: true },
drift: { type: 'boolean', nullable: true, description: 'True when the tree\'s source hashes differ from the loaded atlas. NULL when the tree could not be read.', example: false },
facets: { type: 'array', items: { type: 'string' } },
importedAt: { type: 'string', format: 'date-time', nullable: true },
counts: { type: 'object', nullable: true, additionalProperties: true },
pending: {
type: 'object',
nullable: true,
description: 'A refresh that was parsed but NOT applied because it would remove a facet. `status` is pending or rejected.',
additionalProperties: true,
},
},
},
UoAtlasRefreshResult: {
type: 'object',
description: 'Outcome of a refresh. Reported rather than thrown, so an unreadable tree is an answer and not a 500.',
properties: {
status: {
type: 'string',
enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed', 'rejected', 'none'],
example: 'imported',
},
reason: { type: 'string', nullable: true },
path: { type: 'string', nullable: true },
counts: { type: 'object', nullable: true, additionalProperties: true },
addedFacets: { type: 'array', items: { type: 'string' } },
removedFacets: { type: 'array', items: { type: 'string' } },
},
},
UoClilocStatus: {
type: 'object',
description:
'Admin view of cliloc state: which source the base table comes from, whether it can be read, how many entries are loaded, and whether anything has drifted from them. Nothing configured at all is a supported state — item names then render as ids.',
properties: {
source: {
type: 'string',
enum: ['bridge', 'file'],
description: '`bridge`: the shard reads its own UO client (protocol 8, the normal case). `file`: a converted file on disk — the pre-protocol-8 pipeline, deprecated, kept for installs with no shard link.',
example: 'bridge',
},
configured: { type: 'boolean', example: true },
path: { type: 'string', description: 'On the bridge: where `custom/` overlays are read from. On a file source: the base path too.', example: '/srv/uo-client' },
file: { type: 'string', nullable: true, description: 'The base file in use — the shards own `cliloc.enu` on the bridge, the resolved local file otherwise.', example: 'cliloc.enu' },
fileReadable: { type: 'boolean', example: true },
problem: { type: 'string', nullable: true, description: 'Why the base cannot be used, when it cannot: a shard that is down or has assets switched off, or (on a file source) a missing or still-compressed file.', example: null },
code: { type: 'string', nullable: true, description: 'Machine-readable cause of `problem`.', enum: ['NO_PATH', 'NOT_FOUND', 'NO_FILE', 'UNREADABLE', 'COMPRESSED', 'DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'UNAVAILABLE'] },
shard: {
type: 'object',
nullable: true,
description: 'Present on the bridge: the shards own cliloc file as it is right now. `hashing: true` with a null `sha256` means the hash has not been computed yet — “ask again”, not “changed”.',
properties: {
size: { type: 'integer', example: 4989921 },
mtime: { type: 'integer', description: 'Unix milliseconds.', example: 1757462400000 },
sha256: { type: 'string', nullable: true },
extractorVersion: { type: 'integer', description: 'The version of the shards extraction code. A bump makes everything derived from it drift.', example: 1 },
hashing: { type: 'boolean', example: false },
complete: { type: 'boolean', description: 'Every client file has a hash.', example: true },
},
},
drift: { type: 'boolean', nullable: true, description: 'True when any source hash differs from the loaded table. NULL when the sources could not be read or are not usable.', example: false },
count: { type: 'integer', description: 'Entries currently loaded.', example: 67496 },
sources: {
type: 'array',
items: { type: 'string' },
description: 'Every source found now, root-relative, base first then overlays in merge order.',
example: ['custom/uomysticmoon.tsv'],
},
loadedSources: {
type: 'array',
nullable: true,
description: 'What each source contributed at the last import.',
items: {
type: 'object',
properties: {
label: { type: 'string', example: 'custom/uomysticmoon.tsv' },
kind: { type: 'string', enum: ['shard', 'base', 'custom'], description: '`shard` is the table read over the bridge; `base` a converted file on disk.', example: 'custom' },
entries: { type: 'integer', example: 37 },
added: { type: 'integer', description: 'Ids this source introduced.', example: 25 },
overrode: { type: 'integer', description: 'Ids it replaced from an earlier source.', example: 12 },
},
},
},
missingSources: {
type: 'array',
items: { type: 'string' },
description: 'Sources loaded previously and now absent. An import refuses these without `approve`.',
example: [],
},
importedAt: { type: 'string', format: 'date-time', nullable: true },
sourceBytes: { type: 'integer', nullable: true, example: 4973525 },
},
},
UoClilocRefreshResult: {
type: 'object',
description:
'Outcome of a cliloc refresh. Reported rather than thrown, so a missing or compressed file is an answer and not a 500.',
properties: {
status: {
type: 'string',
enum: ['skipped', 'unavailable', 'unchanged', 'imported', 'needsReview', 'failed'],
description: '`needsReview` means a previously-loaded source has vanished and nothing was applied; re-run with `approve` to accept it.',
example: 'imported',
},
reason: { type: 'string', nullable: true },
source: { type: 'string', nullable: true, enum: ['bridge', 'file'], description: 'Which source this refresh read.', example: 'bridge' },
code: {
type: 'string',
nullable: true,
description: 'Machine-readable cause. Bridge codes describe the shard (`DISABLED`: the operator switched the asset plane off; `NO_SOURCE`: its client has no cliloc file; `SHARD_DOWN`; `SOURCE_CHANGED`: the client was patched mid-import, so nothing was applied). File codes describe the path — `COMPRESSED` means the client\'s own Cliloc.enu was supplied instead of a converted one.',
enum: ['NO_PATH', 'NOT_FOUND', 'NO_FILE', 'UNREADABLE', 'COMPRESSED', 'TRUNCATED', 'EMPTY', 'NOT_BUFFER', 'DISABLED', 'NO_SOURCE', 'SHARD_DOWN', 'PROTOCOL', 'BUSY', 'UNAVAILABLE', 'SOURCE_CHANGED', 'INCOMPLETE', 'STUCK', 'MALFORMED', 'TOO_LARGE'],
},
path: { type: 'string', nullable: true },
file: { type: 'string', nullable: true },
count: { type: 'integer', nullable: true, description: 'Entries stored (blank strings are dropped).', example: 67496 },
parsed: { type: 'integer', nullable: true, description: 'Entries read across every source before blanks were dropped.', example: 123527 },
blank: { type: 'integer', nullable: true, example: 0 },
pages: { type: 'integer', nullable: true, description: 'Bridge only: how many pages the table arrived in (a stock English table is about eleven).', example: 11 },
reported: { type: 'integer', nullable: true, description: 'Bridge only: how many rows the shard said it holds.', example: 67496 },
received: { type: 'integer', nullable: true, description: 'Bridge only: how many arrived. Disagreeing with `reported` means the walk is wrong.', example: 67496 },
overlayProblem: { type: 'string', nullable: true, description: 'The base imported, but the overlay directory could not be read. Reported rather than fatal.' },
sources: {
type: 'array',
nullable: true,
description: 'Per-source breakdown: what each file contributed and how much of it overrode an earlier source.',
items: {
type: 'object',
properties: {
label: { type: 'string' },
kind: { type: 'string', enum: ['shard', 'base', 'custom'] },
entries: { type: 'integer' },
added: { type: 'integer' },
overrode: { type: 'integer' },
},
},
},
missingSources: {
type: 'array',
nullable: true,
items: { type: 'string' },
description: 'On `needsReview`: the sources that vanished. Nothing was applied.',
},
acceptedMissing: {
type: 'array',
nullable: true,
items: { type: 'string' },
description: 'On `imported` with `approve`: the vanished sources the admin accepted.',
},
},
},
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 },
},
},
},
},
}