feat(asset-bridge): the shard's own files stop needing a shared filesystem (Phase 7)
The spawn atlas was the one place the platform's rule -- only the sidecar
bridges the shard -- was broken, and it was broken by the component that faces
the internet: SPAWN_ATLAS.md required the website to read the ServUO tree off a
bind mount or a shared volume. This serves those files over the loopback link
instead (docs/link/v8.md 10).
The measurement came first and changed the shape. 10 said the shard would serve
`tree/<label>` -> bytes; 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 anywhere. Two files on a STOCK tree are in that state.
So a file crosses as 512 KiB chunks, each gzipped: tree/Spawns/trammel.xml/c0
and so on, which is 5's depth scheme doing the same job it does for
body/400/a0/f0 and needing no protocol change to do it. The chunk is the bound
and the compression is only the saving -- nothing guarantees an operator's files
compress, so the ceiling has to hold when they do not, and a 512 KiB chunk that
refuses to compress is still ~683 KiB of base64, inside the wire cap that
AssetBatchBytes' deliberate factor of two leaves room for.
It is a `tree` FAMILY on assets.fetch rather than 14's separate tree.* commands:
phase 5 had already learned that the command is the transport and the family is
a property of the key, and assets.manifest is generalised here the same way.
That reuses the single slot, the paging envelope, the key ceiling and the
mid-import guard -- and leaves `link` with nothing to do for the third phase
running.
But it gets its OWN consent, Bridge.TreeEnabled. AssetsEnabled is an operator
agreeing the website may read their EA-licensed UO client; this is the shard's
own configuration, which they wrote, and which the public bestiary is built
from. One switch could not express both, and the thing that would silently
disappear for an operator who declined the first is their spawn atlas. So the
consent check moved into the family lookup, and assets.sources answers whenever
either plane is on, reporting `families` filtered to what is actually enabled --
which is how a tree-only shard's website discovers there is anything to ask for.
Two defects found, and which harness found which is the part worth keeping:
- An empty `catalog` is not an absent one. `expected != null` refused every
fetch from a caller that sent "", with a sentence naming no catalog at all.
Found by an offline probe that passed one by accident.
- GZipStream writes NOTHING for zero bytes of input -- the header is emitted
lazily, so a stream opened and closed without a write yields a zero-length
buffer rather than the 20-byte empty member. Stock ServUO ships two empty
decoration files, so this broke every import off an untouched tree. The
offline probe reassembled all 141 files and reported success, because .NET's
own decompressor reads an empty stream as empty data and the chunk's
declared length (0) and hash (of nothing) both agreed. Only the live walk,
through a reader on another runtime, disagreed.
Measured end to end against a live shard, the real sidecar and the website's own
reader: 141 files, 11,895,427 bytes, 158 chunks, 3 pages, 1.33 MB on the wire,
512 ms; every file byte-identical to disk; the atlas built over the bridge
identical to the one built off it. A drift check is the manifest alone -- 32 KB,
~70 ms, no file bytes.
The label set is this shard's, never the caller's: a fetch resolves against the
set the shard itself enumerated, and tree/../../Scripts/..., Config/Bridge.cfg
and Saves/Accounts/accounts.xml are all answered `absent` before a path is built
out of them.
Protocol stays 8 and EXTRACTOR_VERSION stays 3 -- this family derives nothing,
it forwards an operator's own file unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -340,6 +340,30 @@ AssetScanMs=3000
|
||||
AssetPlayerDirection=0
|
||||
AssetCreatureDirection=1
|
||||
|
||||
# The tree plane (docs/link/v8.md §10, phase 7). A THIRD switch, for a third consent:
|
||||
# the asset switch above is about this host's UO client, which came from EA. This one is
|
||||
# about the shard's own configuration -- Spawns/*.xml, Data/Regions.xml,
|
||||
# Data/Locations/*.xml, Config/ChampionSpawns.xml and Data/Decoration/**.cfg -- which is
|
||||
# the operator's own work and is what the website's spawn atlas is built from. Before
|
||||
# protocol 8 the website read those files off a shared filesystem; that was the one place
|
||||
# the platform's own rule (only the sidecar bridges the shard) was broken, and broken by
|
||||
# the component that faces the internet. Turning this off closes the bridge route and
|
||||
# leaves that shared-filesystem path as the only way an atlas can be built.
|
||||
#
|
||||
# Reads only, and only those five groups. Nothing here joins a path the website sent: a
|
||||
# request names a label this shard itself enumerated, or it is refused.
|
||||
TreeEnabled=true
|
||||
|
||||
# How much of a tree file one chunk carries, BEFORE compression. Chunking is not an
|
||||
# optimisation here, it is what makes a spawn file transferable: a stock trammel.xml is
|
||||
# 4.03 MB, the sidecar discards any inbound line over 1 MiB, and the whole file as one
|
||||
# base64 row would time out and be re-requested forever with no error anywhere. Each
|
||||
# chunk is gzipped (a spawn file compresses ~18x, so a chunk is typically 40 KB on the
|
||||
# wire), but the BOUND comes from the chunk rather than the compression, because nothing
|
||||
# guarantees input compresses at all. Clamped to [64 KiB, 512 KiB]: at the ceiling a
|
||||
# worst-case incompressible chunk is ~683 KiB of base64, which still fits the wire.
|
||||
TreeChunkBytes=524288
|
||||
|
||||
# The test scaffolding in tools/scaffolding/ reads its own flags from this file
|
||||
# (SeedOnStart, CensusOnStart, ProbeOnStart). They are absent here on purpose:
|
||||
# Config.Get returns the default of false when a key is missing, so a deployed
|
||||
|
||||
Reference in New Issue
Block a user