docs(link): Protocol 8 — client assets over the bridge
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
This commit is contained in:
478
link/v8.md
Normal file
478
link/v8.md
Normal file
@@ -0,0 +1,478 @@
|
||||
# Protocol 8 — Client assets over the bridge
|
||||
|
||||
**Status:** Design of record. Approved in principle 2026-09-09 (architecture, asset scope,
|
||||
built-in cliloc decoder, atlas cleanup); §17 lists what is still open.
|
||||
**Supersedes the manual half of:** [`../website/UOFIDDLER.md`](../website/UOFIDDLER.md),
|
||||
[`../website/CLILOCS.md`](../website/CLILOCS.md) §Converting,
|
||||
[`../website/SPAWN_ATLAS.md`](../website/SPAWN_ATLAS.md) §Artwork and §Configuring the tree.
|
||||
|
||||
Two features on this platform read data that only exists inside a UO client, and today both reach
|
||||
the site by hand: the operator installs UOFiddler, converts `Cliloc.enu` on their own desktop,
|
||||
exports sprites one at a time from a GUI, hand-writes a slug→filename JSON map, and copies the
|
||||
result to the server. A third — the spawn atlas — avoids UOFiddler but pays a different price: the
|
||||
**website** must be able to read the shard's ServUO tree directly, over a bind mount or a shared
|
||||
volume.
|
||||
|
||||
This protocol deletes all three arrangements. The shard already has everything, and the bridge
|
||||
already goes to the website.
|
||||
|
||||
---
|
||||
|
||||
## 1. The premise, which turns out to be free
|
||||
|
||||
**A ServUO shard cannot boot without a UO client installation.** It reads maps, statics, tiledata
|
||||
and multis out of `.mul`/`.uop` files, and `Config/DataPath.cfg` is where an operator declares
|
||||
where those live — *required* on Linux, auto-detected from the registry on Windows. At runtime the
|
||||
resolved directories sit in `Server.Core.DataDirectories`, a public static the plugin can read on
|
||||
any shard, with no new configuration and nothing for an operator to set up.
|
||||
|
||||
So the files the operator has been converting on their desktop are already on the shard host, in a
|
||||
directory the shard already knows the path of, in a process the bridge already runs inside.
|
||||
|
||||
Everything below follows from that.
|
||||
|
||||
### 1.1 What was measured, not assumed
|
||||
|
||||
Against this machine's ServUO 57.4 tree (`C:\Users\colby\Desktop\ServUO`) and client
|
||||
(`D:\Games\Electronic Arts\Ultima Online Classic`, 3.5 GB), loading **ServUO's own
|
||||
`Ultima.dll`** — the assembly `overlay/Scripts/Scripts.csproj:39` already carries a
|
||||
`<ProjectReference>` to:
|
||||
|
||||
| Call | Result |
|
||||
|---|---|
|
||||
| `Art.GetStatic(0…16383)` | 16,384 decoded, 0 errors |
|
||||
| `Art.GetStatic(16384…65535)` | 32,766 decoded, 1 empty, 16,385 clean out-of-range errors |
|
||||
| `Art.GetLand(0…16383)` | 16,384 decoded, 0 errors |
|
||||
| `Animations.GetAnimation(0…2047, 0, 1)` | **1,144** bodies with a decodable first frame, 904 empty, 0 errors |
|
||||
| `Hues.GetHue(33)` | loads |
|
||||
| `Bitmap.Save(…, Png)` | 852-byte PNG from one creature frame |
|
||||
| `Gumps.GetGump(2)` | **hard crash** — `AccessViolationException`, process exit `0xC0000005` |
|
||||
| `new StringList("enu", "Cliloc.enu")` | throws — `Non-negative number required` |
|
||||
|
||||
Two of those rows are load-bearing and are dealt with in §4 and §9. The rest say the same thing:
|
||||
**most of the extraction this protocol needs is already implemented, already compiled, and already
|
||||
referenced by the plugin's own build.**
|
||||
|
||||
Depth, for §11's sizing: body 400 (human male) has **35 actions × 5 directions = 1,050 frames**.
|
||||
One body.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture: the shard extracts, the sidecar forwards, the website decides
|
||||
|
||||
```
|
||||
UO client files (operator's own, on the shard host)
|
||||
│ read by the plugin, off the Core thread
|
||||
▼
|
||||
ServUO shard (servuo-plugins/) ← decodes; resolves body ids; hashes
|
||||
│ loopback JSON, request/reply, one batch outstanding at a time
|
||||
▼
|
||||
uo-link sidecar (link/) ← forwards bytes; decides nothing
|
||||
│ REST, bearer-token auth, X-UOLink-Version: 8
|
||||
▼
|
||||
website (module-uo/) ← stores, names, gates, serves
|
||||
```
|
||||
|
||||
This is deliberately the *only* arrangement that keeps
|
||||
[the bridge's standing rules](PLAN.md) intact:
|
||||
|
||||
- **The sidecar stays a dumb forwarder.** It moves opaque assets and decides nothing about them —
|
||||
no audience, no projection, no capability advertisement. Putting the decoders in Rust would have
|
||||
meant the sidecar deciding what an asset *is*, on top of re-deriving in Rust what is already
|
||||
compiled next door in C#.
|
||||
- **Access control stays on the website**, which has the auth machinery and the admin forms.
|
||||
- **The shard is still never network-reachable.** Nothing here opens a port; the plugin answers
|
||||
requests on the connection it already dialled out on.
|
||||
|
||||
### 2.1 Why not the sidecar, and why not the operator's desktop
|
||||
|
||||
A Rust extractor in the sidecar would need ports of: the Mythic cliloc decompressor, `FileIndex`
|
||||
(including UOP), the ARGB1555 run-length frame decoder, `Body.def`/`Bodyconv.def` translation,
|
||||
`Hues.mul`, and a PNG encoder — weeks of work to re-derive what §1.1 shows already runs. It also
|
||||
cannot do §8: resolving a creature slug to a body id requires being inside ServUO.
|
||||
|
||||
Automating on the operator's desktop (shipping the converter with the installer) removes UOFiddler
|
||||
but keeps a manual step and still cannot do §8. It was considered and rejected.
|
||||
|
||||
---
|
||||
|
||||
## 3. The transport, and the three traps in it
|
||||
|
||||
### 3.1 Assets go over the request/reply path, never the event path
|
||||
|
||||
`link/sidecar/src/app.rs:122` persists **every** non-`pong` event into the SQLite store *and*
|
||||
broadcasts it to every WebSocket subscriber. An asset stream on that path would grow the sidecar's
|
||||
store without bound and fan megabytes out to every connected client, forever.
|
||||
|
||||
`rpc.rs`'s `try_route` consumes a correlated reply and `continue`s **before** either of those
|
||||
happens. So an asset batch is a reply, not an event. This is not a new mechanism — it is the one
|
||||
`char.request`, `account.roster` and `vendor.snapshot` already use.
|
||||
|
||||
### 3.2 One batch outstanding, always
|
||||
|
||||
`BridgeLink.Emit()` enqueues onto a **bounded drop-oldest** queue (`Bridge.QueueCap`, default
|
||||
10,000). It counts **lines, not bytes** — a design that is correct for live events and dangerous
|
||||
for bulk transfer, because 10,000 queued 200 KB replies is 2 GB of shard memory.
|
||||
|
||||
The rule that makes this safe is flow control, not a bigger queue: **the website requests batch
|
||||
*n+1* only after batch *n* has arrived.** Queue depth stays at approximately one. A dropped or
|
||||
lost reply simply times out and the batch is re-requested, which is safe because reading a client
|
||||
file is idempotent and has no world side effects.
|
||||
|
||||
### 3.3 The size ceilings are already fixed, and one of them is missing
|
||||
|
||||
| Limit | Value | Where |
|
||||
|---|---|---|
|
||||
| Sidecar waits for a shard reply | **10 s** | `rpc.rs` `REPLY_TIMEOUT` |
|
||||
| Website waits for the sidecar | **12 s** | `module-uo/server/utils/uoLinkClient.js` `TIMEOUT_MS` |
|
||||
| Sidecar → shard line | 1 MiB | `BridgeLink.cs:283` |
|
||||
| **Shard → sidecar line** | **none** | `shard.rs` uses `read_line` unbounded |
|
||||
|
||||
The first two bound a batch: it must decode, encode, serialise and cross the wire inside ten
|
||||
seconds. The last is a gap this protocol must close — an unbounded `read_line` facing a component
|
||||
that is now deliberately sending large lines is a memory-exhaustion shape we would be inventing
|
||||
ourselves. **Protocol 8 adds an explicit inbound line cap to the sidecar**, set above the largest
|
||||
legal batch and rejecting rather than buffering past it.
|
||||
|
||||
Batches are therefore sized by bytes, not by count, with the emitter cutting a batch short when it
|
||||
would exceed the cap. Base64 costs 33%; the budget must be stated in encoded bytes.
|
||||
|
||||
---
|
||||
|
||||
## 4. The decoders: whose code, and the crash that decides it
|
||||
|
||||
§1.1 found that `Gumps.GetGump(2)` does not fail — it **corrupts the process**. An
|
||||
`AccessViolationException` from `unsafe` pointer code is a corrupted-state exception; on .NET
|
||||
Framework 4.8 it is *not catchable* by an ordinary `try/catch`. In-process, on a live shard, that
|
||||
is a shard crash with players on it.
|
||||
|
||||
Art and animation probed clean across ~66,000 and ~2,000 ids respectively. That is reassuring and
|
||||
it is not a guarantee: the inputs we have not tested are exactly the ones that matter — a shard's
|
||||
own **patched or custom** client files, which is the population this feature exists to serve.
|
||||
|
||||
Two ways to hold this safely:
|
||||
|
||||
**A1 — call ServUO's vendored `Ultima`, isolate the fault.** Fastest to build, and proven for the
|
||||
kinds we need. Requires running the decode where a fault costs one batch rather than the shard:
|
||||
a short-lived child process. That means a binary to deploy, which the overlay (deployed as
|
||||
*source*, compiled by ServUO at boot) has no mechanism for.
|
||||
|
||||
**A2 — own bounds-checked decoders in the overlay.** Roughly 600–900 lines of ordinary safe C#:
|
||||
`FileIndex` (~150), the RLE frame decoder (~60, and it is the same loop, writing to a `ushort[]`
|
||||
instead of through a `LockBits` pointer), the static-art decoder, `Body.def`/`Bodyconv.def`, hue
|
||||
application, and a minimal PNG writer over `System.IO.Compression.DeflateStream` (~80).
|
||||
|
||||
**A2 is recommended**, for four reasons that compound:
|
||||
|
||||
1. **It removes the crash class**, rather than containing it.
|
||||
2. **It removes `System.Drawing` entirely.** ServUO's `Frame` decoder writes ARGB1555 straight
|
||||
into a `Bitmap` via `LockBits`, so System.Drawing is in the *decode*, not just the encode — a
|
||||
Linux/Mono shard needs libgdiplus even to read a sprite. Writing our own pixels and our own PNG
|
||||
makes the feature portable by construction.
|
||||
3. **It removes the UOP gap.** ServUO's vendored `Animations` reads legacy `anim*.mul` only. This
|
||||
client has a full 195 MB `anim.mul` so most bodies resolve — but **gargoyle bodies 666/667
|
||||
returned nothing**, because gargoyles live in `AnimationFrame*.uop`. A player race missing from
|
||||
an asset store built for "player models and everything" is not a caveat, it is a defect.
|
||||
4. **We are already in this business.** §9 writes a cliloc decompressor from scratch regardless.
|
||||
|
||||
A2 also ends the dependency on whatever version of `Ultima` a given ServUO happens to vendor,
|
||||
which is the kind of thing that silently changes under a shard upgrade.
|
||||
|
||||
---
|
||||
|
||||
## 5. Addressing: one key for every asset
|
||||
|
||||
Every asset the bridge can serve is named by a single string key, and the key is the cache key,
|
||||
the hash key, the filename stem and the manifest row id:
|
||||
|
||||
```
|
||||
static/3922 one item graphic
|
||||
static/3922/h33 the same graphic, hue 33 applied
|
||||
land/3 one land tile
|
||||
body/34/a0/d1 creature body 34, action 0, direction 1, first frame
|
||||
body/400/a0/d1/f0..f9 human male, action 0, direction 1, all ten frames
|
||||
cliloc/enu the whole converted string table (not an image)
|
||||
tree/Spawns/Trammel.xml a ServUO tree file (§10)
|
||||
```
|
||||
|
||||
Three properties this shape buys:
|
||||
|
||||
- **Hue is part of the key, not a transform.** `itemId` and `hue` are already on the wire together
|
||||
(`BridgeMarket.cs:582`, `BridgeProfile.cs:314`), so a marketplace listing already knows the exact
|
||||
key for its own picture. Applying hues website-side would mean shipping `Hues.mul` semantics into
|
||||
Node for no gain.
|
||||
- **Depth is expressible without being mandatory.** `body/400/a0/d1` and `body/400/a0/d1/f0..f9`
|
||||
are the same addressing scheme at two depths, which is what lets §11 bulk-import thumbnails and
|
||||
fetch full animations on demand without a second protocol.
|
||||
- **Nothing in the key is client-version-specific**, so a client patch changes an asset's *bytes*,
|
||||
not its name — which is what makes §7's delta work.
|
||||
|
||||
---
|
||||
|
||||
## 6. The manifest, and what the two buttons actually do
|
||||
|
||||
Two stages, which is where **Import** and **Update** come from.
|
||||
|
||||
**Stage 1 — the source gate.** The shard reports a manifest of the client files themselves:
|
||||
size, mtime and content hash of `Cliloc.enu`, `anim*.idx`/`anim*.mul`, `art.mul`/`artidx.mul`,
|
||||
`Body.def`, `Bodyconv.def`, `Hues.mul`. Unchanged since the last import, and nothing else happens.
|
||||
This is the same hash gate the spawn atlas and the cliloc table already use, and for the same
|
||||
reason: the normal case is a restart that changed nothing, and it must cost nothing.
|
||||
|
||||
`anim.mul` is 195 MB and `art.mul` is 148 MB, so the gate is **(size, mtime) first, content hash
|
||||
only when those differ** — a full hash of 343 MB on every status poll would make the admin panel
|
||||
feel broken.
|
||||
|
||||
**Stage 2 — the asset manifest.** For the working set (§11), the shard streams
|
||||
`[{ key, sha256, bytes }]` — no pixels. The website diffs that against what it holds and requests
|
||||
**only the keys whose hash changed**.
|
||||
|
||||
- **Update** = stage 1, then stage 2, then fetch the diff.
|
||||
- **Import** = the same path with the diff skipped and every key fetched.
|
||||
- **A key that has vanished** from the manifest is staged for review, never applied silently —
|
||||
the same rule, and the same reasoning, as a vanished cliloc source or a disappearing atlas
|
||||
facet. An unmounted volume and a deliberate client downgrade look identical from here.
|
||||
|
||||
Clilocs are the exception and stay a **whole-table replace** whenever the file hash changes:
|
||||
the measured cost is 663 ms for 67,496 rows, so per-entry deltas would be complexity bought for
|
||||
nothing.
|
||||
|
||||
---
|
||||
|
||||
## 7. The parser version applies here too
|
||||
|
||||
`spawnAtlasSource.js` carries `PARSER_VERSION` (currently 5) and the cliloc source carries its own,
|
||||
both counted as drift so that a corrected parse reaches an install whose files never change. The
|
||||
asset pipeline inherits the rule and needs it more, not less: a fixed hue application or a
|
||||
corrected frame offset changes the bytes we derive from files that are byte-identical.
|
||||
|
||||
**`EXTRACTOR_VERSION` lives in the plugin**, because the plugin is what derives the bytes, and it
|
||||
is folded into stage 1's gate. Bumping it makes every asset drift, which is correct.
|
||||
|
||||
---
|
||||
|
||||
## 8. Body ids: the part only the shard can do
|
||||
|
||||
The atlas knows creatures by **slug**, derived from type names in `Spawns/*.xml`. The client knows
|
||||
them by **body id**. Nothing in the ServUO tree declares the mapping as data — today an operator
|
||||
bridges it by grepping `Scripts/Mobiles/Normal/<Name>.cs` for `Body =`, which appears variously as
|
||||
a decimal, as hex (`0xD1`), as `Utility.RandomList(35, 36)`, and as an `m_IDs[]` table.
|
||||
|
||||
Inside ServUO the problem does not exist. `BridgeWorld.cs:350` already does exactly the required
|
||||
thing for a different feature:
|
||||
|
||||
```csharp
|
||||
var type = ScriptCompiler.FindTypeByName(name, true);
|
||||
var creature = Activator.CreateInstance(type) as BaseCreature;
|
||||
```
|
||||
|
||||
Construct, read `creature.Body.BodyID`, `Delete()`. Authoritative, no source parsing, and correct
|
||||
for custom creatures a grep would never find.
|
||||
|
||||
**This pass must run on the Core thread** — it constructs and deletes mobiles, which is world
|
||||
mutation — while the decode in §4 must run **off** it. That split is the one genuinely new
|
||||
threading shape in this protocol, and it is why slug→body resolution is its own request kind with
|
||||
its own (small) batch size rather than a step inside asset extraction.
|
||||
|
||||
Constructing arbitrary creature types has side effects: constructors pack items, set skills, start
|
||||
timers. The mitigations are per-type `try`/`catch`, immediate `Delete()`, small batches, and the
|
||||
fact that the whole pass is admin-triggered rather than something that runs at boot.
|
||||
|
||||
---
|
||||
|
||||
## 9. The cliloc decompressor is ours now
|
||||
|
||||
Every modern client ships `Cliloc.*` in the Mythic compressed container — this machine's
|
||||
`Cliloc.enu` is 4,989,921 bytes beginning `E8 79 67 8E`, high byte `0x8E`. ServUO's bundled
|
||||
`Ultima.StringList` implements only the plain layout and throws on it (§1.1), which is also why
|
||||
the shard's own `VendorSearch.GetItemName` is already inert.
|
||||
|
||||
**UOFiddler is released under the Beerware licence**, so porting its decompressor into our
|
||||
GPL-3.0-or-later tree is clean. It lands in the overlay as ordinary C#, alongside §4's decoders,
|
||||
and from that point:
|
||||
|
||||
- No operator installs UOFiddler.
|
||||
- No operator runs `dotnet build` on a converter.
|
||||
- No operator copies a 5 MB file to a server.
|
||||
- `website/server/tools/cliloc-export/` is retired, and `UOFIDDLER.md` is deleted rather than
|
||||
rewritten.
|
||||
|
||||
**What survives untouched is the `custom/` overlay mechanism.** Shard-added items carry cliloc ids
|
||||
no client table has, and ServUO has no server-side notion of a custom cliloc — that is a real gap
|
||||
in the *game*, not an artefact of the manual pipeline, and `CLILOCS.md`'s reasoning for it stands.
|
||||
The base table now arrives over the bridge; overlays still come from a directory the site reads.
|
||||
Measured on the live shard: 16,434 cliloc ids referenced by the script tree, 37 absent from stock.
|
||||
|
||||
---
|
||||
|
||||
## 10. The atlas stops needing a shared filesystem
|
||||
|
||||
Today `SPAWN_ATLAS.md` requires the **website** to read the ServUO tree — "same host, a bind mount,
|
||||
or a shared volume". That is the one place the platform's own rule (only the sidecar bridges the
|
||||
shard) is broken, and it is broken by the component that faces the internet.
|
||||
|
||||
The same transport closes it. `spawnAtlasSource.js` already labels every file it reads with a
|
||||
portable key:
|
||||
|
||||
| Label | Count (stock 57.4) |
|
||||
|---|---|
|
||||
| `Data/Regions.xml` | 1 |
|
||||
| `Data/Locations/*.xml` | 6 |
|
||||
| `Spawns/*.xml` | 13, ~10.5 MB |
|
||||
| `Config/ChampionSpawns.xml` | 1 |
|
||||
| `Data/Decoration/**` | tree |
|
||||
|
||||
So the shard serves `tree/<label>` → bytes over the same batched request/reply path, and
|
||||
`spawnAtlasSource.js` gains a second backend behind its existing interface: **filesystem** (today,
|
||||
kept for same-host installs and for development) or **sidecar** (new, and the default once
|
||||
configured).
|
||||
|
||||
**The parsers do not move.** `spawnAtlasParse.js` is pure, fs-free and CI-covered without a ServUO
|
||||
tree, and every quirk it handles — the two respawn delay units, `:OBJ=` splitting, facet-name
|
||||
reconciliation, the XmlSpawner directive stripping — stays exactly where it is. The shard sends
|
||||
bytes; the website still decides what they mean. That is the same division as §2, and it keeps the
|
||||
sidecar a forwarder here too.
|
||||
|
||||
`SERVUO_PATH` and the `spawn_atlas_servuo_path` setting remain, and select the filesystem backend.
|
||||
|
||||
---
|
||||
|
||||
## 11. What is bulk and what is on demand
|
||||
|
||||
The scope approved is creature art, item art, player models "and everything", against a future
|
||||
project. §1.1's measurements make the sizing question concrete:
|
||||
|
||||
| Kind | Addressable | Bulk? |
|
||||
|---|---|---|
|
||||
| Item statics | **~49,150** | No — on demand, cached, keyed by `itemId` (+ hue) |
|
||||
| Land tiles | **16,384** | No — on demand |
|
||||
| Creature/player bodies, first frame | **1,144** | **Yes** — this is the catalogue |
|
||||
| One body, all actions × directions | **1,050 frames** (body 400) | No — on demand, per body |
|
||||
| All bodies, full animation | ~10⁶ frames | Never |
|
||||
| Cliloc table | 123,490 entries → 67,496 rows | **Yes** — whole-table replace |
|
||||
| ServUO tree files (§10) | ~21 files, ~10.6 MB | **Yes** |
|
||||
|
||||
**The working set is one thumbnail per body, plus the atlas's own creatures.** 1,144 sprites at
|
||||
roughly a kilobyte each is under 2 MB — trivial to import, trivial to re-hash, and it is the set
|
||||
that makes a bestiary, a marketplace listing and a character sheet render.
|
||||
|
||||
Everything deeper is the *same protocol at a deeper key* (§5), fetched on demand and cached. That
|
||||
is what serves the future project without exporting 3.5 GB of someone else's copyrighted client
|
||||
into a database: a viewer that wants body 400's full walk cycle asks for
|
||||
`body/400/a2/d1/f0..f9` and gets it, once, and it is cached from then on.
|
||||
|
||||
**Hued variants are on demand, always.** `static/3922/h33` is generated when something on the wire
|
||||
actually carries hue 33. The cross product of 49,150 statics and ~3,000 hues is not a set anyone
|
||||
enumerates.
|
||||
|
||||
**Gump art is out of scope for Protocol 8** — see §4; it is the one kind whose vendored decoder
|
||||
demonstrably corrupts the process, and A2 would have to reimplement it against the UOP-with-extra-
|
||||
field layout that broke it. It is additive to add later under this same key scheme
|
||||
(`gump/<id>`), which is the point of §5.
|
||||
|
||||
---
|
||||
|
||||
## 12. Where it lands on the website
|
||||
|
||||
Images are written by module-uo into the upload directory. `ctx.uploads`
|
||||
(`{ upload, UPLOAD_DIR, MIME_EXT }`) is **already** exposed to modules and
|
||||
[`MODULE_API.md:626`](../website/MODULE_API.md) already names its consumer as "atlas art import",
|
||||
so no `MODULE_API_VERSION` bump is needed to store them.
|
||||
|
||||
- `shard_spawn_creatures.art` stops being NULL-by-default and starts being filled by the import.
|
||||
- A new asset table carries `key`, `sha256`, `bytes`, `width`, `height`, `imported_at` — the
|
||||
manifest side of §6, and what makes an Update a diff rather than a re-download.
|
||||
- **The operator-supplied `spawnAtlas.art.json` map stays supported** and continues to win over an
|
||||
imported asset. An operator who has drawn their own creature portraits must not have them
|
||||
overwritten by a sprite rip on the next Update.
|
||||
|
||||
Licensing is unchanged and the reasoning is unchanged: these are the operator's own client files,
|
||||
extracted on their own host, for their own shard. Nothing is committed, nothing ships in a repo,
|
||||
and nothing is redistributed. What changes is only that the extraction stopped requiring a GUI on a
|
||||
desktop.
|
||||
|
||||
---
|
||||
|
||||
## 13. Visibility
|
||||
|
||||
New surfaces over shard data are admin-toggleable with an operator-set audience, and this is no
|
||||
exception. Asset serving is gated like every other shard read: a `requireFeature` gate, an
|
||||
audience, and 404-not-403 when the feature is off, so a disabled feature does not advertise itself.
|
||||
|
||||
The default is the least surprising one: assets are as public as the page that uses them. A
|
||||
bestiary that is already anonymous does not become staff-only because its pictures arrived over a
|
||||
new pipe.
|
||||
|
||||
---
|
||||
|
||||
## 14. Routes and commands added
|
||||
|
||||
**Loopback (shard ↔ sidecar), all request/reply:**
|
||||
|
||||
| Command | Reply | Purpose |
|
||||
|---|---|---|
|
||||
| `assets.sources` | `assets.sources.ok` | Stage 1: client file manifest + `EXTRACTOR_VERSION` |
|
||||
| `assets.manifest` | `assets.manifest.ok` | Stage 2: `[{key, sha256, bytes}]`, paged |
|
||||
| `assets.fetch` | `assets.fetch.ok` | Content for an explicit key list, paged |
|
||||
| `assets.bodies` | `assets.bodies.ok` | Slug → body id (§8, Core thread) |
|
||||
| `cliloc.table` | `cliloc.table.ok` | The converted table, paged |
|
||||
| `tree.manifest` / `tree.fetch` | `.ok` | §10, the ServUO tree files |
|
||||
|
||||
**Sidecar REST** mirrors those one for one under `/assets/*`, `/cliloc`, `/tree/*`, carrying
|
||||
`X-UOLink-Version: 8` and forwarding verbatim.
|
||||
|
||||
**Website admin** (`Admin → Shard`, admin-only): status, **Import**, **Update**, approve/reject for
|
||||
a vanished key, and the existing path settings. Every action to the admin activity log, as
|
||||
`shard.assets.*`.
|
||||
|
||||
---
|
||||
|
||||
## 15. Cross-repo obligations
|
||||
|
||||
`PROTOCOL_VERSION` goes **7 → 8** in `link/sidecar/src/main.rs`, and in the **same PR**
|
||||
`servuo-plugins/overlay.toml` — the installer refuses to pair a sidecar and an overlay that
|
||||
disagree, so a split bump means the next bundle silently fails to compose.
|
||||
|
||||
| Repo | Work |
|
||||
|---|---|
|
||||
| `servuo-plugins/` | The decoders (§4), the cliloc decompressor (§9), body resolution (§8), the request handlers, `overlay.toml` |
|
||||
| `link/` | Six command families forwarded, the REST surface, **the inbound line cap (§3.3)**, `PROTOCOL_VERSION` |
|
||||
| `module-uo/` | Client calls, asset store, the atlas source backend (§10), cliloc ingest, admin surface |
|
||||
| `website/` | None expected — `ctx.uploads` already suffices (§12) |
|
||||
| `docs/` | This file; rewrite `CLILOCS.md` §Converting and `SPAWN_ATLAS.md` §Artwork + §Configuring; **delete `UOFIDDLER.md`** |
|
||||
| `installer/` | None expected; the bundle pairing already enforces §15 |
|
||||
| `android-app/` | Consumes images by URL; no parity gate expected until a screen shows one |
|
||||
| `integration-kit/` | A chapter note only — this is UO-specific and teaches nothing about the module contract |
|
||||
|
||||
---
|
||||
|
||||
## 16. Phases
|
||||
|
||||
| # | Scope | Repos |
|
||||
|---|---|---|
|
||||
| 0 | Spike: A2's decoders against this machine's client — statics, land, one body, the Mythic cliloc — proving output matches UOFiddler's byte for byte | servuo-plugins |
|
||||
| 1 | The transport: `assets.sources`, flow control, the sidecar line cap, `EXTRACTOR_VERSION`, protocol bump | servuo-plugins, link |
|
||||
| 2 | Clilocs end to end; retire the converter and `UOFIDDLER.md` §Part 1 | all |
|
||||
| 3 | Body resolution (§8) + the 1,144-body catalogue; `shard_spawn_creatures.art` filled | servuo-plugins, module-uo |
|
||||
| 4 | Item statics and land on demand, hued keys, the cache | servuo-plugins, module-uo |
|
||||
| 5 | Deep animation keys (`body/<id>/a<n>/d<n>/f<n>`) for the future project | servuo-plugins, module-uo |
|
||||
| 6 | The atlas over the sidecar (§10); shared-filesystem requirement retired | module-uo |
|
||||
| 7 | Admin surface, Import/Update, approve/reject, activity log | module-uo |
|
||||
| 8 | Docs pass across five repos; live walk on the real rig | docs |
|
||||
|
||||
Phase 0 exists because §4 chose to own the decoders, and the honest way to hold that choice is to
|
||||
prove byte-identical output *before* building six phases on top of it.
|
||||
|
||||
---
|
||||
|
||||
## 17. Decisions still open
|
||||
|
||||
1. **§4: A2 (own decoders) versus A1 (vendored `Ultima` in a child process).** A2 is recommended
|
||||
and the phase plan assumes it. It costs a spike and ~600–900 lines; it buys the crash class, the
|
||||
libgdiplus dependency, the UOP gap and the vendor-drift risk all going away at once.
|
||||
2. **§11: gump art deferred.** Confirm that paperdoll and equipment gump art is genuinely
|
||||
out of scope for 8, given it is the one kind whose existing decoder crashes.
|
||||
3. **§11: the bulk working set is one frame per body (1,144).** Confirm that "player models and
|
||||
everything" is served by on-demand depth rather than a bulk animation export.
|
||||
4. **§13: the default audience** for asset serving — inheriting the using page's audience is
|
||||
proposed; the operator sets the policy either way.
|
||||
Reference in New Issue
Block a user