feat(asset-bridge): phase 0 spike — the decoders, from inside a live shard

docs/link/v8.md §16 phase 0. §4 chose to CALL ServUO's vendored `Ultima`
rather than reimplement it, on the evidence of a PowerShell probe against a
stock client — neither the process nor the client the extractor will run in.
This runs the same decoders from inside a running ServUO 57.4 against a
client broken in 21 catalogued ways, and it found more than a crash.

Adds, all under tools/ and therefore never deployed:

  * BridgeAssetProbe.cs — the sweep, plus BridgeAssetValidator, a prototype
    of the validate-before-calling response chosen for §4.2's residual risk.
    Runs off the Core thread, snapshots Race.AllRaces on it, and writes the
    id it is ABOUT to touch to a checkpoint file before every call.
  * BridgeMythicCliloc.cs — the §9 Mythic cliloc reader in net48 C#, ported
    from UOFiddler (Beerware) with every file-derived index bounds-checked.
    Phase 2 promotes this into overlay/.
  * patch_client.ps1 — builds the patched client in five tiers. Hashes every
    file it touches in the SOURCE before and after and aborts on a change.
  * an `assetprobe` verb on BridgeRigDriver, so stock and patched can be run
    against one boot rather than two shard processes.

The findings are written up in tools/scaffolding/README.md. The four that
change what phase 1 has to build:

  * FileIndex's UOP constructor ends `MulPath = uopPath`, so artLegacyMUL.uop
    wins outright and art.mul/artidx.mul are never opened on a current
    client. A validator bounding offsets against art.mul is not approximate,
    it is nonsense — the first run refused 34,299 good statics on that
    mistake, and every refusal looked like a real finding.

  * 22,102 WRONG PICTURES on a stock, unmodified client. Empty UOP index
    slots read `lookup 0, length 0`; Seek treats that as a hit, and
    LoadStatic decodes zero bytes into a shared buffer it reuses, only ever
    grows, and fills from a Read whose return value is discarded — so the id
    renders the previously-decoded asset. The mul path does not do this
    (artidx stores -1), which is why the earlier probe counted 32,766 of
    them as "ok". A bulk import that trusted the library would have written
    22,102 duplicate images under ids that have no art.

  * The validator caught all 8 record-level defects — 7 of which the library
    rendered without raising anything, including a verdata lookup past
    verdata.mul's own end (Verdata.Seek is bounds-checked nowhere) and an
    8000x8000 bitmap allocated from two bytes in a file. It refused NOTHING
    across 49,151 statics and 16,384 land tiles on the stock client, which
    is the number that makes the boundary defensible.

  * §4.1's crash reproduces in-process: one Ultima.Gumps.GetGump(2) and the
    ServUO process disappeared — no catch reached, no console line, the
    checkpoint file the only record. "Nothing calls Ultima.Gumps" is now an
    earned safety rule.

§9 is proven: 123,490 entries in 218 ms, byte-identical to UOFiddler's own
output, with no UOFiddler installed and nothing copied to a server.

Not covered, and named as phase 1 work: the animation path has no validator
at all, and the patched wolf decoded something else in silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-10 02:55:37 -05:00
committed by Claude
parent a740365131
commit 0ce92152a1
5 changed files with 2593 additions and 1 deletions

View File

@@ -155,6 +155,14 @@ namespace Server.Custom
case "partprobe":
BridgeParticipationProbe.Run(null, Arg(parts, 1), Int(Arg(parts, 2)), Int(Arg(parts, 3)));
break;
// Asset Bridge phase 0. Here for the same reason as partprobe, and for one more:
// the point of that spike is comparing the STOCK client's answers with a patched
// client's, and `AssetProbeOnStart` can only ever run whichever one the config
// names. Driving it from here runs both against a single boot, so a difference
// between them cannot be a difference between two shard processes.
case "assetprobe":
BridgeAssetProbe.Begin(null, Arg(parts, 1) ?? "all", Arg(parts, 2));
break;
// Phase 12a. `world.despawn` answering `gone` rather than `removed` is the
// path a player takes every time they kill an event creature, and it is the one
// outcome the rig cannot reach by asking the bridge: every bridge verb that