Compare commits
2 Commits
10ae129b94
...
docs/spawn
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b7da860b5 | |||
| ff1c2064a5 |
82
link/v3.md
82
link/v3.md
@@ -335,22 +335,25 @@ frame during verification.
|
||||
> 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.
|
||||
> **§6 below is the original design and is partly superseded.** §6.1 records two decisions that were
|
||||
> rejected in review and replaced (the committed artifact, and the fixed facet list); §6.2 records
|
||||
> the corrections the real ServUO data forced. Read both before trusting §6.
|
||||
|
||||
**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.
|
||||
**Decision (revised at implementation time): the shard's ServUO tree is the single source of truth,
|
||||
re-derived on every server boot.** The original plan here was a committed generated artifact plus an
|
||||
idempotent import. That was rejected in review for two reasons, recorded in §6.1: a snapshot in the
|
||||
repo goes stale as a shard's maps change, and the design leaned on a fixed facet list that no shard
|
||||
is obliged to keep. Still not a browser-served blob; still parsed server-side only.
|
||||
|
||||
New in `website/server/`:
|
||||
|
||||
- `src/utils/spawnAtlasParse.js` — **pure functions, no fs**, so they are unit-testable in CI without
|
||||
a ServUO tree: `parseObjects2()`, `parsePoints()`, `parseRegions()`, `parseLocations()`,
|
||||
`resolveRegion()`.
|
||||
- `scripts/buildSpawnAtlas.js` (`--servuo <path> --out db/data/`) and `scripts/importSpawnAtlas.js`
|
||||
(TRUNCATE + batched INSERT in one transaction); `package.json` scripts `atlas:build`, `atlas:import`.
|
||||
- `db/data/spawnAtlas.<facet>.json` ×13 + `spawnAtlas.index.json` (creatures, champions, regions,
|
||||
landmarks, meta with per-source-file hashes).
|
||||
- ~~`scripts/buildSpawnAtlas.js` and a committed `db/data/spawnAtlas.*.json` artifact~~ — dropped,
|
||||
see §6.1 R1. Replaced by `src/utils/spawnAtlasSource.js` (the only thing that reads a ServUO tree,
|
||||
shared by the boot path and the CLI) and a `scripts/importSpawnAtlas.js` that is a thin CLI over
|
||||
the model. `package.json` gains `atlas:import` only.
|
||||
- `src/model/shardAtlas/{shardAtlas.db.js,shardAtlas.model.js}` following the `shardState` split.
|
||||
- `src/router/v1/public/atlas.{router,controller}.js`; `test/spawnAtlas.parse.test.js`.
|
||||
|
||||
@@ -381,18 +384,48 @@ stays CLI-only.**
|
||||
|
||||
Client: `routes/public/Atlas.jsx` (`/site/atlas`) and `AtlasCreature.jsx` (`/site/atlas/:slug`).
|
||||
|
||||
**Payload risk** — a monolithic artifact would be 2–3 MB of committed JSON. Shard per facet and drop
|
||||
every `<Points>` field the site cannot use (`UniqueId`, all trigger/refractory/proximity/sequential
|
||||
fields, sound ids), keeping Name/Map/X/Y/W/H/Range/MaxCount/MinDelay/MaxDelay/TOD*/types — well under
|
||||
1 MB. The artifact never reaches the browser; the browser sees only paginated API responses.
|
||||
**Payload risk** — *superseded by §6.1 R1; nothing is committed.* The field selection it describes
|
||||
still applies at parse time: every `<Points>` field the site cannot use (`UniqueId`, all
|
||||
trigger/refractory/proximity/sequential fields, sound ids) is dropped, keeping
|
||||
Name/Map/X/Y/W/H/Range/MaxCount/MinDelay/MaxDelay/TOD*/types. Parsed data never reaches the browser;
|
||||
the browser sees only paginated API responses.
|
||||
|
||||
**Operator re-run story** — spawns changed → `npm run atlas:build -- --servuo <path>` on a machine
|
||||
with the tree → commit the regenerated `db/data/spawnAtlas.*.json` → deploy → `npm run atlas:import`
|
||||
(or `POST /admin/shard/atlas/import`). `shard_atlas_meta.source` holds per-file hashes, so
|
||||
`GET /admin/shard/atlas/status` reports when the DB is behind the artifact. Full detail in
|
||||
`docs/website/SPAWN_ATLAS.md`.
|
||||
**Operator re-run story** — *revised by §6.1 R1.* Spawns changed → restart, or
|
||||
`npm run atlas:import` / `POST /admin/shard/atlas/import` to apply without one. `shard_atlas_meta`
|
||||
holds a sha256 per source file, so the server can tell on boot whether anything changed, and
|
||||
`GET /admin/shard/atlas/status` reports drift. If the change would remove a facet it is staged for
|
||||
approval rather than applied (§6.1 R3). Full detail in `docs/website/SPAWN_ATLAS.md`.
|
||||
|
||||
### 6.1 What the build against real data changed
|
||||
### 6.1 What implementation changed
|
||||
|
||||
Two design decisions in §6 were rejected in review and replaced; the rest are corrections the real
|
||||
ServUO data forced. Kept as a diff rather than edited in place, because each is a trap the next
|
||||
person would otherwise re-enter.
|
||||
|
||||
**R1. The committed artifact is gone — the tree is re-parsed on every boot.** §6 proposed building a
|
||||
generated artifact, committing it, and importing it. Two problems. A shard's maps change over its
|
||||
life, so a snapshot in the repo silently drifts from the world players actually see; and the build/
|
||||
import split existed only to work around the website container not having a tree, which is a
|
||||
deployment question (mount it) rather than a reason to freeze data. The server now hashes the source
|
||||
files on boot and re-derives the atlas when they differ. `scripts/buildSpawnAtlas.js`, the 1.41 MB
|
||||
artifact, and the whole encode/decode seam it needed are deleted.
|
||||
|
||||
**R2. Nothing may name a facet.** The first implementation carried a lookup table of the six stock
|
||||
UO facets to reconcile the spelling drift between sources. A shard may add facets, replace them
|
||||
outright, or rename them when its maps are updated, and a built-in list mishandles all three
|
||||
silently. Reconciliation is now by *matching* against the facet set discovered from the shard's own
|
||||
spawn and region data — exact key, then prefix in either direction — with an unmatched name keeping
|
||||
its own rather than being forced into a wrong bucket.
|
||||
|
||||
**R3. Two contracts on the boot path.** It never blocks startup: no path, an unreadable mount, a
|
||||
malformed file or a database error is caught and logged, and the site comes up serving whatever
|
||||
atlas it had. And a refresh that would REMOVE a facet is never applied automatically — facet loss
|
||||
is indistinguishable at boot from a half-copied or mid-update tree, so it is staged in
|
||||
`shard_atlas_pending` for an admin to approve or reject. Only the decision is stored (source hashes
|
||||
+ the facet diff, a few KB); approving re-parses, so what lands matches the tree at approval time.
|
||||
A rejection is remembered against those hashes so it does not re-prompt every restart.
|
||||
|
||||
### 6.2 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.
|
||||
@@ -419,12 +452,11 @@ they invent creatures that do not exist *and* split real ones in two, since `Fai
|
||||
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.
|
||||
**5. The artifact would have been 1.41 MB, not "well under 1 MB" — and is now moot.** Dropping the
|
||||
unused `<Points>` fields as the design directed still left 4.40 MB; three further encodings brought
|
||||
it to 1.41 MB, and getting under 1 MB would have meant dropping the spawner `name`. The size budget
|
||||
in §6 was simply optimistic for 6,455 points. Superseded by §6.1 R1: there is no artifact, so there
|
||||
is no payload to budget and no encode/decode seam to keep in sync.
|
||||
|
||||
**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
|
||||
|
||||
@@ -391,9 +391,15 @@ discarded. See §6.5.
|
||||
### shard_spawn_* / shard_regions / shard_landmarks / shard_champion_spawns / shard_atlas_meta — the spawn atlas (Protocol 3.0)
|
||||
|
||||
Static shard **content**, not live shard state. Nothing here comes from the sidecar: the atlas is
|
||||
built from a ServUO tree by a CLI script, committed as JSON under `server/db/data/`, and loaded with
|
||||
`npm run atlas:import`. These tables stay populated whether the shard is up or not. Full operator
|
||||
detail in [`SPAWN_ATLAS.md`](SPAWN_ATLAS.md); the design is `docs/link/v3.md` §6.
|
||||
derived from the shard's own ServUO tree, re-read on **every server boot** and hash-gated so an
|
||||
unchanged tree costs one read pass and no write. Nothing is precomputed and committed — a shard's
|
||||
maps change over its life, and a snapshot in the repo would silently drift from the world players
|
||||
actually see. These tables stay populated whether the shard is up or not. Full operator detail in
|
||||
[`SPAWN_ATLAS.md`](SPAWN_ATLAS.md); the design is `docs/link/v3.md` §6.
|
||||
|
||||
**No facet name appears anywhere in the code.** A shard may add facets, replace them, or rename them
|
||||
when its maps are updated; the facet set is discovered from the tree, and the loose spellings in
|
||||
`Data/Locations` are matched against it rather than looked up in a table.
|
||||
|
||||
| Table | Key columns |
|
||||
|---|---|
|
||||
@@ -403,13 +409,27 @@ detail in [`SPAWN_ATLAS.md`](SPAWN_ATLAS.md); the design is `docs/link/v3.md` §
|
||||
| `shard_regions` | `facet`, `name`, `type`, `priority`, `parent`, `rects` JSON |
|
||||
| `shard_landmarks` | `facet`, `name`, `grp`, `x`, `y`, `z` |
|
||||
| `shard_champion_spawns` | `slug` PK, `name`, `grp`, `type`, `random_type`, `facet`, `x`, `y`, `z`, `radius`, `label` |
|
||||
| `shard_atlas_meta` | Singleton (`id = 1`), `payload` JSON, `imported_at` |
|
||||
| `shard_atlas_meta` | Singleton (`id = 1`), `payload` JSON (counts + a sha256 per source file), `imported_at` |
|
||||
| `shard_atlas_pending` | Singleton (`id = 1`), `status` (`pending`/`rejected`), `payload` JSON, `detected_at` |
|
||||
|
||||
All of them are **import-owned**: `atlas:import` empties and reloads every one inside a single
|
||||
transaction, so a failed import leaves the previous atlas intact rather than a half-loaded world.
|
||||
The first seven are **import-owned**: a refresh empties and reloads every one inside a single
|
||||
transaction, so a failed reload leaves the previous atlas intact rather than a half-loaded world.
|
||||
Nothing else writes to them, and nothing holds a foreign key to them — no FKs at all, consistent with
|
||||
every other `shard_*` table.
|
||||
|
||||
**`shard_atlas_pending` is the security-relevant one.** A refresh that would REMOVE a facet is never
|
||||
applied automatically: facet loss is indistinguishable at boot from a half-copied or mid-update tree,
|
||||
so it is staged here for an admin to approve or reject, and **startup is never blocked by it**. Only
|
||||
the decision is stored — source hashes plus the facet diff, a few KB — and approving re-parses the
|
||||
tree, so a multi-megabyte blob never lands in the database and what gets applied matches the tree at
|
||||
approval time. A rejection is remembered against those exact hashes so a declined refresh does not
|
||||
re-prompt on every restart. Everything else (new facets, renamed regions, changed spawns) applies
|
||||
immediately, since none of it can destroy data an operator would miss.
|
||||
|
||||
The boot refresh is **best-effort by contract**: no configured path, an unreadable mount, a malformed
|
||||
file or a database error is caught and logged, and the site comes up serving whatever atlas it had.
|
||||
The tree path comes from the `spawn_atlas_servuo_path` setting, falling back to `SERVUO_PATH`.
|
||||
|
||||
Four column choices worth stating, because each one is a trap:
|
||||
|
||||
- **`spawn_range`, not `range`**, and **`grp`, not `group`** — both are reserved words.
|
||||
|
||||
@@ -8,6 +8,19 @@ creatures spawn, where, how many, and which champion altars are configured. It
|
||||
answers "where do I find a lizardman?" with **"Shrines, Yew, Isamu-Jima"** rather
|
||||
than with a list of raw coordinates.
|
||||
|
||||
## Two things that shape the whole design
|
||||
|
||||
**The shard's ServUO tree is the single source of truth.** Nothing is
|
||||
precomputed and committed to the repository. A shard's maps change over its
|
||||
lifetime — facets get added, replaced, or renamed — and a snapshot in the repo
|
||||
would silently drift from the world players actually see. The atlas is therefore
|
||||
re-derived from the tree **on every server boot**.
|
||||
|
||||
**Facets are not a fixed list.** Nothing in the codebase names Felucca, Trammel,
|
||||
or any other stock facet. The facet set is whatever the shard's own files
|
||||
declare, discovered at parse time. A shard running entirely custom maps gets
|
||||
exactly the same treatment as a stock one, with no code change.
|
||||
|
||||
## What it is not
|
||||
|
||||
The atlas is **static shard content, not live shard state.**
|
||||
@@ -24,44 +37,83 @@ The atlas is **static shard content, not live shard state.**
|
||||
Routes live at `/api/v1/public/atlas`, deliberately **not** under `/shard`,
|
||||
because `/shard/*` means sidecar-dependent.
|
||||
|
||||
## The two commands
|
||||
## Configuring the tree
|
||||
|
||||
Building needs a ServUO tree. Importing does not. That split is the whole design:
|
||||
the website container ships the committed artifact but has no ServUO tree, so it
|
||||
can import but never build.
|
||||
The website needs to be able to *read* the ServUO tree — same host, a bind mount,
|
||||
or a shared volume. Two ways to point at it, the setting winning over the
|
||||
environment:
|
||||
|
||||
```bash
|
||||
# On a machine that has the ServUO tree (writes server/db/data/spawnAtlas.*.json)
|
||||
cd website/server
|
||||
npm run atlas:build -- --servuo /path/to/ServUO
|
||||
| Source | Notes |
|
||||
|---|---|
|
||||
| `spawn_atlas_servuo_path` setting | Admin-editable; changes take effect on the next refresh without a redeploy |
|
||||
| `SERVUO_PATH` env var | The deploy-time default, since the path usually describes a mount the deployment sets up |
|
||||
|
||||
# Anywhere, including the deployed container
|
||||
npm run atlas:import
|
||||
With neither set the atlas is simply skipped — the site runs normally without
|
||||
one.
|
||||
|
||||
## The boot path
|
||||
|
||||
On every start the server hashes the source files and compares them against what
|
||||
is loaded. Unchanged (the normal case on a restart) costs one read pass, ~120 ms,
|
||||
and no database write. A real change costs a ~400 ms parse and a reload.
|
||||
|
||||
Two contracts govern it:
|
||||
|
||||
**1. It never blocks startup.** No configured path, an unreadable mount, a
|
||||
malformed file, a database error — every one is caught and logged, and the site
|
||||
comes up serving whatever atlas it already had.
|
||||
|
||||
**2. A facet disappearing is never applied automatically.** Losing a facet looks
|
||||
exactly like a half-copied or mid-update tree, and boot cannot tell that apart
|
||||
from a real map change. That refresh is *staged* for a human instead. Everything
|
||||
else — new facets, renamed regions, changed spawns — applies immediately, since
|
||||
none of it can destroy something an operator would miss.
|
||||
|
||||
```
|
||||
boot
|
||||
└─ path configured? no ──▶ skip
|
||||
└─ tree readable? no ──▶ warn, carry on
|
||||
└─ hashes changed? no ──▶ done (nothing parsed)
|
||||
└─ parse
|
||||
└─ a facet would be removed?
|
||||
no ──▶ import
|
||||
yes ──▶ stage for admin review; atlas unchanged
|
||||
```
|
||||
|
||||
`atlas:build` flags:
|
||||
### Approving or rejecting a staged refresh
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `--servuo` | *(required)* | ServUO server root — the directory holding `Spawns/`, `Data/`, `Config/` |
|
||||
| `--out` | `server/db/data` | Where to write the artifact |
|
||||
| `--landmark-radius` | `200` | Max tile distance for the landmark fallback |
|
||||
Only the *decision* is stored, never the parsed world — a few KB of source hashes
|
||||
plus the facet diff. Approving **re-parses** the tree, so what lands matches the
|
||||
tree at approval time rather than at boot, and a multi-megabyte blob never sits
|
||||
in the database.
|
||||
|
||||
`atlas:import` takes `--dir` (default `server/db/data`).
|
||||
A rejection is remembered against those exact source hashes, so a declined
|
||||
refresh does not re-prompt on every restart. Change the tree and the hashes
|
||||
differ, which asks again.
|
||||
|
||||
## Operator re-run story
|
||||
From the admin panel (second PR), or from the CLI:
|
||||
|
||||
Spawns changed → rebuild → commit → deploy → import.
|
||||
```bash
|
||||
cd website/server
|
||||
npm run atlas:import -- --status # what is loaded, and what is pending
|
||||
npm run atlas:import -- --approve # apply the staged refresh
|
||||
npm run atlas:import -- --reject # keep the current atlas, dismiss it
|
||||
```
|
||||
|
||||
1. `npm run atlas:build -- --servuo <path>` on a machine with the tree.
|
||||
2. Commit the regenerated `server/db/data/spawnAtlas.*.json`.
|
||||
3. Deploy.
|
||||
4. `npm run atlas:import`, or `POST /api/v1/admin/shard/atlas/import`.
|
||||
## The CLI
|
||||
|
||||
`shard_atlas_meta` stores a sha256 per source file, so
|
||||
`GET /api/v1/admin/shard/atlas/status` reports when the database is behind the
|
||||
committed artifact. **Build stays CLI-only** — there is no admin button that
|
||||
reads a ServUO tree.
|
||||
The server refreshes itself on boot, so this is for applying a map change
|
||||
*without* a restart, and for the approve/reject flow above.
|
||||
|
||||
```bash
|
||||
npm run atlas:import # import if the tree differs
|
||||
npm run atlas:import -- --servuo <path> # override the path for this run
|
||||
npm run atlas:import -- --force # reimport even if unchanged
|
||||
```
|
||||
|
||||
`--servuo` is a per-run override and deliberately does **not** persist — changing
|
||||
where the atlas permanently reads from is an admin action, not a side effect of a
|
||||
one-off import.
|
||||
|
||||
## Sources
|
||||
|
||||
@@ -72,11 +124,10 @@ reads a ServUO tree.
|
||||
| `Data/Locations/*.xml` | 6 files | Landmarks (dungeon levels, town markers) |
|
||||
| `Config/ChampionSpawns.xml` | 4.8 KB | Configured champion altars |
|
||||
|
||||
**There are 13 spawn files but only 6 facets.** `Eodon.xml`,
|
||||
**A stock tree has 13 spawn files but only 6 facets.** `Eodon.xml`,
|
||||
`GravewaterLake.xml`, `TreasuresOfKotl.xml` and the other named-area files hold
|
||||
TerMur/Trammel points. The facet always comes from each record's own `<Map>`,
|
||||
never from the file name, and the artifact shards by facet — Felucca, Trammel,
|
||||
Ilshenar, Malas, Tokuno, TerMur.
|
||||
never from the file name.
|
||||
|
||||
## How a coordinate becomes a place name
|
||||
|
||||
@@ -85,16 +136,17 @@ This is the transform the atlas exists for, in `resolveRegion()`:
|
||||
1. The highest-`priority` named region whose rectangle contains the point. Ties
|
||||
break toward the **smallest** rect, so a specific room wins over the
|
||||
dungeon-wide rect enclosing it.
|
||||
2. Otherwise the nearest landmark within `--landmark-radius` tiles, labelled by
|
||||
its **group** ("Covetous"), not its individual marker ("Level 1").
|
||||
2. Otherwise the nearest landmark within the landmark radius (200 tiles by
|
||||
default), labelled by its **group** ("Covetous"), not its individual marker
|
||||
("Level 1").
|
||||
3. Otherwise `"Wilderness"`.
|
||||
|
||||
The radius cap in step 2 is what keeps step 3 reachable. Without it the nearest
|
||||
landmark is always *some* landmark however far away, and open countryside gets
|
||||
labelled with a dungeon on the far side of the map.
|
||||
|
||||
Against stock ServUO this resolves **83.2%** of points (5,369 of 6,455): 3,689 by
|
||||
region, 1,690 by landmark, 1,086 Wilderness.
|
||||
Against stock ServUO this resolves **83.2%** of points (5,369 of 6,455): 3,681 by
|
||||
region, 1,688 by landmark, 1,086 Wilderness.
|
||||
|
||||
## Three quirks in the source data
|
||||
|
||||
@@ -102,11 +154,17 @@ Each of these is silent if unhandled — the atlas still builds, it is just wron
|
||||
|
||||
**Facet names disagree between sources.** `Data/Locations/*.xml` spells them
|
||||
`Ter Mur` and `Tokuno Islands`, while `<Map>` and `<Facet name>` say `TerMur` and
|
||||
`Tokuno`. Unreconciled, the landmark bucket for those two facets 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". All facet names are
|
||||
canonicalised through `normalizeFacet()`; unknown facets pass through unchanged
|
||||
so a custom shard facet still gets an atlas.
|
||||
`Tokuno`. Unreconciled, the landmark bucket is keyed differently from the points
|
||||
looking it up, so the fallback never fires and every unregioned spawn on those
|
||||
facets reads "Wilderness".
|
||||
|
||||
This is reconciled **by matching, not by a lookup table** — there is no list of
|
||||
facet names anywhere. `facetKey()` collapses spelling differences (lowercase,
|
||||
alphanumerics only), and `resolveFacetName()` matches a loose spelling against
|
||||
the canonical set discovered from the shard's own spawn and region data, by exact
|
||||
key then by prefix in either direction. A name matching nothing keeps its own
|
||||
name: forcing a wrong match would file a real custom facet's landmarks under the
|
||||
wrong facet, which is worse than leaving it alone.
|
||||
|
||||
**Spawn type tokens carry XmlSpawner directives.** The `<Objects2>` type is not
|
||||
always a bare class name:
|
||||
@@ -125,38 +183,35 @@ real creatures.
|
||||
file and `lizardman` in another. Slugging collapses them correctly, but the
|
||||
display name is chosen deterministically — most common spelling wins, ties break
|
||||
to the more capitalised form, then alphabetically — because otherwise it would
|
||||
depend on file read order and produce a spurious artifact diff on every unrelated
|
||||
rebuild.
|
||||
depend on file read order and change on an unrelated restart.
|
||||
|
||||
## Artifact format
|
||||
## Tables
|
||||
|
||||
`server/db/data/`, all committed:
|
||||
All are **import-owned**: a refresh empties and reloads them in one transaction,
|
||||
so a failed reload leaves the previous atlas intact rather than a half-loaded
|
||||
world. Nothing else writes to them and nothing holds a foreign key to them — no
|
||||
FKs at all, consistent with every other `shard_*` table. Full column listings in
|
||||
[`BACKEND_DESIGN.md`](BACKEND_DESIGN.md).
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `spawnAtlas.meta.json` | Indented. Build time, counts, sha256 per source file |
|
||||
| `spawnAtlas.index.json` | Compact. Facets, creatures, regions, landmarks, champions |
|
||||
| `spawnAtlas.<facet>.json` × 6 | Compact. That facet's spawn points |
|
||||
| Table | Rows (stock) | Notes |
|
||||
|---|---|---|
|
||||
| `shard_spawn_creatures` | 800 | `slug` PK; `total` = sum of each type's own max; nullable `art` |
|
||||
| `shard_spawn_points` | 6,455 | `spawn_range`, since `range` is reserved in MariaDB |
|
||||
| `shard_spawn_point_types` | 23,927 | The many-to-many; one spawner commonly carries six types |
|
||||
| `shard_regions` | 387 | Flattened out of the nesting; `rects` JSON |
|
||||
| `shard_landmarks` | 558 | `grp`, since `group` is reserved in SQL |
|
||||
| `shard_champion_spawns` | 25 | Configured altars, not the live feed |
|
||||
| `shard_atlas_meta` | 1 | Singleton; source hashes, for the change check |
|
||||
| `shard_atlas_pending` | 0–1 | Singleton; a staged refresh awaiting admin review |
|
||||
|
||||
**1.41 MB total**, down from 4.40 MB. The design budgeted "well under 1 MB",
|
||||
which turned out optimistic for 6,455 points; three encodings closed most of the
|
||||
gap:
|
||||
`shard_spawn_creatures.name` carries a plain `INDEX`, deliberately **not
|
||||
`FULLTEXT`**: ~800 rows makes a `LIKE` scan free, and FULLTEXT's minimum token
|
||||
length would break searches for names like "orc".
|
||||
|
||||
- `facet` is dropped per record — the shard file names it once at the top.
|
||||
- Fields at their default are omitted rather than written as `0`. Most spawners
|
||||
are a single point with no time-of-day gating, so `width`, `height`, `range`
|
||||
and the three `tod*` fields are zero on the large majority of records.
|
||||
- `types` are `[name, max]` tuples. There are ~24,000 type entries and
|
||||
`{"type":"Orc","max":1}` spends 15 bytes apiece restating two key names that
|
||||
never vary.
|
||||
|
||||
`label` is not stored at all — it is exactly `region || landmark || "Wilderness"`
|
||||
and the importer recomputes it.
|
||||
|
||||
`buildSpawnAtlas.encodePoint()` and `importSpawnAtlas.readPoint()` are exact
|
||||
inverses, round-tripped in `test/spawnAtlas.build.test.js`. **Change one, change
|
||||
both.** The artifact never reaches the browser; the browser sees only paginated
|
||||
API responses.
|
||||
The reload uses `DELETE`, not `TRUNCATE` — `TRUNCATE` is DDL in MariaDB and would
|
||||
implicitly commit, defeating the all-or-nothing guarantee. Point ids are assigned
|
||||
explicitly rather than left to `AUTO_INCREMENT`, because the join rows need them
|
||||
and `conn.batch()` reports no usable `insertId`.
|
||||
|
||||
## Artwork — operator-supplied, never shipped
|
||||
|
||||
@@ -175,41 +230,22 @@ An operator who wants art:
|
||||
2. Drops the images under `server/uploads/atlas/`.
|
||||
3. Copies `server/db/data/spawnAtlas.art.example.json` to `spawnAtlas.art.json`
|
||||
and maps creature slugs to file names.
|
||||
4. Re-runs `npm run atlas:import`.
|
||||
4. Restarts, or runs `npm run atlas:import -- --force`.
|
||||
|
||||
Both `spawnAtlas.art.json` and `server/uploads/` are gitignored, so neither the
|
||||
map nor the images can be committed by accident.
|
||||
|
||||
## Tables
|
||||
## Code layout
|
||||
|
||||
All six are **import-owned**: `atlas:import` empties and reloads them in one
|
||||
transaction. Nothing else writes to them and nothing holds a foreign key to them
|
||||
— no FKs at all, consistent with every other `shard_*` table. Full column
|
||||
listings in [`BACKEND_DESIGN.md`](BACKEND_DESIGN.md).
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/utils/spawnAtlasParse.js` | **Pure and fs-free** parsers, so CI covers them with no ServUO tree. Zero dependencies. |
|
||||
| `src/utils/spawnAtlasSource.js` | The only thing that reads a ServUO tree; shared by the boot path and the CLI |
|
||||
| `src/model/shardAtlas/shardAtlas.db.js` | The one-transaction replace |
|
||||
| `src/model/shardAtlas/shardAtlas.model.js` | The refresh decision, staging, approve/reject |
|
||||
| `scripts/importSpawnAtlas.js` | Thin CLI over the model |
|
||||
|
||||
| Table | Rows (stock) | Notes |
|
||||
|---|---|---|
|
||||
| `shard_spawn_creatures` | 800 | `slug` PK; `total` = sum of each type's own max; nullable `art` |
|
||||
| `shard_spawn_points` | 6,455 | `spawn_range`, since `range` is reserved in MariaDB |
|
||||
| `shard_spawn_point_types` | 23,927 | The many-to-many; one spawner commonly carries six types |
|
||||
| `shard_regions` | 387 | Flattened out of the nesting; `rects` JSON |
|
||||
| `shard_landmarks` | 558 | `grp`, since `group` is reserved in SQL |
|
||||
| `shard_champion_spawns` | 25 | Configured altars, not the live feed |
|
||||
| `shard_atlas_meta` | 1 | Singleton (`id = 1`); source hashes for the drift check |
|
||||
|
||||
`shard_spawn_creatures.name` carries a plain `INDEX`, deliberately **not
|
||||
`FULLTEXT`**: ~800 rows makes a `LIKE` scan free, and FULLTEXT's minimum token
|
||||
length would break searches for names like "orc".
|
||||
|
||||
The importer uses `DELETE`, not `TRUNCATE` — `TRUNCATE` is DDL in MariaDB and
|
||||
would implicitly commit, defeating the all-or-nothing reload. Point ids are
|
||||
assigned explicitly rather than left to `AUTO_INCREMENT`, because the join rows
|
||||
need to know them and `conn.batch()` reports no usable `insertId`.
|
||||
|
||||
## Parsing notes
|
||||
|
||||
`server/src/utils/spawnAtlasParse.js` is **pure and fs-free**, so CI covers it
|
||||
with no ServUO tree. It adds **zero dependencies**.
|
||||
Parsing notes:
|
||||
|
||||
- `Regions.xml`, `Locations/*.xml` and `ChampionSpawns.xml` genuinely nest, and
|
||||
get a small hand-rolled **subset** tokenizer — elements, attributes,
|
||||
|
||||
Reference in New Issue
Block a user