Completes the uMod mirror beyond the Rust hook table, and adds agent/ — the
same facts in TSV/JSONL at ~46% of the tokens.
New prose:
- OXIDE_API.md: the 19 developer pages under umod.org/documentation/api/ —
plugin structure, hooks, commands, IPlayer, permissions, config, data files,
database, localization, timers, web requests, dependencies, integration,
preprocessor directives, security, style guide, CI, review. This is the
framework our plugin is a guest in, where HOOKS.md is what the game says.
- OPERATING.md: the 6 operator pages — installing Oxide on a server, then
installing, configuring and permissioning plugins.
New machine-readable set (agent/):
- hooks.tsv 477 rows, ~31% of HOOKS.md
- items.tsv 678 rows, ~80% of DEFINITIONS.md's item table
- skins.tsv 104 rows covering 2,590 skins, ~77%
- api.jsonl 150 code examples, ~47% of the two prose docs
Generated in the same pass as the markdown, so the two cannot drift.
HOOKS.md gains the universal-hook split: 34 of the 477 are uMod's own
Covalence hooks, raised identically on every game uMod supports. Verified
against /documentation/games/universal in the same capture - all 34 are in
the Rust set and the Rust page adds none of its own, so the overlap is exact.
The distinction is architectural: a universal hook is the portable part of
the surface.
Two things worth recording from building it:
- The first skins.tsv was one row per skin and came out 11% LARGER than the
markdown it replaces. Grouping it one row per item is what made it a
saving. The token win is real for prose (3.2x on hooks) and small for
tables that were already dense - agent/README.md says so plainly rather
than claiming a flat number.
- Signature extraction by brace depth silently captured body lines (a nested
'}' in OnUserConnected's example ended the block early). It now matches on
the hook's own name; all 477 rows verified to carry a real signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Mirrors uMod's Rust game API into docs/modules/rust/ so the upcoming
module-rust can be designed against it without a round trip upstream.
- HOOKS.md: all 477 hooks in 20 categories, scraped verbatim from
umod.org/documentation/games/rust. Each entry keeps its description,
its return contract, its tags and every C# overload uMod publishes.
Opens with a category table and a full alphabetical index
(name -> category -> return contract).
- DEFINITIONS.md: 678 items (short name, item id, display name) and
2,590 workshop skin ids across 104 items.
- README.md: why the reference exists, how an Oxide hook actually binds
(by name and arity, by reflection - a typo is silently never called),
the four return contracts, and how the bridge invariants restate
against Oxide. Records provenance and the exact refresh procedure:
the hooks JSON endpoint is not Cloudflare-gated even though the HTML
page is, and the definitions page has to come out of a browser.
The mirror is reference only - uMod stays upstream and normative, and
nothing here is a Runic Gateway contract. Rust wipes monthly, so both
files carry their capture date (2026-09-15) and a re-capture note.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Phase 9c is done, and with it the Asset Bridge. §16's row 9c records what the leg
actually was, and §17.17 what it found.
The site is the only repo whose checks read the platform over the API instead of
describing it, so its starting state is a measurement rather than an opinion:
- `checkFacts` red in **ten** places — the protocol in all three independent reads,
the bundle triple, and the three releases the cutover cut. `moduleApi` and the
capability list did not move, which is §17.16's finding restated by a different
mechanism.
- `checkReference` red in **nine nobody planned for** — every `Bridge.cfg` key phases
5, 6 and 7 added, none of them ever mentioned on the public reference page. No phase
row in this plan could have caught that: the rows name the repos a change edits, and
this is a repo that only describes them.
Two corrections only prose could make: §17.15's "five repositories" had become a page
on the site that named `website` among them (protocol 8 touched four, and core's
absence is the interesting half), and the site said the bridge has two ways in when the
asset plane is a third — a paged bulk read on the reply path, which must not ride the
event stream for a structural reason rather than a throughput one.
The defect the phase found is a version pinned in a rig: `seedDemo.mjs` pinned protocol
**4** under a comment explaining a debt since paid. It reads `platform.json` now.
Screenshots retaken against the real shard (D42 holds): 1,095 portraits in 2.1 s,
67,496 names in 1.4 s, 313 item pictures in 0.8 s.
Site: RunicGateway/runicgateway.com#31.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Adds §17.16 — what the five-repo cutover published, and the one step that
needed a hand.
sidecar v2.3.0, overlay v1.3.0, module-uo v1.3.0, installer v0.2.0, bundle
2026.09.15, all at protocol 8. `edge` stays standing everywhere.
## The bundle did not recompose on its own
Two compose runs failed for two unrelated reasons and only their logs separate
them. Run 111 was **Gate 1 working**, refusing `sidecar v2.2.0 speaks 7,
overlay v1.3.0 declares 8` in the nine-minute window between the two releases --
the concrete case for landing the pair together. Run 114 was the **runner**:
`Set up job` 14m33s, every later step failing at 0s, no log blob stored.
A failed compose is silent from outside. The bundle keeps its previous value,
so operators keep installing the previous protocol while `main` carries the new
one, and nothing says so. Dispatching bundle.yml recovered it within a minute.
The lesson recorded for the next cutover: publishing the releases is not the
last step, verifying the bundle moved is -- and read `current.json` through
`/contents/`, never `/raw/`, which is CDN-cached for six hours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Records the Asset Bridge `edge -> main` cutover: §16's 9b row, §17.15's four
org-lead decisions, and the `PARSER_VERSION` correction those forced through
§7, §10.4 and `SPAWN_ATLAS.md`.
## The number had to move, and the reason generalises
§10.4 wrote phase 7's canonical-read-order bump as `PARSER_VERSION` 4 -> 5.
`main` had meanwhile bumped 4 -> 5 for the `UniqueId` fix and RELEASED it as
v1.2.2. Same number, different derivation. An install that imported under
v1.2.2 stores 5, so a phase-7 build also declaring 5 is read as current by
`currentParser` and never re-reads -- the exact failure the constant exists to
prevent, reached through a merge instead of through forgetting to bump.
Two long-lived branches bumping one counter for different reasons is a defect
the counter cannot see; only the merge can. The docs now carry both meanings --
5 as main's released one, 6 as phase 7's -- rather than collapsing them.
## The cutover is five repos
`installer`'s `edge` carried 9a's `libgdiplus` `doctor` check, and
`SHARD_PREREQS.md` on `main` had been describing that check as existing since
phase 1. §16's 9b row named four repos and would have left an operator-facing
doc naming a check in no released binary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The Asset Bridge's docs pass, and the acceptance walk that shaped it (v8.md §16
row 9a, §17.13-14). Phase 9 is three legs now: this one, the edge->main cutover,
and the site.
## INTEGRATION.md had stopped at 6 and contradicted itself
Its §2 said "the current version is 6" above examples already carrying
`X-UOLink-Version: 8`, there was no protocol-7 paragraph, and `assets.` appeared
zero times in 1,306 lines. It is the only document an integrator outside this org
has, so it is carried the whole way: the version block corrected, v7 (the event
plane's command half) and v8 (the asset plane) written, a §5 section for the five
routes, 425/422 in the status table, and a caveat that the asset plane is a
working set rather than a stream.
Protocol 7's absence is the Events workstream's debt rather than this one's, but
it cannot be stepped over on the way to 8.
## The operator-facing half
`UPGRADE_NOTES.md` gains the entry an operator reads when this ships: what
changed, the one required action on a Linux host, and the thing that will not
announce itself -- nothing here happens on a restart, so a patched client keeps
serving the old pictures until somebody presses a button.
`installer/INSTALL.md` gains libgdiplus as a prerequisite row and the `doctor`
row that checks it. The index rows for SPAWN_ATLAS and CLILOCS described the
workflows this protocol deleted; v8.md now has an index row of its own, and v7 is
marked as the released protocol.
## The walk
Wiped every asset row and every imported sprite, then walked it as a new
operator: 1,095 portraits in 3.18 s, 67,496 names in 1.42 s, 313 item pictures in
1.38 s, the atlas over the bridge in ~2.0 s, an Update with no drift answered in
0.99 s. Bestiary portraits are the right animals by eye; the marketplace shows
hued item art with cliloc names.
It found two defects (§17.14) and one cutover hazard: module-uo's `edge` is
behind its `main`, missing #35, so the walk measured 0 of 6,455 spawners carrying
a UniqueId. 9b's row says to sync before merging or the cutover ships a
regression.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
v8.md section 12.2 records the admin surface as built: one page, Admin -> Client
Files, over the creature catalogue, item and land art, and the cliloc table --
one page because they come off one client install and change on one event. The
four decisions are written down, including the one that kept the phase to a
single repo: section 14's "activity view" is the last import's own summary
rather than a filtered feed, which would have needed a core activity filter, a
`ctx.activity.list` helper and a MODULE_API bump to re-read rows the panel can
keep itself.
The part worth reading twice is what the walk found. Two of the three defects
were DELETIONS, both predate phase 8, and neither was reachable by a test that
did not already suspect them:
* the body import diffing its manifest against every family's rows, so 313
item pictures were staged for deletion under a sentence that was wrong about
what had happened;
* an approved vanish unlinking the sprite and keeping the row, so the key came
back for review forever and the count never moved.
The rule they share is now stated: after phase 5, `shard_assets` is three
collections in one table, and every read of it that feeds a decision has to say
which one it means. Two of the three that did not were deletions.
CLILOCS.md and SPAWN_ATLAS.md now name the button that exists (Admin -> Client
Files) instead of the one they promised, and modules/uo/API.md records that a
`needsReview` reply carries each vanished key's picture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
10.3 recorded the empty-`catalog` defect as a tree-family bug. It was inline in
three places -- the body catalogue, statics and land, and the tree -- and all
three now share one comparison. Records why BridgeLeases keeps the other shape,
and names the two defects as one kind of thing: a value meaning "I am not
asserting anything" has two spellings on a wire, and a stock install finds
whichever one you did not think about.
Adds the whole-plane check against a stock ServUO install: every family asked
with a real catalog, an absent one and an empty one, plus the cross-family and
stale-catalog refusals and both of stock ServUO's empty decoration files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
10 said the shard would serve `tree/<label>` -> bytes. Measured against a stock
57.4 tree it cannot: Spawns/trammel.xml is 4.03 MB, the sidecar discards any
inbound line over 1 MiB, and that file as one base64 row is 5.4 MiB. It would be
dropped, time out, and be re-requested forever with no error in it anywhere --
and two files on a STOCK tree are in that state.
10.1 records the three carriages measured before anything was written, and why
the winner is not the smallest one: whole-file gzip is 1.21 MB against chunked
gzip's 1.26 MB and is bounded by nothing, so it works on every tree anyone would
test and fails on the first one nobody did. The chunk is the guarantee; the
compression is only the saving.
10.2: it is a `tree` family on assets.fetch, not 14's separate tree.* commands
-- phase 5's registry already owns the single slot, the envelope, the ceiling
and the mid-import guard, so reusing it left `link` with nothing to do for the
third phase running. Its CONSENT is its own, though: Bridge.TreeEnabled, because
declining to serve an EA-licensed client is not the same as declining to serve
the spawn files an operator wrote, and the atlas would have been the casualty.
10.3 records the two defects and which harness found which. An empty `catalog`
is not an absent one. And GZipStream writes nothing at all for zero bytes of
input, which stock ServUO's two empty decoration files walk straight into -- an
offline probe called that a success, because .NET's own decompressor reads an
empty stream as empty data and the declared length and hash both agree with it.
Only a live walk through a reader on another runtime disagreed.
10.4: one canonical read order, because the decoration index keeps the first
item id it sees and the two readers agreed by coincidence rather than by
construction. PARSER_VERSION 4 -> 5.
10.5 has the end-to-end numbers against a live shard. 17 gains the phase's three
decisions, including the one that departed from the recommendation: boot never
calls the shard, so an install on the bridge has no automatic refresh at all.
SPAWN_ATLAS.md is rewritten around the two sources and stops requiring a shared
filesystem. Protocol stays 8; EXTRACTOR_VERSION stays 3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
§16 planned deep animation keys and a bulk-fill switch. Measuring first changed
the phase: a complete one-direction animation set is 174,453 frames / 281.5 MB
(not the ~119,000 / ~117 MB §11 estimated), every frame carries its own centre
and 23,818 of 26,274 actions have frames of differing size — and the site
displays still pictures. So none of it was built (§11.2, org lead 2026-09-11);
`body/<id>/a<n>/f<k>` stays named in §5 and refused in practice.
§4.10 is what the walk found instead. A body's actions are a contiguous band and
the next body's begin immediately after, so one action past the ceiling is a real
record of another creature: 643 of 795 legacy bodies pass every check there and
452 are byte-identical to body+1's action 0. `Animations.GetAnimLength` is not
the ceiling — it disagrees with the index arithmetic on one body of this client,
by nine actions.
What shipped: the 73 bodies with no art at action 0 (a horse at `body/820/a23`),
the catalogue key carrying its action, the atlas join that reads it instead of
hardcoding `a0`, and the ceiling that makes the fallback walk safe. Catalogue
1,022 -> 1,095; EXTRACTOR_VERSION 2 -> 3; protocol stays 8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
§11.1 is new and carries what phase 5 measured: 49,152 addressable static ids
(not the 81,884 `artidx.mul` declares -- `FileIndex` sizes its table from its
length ARGUMENT), 39,189 with art, 4,244 land tiles, 9,963 + 12,140 empty index
slots, and the whole set at 81 MB decoding in 34 s. That last number reopens the
bulk question and the answer is still no: 108 MB of base64 through a 512 KB
single-slot channel to store 43,433 pictures a shard displays a few hundred of.
Two traps, both §4.5's failure mode -- a confident, plausible, wrong picture:
- `Art.GetStatic` hands back the SAME cached Bitmap and `Hue.ApplyTo` repaints in
place, so hueing edits the library's own copy: the plain key comes back hued
from then on, and the next hue stacks. `Files.CacheData` off process-wide fixes
it and also stops a game server retaining 74 MB of Bitmap. Copying instead does
not solve the retention, and `new Bitmap(src)` throws on ARGB1555 anyway.
- `PartialHue` (13,259 of 65,536 ids) decides whether a hue repaints every pixel
or only the grey ones, from a file only the shard has. Item 597 is a wooden
screen with painted flowers; one mode reddens the flowers, the other the whole
screen, and both decode. Hence land takes no hue segment and `h0` is not a key.
Plus the namespace trap that compiled: unqualified `TileData` binds to ServUO's
own `Server.TileData`, because the enclosing namespace beats `using Ultima;`.
§14 records what the wire gained -- the `static` and `land` families, `families`
on `assets.sources`, and `assets.fetch` becoming shared plumbing whose family is
DERIVED from the keys (§5 made the key the address; a request naming its family
too would have two places to be wrong and one of them silent). Additive, so the
protocol stays 8 and EXTRACTOR_VERSION stays 2. §15 records that `link` needed
nothing in phases 4 or 5: it forwards verbatim in both directions.
§17.10 is the four org-lead decisions. §12 and modules/uo/SCHEMA.md carry the
website side: `uploads/items/`, per-row `catalog` staleness, and why a key with
no art writes no row at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Phase 4 built 4.3's UOP animation reader. What it found first changed what the phase
was worth, so the plan is corrected rather than merely annotated.
- New 4.9: what phase 4 measured. Of the EIGHT player bodies 4.8 assigned this
phase, two are in the client at all -- gargoyles 666/667, in AnimationFrame3.uop.
The six ghost bodies are in no package, and that is established rather than
unfound: the five packages hold 10,724 entries and the
build/animationlegacyframe/%06d/%02d.bin scheme claims every one, leaving no room
for another naming. Also the format as read (AMOU, a per-frame ARGB1555 palette,
direction as a slice of the frame table), the nine bodies whose frame count is not
a multiple of five, the validate-as-we-go bounds and the measurement that says
they refuse nothing real, and the live rig.
- 5.2 rewritten: the player-body set is the LIVING pair per race, six ids not
twelve. Ghost ids left it because no client has art for any of them. Still asked
of the shard, never hardcoded -- only the question changed. And with phase 4 in,
all six have art for the first time.
- 4.3 rewritten against what was measured, including why searching five UOP packages
for one body is NOT the never-sweep rule being broken: a legacy index is addressed
by position, a UOP entry by the hash of a name carrying the body id, which the
payload then declares again.
- 11 sizing: the catalogue is 1,022, not 787. The mix is recorded because "add every
body" sounds like it changes what a catalogue is, and it does not -- the legacy
787 was already 366 equipment bodies.
- 14: manifest and fetch rows carry `source` (legacy/uop). Additive, so protocol
stays 8; EXTRACTOR_VERSION 1 -> 2 is the change consumers actually see.
- 16 phase 4 marked DONE; 17.9 records the four org-lead decisions (fallback applies
to every body; ghost ids leave the set; own PNG encoder; NO_IMAGING stays flat).
- 4.8 and 8.1 keep their numbers as the record of what those phases measured, with a
pointer to where the answer landed.
Two consumer docs repeated the ghost claim as fact and are corrected:
website/SPAWN_ATLAS.md (787 -> 1,022, and "two thirds of the playable ghost and
gargoyle bodies have no art" -> about half the addressable body range) and
modules/uo/API.md (same sentence).
Code: servuo-plugins#31.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
SCHEMA.md's cliloc section still described the pre-protocol-8 world, two
phases after phase 2 changed it. Three sentences said the same false thing,
so fixing only the flagged one would have left the section arguing with
itself:
- "Sourced from files the operator supplies" -- the BASE comes from the shard
on any install with uo-link configured; only the overlays are the
filesystem's, and that asymmetry has a reason worth stating (ServUO has no
server-side notion of a custom cliloc, so there is nothing to ask for).
- "A base (the converted client table)" -- not converted any more.
- "The conversion step is not avoidable ... so the shard cannot supply names
on our behalf" -- it does supply them. Phase 2 ported UOFiddler's Mythic
decompressor into the overlay precisely so nobody converts anything.
Rewritten to say what is true and why the file path still exists (deprecated,
not removed, so an existing install keeps working), plus the two things a
reader of this table actually needs: `shard_cliloc_meta.payload` keeps the
base's fingerprint under `base` SEPARATELY from the overlay hashes -- because
on the bridge the old `clilocs.plain` label is supposed to disappear and one
flat hash map would read that upgrade as a vanished source -- and boot does
not import on the bridge path at all.
Each claim checked against the code rather than from memory:
shardClilocs.model.js:307 (`base: fingerprint`), :438-440 (refreshOnBoot
returns `skipped` on the bridge), clilocSource.js:333 (`missingOverlays`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Phase 3 is built and walked on a live shard. What the walk measured, and the
two places the design of record needed correcting:
§8.1, new: the catalogue is 787 exactly as §4.8 predicted, and the whole scan
of bodies 1-2047 takes 734 ms cold -- so the wall-clock paging §11 designed
never fires on this client. Every §4.8/§5.2 prediction held when the bytes were
rendered and LOOKED at: 320, 607, 608 and 666 come back absent rather than as
another creature's picture, and the direction split is 783 at index 1 against 4
at index 0 -- four player bodies, not six.
44 of the 787 hashes are shared by two or three bodies, which is the exact
signature of the wrong-picture bug, so it was chased rather than assumed. It is
the client's own Body.def aliasing (83 {1}, 84 {1}, 106 {12, 59}), and the check
that settles it is at the source: Translate(ref body, ref hue) rewrites `body`
only when bit 31 is set, unlike the one-argument overload -- and ResolveAnimation
calls that same two-argument overload, so validator and decoder resolve the
identical record.
§12.1, new: **§12 is right about the outcome and wrong about the mechanism.**
`shard_spawn_creatures` is emptied and refilled by every atlas refresh, and a
refresh runs on every boot -- so an imported filename written to that row is
destroyed by an ordinary re-parse of the ServUO tree, and the next Update finds
the client files unchanged and never restores it. Three tables outside that
blast radius, and the atlas import re-derives `art` on the way past.
§14: **§16 listed phase 3 as servuo-plugins + module-uo and that was wrong.**
web.rs routes every command explicitly, so `link` is in the phase. Corrected in
both places.
UOFIDDLER.md is DELETED, two phases earlier than §9.1 predicted -- creature art
was the only thing still on it. SPAWN_ATLAS.md §Artwork is rewritten around the
import, keeping the operator's own map as the thing that wins; the module's
SCHEMA.md gains the three tables and API.md the two admin routes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Phase 2 of the Asset Bridge is built, so the documentation stops telling an
operator to install a GUI tool.
`v8.md` gains §9.1 and §9.2 — what the port cost, what it measured, and where the
base table comes from now. The measurement worth keeping: **67,496 rows in
290 ms**, which is exactly what UOFiddler's own `Ultima.dll` produced from this
same client through the converter this phase deletes. An independent
implementation agreeing to the row is the strongest check available that a format
decoder is correct, and it is not something a subtly-wrong one produces.
§17 records the four shapes the org lead settled before any of it was written.
Two departed from the recommendation: **the bridge always wins** (no source
setting — there is no version of that question an operator benefits from
answering) and **import is admin-triggered** (boot does not call the shard at
all).
`CLILOCS.md` is rewritten around that: where the table comes from, what arrives
and in how many pieces, the refusals — including the two the file pipeline had no
equivalent of (a client patched mid-import, and the base's exemption from the
vanished-source rule, which exists so an upgraded install is not asked to approve
a change the upgrade itself made).
`UOFIDDLER.md` loses Part 1 entirely rather than having it rewritten. What is
left is creature art, which phase 5 takes, after which the page goes away. `v3.md`
§8.6 keeps its reasoning with a note saying what superseded it, because the
argument for why the manual step existed is still the argument for why this was
worth building.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Asset Bridge phase 1, docs half. Code: RunicGateway/servuo-plugins#28,
RunicGateway/link#41.
## The correction, which is most of this
**New §4.8.** The animation path has §4.5's shared-buffer defect too, and 357 of the
1,144 bodies §1.1 counted are **wrong pictures on a stock client** — ids with `length 0`
that return whichever body was decoded before them. Proved by decoding body 320 after a
dragon (a dragon), a wolf (a wolf) and a human (a human).
So the catalogue is **787 bodies**, and the numbers that were derived from 1,144 move
with it: §11's working set, its ~173,000-frame full set (now ~119,000), phase 3's scope.
**§5.2's table was wrong in the direction that matters.** The elf ghosts were listed as
decoding; their index entry has no record, and what came back was the elf female. Four
of twelve player bodies have art, not six — which takes phase 4's UOP decoder from six
ids to eight.
§1.1 now says outright that every "decoded" count in it is an upper bound. It is not a
table to size anything from any more.
## What phase 1 settled
- **§3.3** — the two numbers: a 512 KiB batch budget under a 1 MiB inbound line cap, with
the factor of two load-bearing rather than cautious.
- **§3.2** — flow control is enforced **on the shard**, as a single slot answering
`bridge.busy`, not serialised in the sidecar and not left to the website as a
convention. Records what it costs: a status poll shares the slot.
- **§3.4, new** — one paging envelope (`more`/`cursor`/`cut`) for all five families that
will page, defined before the first one needs it. `cut` because "short page" has three
meanings and only one of them means finished.
- **§6** — hashing had to come off the request path entirely. The gate is unchanged; what
changed is that "the normal case must cost nothing" now also means "and the abnormal
case must not time out", because the first hash of 1.06 GB does not fit in 10 s.
- **§14** — which commands exist now, and which phase brings the rest.
- **§16, §17** — phase 1 done; decisions 6 and 7.
## Elsewhere
- **`SHARD_PREREQS.md`** gains the libgdiplus requirement (§4.4) — Linux hosts only, with
the archived-upstream caveat and the `NO_IMAGING` status the shard now reports on the
source gate.
- **`INTEGRATION.md`** advertised `X-UOLink-Version: 6`. It was already two versions stale
before this change; now 8.
- [x] AI-assisted — Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Records the Asset Bridge phase 0 spike (§16) against v8.md, and closes the
last open decision.
§4's choice to call ServUO's vendored `Ultima` STANDS: nothing faulted on a
path this protocol calls, and §9's cliloc reader reproduced UOFiddler's
123,490-entry table byte for byte in 218 ms from inside the shard.
But the spike was looking for the wrong kind of failure. `LoadStatic` and
`LoadLand` decode out of a buffer that is reused, only ever grown, and
filled by a `Read` whose return value is discarded — so a short, absent or
out-of-bounds record does not throw, it renders the PREVIOUS asset. On the
stock, unmodified client on this machine that is 22,102 ids whose index
entry reads `lookup 0, length 0`, all of which the library returns a picture
for. §1.1's "32,766 decoded" was counting these.
New §4.5 states the rule that answers it — validate before calling — with
the six checks phase 0 implemented, the eight deliberate defects they caught
(seven of which the library rendered silently, including a verdata lookup
past verdata.mul's own end, which `Verdata.Seek` bounds-checks nowhere), and
the number that makes the boundary defensible: zero false refusals across
49,151 statics and 16,384 land tiles on a clean client.
New §4.6: `FileIndex`'s UOP constructor ends `MulPath = uopPath`, so
`artLegacyMUL.uop` wins outright and `art.mul` is never opened on a current
client. Bounding an offset against the wrong file is not approximate, it is
meaningless — the spike's first run refused 34,299 good statics that way,
and every refusal read like a real finding.
New §4.7: `Ultima.Gumps.GetGump(2)`, called once from inside a running
shard, made the ServUO process disappear — no catch reached, no console
line, the probe's checkpoint file the only record. §4.1's rule is earned.
§17 now has nothing open:
* item 4 — the default audience — SETTLED: an asset inherits the audience
of the page that uses it.
* item 5 is new: validate-before-calling, chosen ahead of the spike over
a child-process extractor and over reversing §4, and confirmed by it.
The dangerous failure turns out to be a wrong picture, which no
containment strategy would have caught.
§16 marks phase 0 done and adds the half it deliberately left unbuilt to
phase 1: the animation path has no validator, and the patched client's wolf
decoded something else in silence to prove it.
Full measurements and the rig recipe live in servuo-plugins
`tools/scaffolding/README.md`; the code is RunicGateway/servuo-plugins#27.
- [x] AI-assisted — Claude Code (Opus 5)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The work has a name now — the Asset Bridge — for commits, PR titles, branches
(feat/asset-bridge-p<n>) and conversation. Protocol number stays 8 and the file
stays docs/link/v8.md.
§4.4 closes the last real open question rather than deferring it to phase 0, and
takes all three answers instead of choosing one. ServUO targets net48, so a Linux
host runs it under Mono, and Mono's System.Drawing is a thin layer over
libgdiplus — which §4.2 put in the decode path, not just the encode. So on Linux
it is a hard prerequisite for art. Cliloc and atlas import are unaffected; neither
touches pixels. Windows hosts need nothing at all.
It is now written down as: a SHARD_PREREQS.md entry, a doctor check in the
installer, and a named NO_IMAGING status when it is missing, in the same family
as the cliloc reader's COMPRESSED — never a stack trace, never a 500. Install
routes per distro are in the section, apt-get install libgdiplus being the
normal one.
One fact recorded because depending on something unmaintained should be a
conscious act: github.com/mono/libgdiplus was ARCHIVED in March 2025 and is
read-only. Distributions still package and patch it, so installing it today is
supported and ordinary — but it is the strongest long-term argument for moving
extraction off System.Drawing eventually, and phase 4's UOP reader is written
without it so that door stays open.
§17 restructured: three settled items kept because each changes numbers
elsewhere, and one genuinely open question (the default audience) that does not
block starting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The six player-character bodies the vendored Animations cannot reach are the
player character, and the scope says player models, so they get a decoder rather
than a caveat. New §4.3, new phase 4, scoped as narrowly as possible: a reader
for AnimationFrame*.uop used ONLY for bodies the legacy path cannot resolve.
Everything the vendored code already decodes keeps going through it.
Verified genuinely absent rather than mis-addressed, and the way that was
established is now the most important warning in the document.
Bodyconv.def maps gargoyle 666 to anim5 and BodyConverter.Convert faithfully
returns fileType 5, where this client has nothing. Asking the OTHER anim files
for index 666 does not fail — it returns 175 decodable action/direction
combinations of a giant spider, because something unrelated occupies that index
in anim2.mul, while fileTypes 3 and 4 return misaligned colour fragments. All of
it rendered and looked at, which is the only reason it was caught: every one of
those reads reports success.
So the extractor takes Convert's answer and reports nothing when that yields
nothing. It must never sweep file types looking for a hit. That does not find
missing art — it silently puts a spider on the gargoyle page, with no error
raised anywhere and nothing downstream able to detect it. A "0 rows" outcome is
correct behaviour; a confident wrong picture is the failure this protocol most
needs to avoid.
Phase 4 sits after the catalogue, not inside it: the catalogue is useful with
1,138 of 1,144 bodies, and the UOP reader is the only genuinely new format work
here, so putting it on the critical path would hold up every website-side phase
behind it. Its acceptance test is that a gargoyle looks like a gargoyle, checked
by eye.
References available and license-compatible: ClassicUO's animation loader
(GPL-3) and UOFiddler 4.22 (Beerware).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Decision: no decoders are reimplemented. Extraction goes through ServUO's own
vendored Ultima, which Scripts.csproj already references, so the art half of
this protocol is plumbing and the Mythic cliloc reader (§9) becomes the only
decoder Protocol 8 writes rather than calls.
What makes that safe rather than merely cheap is a distinction §1.1 did not
draw. All three decoders build a FileIndex, but only Gumps passes
hasExtra: true — and FileIndex.cs's own comment says that branch exists FOR
gumpartlegacy.uop, the one UOP layout with an extra field. Art passes
hasExtra: false and probed 49,150 statics plus 16,384 land tiles with zero
faults; Animations touches no UOP at all and probed 1,144 bodies clean. The
access violation is a bug on a branch exactly one decoder reaches, and that
decoder was already out of scope. So "nothing calls Ultima.Gumps" is now a
safety rule, and adding gump art later means fixing that path first.
§4.2 records the three costs this accepts: six of twelve stock player bodies
have no art (UOP-only, not reachable by calling the existing code differently),
System.Drawing stays in the decode path, and we inherit whatever Ultima a shard
vendors — EXTRACTOR_VERSION already covers the last one.
Phase 0 changes shape with it. It was going to prove new decoders byte-identical;
it now tries to BREAK the vendored ones on purpose, from inside a running ServUO
against a deliberately patched client, because the probes behind §1.1 ran in
PowerShell against a stock client and neither is the real environment.
§17 is down to one real question: libgdiplus on Linux/Mono shards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Player character bodies take direction index 0 (head-on); every other body takes
index 1 (front three-quarter). Direction still does not appear in the key — both
indices are extractor configuration.
The split follows the legibility caveat rather than fighting it: a humanoid
reads fine head-on and a character portrait should look at you, while a wolf
seen head-on is a dark blob and the same wolf at index 1 is obviously a wolf.
§5.2 is new: which bodies count as player characters is asked of the shard, via
Race.AllRaces and each race's MaleBody/FemaleBody/MaleGhostBody/FemaleGhostBody,
never hardcoded. Twelve ids on stock 57.4. Two things say a hardcoded list would
have been wrong — RaceDefinitions.cs passes the gargoyle's ghost bodies in the
opposite order to the other two races (695 male, 694 female), and a shard that
calls RegisterRace adds ids no table of ours would contain.
And the finding that matters most: SIX OF THOSE TWELVE do not decode at all with
ServUO's vendored Animations — both human ghosts and every gargoyle body,
because they live in AnimationFrame*.uop which that library never reads. The one
part of the scope with the most attention on it is the part the vendored library
serves worst, which is now the strongest single argument for §4's recommendation
to own the decoders.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Only the viewer-facing direction is wanted for players and monsters, so
direction leaves the key entirely (§5.1) rather than being a segment nobody
varies. Every depth count in §11 falls by exactly 5x.
Which index that is was rendered, not looked up: all five directions for a
human, a wolf and a dragon. Index 0 is head-on — face, chest and front legs —
and index 4 is directly away, with a quadruped's tail toward the camera. The
small-thumbnail version of the same test suggested the opposite, which is why
the finding is in the doc rather than in someone's head.
Measured consequence: body 400 drops 1,050 -> 210 frames, and a complete
one-direction set for all 1,144 bodies is ~173,000 frames (~170 MB) rather than
~865,000. That moves a bulk-fill-everything switch from "never" to something
phase 5 should leave room for.
One caveat kept as an open question: index 0 is the least legible view for
four-legged creatures — a head-on wolf is a dark blob, a side-on wolf is a wolf
— so the extractor takes the index as configuration defaulting to 0, and §17
asks whether the catalogue should default to 1 or 2 instead.
No client-derived image is committed; the render was inspected and discarded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The shard host already has the UO client — ServUO cannot boot without one, and
Config/DataPath.cfg resolves into Server.Core.DataDirectories at runtime. So the
cliloc table, the creature and item art, and the spawn atlas's own source files
can all reach the website over the bridge that already exists, and UOFiddler,
the desktop conversion step and the website's shared-filesystem view of the
ServUO tree all go away.
Design of record for the work: the shard extracts, the sidecar forwards, the
website decides — which is the only arrangement that keeps the sidecar a dumb
forwarder while still resolving creature slug -> body id, something only code
running inside ServUO can do.
Measured against this machine's ServUO 57.4 tree and client rather than assumed:
Art.GetStatic and GetLand decode ~66,000 ids with no faults, 1,144 bodies have a
decodable first frame, and body 400 alone is 1,050 frames across its actions and
directions — which is what makes the bulk set one thumbnail per body and
everything deeper on demand.
Two findings shape the build. Gumps.GetGump(2) does not fail, it corrupts the
process (AccessViolationException, 0xC0000005) — uncatchable on .NET Framework
4.8 and a shard crash in-process — so §4 recommends owning bounds-checked
decoders rather than calling ServUO's vendored Ultima, which also removes the
System.Drawing/libgdiplus dependency and the UOP gap that leaves gargoyle bodies
666/667 empty. And the shard -> sidecar direction has no line cap today, which
Protocol 8 must close before it starts sending large lines deliberately.
UOFiddler is Beerware, so its Mythic cliloc decompressor can be ported into this
GPL-3.0-or-later tree and the conversion step retired entirely.
Nine phases, four decisions still open in §17.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The last leg of the events plan, and the record of what it turned up.
**16c began by closing 16b's missing seventh step.** Six repositories were cut
over and every one showed 0 commits on `edge` that are not on `main`. This one
showed 46: step 6 landed the record *on* `edge` rather than cutting `edge` over,
so `docs` `main` opened `EVENTS.md` with "revision 5. No code written. Read
against ... MODULE_API_VERSION 1.9.0 - sidecar protocol 5" while six repositories
shipped the engine on protocol 7 — and `link/v6.md` and `v7.md` existed on no
default branch anywhere.
Nothing in this workstream could have caught that, and the reason is worth
writing down: every check that guards a contract lives in the repository that
DEPENDS on the contract, and a documentation repository has no dependants. The
one check that reads `docs` from outside is `runicgateway.com`'s
`checkReference.mjs`, and 16c is the only phase that would ever have run it.
Closed by #232.
**The site.** Nine `checkFacts` values and twenty-seven `Bridge.cfg` keys, both
red before the phase started — which is the bargain that repository's §12 struck.
Two pages, the treatment Teams has. Two capability entries, the calendar one
deliberately not `needsModule` because a bare core can author and run an event.
A `deploy-events` privacy row, because the participation ledger is personal data
and nothing named it. And `reference/event-catalog` retitled "Shard event
catalog", route unchanged, because two things in the documentation were called an
event catalog.
**`.profile`.** One bullet saying the posture rather than the feature list, and
two stale numbers: protocol 5 → 7 in the four values the installer prints, and
module-uo v1.1.0 → v1.2.2.
**And the plan is marked COMPLETE.** All seventeen phases on `main`, in every
repository they touch. No contract moves in this phase; it makes the ones already
moved legible from outside the organisation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The leg the first commit named as still owed is walked. Module-uo#34 cut v1.2.1,
so the module could arrive the way an operator's does: core fetched the release
MANIFEST over https from the allowlisted host, verified its sha256, and mounted
it; the four values the installer printed then produced `status: connected`,
`pluginConnected: true`, `protocol: 7`. Released core, released module, released
sidecar, released overlay.
That rig confirmed both Phase 16a fixes in the shipped artefacts rather than in a
working tree -- the atlas imports off a stock tree, and a world verb's teardown
leaves the shard answering `owned: [], pruned: 0`, which is the check the no-op
teardown hid behind.
It also found two more defects, both in the released bundle (Module-uo#35). The
aggregator discarded the UniqueId, so `uo.options.spawners` was empty and no
Phase 12b property lease was authorable at all -- while `PARSER_VERSION = 4`'s
own note said a point keeps that field and named Phase 12b as the reason. And a
landmark option value named 23 places at once: 558 landmarks under 320 distinct
`facet/name`, resolved by `.find()`, so an author who picked "Entrance - Destard"
got Blighted Grove with a successful run and no warning.
Both are recorded as one class, because that is the useful part: an option source
that answers empty, or answers with a value that does not identify one thing,
disables a feature silently. Nothing errors; the form simply cannot express the
thing, and a test that checks the parser, or the query, or the column in
isolation passes throughout.
Diff is 45/6, content only -- no CRLF rewrite (checked against --numstat).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
`EVENTS_PLAN.md` gains the 16b record: the six steps and why core lands before
the module, the four decisions taken, the releases cut, and the re-verify against
artefacts an operator would actually download.
Three things in it are worth more than the chronology.
A job's log IS readable on this Gitea, through the web route rather than the
API. Every earlier phase diagnosed CI by reproducing jobs locally on the belief
that logs were unreachable; reading one turned four red jobs into four known
causes in about ten minutes. Three were the ten-minute Cloudflare outage in the
middle of the window and a runner that could not resolve sh.rustup.rs -- and
because neither release pushed its tag before dying, the orphaned-tag failure
mode did not occur and a plain workflow_dispatch recovered both.
The seventh defect of this phase: `server-tests` had been red on every events PR
since Phase 10, always the same single test, and the workstream merged over it
eight times. `announce.js` asked for `hour12: true`, which is not the same
request as a 12-hour clock -- for a locale whose default cycle is h23, Node 20
resolves it to h11 and midnight renders "0:00 am", while Node 22+ resolves it to
h12. Same ICU on both sides, so it is V8's ECMA-402 behaviour and not locale
data; the image ships node:20-alpine and a dev machine is newer, so it rendered
correctly for everyone who reviewed it and wrongly for every real recipient. The
rule is now written down: `hour12` is a request about a locale's preference,
`hourCycle` is a request about the clock -- ask for the clock.
And the re-verify itself: the released installer resolved bundle 2026.09.10,
verified both checksums, did a first install into a stock 57.4 tree, the overlay
compiled 0/0 -- which no release had ever been asked to prove -- the shard came
up with the events plane on and dialed the sidecar, the whole protocol-7 event
plane answered, and an event published on released `main` ran to `completed`
with its results published and its finished run visible on /site/events two
minutes later. That last line is 16a's calendar fix holding on `main`.
The module's own install through core's https installer is named as the one leg
still owed: it cannot run until Module-uo#34 cuts the release it fetches.
Diff is 102/1, content only -- no CRLF rewrite (checked against --numstat).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4