refactor(modules)!: move the UO server half out to module-uo (phase 3, slice 1) #137
@@ -1,6 +1,13 @@
|
|||||||
-- Runic Gateway database schema (MariaDB)
|
-- Runic Gateway database schema (MariaDB)
|
||||||
-- Run automatically by the MariaDB container (docker-entrypoint-initdb.d) on a
|
-- Run automatically by the MariaDB container (docker-entrypoint-initdb.d) on a
|
||||||
-- fresh volume, and idempotently by ensureSchema() on every server boot.
|
-- fresh volume, and idempotently by ensureSchema() on every server boot.
|
||||||
|
--
|
||||||
|
-- CORE ONLY. The 27 shard_* / uo_link_* tables left with module-uo in Phase 3
|
||||||
|
-- and live in its schema fragment, which core replays immediately after this
|
||||||
|
-- file (MODULE_API.md 2.6). Two of them carry a foreign key INTO users, which
|
||||||
|
-- is why that order matters and why the reverse -- a core table referencing a
|
||||||
|
-- module table -- must never appear here: it would make core unable to boot
|
||||||
|
-- without a module installed.
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -343,395 +350,11 @@ CREATE TABLE IF NOT EXISTS email_config (
|
|||||||
CONSTRAINT chk_email_config_singleton CHECK (id = 1)
|
CONSTRAINT chk_email_config_singleton CHECK (id = 1)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ── uo-link sidecar ────────────────────────────────────────────────────────
|
|
||||||
-- Connection config for the uo-link sidecar (the HTTP + WebSocket bridge to the
|
|
||||||
-- ServUO shard). Singleton row (id = 1), mirroring bot_config/email_config: the
|
|
||||||
-- DB only ever holds the AES-256-GCM-encrypted shared-secret auth token, never
|
|
||||||
-- plaintext, and it is only decrypted server-side (to call the sidecar). It is
|
|
||||||
-- never returned to the admin UI — responses expose only `hasToken`. base_url is
|
|
||||||
-- the REST endpoint, ws_url the live-feed endpoint; both are configurable because
|
|
||||||
-- in production the sidecar runs on a different host from the website. `status`/
|
|
||||||
-- `plugin_connected`/`last_event_at`/`boot_id` mirror the sidecar's last-known
|
|
||||||
-- state for the admin panel between polls; `boot_id` tracks server.hello.bootId
|
|
||||||
-- so a shard restart can be detected (and caches dropped).
|
|
||||||
CREATE TABLE IF NOT EXISTS uo_link_config (
|
|
||||||
id INT PRIMARY KEY DEFAULT 1,
|
|
||||||
base_url VARCHAR(255) NULL,
|
|
||||||
ws_url VARCHAR(255) NULL,
|
|
||||||
auth_token_enc TEXT NULL,
|
|
||||||
protocol INT NOT NULL DEFAULT 3,
|
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'disconnected',
|
|
||||||
status_detail VARCHAR(500) NULL,
|
|
||||||
plugin_connected TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
last_event_at DATETIME NULL,
|
|
||||||
boot_id VARCHAR(64) NULL,
|
|
||||||
updated_by INT NULL,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT fk_uo_link_config_user FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
CONSTRAINT chk_uo_link_config_singleton CHECK (id = 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Append-only log of notable shard events ingested from the uo-link WebSocket
|
|
||||||
-- feed. The site OWNS this data (it does not query the sidecar's SQLite): the WS
|
|
||||||
-- client writes here, and the public/admin read endpoints + live feeds read from
|
|
||||||
-- here. Only "notable" kinds are logged (sales, deaths, murders, mob.killed,
|
|
||||||
-- IDOC transitions, quests, skill.gain, fame/karma, audit.*, cheat.*, link.*,
|
|
||||||
-- server.*). High-frequency kinds (char.vitals, economy.supply) are NOT logged
|
|
||||||
-- here — they update shard_online / shard_economy instead, keeping the log lean.
|
|
||||||
-- dedupe_key = sha256(kind + t + stable-json(payload)) truncated to 40 hex chars
|
|
||||||
-- (fits CHAR(40)); with the UNIQUE index it makes INSERT IGNORE idempotent so
|
|
||||||
-- WS-reconnect backfill never double-inserts.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_events (
|
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
kind VARCHAR(48) NOT NULL,
|
|
||||||
t BIGINT NOT NULL, -- event time, epoch ms (from the sidecar)
|
|
||||||
boot_id VARCHAR(64) NULL, -- shard boot id at ingest (server.hello.bootId)
|
|
||||||
payload JSON NOT NULL, -- the full event object
|
|
||||||
dedupe_key CHAR(40) NOT NULL UNIQUE,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_events_kind_t (kind, t),
|
|
||||||
INDEX idx_shard_events_t (t)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Current online players. Upserted on mob.login, refreshed on char.vitals, and
|
|
||||||
-- removed on mob.logout. Cleared wholesale when the shard restarts (a new
|
|
||||||
-- server.hello.bootId). web_id is the linked website user id (present when the
|
|
||||||
-- account is linked), so the roster can be correlated to site accounts.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_online (
|
|
||||||
serial VARCHAR(20) NOT NULL PRIMARY KEY, -- mobile serial (opaque hex key)
|
|
||||||
name VARCHAR(120) NULL,
|
|
||||||
acct VARCHAR(120) NULL,
|
|
||||||
web_id INT NULL,
|
|
||||||
map VARCHAR(40) NULL,
|
|
||||||
x INT NULL,
|
|
||||||
y INT NULL,
|
|
||||||
z INT NULL,
|
|
||||||
hits INT NULL,
|
|
||||||
hits_max INT NULL,
|
|
||||||
mana INT NULL,
|
|
||||||
mana_max INT NULL,
|
|
||||||
stam INT NULL,
|
|
||||||
stam_max INT NULL,
|
|
||||||
str INT NULL,
|
|
||||||
dex INT NULL,
|
|
||||||
`int` INT NULL,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_online_acct (acct)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Total-gold-supply time series (from the periodic economy.supply event). Kept
|
|
||||||
-- append-only so the public status page can render a supply-over-time sparkline.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_economy (
|
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
accounts INT NULL, -- number of accounts included in the total
|
|
||||||
gold BIGINT NULL, -- total gold supply across all accounts
|
|
||||||
t BIGINT NOT NULL, -- sample time, epoch ms
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_economy_t (t)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Current decay stage per house, upserted on house.decay. is_idoc is a derived
|
|
||||||
-- flag (stage == 'IDOC') so the public "houses in danger" list is a cheap
|
|
||||||
-- indexed lookup rather than a scan.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_houses (
|
|
||||||
serial VARCHAR(20) NOT NULL PRIMARY KEY,
|
|
||||||
stage VARCHAR(24) NULL, -- Somewhat | Fairly | Greatly | IDOC | Collapsed | ...
|
|
||||||
map VARCHAR(40) NULL,
|
|
||||||
x INT NULL,
|
|
||||||
y INT NULL,
|
|
||||||
z INT NULL,
|
|
||||||
region VARCHAR(120) NULL,
|
|
||||||
name VARCHAR(160) NULL,
|
|
||||||
owner_serial VARCHAR(20) NULL,
|
|
||||||
owner_acct VARCHAR(120) NULL,
|
|
||||||
built_on DATETIME NULL,
|
|
||||||
last_refreshed DATETIME NULL,
|
|
||||||
is_idoc TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_houses_idoc (is_idoc)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Site-side mirror of in-game-account → website-user links. The sidecar is the
|
-- Site-side mirror of in-game-account → website-user links. The sidecar is the
|
||||||
-- source of truth (it tags the game account with the websiteUserId on
|
-- source of truth (it tags the game account with the websiteUserId on
|
||||||
-- /link/confirm); this table mirrors it so the player portal can list a user's
|
-- /link/confirm); this table mirrors it so the player portal can list a user's
|
||||||
-- linked accounts and enforce ownership on roster/vendor reads without a shard
|
-- linked accounts and enforce ownership on roster/vendor reads without a shard
|
||||||
-- round-trip. account is unique (one game account maps to at most one site user);
|
-- round-trip. account is unique (one game account maps to at most one site user);
|
||||||
-- a single user may link several game accounts.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_account_links (
|
|
||||||
account VARCHAR(120) NOT NULL PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
char_name VARCHAR(120) NULL,
|
|
||||||
linked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT fk_shard_links_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
INDEX idx_shard_links_user (user_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Current champion-spawn board, upserted on champ.update and removed on
|
|
||||||
-- champ.remove. Mirrors the sidecar's /champs projection into our own store so
|
|
||||||
-- the public Champions page (and its live deltas) survive a shard outage, the
|
|
||||||
-- same way shard_online / shard_houses do. Three families share one table, told
|
|
||||||
-- apart by `category` (champion | mini | sea); category-specific fields (level,
|
|
||||||
-- kills, boss, restartAt, hits, …) live in the JSON `payload` so the schema does
|
|
||||||
-- not have to model every variant.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_champs (
|
|
||||||
serial VARCHAR(20) NOT NULL PRIMARY KEY, -- controller/mobile serial (opaque hex)
|
|
||||||
category VARCHAR(16) NULL, -- champion | mini | sea
|
|
||||||
type VARCHAR(80) NULL,
|
|
||||||
name VARCHAR(120) NULL,
|
|
||||||
status VARCHAR(16) NULL, -- active | cooldown | dormant
|
|
||||||
active TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
map VARCHAR(40) NULL,
|
|
||||||
x INT NULL,
|
|
||||||
y INT NULL,
|
|
||||||
z INT NULL,
|
|
||||||
boss_up TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
payload JSON NOT NULL, -- the full champ.update object
|
|
||||||
t BIGINT NULL, -- event time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_champs_category (category)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Current open help-page (support ticket) queue, upserted on page.new/page.updated
|
|
||||||
-- and removed on page.closed. Snapshotted authoritatively from the sidecar's
|
|
||||||
-- GET /pages on every (re)connect. page_id is the sender's serial (one page per
|
|
||||||
-- player). Staff-only data — served on the admin channel, never public.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_pages (
|
|
||||||
page_id VARCHAR(20) NOT NULL PRIMARY KEY, -- sender serial (one page per player)
|
|
||||||
type VARCHAR(40) NULL, -- Bug | Stuck | Account | Question | ...
|
|
||||||
sender_name VARCHAR(120) NULL,
|
|
||||||
sender_acct VARCHAR(120) NULL,
|
|
||||||
web_id INT NULL, -- linked website user id, if any
|
|
||||||
message TEXT NULL,
|
|
||||||
map VARCHAR(40) NULL,
|
|
||||||
x INT NULL,
|
|
||||||
y INT NULL,
|
|
||||||
z INT NULL,
|
|
||||||
sent_ms BIGINT NULL, -- when the page was opened, epoch ms
|
|
||||||
handled TINYINT(1) NOT NULL DEFAULT 0, -- a staffer claimed it in game
|
|
||||||
handler VARCHAR(120) NULL,
|
|
||||||
payload JSON NOT NULL, -- the full page.new/updated object
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_pages_handled (handled)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Guild roster board (Protocol 2.0). Upserted on guild.update (a full-state
|
|
||||||
-- snapshot emitted only on change) and removed on guild.remove. The leader is an
|
|
||||||
-- actor object flattened into leader_* columns; the full event is kept in
|
|
||||||
-- `payload` for anything not hoisted. Mirrors the sidecar's GET /guilds
|
|
||||||
-- projection into our store so the public Guilds page survives a shard outage.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_guilds (
|
|
||||||
id INT NOT NULL PRIMARY KEY, -- in-game guild id
|
|
||||||
name VARCHAR(120) NULL,
|
|
||||||
abbr VARCHAR(24) NULL,
|
|
||||||
members INT NULL,
|
|
||||||
online INT NULL,
|
|
||||||
alliance VARCHAR(120) NULL,
|
|
||||||
leader_serial VARCHAR(20) NULL,
|
|
||||||
leader_name VARCHAR(120) NULL,
|
|
||||||
leader_acct VARCHAR(120) NULL,
|
|
||||||
leader_web_id INT NULL,
|
|
||||||
payload JSON NOT NULL, -- the full guild.update object
|
|
||||||
t BIGINT NULL, -- event time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_guilds_name (name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Town-governor board (Protocol 2.0, City Loyalty). One row per city, upserted on
|
|
||||||
-- city.update (full-state, emitted only on change; there is no remove event since
|
|
||||||
-- the set of cities is fixed). governor / governorElect are actor objects
|
|
||||||
-- flattened into columns; the full event is kept in `payload`. Empty on shards
|
|
||||||
-- that do not run the City Loyalty system.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_governors (
|
|
||||||
city VARCHAR(40) NOT NULL PRIMARY KEY, -- Britain | Moonglow | ...
|
|
||||||
governor_serial VARCHAR(20) NULL,
|
|
||||||
governor_name VARCHAR(120) NULL,
|
|
||||||
governor_acct VARCHAR(120) NULL,
|
|
||||||
governor_web_id INT NULL,
|
|
||||||
elect_serial VARCHAR(20) NULL,
|
|
||||||
elect_name VARCHAR(120) NULL,
|
|
||||||
elect_acct VARCHAR(120) NULL,
|
|
||||||
election_phase VARCHAR(16) NULL, -- none | nominate | vote | pending
|
|
||||||
candidates INT NULL,
|
|
||||||
auto_pick_at DATETIME NULL,
|
|
||||||
payload JSON NOT NULL, -- the full city.update object
|
|
||||||
t BIGINT NULL, -- event time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Governor term history — the "who governed when" ledger behind the Governors
|
|
||||||
-- board. Captured from day one (history cannot be backfilled) on every observed
|
|
||||||
-- governor CHANGE: the open term (ended_at IS NULL) is closed and a new one
|
|
||||||
-- opened. `votes` stays NULL — the city.update feed exposes only the candidate
|
|
||||||
-- COUNT and election phase, not per-candidate tallies, so we record who governed
|
|
||||||
-- and when (reliable) and never fabricate vote numbers. The look-back UI ("who
|
|
||||||
-- were all the governors of Britain?") reads this table.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_governor_terms (
|
|
||||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
city VARCHAR(40) NOT NULL,
|
|
||||||
governor_serial VARCHAR(20) NULL,
|
|
||||||
governor_name VARCHAR(120) NULL,
|
|
||||||
governor_acct VARCHAR(120) NULL,
|
|
||||||
governor_web_id INT NULL,
|
|
||||||
started_at BIGINT NOT NULL, -- term start, epoch ms
|
|
||||||
ended_at BIGINT NULL, -- term end epoch ms (NULL = current)
|
|
||||||
votes INT NULL, -- not in the feed (reserved)
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_gov_terms_city (city, started_at),
|
|
||||||
INDEX idx_shard_gov_terms_open (city, ended_at)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Online-population snapshot (Protocol 2.0). Singleton row (id = 1) holding the
|
|
||||||
-- latest presence.online aggregate: total count plus per-facet and per-region
|
|
||||||
-- breakdown maps (stored as JSON). Distinct from shard_online (per-player) — this
|
|
||||||
-- is the rolled-up headcount the public "Players Online" widget renders. The
|
|
||||||
-- time series, if ever needed, is available from GET /history?kind=presence.online.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_presence (
|
|
||||||
id INT PRIMARY KEY DEFAULT 1,
|
|
||||||
count INT NOT NULL DEFAULT 0,
|
|
||||||
by_facet JSON NULL, -- { "Felucca": 12, "Trammel": 30 }
|
|
||||||
by_region JSON NULL, -- { "Britain": 18, "Wilderness": 9 }
|
|
||||||
t BIGINT NULL, -- snapshot time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT chk_shard_presence_singleton CHECK (id = 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- The shard's published ruleset (Protocol 3.0 world.ruleset). Singleton row
|
|
||||||
-- (id = 1) holding the latest frame: expansion, which optional systems are on,
|
|
||||||
-- skill/stat caps, account and house limits, champion scroll rules, the
|
|
||||||
-- save/restart schedule. The shard re-emits it on every sidecar connect, so this
|
|
||||||
-- row is simply overwritten; `rev` is the shard's own FNV-1a of the body, which
|
|
||||||
-- distinguishes "same ruleset, re-sent on reconnect" from "an operator changed a
|
|
||||||
-- .cfg". No row at all means the shard has never published one — served as null,
|
|
||||||
-- which the rules page renders differently from a published ruleset.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_ruleset (
|
|
||||||
id INT PRIMARY KEY DEFAULT 1,
|
|
||||||
rev VARCHAR(32) NULL,
|
|
||||||
expansion VARCHAR(16) NULL, -- hoisted for cheap display
|
|
||||||
payload JSON NOT NULL, -- the whole world.ruleset frame
|
|
||||||
t BIGINT NULL, -- frame time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT chk_shard_ruleset_singleton CHECK (id = 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Points/loyalty leaderboards (Protocol 3.0 points.board). One row per point
|
|
||||||
-- system, keyed by the shard's own PointsType name. The shard publishes ~25 of
|
|
||||||
-- these (Queen's Loyalty, Void Pool, the nine city loyalties, …), each a standing
|
|
||||||
-- players accumulate over months.
|
|
||||||
--
|
|
||||||
-- The top-N list stays inside `payload` rather than being normalized into a
|
|
||||||
-- shard_points_entries table. It is a fixed-size list (10 by default) that is only
|
|
||||||
-- ever read whole, exactly like shard_governors.candidates — normalizing it would
|
|
||||||
-- buy nothing until something needs a per-character reverse lookup, and a
|
|
||||||
-- character's own standings already ride inside char.profile instead.
|
|
||||||
--
|
|
||||||
-- No delete path: the shard's set of systems is fixed at startup, so there is no
|
|
||||||
-- points.remove to mirror.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_points_boards (
|
|
||||||
system VARCHAR(48) PRIMARY KEY, -- PointsType name, e.g. QueensLoyalty
|
|
||||||
name VARCHAR(128) NULL, -- resolved display name, if the shard sent a literal
|
|
||||||
name_cliloc INT NULL, -- cliloc id when the name is a TextDefinition number
|
|
||||||
max_points BIGINT NULL,
|
|
||||||
players INT NULL, -- players actually holding points in this system
|
|
||||||
show_on_gump TINYINT(1) NOT NULL DEFAULT 1, -- the shard's own "is this player-facing?" flag
|
|
||||||
payload JSON NOT NULL, -- the whole points.board frame, incl. `top`
|
|
||||||
t BIGINT NULL, -- frame time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Player-vendor market index (Protocol 3.0 vendor.listing). One row per player
|
|
||||||
-- vendor and one per priced listing, so the site can offer the search the in-game
|
|
||||||
-- Vendor Search gump offers — from outside the game.
|
|
||||||
--
|
|
||||||
-- The shard sweeps vendors round-robin and emits one AUTHORITATIVE frame per
|
|
||||||
-- vendor, so ingest is delete-then-insert of that vendor's items inside one
|
|
||||||
-- transaction (see shardMarket.db.js). No foreign key from items to vendors, in
|
|
||||||
-- keeping with every other shard_* table: the ingest transaction is what keeps
|
|
||||||
-- them consistent, and an FK would turn a malformed frame into a failed write
|
|
||||||
-- rather than a dropped row.
|
|
||||||
--
|
|
||||||
-- Only vendors whose owner left the in-game Vendor Search flag ON are ever sent,
|
|
||||||
-- so a player who hid their shop in game is hidden here too — see BridgeMarket.cs.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_vendors (
|
|
||||||
serial VARCHAR(20) NOT NULL PRIMARY KEY, -- "0x40001234"
|
|
||||||
shop_name VARCHAR(160) NULL,
|
|
||||||
owner_serial VARCHAR(20) NULL,
|
|
||||||
owner_name VARCHAR(64) NULL,
|
|
||||||
map VARCHAR(40) NULL,
|
|
||||||
x INT NULL,
|
|
||||||
y INT NULL,
|
|
||||||
z INT NULL,
|
|
||||||
region VARCHAR(80) NULL,
|
|
||||||
house VARCHAR(160) NULL, -- the house SIGN's name, not the house type
|
|
||||||
item_count INT NOT NULL DEFAULT 0, -- listings published in the frame
|
|
||||||
item_total INT NOT NULL DEFAULT 0, -- listings the shop actually holds
|
|
||||||
truncated TINYINT(1) NOT NULL DEFAULT 0, -- item_total > item_count
|
|
||||||
t BIGINT NULL, -- frame time, epoch ms
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_shard_vendors_owner (owner_name),
|
|
||||||
INDEX idx_shard_vendors_map (map),
|
|
||||||
INDEX idx_shard_vendors_region (region),
|
|
||||||
-- The market page's staleness banner is MIN(updated_at) over this column: the
|
|
||||||
-- round-robin sweep means the oldest row is how far behind the index can be.
|
|
||||||
INDEX idx_shard_vendors_updated (updated_at)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- One priced listing. Unlike the points board's top-N — a fixed-size list read
|
|
||||||
-- whole — these are the searchable rows the whole feature exists for, so they are
|
|
||||||
-- normalized rather than left inside a payload column, and there is no payload
|
|
||||||
-- column on shard_vendors at all.
|
|
||||||
--
|
|
||||||
-- `display_name` is DENORMALIZED at ingest: the shard sends `cliloc` (the item's
|
|
||||||
-- LabelNumber) and, rarely, a literal `name`, and resolving 50 clilocs per page
|
|
||||||
-- at query time would make the cliloc table a join on the hot path AND make
|
|
||||||
-- search-by-name impossible. Resolving once on write buys the index. It is
|
|
||||||
-- re-resolved in bulk after a cliloc import, because the diff sweep will not
|
|
||||||
-- re-send an unchanged shop just because the site learned what its items are
|
|
||||||
-- called.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_vendor_items (
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
vendor_serial VARCHAR(20) NOT NULL,
|
|
||||||
serial VARCHAR(20) NOT NULL,
|
|
||||||
item_id INT NOT NULL DEFAULT 0, -- ItemID (the art/graphic id)
|
|
||||||
hue INT NOT NULL DEFAULT 0,
|
|
||||||
amount INT NOT NULL DEFAULT 1,
|
|
||||||
price BIGINT NOT NULL DEFAULT 0,
|
|
||||||
name VARCHAR(160) NULL, -- the item's literal Name, null for most
|
|
||||||
cliloc INT NULL, -- LabelNumber, resolved against shard_clilocs
|
|
||||||
display_name VARCHAR(160) NULL, -- resolved at ingest; what search matches
|
|
||||||
child TINYINT(1) NOT NULL DEFAULT 0, -- priced by an enclosing container, not itself
|
|
||||||
INDEX idx_shard_vendor_items_vendor (vendor_serial),
|
|
||||||
INDEX idx_shard_vendor_items_price (price),
|
|
||||||
INDEX idx_shard_vendor_items_item (item_id),
|
|
||||||
INDEX idx_shard_vendor_items_name (display_name),
|
|
||||||
-- Search filters on name and sorts on price; the composite covers the common
|
|
||||||
-- "cheapest matching X" without a filesort over the whole table.
|
|
||||||
INDEX idx_shard_vendor_items_name_price (display_name, price)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Per-feature visibility for every shard-derived surface (Protocol 3.0). One row
|
|
||||||
-- per feature; an absent row means "use the compiled default", and the compiled
|
|
||||||
-- defaults reproduce the behavior that shipped before v3 — so an empty table is
|
|
||||||
-- a no-op. See utils/shardVisibility.js for the catalog and the ladder, and
|
|
||||||
-- docs/link/v3.md §3 for the contract.
|
|
||||||
--
|
|
||||||
-- audience the minimum rung on anonymous < logged_in < player < staff < admin
|
|
||||||
-- stream whether this feature's kinds fan out over SSE at all (the market
|
|
||||||
-- index ships with this off: no page needs a live firehose of
|
|
||||||
-- whole vendor inventories)
|
|
||||||
-- field_rules {"<field>": "<rung>"} for SENSITIVE fields only. `acct` and
|
|
||||||
-- `webId` are admin-only always and are rejected here — they are
|
|
||||||
-- not in-game visible and are deliberately not configurable.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_feature_visibility (
|
|
||||||
feature VARCHAR(48) NOT NULL PRIMARY KEY,
|
|
||||||
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
|
||||||
audience VARCHAR(20) NOT NULL DEFAULT 'anonymous',
|
|
||||||
stream TINYINT(1) NOT NULL DEFAULT 1,
|
|
||||||
field_rules JSON NULL,
|
|
||||||
updated_by INT NULL,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Admin email invites (Protocol 2.0 provisioning). A staff member invites someone
|
-- Admin email invites (Protocol 2.0 provisioning). A staff member invites someone
|
||||||
-- by email at a pre-chosen access level; the invitee accepts via a tokened link,
|
-- by email at a pre-chosen access level; the invitee accepts via a tokened link,
|
||||||
@@ -1192,175 +815,6 @@ ALTER TABLE announce_jobs
|
|||||||
|
|
||||||
-- One row per spawnable type, aggregated across the world. `total` is the sum of
|
-- One row per spawnable type, aggregated across the world. `total` is the sum of
|
||||||
-- each type's own MX across every point that spawns it (how many exist at once);
|
-- each type's own MX across every point that spawns it (how many exist at once);
|
||||||
-- `facets` is a per-facet point count, so the facet filter and "where does this
|
|
||||||
-- live" both answer without touching shard_spawn_points.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_spawn_creatures (
|
|
||||||
slug VARCHAR(120) NOT NULL PRIMARY KEY, -- slugified class name; the /atlas/:slug key
|
|
||||||
name VARCHAR(120) NOT NULL, -- display spelling chosen by the build
|
|
||||||
total INT NOT NULL DEFAULT 0,
|
|
||||||
points INT NOT NULL DEFAULT 0,
|
|
||||||
facets JSON NULL, -- { "Felucca": 171, "Trammel": 160, ... }
|
|
||||||
-- Operator-supplied artwork, always NULL on a fresh import. The repo ships no
|
|
||||||
-- creature art: sprites live in the operator's own client .mul/.uop files and
|
|
||||||
-- are theirs to extract and place under uploads/atlas/. The UI renders without
|
|
||||||
-- art when this is NULL, which is the normal case.
|
|
||||||
art VARCHAR(255) NULL,
|
|
||||||
-- Plain INDEX, deliberately NOT FULLTEXT: ~800 rows makes a LIKE scan free,
|
|
||||||
-- and FULLTEXT's min-token-length would break searches for names like "orc".
|
|
||||||
INDEX idx_shard_spawn_creatures_name (name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- One row per spawner. `region`/`landmark` are the resolved place name — the
|
|
||||||
-- point-in-rect transform that turns "5411,1234" into "Despise" — and `label` is
|
|
||||||
-- the resolved display string (region, else landmark, else 'Wilderness').
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_spawn_points (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
facet VARCHAR(40) NOT NULL,
|
|
||||||
name VARCHAR(120) NULL, -- the ServUO spawner's own name
|
|
||||||
x INT NOT NULL,
|
|
||||||
y INT NOT NULL,
|
|
||||||
width INT NOT NULL DEFAULT 0,
|
|
||||||
height INT NOT NULL DEFAULT 0,
|
|
||||||
spawn_range INT NOT NULL DEFAULT 0, -- `range` is reserved in MariaDB
|
|
||||||
max_count INT NOT NULL DEFAULT 0,
|
|
||||||
min_delay INT NOT NULL DEFAULT 0,
|
|
||||||
max_delay INT NOT NULL DEFAULT 0,
|
|
||||||
tod_start INT NOT NULL DEFAULT 0, -- meaningless unless tod_mode <> 0
|
|
||||||
tod_end INT NOT NULL DEFAULT 0,
|
|
||||||
tod_mode INT NOT NULL DEFAULT 0,
|
|
||||||
region VARCHAR(120) NULL,
|
|
||||||
landmark VARCHAR(120) NULL,
|
|
||||||
label VARCHAR(120) NOT NULL DEFAULT 'Wilderness',
|
|
||||||
INDEX idx_shard_spawn_points_facet (facet),
|
|
||||||
INDEX idx_shard_spawn_points_label (label)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- The many-to-many between the two above: one spawner commonly carries several
|
|
||||||
-- types (a single Trammel point spawns six), each with its own max. This is how
|
|
||||||
-- /atlas/creatures/:slug finds the places a creature appears.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_spawn_point_types (
|
|
||||||
point_id INT NOT NULL,
|
|
||||||
slug VARCHAR(120) NOT NULL, -- → shard_spawn_creatures.slug (no FK)
|
|
||||||
max_count INT NOT NULL DEFAULT 1,
|
|
||||||
PRIMARY KEY (point_id, slug),
|
|
||||||
INDEX idx_shard_spawn_point_types_slug (slug)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Named regions from Data/Regions.xml, flattened out of their nesting. `rects`
|
|
||||||
-- holds the region's rectangles; `priority` and rect area are what resolved each
|
|
||||||
-- spawn point at build time, kept here so the admin drift check can re-derive.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_regions (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
facet VARCHAR(40) NOT NULL,
|
|
||||||
name VARCHAR(120) NOT NULL,
|
|
||||||
type VARCHAR(80) NULL, -- ServUO region class
|
|
||||||
priority INT NOT NULL DEFAULT 0,
|
|
||||||
parent VARCHAR(120) NULL, -- enclosing named region, if any
|
|
||||||
rects JSON NULL,
|
|
||||||
INDEX idx_shard_regions_facet (facet),
|
|
||||||
INDEX idx_shard_regions_name (name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Points of interest from Data/Locations/*.xml. `grp` is the innermost enclosing
|
|
||||||
-- parent ("Covetous"), which is the label worth showing — "Covetous" reads
|
|
||||||
-- better than the individual marker "Level 1". (`group` is reserved in SQL.)
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_landmarks (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
facet VARCHAR(40) NOT NULL,
|
|
||||||
name VARCHAR(120) NOT NULL,
|
|
||||||
grp VARCHAR(120) NULL,
|
|
||||||
x INT NOT NULL,
|
|
||||||
y INT NOT NULL,
|
|
||||||
z INT NOT NULL DEFAULT 0,
|
|
||||||
INDEX idx_shard_landmarks_facet (facet),
|
|
||||||
INDEX idx_shard_landmarks_name (name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Configured champion altars from Config/ChampionSpawns.xml. This is static
|
|
||||||
-- roster data ("there is an Unholy Terror altar in Deceit") and is distinct from
|
|
||||||
-- the live champ.update feed in shard_champs ("it is on level 3 right now").
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_champion_spawns (
|
|
||||||
slug VARCHAR(160) NOT NULL PRIMARY KEY, -- facet-name, e.g. "felucca-deceit"
|
|
||||||
name VARCHAR(120) NOT NULL,
|
|
||||||
grp VARCHAR(80) NULL, -- spawn group; one active per group
|
|
||||||
type VARCHAR(80) NULL, -- '' when randomised per activation
|
|
||||||
random_type TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
facet VARCHAR(40) NOT NULL,
|
|
||||||
x INT NOT NULL,
|
|
||||||
y INT NOT NULL,
|
|
||||||
z INT NOT NULL DEFAULT 0,
|
|
||||||
radius INT NOT NULL DEFAULT 0,
|
|
||||||
label VARCHAR(120) NULL, -- resolved place name
|
|
||||||
INDEX idx_shard_champion_spawns_facet (facet)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- UO's localization table: cliloc id -> display string. Items carry a
|
|
||||||
-- `LabelNumber` rather than a name, so without this the site can only render
|
|
||||||
-- `id 1023721` where the game shows "quarter staff". The shard has always sent
|
|
||||||
-- the id (char.profile's `cliloc`, and one per marketplace listing) — the number
|
|
||||||
-- was never the missing piece, the table was.
|
|
||||||
--
|
|
||||||
-- Sourced from a file the OPERATOR converts once from their own UO client and
|
|
||||||
-- points the site at (docs/website/CLILOCS.md); nothing derived from the client
|
|
||||||
-- is committed, the same rule the spawn atlas and the creature art map follow.
|
|
||||||
-- A shard with no cliloc file configured simply renders item ids, which is what
|
|
||||||
-- it did before this table existed.
|
|
||||||
--
|
|
||||||
-- `text` is TEXT, not VARCHAR: real tables top out around 12 KB for the long
|
|
||||||
-- property descriptions, and truncating them silently would be worse than
|
|
||||||
-- storing them. Item NAMES are all short — the index that matters for search is
|
|
||||||
-- on the denormalized `shard_vendor_items.display_name`, not here.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_clilocs (
|
|
||||||
number INT NOT NULL PRIMARY KEY,
|
|
||||||
flag SMALLINT NOT NULL DEFAULT 0,
|
|
||||||
text TEXT NOT NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Singleton (id = 1) describing the cliloc table currently loaded: the source
|
|
||||||
-- file, its sha256, the entry count and the parser version. The boot path
|
|
||||||
-- compares the stored hash against the file on disk and skips the parse when
|
|
||||||
-- they match, which is every restart that did not follow a client patch.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_cliloc_meta (
|
|
||||||
id TINYINT NOT NULL PRIMARY KEY DEFAULT 1,
|
|
||||||
payload JSON NOT NULL,
|
|
||||||
imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT chk_shard_cliloc_meta_singleton CHECK (id = 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Singleton (id = 1) describing the artifact currently loaded: when it was
|
|
||||||
-- built, its counts, and a sha256 per ServUO source file. The admin drift check
|
|
||||||
-- compares this against db/data/spawnAtlas.meta.json to report when the database
|
|
||||||
-- is behind the committed artifact.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_atlas_meta (
|
|
||||||
id TINYINT NOT NULL PRIMARY KEY DEFAULT 1,
|
|
||||||
payload JSON NOT NULL,
|
|
||||||
imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT chk_shard_atlas_meta_singleton CHECK (id = 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Singleton (id = 1) holding an atlas refresh that was parsed but deliberately
|
|
||||||
-- NOT applied, because it would remove a facet the site currently serves.
|
|
||||||
--
|
|
||||||
-- Losing a facet is the signature of a half-copied or mid-update ServUO tree as
|
|
||||||
-- much as of a real map change, and boot cannot tell the two apart — so the
|
|
||||||
-- refresh is staged here for a human instead of being applied. Startup is never
|
|
||||||
-- blocked by it: the site comes up serving the atlas it already had.
|
|
||||||
--
|
|
||||||
-- Only the DECISION is stored, not the parsed world: `payload` holds the source
|
|
||||||
-- hashes and the facet diff (a few KB), and approving re-parses the tree. That
|
|
||||||
-- keeps a multi-megabyte blob out of the database and guarantees the applied
|
|
||||||
-- atlas matches the tree as it is at approval time, not as it was at boot.
|
|
||||||
--
|
|
||||||
-- `rejected` is remembered against those exact source hashes so a declined
|
|
||||||
-- refresh does not re-prompt on every restart; changing the tree changes the
|
|
||||||
-- hashes and asks again.
|
|
||||||
CREATE TABLE IF NOT EXISTS shard_atlas_pending (
|
|
||||||
id TINYINT NOT NULL PRIMARY KEY DEFAULT 1,
|
|
||||||
status ENUM('pending','rejected') NOT NULL DEFAULT 'pending',
|
|
||||||
payload JSON NOT NULL, -- source hashes + facet diff
|
|
||||||
detected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT chk_shard_atlas_pending_singleton CHECK (id = 1)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
||||||
|
|
||||||
-- Installed modules (module system, docs/website/MODULE_SYSTEM.md §2.4). One row
|
-- Installed modules (module system, docs/website/MODULE_SYSTEM.md §2.4). One row
|
||||||
-- per module the operator has installed onto the modules volume, keyed by the
|
-- per module the operator has installed onto the modules volume, keyed by the
|
||||||
@@ -1456,23 +910,6 @@ ALTER TABLE wiki_pages ADD FULLTEXT INDEX IF NOT EXISTS idx_wiki_search (title,
|
|||||||
ALTER TABLE posts ADD COLUMN IF NOT EXISTS announced_at DATETIME NULL;
|
ALTER TABLE posts ADD COLUMN IF NOT EXISTS announced_at DATETIME NULL;
|
||||||
ALTER TABLE posts ADD COLUMN IF NOT EXISTS announce_job_id INT NULL;
|
ALTER TABLE posts ADD COLUMN IF NOT EXISTS announce_job_id INT NULL;
|
||||||
|
|
||||||
-- House registry (Protocol 2.0). The house.update full-state feed carries richer
|
|
||||||
-- fields than the house.decay transition feed shard_houses was built for. Rather
|
|
||||||
-- than a second table for one entity, extend shard_houses: house.update writes the
|
|
||||||
-- registry columns below (owner display name, co-owner/friend counts, placement
|
|
||||||
-- price, decay level name) while house.decay keeps owning `stage`/`is_idoc`. Each
|
|
||||||
-- upsert only touches its own columns, so the two feeds never clobber each other.
|
|
||||||
-- `price` is the placement value, NOT a "for sale" flag (stock ServUO has none).
|
|
||||||
ALTER TABLE shard_houses ADD COLUMN IF NOT EXISTS owner_name VARCHAR(120) NULL;
|
|
||||||
ALTER TABLE shard_houses ADD COLUMN IF NOT EXISTS co_owners INT NULL;
|
|
||||||
ALTER TABLE shard_houses ADD COLUMN IF NOT EXISTS friends INT NULL;
|
|
||||||
ALTER TABLE shard_houses ADD COLUMN IF NOT EXISTS price BIGINT NULL;
|
|
||||||
ALTER TABLE shard_houses ADD COLUMN IF NOT EXISTS decay VARCHAR(24) NULL;
|
|
||||||
-- Distinguishes a full registry row (seen via house.update) from a decay-only row,
|
|
||||||
-- so the public Houses browser can list registered houses without pulling in rows
|
|
||||||
-- we only ever saw an IDOC transition for.
|
|
||||||
ALTER TABLE shard_houses ADD COLUMN IF NOT EXISTS in_registry TINYINT(1) NOT NULL DEFAULT 0;
|
|
||||||
|
|
||||||
-- Mobile device sessions (M9): a friendly label the app may send at login, and
|
-- Mobile device sessions (M9): a friendly label the app may send at login, and
|
||||||
-- the last time this session token was issued/used, for the "Active Devices"
|
-- the last time this session token was issued/used, for the "Active Devices"
|
||||||
-- self-service list. Both nullable and additive; existing rows get them here.
|
-- self-service list. Both nullable and additive; existing rows get them here.
|
||||||
@@ -1484,19 +921,4 @@ ALTER TABLE mobile_refresh_tokens ADD COLUMN IF NOT EXISTS last_used_at DATETIME
|
|||||||
-- trust token. A boolean only — the token is returned over that app→server call
|
-- trust token. A boolean only — the token is returned over that app→server call
|
||||||
-- and never persisted here (only its sha256 lands in trusted_devices).
|
-- and never persisted here (only its sha256 lands in trusted_devices).
|
||||||
ALTER TABLE mobile_auth_sessions ADD COLUMN IF NOT EXISTS trust_device TINYINT(1) NOT NULL DEFAULT 0;
|
ALTER TABLE mobile_auth_sessions ADD COLUMN IF NOT EXISTS trust_device TINYINT(1) NOT NULL DEFAULT 0;
|
||||||
|
INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_3_migrated', '1');
|
||||||
-- Protocol 3.0 cutover: this build speaks wire protocol 3 (world.ruleset,
|
|
||||||
-- points.board, vendor.listing), so the pinned version an existing install
|
|
||||||
-- carries has to move with it — a 2 against a v3 sidecar 409s every REST call
|
|
||||||
-- and closes the WS on ws.hello. MODIFY fixes the column default for installs
|
|
||||||
-- created before the bump (idempotent, like the other MODIFYs here).
|
|
||||||
ALTER TABLE uo_link_config MODIFY COLUMN protocol INT NOT NULL DEFAULT 3;
|
|
||||||
-- The row itself is admin-editable, and schema.sql runs on EVERY boot, so this
|
|
||||||
-- must be one-shot: an operator who deliberately pins an older sidecar in
|
|
||||||
-- Admin → Shard has to stay pinned. The marker row in `settings` is what makes
|
|
||||||
-- it fire once — written after the UPDATE, and on a fresh install (no
|
|
||||||
-- uo_link_config row yet) it is simply written with nothing to update.
|
|
||||||
UPDATE uo_link_config SET protocol = 3
|
|
||||||
WHERE id = 1 AND protocol < 3
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM settings WHERE `key` = 'uo_link_protocol_3_migrated');
|
|
||||||
INSERT IGNORE INTO settings (`key`, value) VALUES ('uo_link_protocol_3_migrated', '1');
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
"seed": "node db/seed.js",
|
"seed": "node db/seed.js",
|
||||||
"swagger": "node swagger/swagger.js",
|
"swagger": "node swagger/swagger.js",
|
||||||
"routes:manifest": "node scripts/routeManifest.js",
|
"routes:manifest": "node scripts/routeManifest.js",
|
||||||
"atlas:import": "node scripts/importSpawnAtlas.js",
|
|
||||||
"test": "node --test --require ./test/_setup.js"
|
"test": "node --test --require ./test/_setup.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -635,272 +635,6 @@
|
|||||||
"multerMiddleware"
|
"multerMiddleware"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/account",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/accounts",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/atlas",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/approve",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/import",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/path",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/reject",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/audit",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/ban",
|
|
||||||
"handlers": 7,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/broadcast",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/char/:serial",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/clilocs",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/clilocs/import",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/shard/clilocs/path",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/houses",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/kick",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/link",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/pages",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/pages/:id/close",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/pages/:id/respond",
|
|
||||||
"handlers": 6,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/roster/:account",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/sales",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/unban",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/vendors/:account",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/visibility",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/shard/visibility",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"path": "/api/v1/admin/site-mode",
|
"path": "/api/v1/admin/site-mode",
|
||||||
@@ -912,57 +646,6 @@
|
|||||||
"validate"
|
"validate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/uo-link/config",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/uo-link/config",
|
|
||||||
"handlers": 8,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/uo-link/stream",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/uo-link/towncrier",
|
|
||||||
"handlers": 7,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/admin/uo-link/towncrier/:id",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/admin/uploads",
|
"path": "/api/v1/admin/uploads",
|
||||||
@@ -1037,72 +720,6 @@
|
|||||||
"validate"
|
"validate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/accounts",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/houses",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/link/:account",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/online",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/sales",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/standing",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
"path": "/api/v1/admin/users/:id/trusted-devices",
|
"path": "/api/v1/admin/users/:id/trusted-devices",
|
||||||
@@ -1798,146 +1415,6 @@
|
|||||||
"requireAuth"
|
"requireAuth"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/shard/account",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/accounts",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/char/:serial",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/houses",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/shard/link",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/roster/:account",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/sales",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/vendors/:account",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"noindex",
|
|
||||||
"requireAuth",
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/champions",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate",
|
|
||||||
"siteMode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/creatures",
|
|
||||||
"handlers": 8,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate",
|
|
||||||
"siteMode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/creatures/:slug",
|
|
||||||
"handlers": 7,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate",
|
|
||||||
"siteMode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/landmarks",
|
|
||||||
"handlers": 6,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate",
|
|
||||||
"siteMode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/meta",
|
|
||||||
"handlers": 3,
|
|
||||||
"gates": [
|
|
||||||
"siteMode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/regions",
|
|
||||||
"handlers": 6,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate",
|
|
||||||
"siteMode"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/public/contact",
|
"path": "/api/v1/public/contact",
|
||||||
@@ -1989,135 +1466,6 @@
|
|||||||
"handlers": 1,
|
"handlers": 1,
|
||||||
"gates": []
|
"gates": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/champs",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/economy",
|
|
||||||
"handlers": 4,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/features",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/feed",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/governors",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/governors/:city/history",
|
|
||||||
"handlers": 5,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/guilds",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/houses",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/idoc",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/market",
|
|
||||||
"handlers": 13,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/market/meta",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/market/vendors/:serial",
|
|
||||||
"handlers": 7,
|
|
||||||
"gates": [
|
|
||||||
"middleware",
|
|
||||||
"validate"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/online",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/points",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/points/:system",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/presence",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/ruleset",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/status",
|
|
||||||
"handlers": 2,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/stream",
|
|
||||||
"handlers": 1,
|
|
||||||
"gates": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/public/status",
|
"path": "/api/v1/public/status",
|
||||||
|
|||||||
@@ -257,134 +257,10 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/admin/settings/brand-asset/:slot"
|
"path": "/api/v1/admin/settings/brand-asset/:slot"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/accounts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/atlas"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/approve"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/import"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/atlas/reject"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/audit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/ban"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/broadcast"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/char/:serial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/clilocs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/clilocs/import"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/shard/clilocs/path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/houses"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/kick"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/pages"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/pages/:id/close"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/pages/:id/respond"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/roster/:account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/sales"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/shard/unban"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/vendors/:account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/shard/visibility"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/shard/visibility"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"path": "/api/v1/admin/site-mode"
|
"path": "/api/v1/admin/site-mode"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/uo-link/config"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"path": "/api/v1/admin/uo-link/config"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/uo-link/stream"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/admin/uo-link/towncrier"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/admin/uo-link/towncrier/:id"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/admin/uploads"
|
"path": "/api/v1/admin/uploads"
|
||||||
@@ -413,30 +289,6 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/admin/users/:id/mfa/reset"
|
"path": "/api/v1/admin/users/:id/mfa/reset"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/accounts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/houses"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "DELETE",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/link/:account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/online"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/sales"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/admin/users/:id/shard/standing"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "DELETE",
|
"method": "DELETE",
|
||||||
"path": "/api/v1/admin/users/:id/trusted-devices"
|
"path": "/api/v1/admin/users/:id/trusted-devices"
|
||||||
@@ -721,62 +573,6 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/player/appeals/eligible"
|
"path": "/api/v1/player/appeals/eligible"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/shard/account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/accounts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/char/:serial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/houses"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/player/shard/link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/roster/:account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/sales"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/player/shard/vendors/:account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/champions"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/creatures"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/creatures/:slug"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/landmarks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/meta"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/atlas/regions"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/api/v1/public/contact"
|
"path": "/api/v1/public/contact"
|
||||||
@@ -805,82 +601,6 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/public/settings"
|
"path": "/api/v1/public/settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/champs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/economy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/features"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/feed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/governors"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/governors/:city/history"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/guilds"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/houses"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/idoc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/market"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/market/meta"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/market/vendors/:serial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/online"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/points"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/points/:system"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/presence"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/ruleset"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/status"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"path": "/api/v1/public/shard/stream"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/api/v1/public/status"
|
"path": "/api/v1/public/status"
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ const postsRouter = require('./posts.router')
|
|||||||
const uploadsRouter = require('./uploads.router')
|
const uploadsRouter = require('./uploads.router')
|
||||||
const wikiRouter = require('./wiki.router')
|
const wikiRouter = require('./wiki.router')
|
||||||
const pagesRouter = require('./pages.router')
|
const pagesRouter = require('./pages.router')
|
||||||
const shardRouter = require('./shard.router')
|
|
||||||
const uoLinkRouter = require('./uoLink.router')
|
|
||||||
const emailRouter = require('./email.router')
|
const emailRouter = require('./email.router')
|
||||||
const discordBotRouter = require('./discordBot.router')
|
const discordBotRouter = require('./discordBot.router')
|
||||||
const settingsRouter = require('./settings.router')
|
const settingsRouter = require('./settings.router')
|
||||||
@@ -64,12 +62,14 @@ adminRouter.use('/posts', postsRouter)
|
|||||||
adminRouter.use('/uploads', uploadsRouter)
|
adminRouter.use('/uploads', uploadsRouter)
|
||||||
adminRouter.use('/wiki', wikiRouter)
|
adminRouter.use('/wiki', wikiRouter)
|
||||||
adminRouter.use('/pages', pagesRouter)
|
adminRouter.use('/pages', pagesRouter)
|
||||||
// Ops and configuration. /shard mixes tiers on one prefix — self-service game
|
// Ops and configuration.
|
||||||
// account linking (no extra gate) alongside modAccess in-game staff ops — so
|
//
|
||||||
// one router owns the prefix and gates per route. The rest are admin-only.
|
// `/shard` and `/uo-link` are absent here and are still served: they are
|
||||||
// /admin/shard/pages is the in-game help-page queue, unrelated to /admin/pages.
|
// module-uo's, mounted onto this same router by the loader after every core
|
||||||
adminRouter.use('/shard', shardRouter)
|
// mount above (MODULE_API.md §2.4). The URLs did not move — the code did. That
|
||||||
adminRouter.use('/uo-link', uoLinkRouter)
|
// ordering is also what makes the prefixes unclaimable by anyone else: the
|
||||||
|
// loader asks this live router what core owns, so a second module claiming
|
||||||
|
// `/shard` is rejected against the mounts actually present, not against a list.
|
||||||
adminRouter.use('/email', emailRouter)
|
adminRouter.use('/email', emailRouter)
|
||||||
adminRouter.use('/discord-bot', discordBotRouter)
|
adminRouter.use('/discord-bot', discordBotRouter)
|
||||||
adminRouter.use('/settings', settingsRouter)
|
adminRouter.use('/settings', settingsRouter)
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
// Adding a requireRole('player') here would 403 an admin off their own characters
|
// Adding a requireRole('player') here would 403 an admin off their own characters
|
||||||
// (it happened once — see docs/website/BACKEND_DESIGN.md). Staff also reach the
|
// (it happened once — see docs/website/BACKEND_DESIGN.md). Staff also reach the
|
||||||
// identical self-scoped handlers under /admin/shard and /auth/me/account; those
|
// identical self-scoped handlers under /admin/shard and /auth/me/account; those
|
||||||
// are alternative URLs onto the same controllers, not duplicated logic.
|
// are alternative URLs onto the same controllers, not duplicated logic — and
|
||||||
|
// both of those live in module-uo now, which changes where they are defined and
|
||||||
|
// nothing about which URLs answer.
|
||||||
//
|
//
|
||||||
// See docs/website/API_V2_PLAN.md § Phase 2 for the split.
|
// See docs/website/API_V2_PLAN.md § Phase 2 for the split.
|
||||||
|
|
||||||
@@ -23,7 +25,6 @@ const { requireAuth } = require('../../../auth/session.middleware')
|
|||||||
const noindex = require('../../../middleware/noindex')
|
const noindex = require('../../../middleware/noindex')
|
||||||
|
|
||||||
const accountRouter = require('./account.router')
|
const accountRouter = require('./account.router')
|
||||||
const shardRouter = require('./shard.router')
|
|
||||||
const appealsRouter = require('./appeals.router')
|
const appealsRouter = require('./appeals.router')
|
||||||
|
|
||||||
const playerRouter = express.Router()
|
const playerRouter = express.Router()
|
||||||
@@ -37,7 +38,6 @@ const playerRouter = express.Router()
|
|||||||
playerRouter.use(noindex, requireAuth)
|
playerRouter.use(noindex, requireAuth)
|
||||||
|
|
||||||
playerRouter.use('/account', accountRouter)
|
playerRouter.use('/account', accountRouter)
|
||||||
playerRouter.use('/shard', shardRouter)
|
|
||||||
playerRouter.use('/appeals', appealsRouter)
|
playerRouter.use('/appeals', appealsRouter)
|
||||||
|
|
||||||
module.exports = playerRouter
|
module.exports = playerRouter
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ const express = require('express')
|
|||||||
const postsRouter = require('./posts.router')
|
const postsRouter = require('./posts.router')
|
||||||
const wikiRouter = require('./wiki.router')
|
const wikiRouter = require('./wiki.router')
|
||||||
const pagesRouter = require('./pages.router')
|
const pagesRouter = require('./pages.router')
|
||||||
const shardRouter = require('./shard.router')
|
|
||||||
const atlasRouter = require('./atlas.router')
|
|
||||||
const modulesRouter = require('./modules.router')
|
const modulesRouter = require('./modules.router')
|
||||||
const siteRouter = require('./site.router')
|
const siteRouter = require('./site.router')
|
||||||
|
|
||||||
@@ -32,13 +30,6 @@ const publicRouter = express.Router()
|
|||||||
publicRouter.use('/posts', postsRouter)
|
publicRouter.use('/posts', postsRouter)
|
||||||
publicRouter.use('/wiki', wikiRouter)
|
publicRouter.use('/wiki', wikiRouter)
|
||||||
publicRouter.use('/pages', pagesRouter)
|
publicRouter.use('/pages', pagesRouter)
|
||||||
// Live shard data, never site-mode gated.
|
|
||||||
publicRouter.use('/shard', shardRouter)
|
|
||||||
// The spawn atlas: static shard CONTENT, parsed from the shard's ServUO tree
|
|
||||||
// rather than fetched from the sidecar. Deliberately not under /shard — nothing
|
|
||||||
// here depends on the bridge — and site-mode gated per route like the content
|
|
||||||
// routers above, which is the other half of that distinction.
|
|
||||||
publicRouter.use('/atlas', atlasRouter)
|
|
||||||
// What this backend serves beyond core. A real prefix layer rather than a fifth
|
// What this backend serves beyond core. A real prefix layer rather than a fifth
|
||||||
// singleton in site.router.js, because the loader's prefix-collision probe reads
|
// singleton in site.router.js, because the loader's prefix-collision probe reads
|
||||||
// the live tier stack and skips root-mounted layers — this mount is what makes
|
// the live tier stack and skips root-mounted layers — this mount is what makes
|
||||||
|
|||||||
@@ -4,19 +4,12 @@ const http = require('http')
|
|||||||
const app = require('./app')
|
const app = require('./app')
|
||||||
const internalApp = require('./internalApp')
|
const internalApp = require('./internalApp')
|
||||||
const botScore = require('./middleware/botScore')
|
const botScore = require('./middleware/botScore')
|
||||||
const uoLinkSocket = require('./utils/uoLinkSocket')
|
|
||||||
const uoLinkClient = require('./utils/uoLinkClient')
|
|
||||||
const uoLinkConfig = require('./model/uoLinkConfig/uoLinkConfig.model')
|
|
||||||
const shardBroadcast = require('./utils/shardBroadcast')
|
|
||||||
const announceWorker = require('./utils/announceWorker')
|
const announceWorker = require('./utils/announceWorker')
|
||||||
const { ensureSchema, close } = require('./utils/db')
|
const { ensureSchema, close } = require('./utils/db')
|
||||||
const { seedDefaults, createInitialAdminFromEnv } = require('../db/seed')
|
const { seedDefaults, createInitialAdminFromEnv } = require('../db/seed')
|
||||||
const settings = require('./model/settings/settings.model')
|
const settings = require('./model/settings/settings.model')
|
||||||
const revokedSessions = require('./model/revokedSessions/revokedSessions.model')
|
const revokedSessions = require('./model/revokedSessions/revokedSessions.model')
|
||||||
const mobileAuthBridge = require('./model/mobileAuthBridge/mobileAuthBridge.model')
|
const mobileAuthBridge = require('./model/mobileAuthBridge/mobileAuthBridge.model')
|
||||||
const shardAtlas = require('./model/shardAtlas/shardAtlas.model')
|
|
||||||
const shardClilocs = require('./model/shardClilocs/shardClilocs.model')
|
|
||||||
const shardMarket = require('./model/shardMarket/shardMarket.model')
|
|
||||||
const moduleLifecycle = require('./modules/lifecycle')
|
const moduleLifecycle = require('./modules/lifecycle')
|
||||||
const createLogger = require('./utils/logger')
|
const createLogger = require('./utils/logger')
|
||||||
const { evaluateBotInternalKey } = require('./utils/botInternalKey')
|
const { evaluateBotInternalKey } = require('./utils/botInternalKey')
|
||||||
@@ -81,37 +74,18 @@ async function start() {
|
|||||||
log.warn('mobile-auth-bridge prune failed', { error: err.message })
|
log.warn('mobile-auth-bridge prune failed', { error: err.message })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-derive the spawn atlas from the shard's own ServUO tree. The shard's maps
|
|
||||||
// change over its lifetime — facets get added, replaced or renamed — so the
|
|
||||||
// atlas is rebuilt on every boot rather than shipped as a snapshot that would
|
|
||||||
// silently go stale. Hash-gated, so an unchanged tree costs one read pass and
|
|
||||||
// no database write.
|
|
||||||
//
|
|
||||||
// Best-effort by contract: no configured path, an unreadable mount or a
|
|
||||||
// malformed file must never stop the site coming up. A refresh that would
|
|
||||||
// REMOVE a facet is staged for admin approval instead of being applied.
|
|
||||||
await shardAtlas.refreshOnBoot()
|
|
||||||
|
|
||||||
// Refresh the cliloc table (UO's id → display-string map) from the file the
|
|
||||||
// operator converted out of their own client. Same contract as the atlas:
|
|
||||||
// hash-gated so an unchanged file costs one read, and best-effort so a missing
|
|
||||||
// or wrong-format file never stops the site coming up — it just means item
|
|
||||||
// names render as ids, which is what they did before the table existed.
|
|
||||||
const clilocResult = await shardClilocs.refreshOnBoot()
|
|
||||||
|
|
||||||
// A cliloc import changes what item names RESOLVE to, and the marketplace
|
|
||||||
// stores those names denormalized (shard_vendor_items.display_name) so it can
|
|
||||||
// index and search them. The shard's market sweep will not re-send an unchanged
|
|
||||||
// shop just because the site learned what its items are called, so the backfill
|
|
||||||
// has to be pulled rather than waited for. Only after an actual import — the
|
|
||||||
// common boot is hash-gated to a no-op and must stay one.
|
|
||||||
if (clilocResult && clilocResult.status === 'imported') await shardMarket.refreshDisplayNames()
|
|
||||||
|
|
||||||
const mode = await settings.get('site_mode')
|
const mode = await settings.get('site_mode')
|
||||||
log.info(`site mode: ${String(mode || 'live').toUpperCase()}`)
|
log.info(`site mode: ${String(mode || 'live').toUpperCase()}`)
|
||||||
|
|
||||||
// Reconcile installed_modules with what the loader found on the volume at
|
// Reconcile installed_modules with what the loader found on the volume at
|
||||||
// require time, then run each module's onBoot (MODULE_API.md §2.5). Placed
|
// require time, then run each module's onBoot (MODULE_API.md §2.5).
|
||||||
|
//
|
||||||
|
// This is where the shard now warms up. Core used to do it inline just above —
|
||||||
|
// rebuild the spawn atlas from the ServUO tree, refresh the cliloc table, open
|
||||||
|
// the uo-link WebSocket — and all of it is module-uo's `onBoot` since Phase 3.
|
||||||
|
// The ordering guarantee is unchanged and is why it belongs here rather than
|
||||||
|
// after the listener: the tables exist by now, and nothing is served until the
|
||||||
|
// warm-up finishes. Placed
|
||||||
// after core's own boot work and BEFORE the listener binds, for both reasons
|
// after core's own boot work and BEFORE the listener binds, for both reasons
|
||||||
// the contract gives: a module's warm-up may depend on core being up, and a
|
// the contract gives: a module's warm-up may depend on core being up, and a
|
||||||
// module that must not serve traffic until it has warmed a cache gets that
|
// module that must not serve traffic until it has warmed a cache gets that
|
||||||
@@ -132,17 +106,6 @@ async function start() {
|
|||||||
log.info(`internal API listening on http://${HOST}:${INTERNAL_PORT} (server<->bot only — do NOT proxy)`)
|
log.info(`internal API listening on http://${HOST}:${INTERNAL_PORT} (server<->bot only — do NOT proxy)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start the uo-link WebSocket ingest client. Self-guards: it only actually
|
|
||||||
// connects when the admin has enabled the integration and saved a token, so
|
|
||||||
// this is a no-op on shards that haven't configured the sidecar. Never let a
|
|
||||||
// sidecar problem block server startup.
|
|
||||||
try {
|
|
||||||
await uoLinkSocket.start()
|
|
||||||
await checkUoLink()
|
|
||||||
} catch (err) {
|
|
||||||
log.warn('uo-link socket failed to start (continuing)', { error: err.message })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the news-announcement dispatcher: a light in-process poller that pushes
|
// Start the news-announcement dispatcher: a light in-process poller that pushes
|
||||||
// published news posts to the in-game town crier + Discord with independent
|
// published news posts to the in-game town crier + Discord with independent
|
||||||
// retry per leg. No-op until a news post is actually published.
|
// retry per leg. No-op until a news post is actually published.
|
||||||
@@ -151,33 +114,6 @@ async function start() {
|
|||||||
setupShutdown(server, internalServer)
|
setupShutdown(server, internalServer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort startup probe of the uo-link sidecar: if the integration is
|
|
||||||
// enabled, log whether it is reachable and warn loudly on a protocol mismatch
|
|
||||||
// (fail-fast visibility rather than silently mis-parsing a newer wire format).
|
|
||||||
async function checkUoLink() {
|
|
||||||
const config = await uoLinkConfig.getSafe()
|
|
||||||
if (!config.enabled) return
|
|
||||||
const health = await uoLinkClient.health()
|
|
||||||
if (!health.ok) {
|
|
||||||
log.warn('uo-link is enabled but the sidecar is unreachable at startup', {
|
|
||||||
baseUrl: config.baseUrl,
|
|
||||||
error: health.error || `status ${health.status}`,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (health.data && health.data.protocol && health.data.protocol !== config.protocol) {
|
|
||||||
log.error('uo-link PROTOCOL MISMATCH — pinned vs sidecar', {
|
|
||||||
pinned: config.protocol,
|
|
||||||
sidecar: health.data.protocol,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log.info('uo-link sidecar reachable', {
|
|
||||||
pluginConnected: health.data && health.data.plugin_connected,
|
|
||||||
protocol: health.data && health.data.protocol,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupShutdown(server, internalServer) {
|
function setupShutdown(server, internalServer) {
|
||||||
let closing = false
|
let closing = false
|
||||||
const shutdown = async (signal) => {
|
const shutdown = async (signal) => {
|
||||||
@@ -192,8 +128,6 @@ function setupShutdown(server, internalServer) {
|
|||||||
await moduleLifecycle.shutdown()
|
await moduleLifecycle.shutdown()
|
||||||
botScore.stopSweeper() // stop the bot-store cleanup interval
|
botScore.stopSweeper() // stop the bot-store cleanup interval
|
||||||
announceWorker.stop() // stop the news-announcement dispatcher poller
|
announceWorker.stop() // stop the news-announcement dispatcher poller
|
||||||
uoLinkSocket.stop() // close the uo-link WS ingest client
|
|
||||||
shardBroadcast.closeAll() // end any open shard live-feed SSE streams
|
|
||||||
server.close(() => log.info('http server closed'))
|
server.close(() => log.info('http server closed'))
|
||||||
if (internalServer) internalServer.close(() => log.info('internal http server closed'))
|
if (internalServer) internalServer.close(() => log.info('internal http server closed'))
|
||||||
try {
|
try {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user