docs(link): the shard-name fallback, the atlas places shape, and two traps
Records what the live Protocol 3.0 smoke test (ServUO + sidecar + website +
AVD) turned up, so none of it has to be rediscovered.
v3.md §5.3 — the ruleset `shard` field now falls back to the instance's own
name when the shard publishes ServUO's stock "My Shard", why that is done at
ingest rather than on read (the frame is also broadcast live), and why the
backfill snapshot must go through the dispatcher instead of writing state
directly: a direct call made it a second writer that skipped the
normalization.
v3.md §7.4 — an unscored board renders a placeholder row rather than a blank
card, and why it is deliberately not shaped like a real entry.
PLAN.md §9 M11 — `places` is a list of {facet,label,spawners,maxAlive} OBJECTS,
not of place-name strings, and typing it `List<String>` makes the whole detail
route fail to decode while the request itself returns 200. Adds the rule that
came out of it: decode tests must feed real captured JSON, because the fakes
build DTOs in Kotlin and can never catch a wire mismatch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
This commit is contained in:
@@ -970,14 +970,22 @@ push, and Play (M6–M8) follow the designed app.
|
|||||||
(`atlas`). Note the path: `/public/atlas`, **not** `/public/shard` — the atlas is static shard
|
(`atlas`). Note the path: `/public/atlas`, **not** `/public/shard` — the atlas is static shard
|
||||||
*content*, not live shard *state*, and unlike `/shard/*` it **is** `siteMode`-gated like
|
*content*, not live shard *state*, and unlike `/shard/*` it **is** `siteMode`-gated like
|
||||||
`/posts` and `/wiki`, so a site in maintenance mode withholds it independently of the sidecar.
|
`/posts` and `/wiki`, so a site in maintenance mode withholds it independently of the sidecar.
|
||||||
Two units/naming traps from `v3.md` §6.3: respawn delays are **seconds** throughout, and
|
Three traps. Two are units/naming, from `v3.md` §6.3: respawn delays are **seconds**
|
||||||
`points` is a *count* on the search route while `spawners` is the *list* on the detail route.
|
throughout, and `points` is a *count* on the search route while `spawners` is the *list* on
|
||||||
|
the detail route. The third is a **shape**: `places` is a list of
|
||||||
|
`{facet, label, spawners, maxAlive}` **objects**, not of place-name strings — it is the
|
||||||
|
aggregate the screen exists to show ("Shrines, Isamu-Jima, Yew"), it arrives only on the
|
||||||
|
detail route, and typing it `List<String>` makes that whole route fail to decode while the
|
||||||
|
request itself returns `200`.
|
||||||
- **Verification** — the five-rung walk (`anonymous`, `logged_in`, `player`, `staff`, `admin`)
|
- **Verification** — the five-rung walk (`anonymous`, `logged_in`, `player`, `staff`, `admin`)
|
||||||
against a local website on the cutover branch, per
|
against a local website on the cutover branch, per
|
||||||
[`../link/v3.md`](../link/v3.md) §11 and the shard-visibility smoke harness; plus one pass with
|
[`../link/v3.md`](../link/v3.md) §11 and the shard-visibility smoke harness; plus one pass with
|
||||||
**every feature disabled** in Admin → Shard Visibility, confirming the app *hides* each surface
|
**every feature disabled** in Admin → Shard Visibility, confirming the app *hides* each surface
|
||||||
instead of erroring on it. Unit tests cover the menu filter (role × feature set), the
|
instead of erroring on it. Unit tests cover the menu filter (role × feature set), the
|
||||||
`404`/`403`/`503` mapping, and DTO decode for each new shape.
|
`404`/`403`/`503` mapping, and DTO decode for each new shape. **Decode tests must feed real
|
||||||
|
captured JSON**, not DTOs built in Kotlin: the fakes under `data/api/fake/` construct objects
|
||||||
|
directly, so they can never catch a wire/type mismatch — which is how the `places` shape above
|
||||||
|
shipped past a green suite.
|
||||||
- **Excluded**, in the same class as M10's exclusions: the admin *configuration* panels — Shard
|
- **Excluded**, in the same class as M10's exclusions: the admin *configuration* panels — Shard
|
||||||
Visibility, Spawn Atlas and Cliloc import — alongside the hero/CMS block editor, Discord-bot
|
Visibility, Spawn Atlas and Cliloc import — alongside the hero/CMS block editor, Discord-bot
|
||||||
config, uo-link config and OAuth-provider setup.
|
config, uo-link config and OAuth-provider setup.
|
||||||
|
|||||||
27
link/v3.md
27
link/v3.md
@@ -350,8 +350,11 @@ Sidecar — `store.rs`: singleton `ruleset(id CHECK(id=1), rev, json, updated_t)
|
|||||||
`main.rs`: new arm in the board-projection match; `web.rs`: `GET /ruleset` served from the store, so
|
`main.rs`: new arm in the board-projection match; `web.rs`: `GET /ruleset` served from the store, so
|
||||||
it answers during a shard outage (`PROTOCOL_2.md` §12.2).
|
it answers during a shard outage (`PROTOCOL_2.md` §12.2).
|
||||||
|
|
||||||
Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so follow the
|
Website — `uoLinkClient.getRuleset()`; `uoLinkSocket.backfill()` (object-shaped, so it cannot use the
|
||||||
`getPresence()` block's explicit form, not the array-only `snapshot()` helper); `shardIngest.js` →
|
array-only `snapshot()` helper — but it **must still go through `shardIngest.ingest()`**, as
|
||||||
|
`ingestEach` does, rather than calling `shardState.setRuleset` directly: the two arrival orders have
|
||||||
|
to produce the same stored frame, and a direct call quietly made backfill a second writer that
|
||||||
|
skipped the normalization below); `shardIngest.js` →
|
||||||
`shardState.setRuleset`, **not** in `LOGGED_KINDS` (it re-arrives every reconnect and `server.hello`
|
`shardState.setRuleset`, **not** in `LOGGED_KINDS` (it re-arrives every reconnect and `server.hello`
|
||||||
already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_ruleset` singleton table
|
already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_ruleset` singleton table
|
||||||
(`rev`, `expansion`, `payload JSON`, `t`); `GET /public/shard/ruleset` behind
|
(`rev`, `expansion`, `payload JSON`, `t`); `GET /public/shard/ruleset` behind
|
||||||
@@ -360,6 +363,17 @@ already marks those); `KIND_FEATURE['world.ruleset'] = 'ruleset'`; `shard_rulese
|
|||||||
Client — NEW `routes/public/Rules.jsx` at `/site/rules`, alongside
|
Client — NEW `routes/public/Rules.jsx` at `/site/rules`, alongside
|
||||||
`/site/champs|guilds|governors|houses`; live via `useShardFeed({ filter: new Set(['world.ruleset']) })`.
|
`/site/champs|guilds|governors|houses`; live via `useShardFeed({ filter: new Set(['world.ruleset']) })`.
|
||||||
|
|
||||||
|
**The `shard` field falls back to the instance's own name.** ServUO ships `Server.cfg` with
|
||||||
|
`Name=My Shard`, so an operator who never edited it publishes that verbatim — which is the shard
|
||||||
|
saying *unnamed*, not naming anything, and the rules page then reads "My Shard" under a header
|
||||||
|
carrying the real one. `shardIngest` substitutes `settings.getInstanceName()` (the admin-editable
|
||||||
|
site title, else `BRAND_NAME` — the same resolution `getPublic().brand.name` uses, so one install
|
||||||
|
never shows two names) when `shard` is absent, blank, or exactly the stock default, matched
|
||||||
|
case-insensitively and trim-tolerantly but only as a **whole** value: a shard genuinely called
|
||||||
|
*"My Shard Reborn"* has named itself and keeps it. Applied at **ingest**, not on read, because the
|
||||||
|
ruleset is also broadcast live — the same object goes to the SSE fan-out, so a read-time
|
||||||
|
substitution would be undone by the next reconnect's frame.
|
||||||
|
|
||||||
### 5.4 Risk
|
### 5.4 Risk
|
||||||
|
|
||||||
Perf is nil (~3 KB per connect). The only real risk is publishing a secret, mitigated by the explicit
|
Perf is nil (~3 KB per connect). The only real risk is publishing a secret, mitigated by the explicit
|
||||||
@@ -638,6 +652,15 @@ Client — NEW `routes/public/Leaderboards.jsx` at `/site/leaderboards`; a "Loya
|
|||||||
added to `components/CharacterSheet.jsx`, one edit serving both `PlayerCharacter.jsx` and
|
added to `components/CharacterSheet.jsx`, one edit serving both `PlayerCharacter.jsx` and
|
||||||
`AdminCharacter.jsx`.
|
`AdminCharacter.jsx`.
|
||||||
|
|
||||||
|
**An unscored board still renders a row.** Most systems on a young shard have `top: []`, and a page
|
||||||
|
of blank cards reads as broken rather than as new — so a board with no entries shows a single
|
||||||
|
placeholder bearing the **instance's own name** with an em dash where a score goes, above the
|
||||||
|
existing "nobody has earned points here yet" line. It is deliberately **not** shaped like an entry —
|
||||||
|
no rank, no medal, no bar, muted — because a placeholder that looked like a real standing would be a
|
||||||
|
fabricated one; the first real entry replaces it outright. Purely presentational: the API keeps
|
||||||
|
sending an empty `top`, so no consumer ever receives an invented row. Web and app render it the same
|
||||||
|
way (`Leaderboards.jsx`, `LeaderboardsScreen.kt`).
|
||||||
|
|
||||||
### 7.5 What the run against a real shard changed
|
### 7.5 What the run against a real shard changed
|
||||||
|
|
||||||
The plan above was written from reading `PointsSystem.cs`. Booting the actual shard (ServUO 57.4, a
|
The plan above was written from reading `PointsSystem.cs`. Booting the actual shard (ServUO 57.4, a
|
||||||
|
|||||||
Reference in New Issue
Block a user