docs(website): record the spawn atlas pipeline and what real data changed
Protocol 3.0 order 3 (Part C), docs half of website #112. Part C is website-only — no plugin, no sidecar, no new kinds, no wire change. ## New: website/SPAWN_ATLAS.md The operator-facing reference: the build/import split and why it exists (build needs a ServUO tree, import does not, and the container has the artifact but not the tree), the re-run story, the artifact format, the placement transform, and the three quirks in the source data that are silent when unhandled. Also documents the artwork policy explicitly: **the project ships no creature art and no extraction tooling.** Sprites live in the operator's own client .mul/.uop files and are theirs, not ours to redistribute. `art` is nullable and NULL on every fresh import; an operator who wants art extracts it themselves into the gitignored uploads/atlas/ and maps slugs in a gitignored art map. Text-only is the normal, supported state — not a degraded one. ## New: v3.md §6.1 — what the build against real data changed Six corrections, kept as a diff rather than edited into §6 in place, because each is a trap the next person would otherwise re-enter: 1. **Six facets, not thirteen.** Eodon.xml and the other named-area files carry TerMur/Trammel points; the facet comes from each record's `<Map>`. 2. **The XML dependency call resolved: hand-rolled, zero deps.** §6 left fast-xml-parser vs a tokenizer open. 3. **Facet names disagree between sources** — Locations says `Ter Mur`, `<Map>` says `TerMur`. Unreconciled the landmark fallback never fires there and every unregioned Ter Mur/Tokuno spawn silently reads "Wilderness". 4. **Spawn type tokens carry XmlSpawner directives** (`Fairy,{RND,4,8}`, `alchemist/z/-50`). Taken literally they invent creatures that do not exist and split real ones in two. 71 of 845 affected; 800 remain after stripping. 5. **The artifact is 1.41 MB, not "well under 1 MB"** — down from 4.40 MB via three encodings. Getting under 1 MB would mean dropping the spawner name. 6. **DELETE, not TRUNCATE** — TRUNCATE is DDL in MariaDB and implicitly commits, which would defeat the all-or-nothing reload the design asked for. §6 also now records that Part C ships as two website PRs: the parsing half is where the correctness risk lives and should not be reviewed inside a 10k-line diff alongside routes and React. ## BACKEND_DESIGN.md The seven atlas tables, the import-owned contract, the four column choices that are traps (`spawn_range`/`grp` reserved words, DELETE vs TRUNCATE, explicit point ids, plain INDEX not FULLTEXT), and the distinction between the configured champion roster and the live champ.update feed. PROJECT_TREE.md is left alone — it is auto-generated by the sync-project-tree workflow. --- - [x] AI-assisted: written with **Claude Code** (Claude Opus 5), reviewed before opening. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
This commit is contained in:
60
link/v3.md
60
link/v3.md
@@ -13,7 +13,7 @@ Each part is marked off here as it lands on `edge`. §9 carries the same state p
|
||||
|---|---|---|---|
|
||||
| 1 | **A** — visibility framework + actor-leak fix (§3) | ✅ **Done** | website [#109](https://gitea.whitlocktech.com/RunicGateway/website/pulls/109) + [#110](https://gitea.whitlocktech.com/RunicGateway/website/pulls/110), docs [#64](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/64) + [#65](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/65) |
|
||||
| 2 | **B/1** — `world.ruleset` (§5) | ✅ **Done** | servuo-plugins [#3](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/3), link [#17](https://gitea.whitlocktech.com/RunicGateway/link/pulls/17), website [#111](https://gitea.whitlocktech.com/RunicGateway/website/pulls/111), docs [#66](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/66) |
|
||||
| 3 | **C** — spawn atlas (§6) | ⬜ **Next** | — |
|
||||
| 3 | **C** — spawn atlas (§6) | 🟡 **Data pipeline done** | website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112) (parsers + CLI + tables); API/client PR next |
|
||||
| 4 | **B/2** — `points.board` (§7) | ⬜ Not started | — |
|
||||
| 5 | **B/3** — `vendor.listing` (§8) | ⬜ Not started | — |
|
||||
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3 (§4) | ⬜ Not started | — |
|
||||
@@ -329,6 +329,15 @@ frame during verification.
|
||||
|
||||
**No plugin, no sidecar, no `Bridge.cfg` knob, no new kinds.** Not part of the v3 wire change.
|
||||
|
||||
> **Status:** data pipeline landed on `edge` — website [#112](https://gitea.whitlocktech.com/RunicGateway/website/pulls/112)
|
||||
> (parsers, build/import CLI, tables, artifact). API + client pages are the second website PR.
|
||||
> Part C ships as **two** website PRs, not one: the parsing half is where the correctness risk
|
||||
> lives, and burying it under routes and React would have meant reviewing it in a 10k-line diff.
|
||||
> Full operator documentation: [`docs/website/SPAWN_ATLAS.md`](../website/SPAWN_ATLAS.md).
|
||||
>
|
||||
> §6.1 below records where the shipped implementation differs from this design. The differences are
|
||||
> all things the real ServUO data forced, not changes of mind.
|
||||
|
||||
**Decision: committed generated artifact + idempotent DB import**, split in two because the build
|
||||
needs the ServUO tree (which the website container does not have) and the import does not. Not
|
||||
runtime import (10.5 MB of XML per boot), not a browser-served blob.
|
||||
@@ -383,6 +392,53 @@ with the tree → commit the regenerated `db/data/spawnAtlas.*.json` → deploy
|
||||
`GET /admin/shard/atlas/status` reports when the DB is behind the artifact. Full detail in
|
||||
`docs/website/SPAWN_ATLAS.md`.
|
||||
|
||||
### 6.1 What the build against real data changed
|
||||
|
||||
Six corrections to the design above, from running it against stock ServUO 57.4. Kept as a diff
|
||||
rather than edited in place, because each one is a trap the next person would otherwise re-enter.
|
||||
|
||||
**1. Six facets, not thirteen.** The design said `spawnAtlas.<facet>.json ×13`, assuming one facet
|
||||
per spawn file. There are 13 files but only **6** facets — `Eodon.xml`, `GravewaterLake.xml`,
|
||||
`TreasuresOfKotl.xml` and the other named-area files carry TerMur/Trammel points. The facet comes
|
||||
from each record's own `<Map>`, never the file name, and the artifact shards 6 ways.
|
||||
|
||||
**2. The XML dependency call: hand-rolled, zero deps.** §6 left `fast-xml-parser` vs a ~120-line
|
||||
tokenizer open. Resolved as the tokenizer — a deliberate *subset* parser covering only what these
|
||||
files use. The server keeps zero XML dependencies at any tier.
|
||||
|
||||
**3. Facet names disagree between sources — a silent failure.** `Data/Locations/*.xml` spells them
|
||||
`Ter Mur` and `Tokuno Islands`; `<Map>` and `<Facet name>` say `TerMur` and `Tokuno`. Unreconciled,
|
||||
the landmark bucket is keyed differently from the points looking it up, so the fallback never fires
|
||||
and **every unregioned spawn in Ter Mur and Tokuno reads "Wilderness"** — a plausible-looking atlas
|
||||
that is quietly wrong for two facets. All facet names now pass through `normalizeFacet()`.
|
||||
|
||||
**4. Spawn type tokens carry XmlSpawner directives.** `<Objects2>` types are not always bare class
|
||||
names: `Fairy,{RND,4,8}`, `alchemist/z/-50`, `Agralem/Name/Agralem`, `greatape,true`. Taken literally
|
||||
they invent creatures that do not exist *and* split real ones in two, since `Fairy` and
|
||||
`Fairy,{RND,4,8}` slug apart. 71 of 845 entries were affected; stripping at the first `/` or `,`
|
||||
leaves **800** real creatures. (The design's "~1,500 creature rows" estimate was high; 800 only
|
||||
reinforces the plain-`INDEX`-not-`FULLTEXT` call.)
|
||||
|
||||
**5. The artifact is 1.41 MB, not "well under 1 MB".** Dropping the unused `<Points>` fields as the
|
||||
design directed still left 4.40 MB. Three further encodings — `facet` dropped per record,
|
||||
default-valued fields omitted rather than written as `0`, and `types` as `[name, max]` tuples
|
||||
(~24,000 entries × 15 bytes of repeated key names) — brought it to 1.41 MB. Getting under 1 MB would
|
||||
mean dropping the spawner `name`, which is the only human handle on a specific spawner and worth
|
||||
keeping. `encodePoint()` and `readPoint()` are exact inverses and are round-tripped in tests.
|
||||
|
||||
**6. `DELETE`, not `TRUNCATE`.** The design said "TRUNCATE + batched INSERT in one transaction",
|
||||
which does not hold: `TRUNCATE` is DDL in MariaDB and implicitly commits, so a mid-import failure
|
||||
would leave the atlas half-loaded. `DELETE` is transactional, and at ~7k rows the cost is
|
||||
irrelevant. Point ids are also assigned explicitly rather than by `AUTO_INCREMENT`, because the
|
||||
join rows need them and `conn.batch()` reports no usable `insertId`.
|
||||
|
||||
**Measured result:** 6,455 points, 800 creatures, 23,927 point/type rows, 387 regions, 558
|
||||
landmarks, 25 champion altars. The placement transform resolves **83.2%** of points (3,689 by
|
||||
region, 1,690 by landmark, 1,086 Wilderness).
|
||||
|
||||
**One thing the design got exactly right:** the point-in-rect transform really is the reason to
|
||||
build this. "Where does a lizardman spawn?" answers *Shrines, Isamu-Jima, Yew* across three facets.
|
||||
|
||||
---
|
||||
|
||||
## 7. Part B/2 — `points.board`
|
||||
@@ -577,7 +633,7 @@ inherently up to one full cycle old, and the UI must say so.
|
||||
|---|---|---|---|---|
|
||||
| 1 | **A** — visibility framework + actor-leak fix | website, docs | none | ✅ Done |
|
||||
| 2 | **B/1** — `world.ruleset` (§5) | all four | new kind | ✅ Done |
|
||||
| 3 | **C** — spawn atlas (§6) | website, docs | none | ⬜ **Next** |
|
||||
| 3 | **C** — spawn atlas (§6) | website, docs | none | 🟡 Pipeline done, API/client next |
|
||||
| 4 | **B/2** — `points.board` (§7) | all four | new kind + `char.profile` field | ⬜ |
|
||||
| 5 | **B/3** — `vendor.listing` (§8) | all four | new kinds | ⬜ |
|
||||
| 6 | **Cutover** — `PROTOCOL_VERSION` 2→3, `edge` → `main` | all four | the bump | ⬜ |
|
||||
|
||||
Reference in New Issue
Block a user