docs(link): cliloc overlays for shard-added and shard-edited items
Shards edit items and add new ones, carrying cliloc ids no stock client table has. The cliloc table now reads a SET of sources rather than one file — a base plus every operator-maintained overlay under `custom/`, hash-gated together and re-read on every boot, exactly as §6 reads the ServUO tree. Matching docs for website #115. website/CLILOCS.md gains a "Shard-added and shard-edited items" section: the directory layout, merge precedence, the per-source breakdown an operator uses to confirm an overlay took effect, and why `custom/` is a convention we chose rather than one discovered from the shard — ServUO has no server-side notion of a custom cliloc, they live in the patched client a shard distributes and nothing in the tree declares them. Scale recorded: the live shard's script tree references 16,434 cliloc ids and only 37 are absent from stock, which is why this is an overlay and not a second table. "Why there is no staged-approval flow" is replaced by "Two ways a refresh is refused", because the set brings back the hazard a single file did not have. A corrupt source fails the parse loudly; a source that has VANISHED parses perfectly and imports a table quietly missing everything it contributed. That is the same ambiguity §6 stages a facet removal for, so it is staged here too (`needsReview`, `{approve:true}` to accept) — as a flag rather than §6's approve/reject pair, because the atlas stores a pending decision SO THAT approving re-parses, and here nothing is stored. Two more traps recorded in §8.6 (now five), both found by running a shard-style overlay rather than another stock-table fixture: - Tidying punctuation unconditionally corrupts real names — a custom "Runic Gateway Sigil (v2)" rendered as "(v2". Stripping leftover brackets is right after a placeholder is removed and wrong otherwise, the same condition the `%` rule already had. - Source labels must be forward-slashed and root-relative or the same directory fingerprints differently on Windows and Linux and every boot looks like a change — the identical bug §6 records. BACKEND_DESIGN.md: the source set, the two refusals, and the updated admin route notes (`approve`, `missingSources`, overlays found beside a file path). Documentation only. Every claim was written after the behaviour was observed: the counts come from the live shard's script tree, and the refusal/approve flow and per-source breakdown are copied from real runs in website #115. PROJECT_TREE.md files are deliberately untouched — CI-generated by the sync-project-tree workflow. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -80,21 +80,74 @@ dotnet run -- "<UOFiddler>/Ultima.dll" "<UO client>/Cliloc.enu" /srv/uo-data/cli
|
||||
A UOFiddler GUI export works equally well — anything producing one of the two
|
||||
shapes above is fine.
|
||||
|
||||
## Shard-added and shard-edited items
|
||||
|
||||
**Shards edit items and add new ones**, and those carry cliloc ids no stock
|
||||
client table has. The table is therefore built from a **set** of sources, all
|
||||
re-read on every boot and hash-gated together — the same shape as the spawn
|
||||
atlas, which reads `Regions.xml` + `Locations/*.xml` + `Spawns/*.xml` +
|
||||
`ChampionSpawns.xml` and merges them:
|
||||
|
||||
```
|
||||
<cliloc path>/
|
||||
clilocs.plain ← base: the converted client table
|
||||
custom/
|
||||
01-uomysticmoon.tsv ← overlays: shard additions and overrides
|
||||
02-events.tsv
|
||||
```
|
||||
|
||||
Overlays use the same delimited-text format, are read in **sorted order**, and
|
||||
**later sources win** — so an overlay both *adds* ids the client never had and
|
||||
*overrides* stock ones the shard has re-purposed. Any `.tsv`, `.csv`, `.txt`,
|
||||
`.enu` or `.plain` file in `custom/` is picked up; anything else (a `README.md`,
|
||||
say) is ignored.
|
||||
|
||||
Adding, editing or removing any overlay counts as drift, so a new custom item
|
||||
needs only a file edit and a restart — or the admin panel's Import button.
|
||||
**Adding one item never means re-exporting a 5 MB client file.**
|
||||
|
||||
The import result reports what each source contributed, which is how you confirm
|
||||
an overlay took effect — `overrode: 0` on a file meant to re-label stock items
|
||||
says it did not:
|
||||
|
||||
```json
|
||||
"sources": [
|
||||
{ "label": "clilocs.plain", "kind": "base", "entries": 123490, "added": 123490, "overrode": 0 },
|
||||
{ "label": "custom/uomysticmoon.tsv", "kind": "custom", "entries": 2, "added": 1, "overrode": 1 }
|
||||
]
|
||||
```
|
||||
|
||||
**Why a convention rather than discovery.** Everywhere else this pipeline follows
|
||||
the shard's own files, but **ServUO has no server-side notion of a custom
|
||||
cliloc** — they live in the patched client a shard distributes to its players,
|
||||
and nothing in the tree declares them. There is nothing to discover, so `custom/`
|
||||
is the one thing here that is our convention rather than the shard's. (An
|
||||
operator who *does* patch their client cliloc needs no overlay at all: convert
|
||||
the patched file and their edits are simply in the base.)
|
||||
|
||||
Measured on the live shard for scale: its script tree references **16,434** cliloc
|
||||
ids and only **37** are absent from the stock client table — tens of entries
|
||||
against a 67k base, which is what makes an overlay the right shape rather than a
|
||||
second full table.
|
||||
|
||||
## Configuring the path
|
||||
|
||||
Two ways to point at the converted file, the setting winning over the
|
||||
environment:
|
||||
Two ways to point at the sources, the setting winning over the environment:
|
||||
|
||||
| Source | Notes |
|
||||
|---|---|
|
||||
| `cliloc_client_path` setting | Admin-editable (Admin → Shard); takes effect on the next refresh without a redeploy |
|
||||
| `UO_CLIENT_PATH` env var | The deploy-time default, since the path usually describes a mount the deployment sets up |
|
||||
|
||||
The value may be **the file itself or a directory to search**, because both are
|
||||
natural answers to "where is it". A directory is searched case-insensitively (the
|
||||
client writes `Cliloc.enu` on Windows; the site usually runs on Linux) for, in
|
||||
order: `clilocs.tsv`, `clilocs.csv`, `cliloc.plain.enu`, `cliloc.enu.plain`,
|
||||
`clilocs.txt`, `cliloc.enu`.
|
||||
The value may be **the base file itself or a directory to search**, because both
|
||||
are natural answers to "where is it". Overlays are read from a `custom/`
|
||||
directory beside the base **either way** — pointing at a file does not forfeit
|
||||
them.
|
||||
|
||||
A directory is searched case-insensitively (the client writes `Cliloc.enu` on
|
||||
Windows; the site usually runs on Linux) for, in order: `clilocs.tsv`,
|
||||
`clilocs.csv`, `clilocs.plain`, `cliloc.plain`, `cliloc.plain.enu`,
|
||||
`cliloc.enu.plain`, `clilocs.txt`, `cliloc.enu`.
|
||||
|
||||
That ordering puts explicitly-converted names first on purpose. Pointing the
|
||||
setting straight at an unconverted client directory finds `cliloc.enu`, which is
|
||||
@@ -120,22 +173,44 @@ Identical in shape to the spawn atlas, and for the same reasons:
|
||||
- **A `PARSER_VERSION` bump also counts as drift**, so a corrected parse reaches
|
||||
an install whose client never patches.
|
||||
|
||||
### Why there is no staged-approval flow
|
||||
### Two ways a refresh is refused
|
||||
|
||||
The atlas stages a refresh that would *remove a facet*, because a half-copied
|
||||
tree and a real map change are indistinguishable from inside the process. A
|
||||
cliloc file is one file with one hash, and its realistic corruption — a partial
|
||||
copy — makes the parser fail on a truncated record instead of yielding a
|
||||
plausible-but-short table. **The ambiguity the atlas has to escalate to a human is
|
||||
one this parser can simply detect**, so it refuses the import and leaves the
|
||||
previous table serving. Verified: a file truncated to half its length reports
|
||||
**A corrupt file** — the realistic failure for any single source — makes the
|
||||
parser fail on a truncated record rather than yield a plausible-but-short table,
|
||||
so it is caught outright. Verified: a file truncated to half its length reports
|
||||
|
||||
```
|
||||
code: TRUNCATED
|
||||
reason: Truncated record header at byte 2486759 (74909 entries read)
|
||||
```
|
||||
|
||||
and the 67,496 rows already loaded are untouched.
|
||||
and the rows already loaded are untouched. A malformed overlay names the file it
|
||||
came from (`custom/broken.tsv: No cliloc entries found…`), because "which of my
|
||||
six overlay files is broken" is otherwise a guessing game.
|
||||
|
||||
**A source that has VANISHED** is the hazard a single file did not have. It
|
||||
parses perfectly and imports a table quietly missing everything that file
|
||||
contributed — and an unmounted volume looks exactly like a deliberate deletion
|
||||
from here. This is the same ambiguity the atlas stages a facet removal for, so it
|
||||
is escalated rather than applied:
|
||||
|
||||
```
|
||||
status: needsReview
|
||||
reason: 1 previously-loaded cliloc source(s) are missing;
|
||||
the existing table is unchanged
|
||||
missingSources: ["custom/uomysticmoon.tsv"]
|
||||
```
|
||||
|
||||
`status()` reports `missingSources` too, so the panel can show it before anyone
|
||||
clicks Import. An admin accepts it by re-running the import with
|
||||
`{ "approve": true }`.
|
||||
|
||||
**Why that is a flag and not the atlas's approve/reject pair.** The atlas stores
|
||||
a pending decision in its own table so that approving *re-parses the tree*, which
|
||||
is what keeps a multi-megabyte blob out of the database and makes the applied
|
||||
result match the tree at approval time. Here nothing is stored, so re-reading at
|
||||
approval time is automatic — the decision is a single boolean on the import an
|
||||
admin was already going to run.
|
||||
|
||||
## What gets stored
|
||||
|
||||
@@ -182,11 +257,15 @@ not the packet. So a name carrying them is reduced to what is actually knowable.
|
||||
| `cold damage ~1_val~%` | `cold damage` |
|
||||
| `[~1_stuff~]` | *(nothing — the whole string was the argument)* |
|
||||
| `50%` | `50%` |
|
||||
| `Runic Gateway Sigil (v2)` | `Runic Gateway Sigil (v2)` |
|
||||
|
||||
The trailing `%` in row two is dropped **because a placeholder was removed** — it
|
||||
is the unit belonging to the number we never had. Row four shows why that
|
||||
condition matters: stripping `%` unconditionally would corrupt a string that
|
||||
legitimately ends in one.
|
||||
**Punctuation is only tidied when a placeholder was actually removed.** The
|
||||
trailing `%` in row two is the unit belonging to the number we never had, and the
|
||||
brackets in row three only ever wrapped the argument — but a string with no
|
||||
placeholder has no such debris, and trimming it anyway corrupts real names. Rows
|
||||
four and five are the ones that caught it: a shard's custom
|
||||
`"Runic Gateway Sigil (v2)"` rendered as `"(v2"` while the bracket trim was
|
||||
unconditional.
|
||||
|
||||
### Consumers
|
||||
|
||||
@@ -205,8 +284,8 @@ All admin-only, alongside the atlas under Admin → Shard:
|
||||
|
||||
| Route | Purpose |
|
||||
|---|---|
|
||||
| `GET /api/v1/admin/shard/clilocs` | Path, resolved file, readability, drift, entry count |
|
||||
| `POST /api/v1/admin/shard/clilocs/import` | Reload after a client patch; `{ "force": true }` reimports an unchanged file |
|
||||
| `GET /api/v1/admin/shard/clilocs` | Sources found, what each contributed at the last import, readability, drift, entry count, `missingSources` |
|
||||
| `POST /api/v1/admin/shard/clilocs/import` | Reload after a client patch or an overlay edit; `{ "force": true }` reimports an unchanged set, `{ "approve": true }` accepts a vanished source |
|
||||
| `PUT /api/v1/admin/shard/clilocs/path` | Set the path; blank disables resolution |
|
||||
|
||||
A refresh **result is not an exception**: a missing file, or the likely mistake of
|
||||
|
||||
Reference in New Issue
Block a user