0ce92152a1da6ffd4eff7e32ce0c12d1764d91bb
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 0ce92152a1 |
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
|
|||
| f6a86ff8c2 |
feat(bridge): what an event borrows, and the two one-shots (Phase 12b)
The shard half of protocol 7 part b. Two lease planes whose value lives on something already in the world, and two verbs that cannot be taken back. A LEASE HERE MUST BE PERSISTED, AND THE CONFIG PLANE'S MUST NOT 11b's fail-safe is stated plainly in its own header: a lease that never reaches disk means a shard restart is a FREE restore. That argument depends entirely on the leased value being memory-only too, and here it is not. A spawner is an Item and is in the world save; a seasonal entry is written to `Saves/Misc/SeasonalEvents.bin` by ServUO's own `EventSink.WorldSave`. So a restart does not put either back -- it puts the CHANGE back and throws away the deadline timer that was going to undo it, leaving the world at the leased value with nothing here remembering it is borrowed. So the Bridge gains its THIRD save file, `Saves/Bridge/Leases.bin`, written by the same `EventSink.WorldSave` that writes what it describes, with deadlines re-armed at load. A deadline that passed while the shard was down fires AT ONCE: the promise was "back at baseline by then", and extending it would silently turn a two-hour lease into however long the outage was. Config holds are still not written down -- the same argument, applied to planes where its premise is false. A TARGET IS A SERIAL OR A UniqueId, AND BOTH ARE NEEDED A serial is what `[props` shows a GM. An `XmlSpawner.UniqueId` is what the shard's own `Spawns/*.xml` carry -- and it is not a convenience: a dropdown built from serials is IMPOSSIBLE, because serials are assigned when the world is built and nothing off-shard knows them. A lease addressable only by serial could have no authoring list at all. `Spawner` and `XmlSpawner` share all four property names, which is a fact about this tree rather than a convenience: the spawn files load as XmlSpawners while `[add spawner` makes the native one. And it is `MaxCount`, not the `Amount` EVENTS_PLAN.md named -- there is no such property. `MinDelay`/`MaxDelay` are TimeSpans, so the wire carries seconds. The allowlist is checked against the object's OWN type, which is the sentence the whole plane rests on: a serial is a number a caller chooses, so that check is all that stands between `Spawner.MaxCount` and any item on the shard. Reflection is bounded three ways -- the pair must be in the catalog, the property must carry `CommandProperty` (so this can never reach further than `[set` could), and its CLR type must be one this file renders. THE SELF-CHECK, AND THE ONE FAILURE NO PROBE CAN CATCH §N10 in full: a config key is probed live (write, read back, restore) because there is exactly one of it. A property CANNOT be -- thousands of instances and no canonical one, so probing would mean writing to somebody's spawner at boot. What is verified instead is everything verifiable without touching the world. And `TreasuresOfTokuno` is excluded by name, because `IsActive()` reads its own `DropEra` rather than `Status`: the write succeeds, the value reads back, a compare-and-set restore passes, and the capability does nothing at all. That is N10's "capability that lies" in its purest form and the only way to find it is to read the source. §G also called this toggle "small and safe" -- it is safe, but `OnStatusChange()` generates or removes world content for six of the eight. THE ONE-SHOTS Who receives a grant is answered HERE. The website has the list too, but a module cannot read core's tables, so the alternative was a new core surface handing participants to a module. Not needed: 11b's participation ledger already holds them, keyed by the same serials. A run with no ledger is a 404; a run whose ledger is open and empty is a 200 with `granted: 0`, because an event nobody attended still happened. An undeliverable grant is DELETED rather than dropped -- `AddItem` failing on a full backpack would otherwise leave it at (0,0). A save stops the world, so it is rate-limited rather than capped, counting ServUO's own autosave as the last one. Refused, never queued: a queued save would land at a moment nobody chose. VERIFIED Compiles clean (0 warnings, 0 errors). Then a full walk on the real local ServUO 57.4 world against the release sidecar: - all six catalog keys survive the boot self-check; `current` is correctly absent on the targeted rows and filled when a target is named; - a spawner reads the same by UniqueId and by serial; - TWO RUNS ON TWO SPAWNERS BOTH SUCCEED while a second run on the SAME spawner is refused -- the whole reason for the targeted ref; - a GM edit behind the plane's back yields `lease.drifted` and the world is left at 55, not reverted; a clean release restores to baseline; - ToT refused with its own reason, a bad status refused, Fellowship toggled; - grant: no ledger 404, empty ledger 200 `granted: 0`, unknown item 400, over the stack bound 400; save 200 then 429 inside the interval; - BOTH HOLDS AND BOTH LEASED VALUES SURVIVE save + clean shutdown + restart, the deadlines re-arm, and a release across the restart still compare-and-sets; - with only a CONFIG lease held, `Leases.bin` is 8 bytes and names nothing; - refusals: targeted-with-no-target, untargeted-with-a-target, out of range, over 30 days, a target that is not there, and a `ChainChest` refused as a spawner; - 90 seconds becomes `00:01:30` and the baseline reads back as 18000; - a deleted target reads `unreadable` and releases `targetGone: true`. The test world was never saved after the deliberate deletion, so it is intact. Refs: docs/link/v7.md §11-§14, docs/website/EVENTS_PLAN.md Phase 12b Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| 0182732d63 |
feat(bridge): the world verbs an event owns (protocol 7, Phase 12a)
Five verbs an author sees -- creatures, an enhanced "boss", an oracle NPC, a temporary gate, decoration -- and ONE command family underneath them, because every one of them ends in the same sentence: an object exists, and this run owns it. `world.spawn` / `world.despawn` / `world.owned` carry a `what` discriminator, and the per-verb differences are fields rather than kinds. The ownership registry is PERSISTED, and that is forced rather than chosen. A spawned creature is in the world save, so it survives the restart that proves a town-crier line gone -- which already rules out reconcile-by-boot-stamp. But the record of which run owns which serial has nowhere else to live: in memory it is lost in the restart the creatures survive, and only in the website's ledger it is not held here at all, so `world.despawn` would delete whatever serial it was handed and "never touches a creature it did not create" would have no mechanism behind it. So the Bridge gains its second persisted file beside `Participation.bin` -- written by the same world save as the objects it describes, so the two cannot get out of step. The oracle is ours rather than `XmlSpawner2.XmlDialog`'s, and that engine is the reason for both halves of the decision. Its `SpeechEntry` is the evidence the shape is right -- `Text` plus comma-separated `Keywords`, a keyword-less entry as the greeting, a proximity range, a conversation lock. It is also why not to build on it: `SpeechEntry` carries an `Action` string, XmlSpawner's command-scripting language, which would leave an arbitrary-command field one step from an event author. `Mobile.OnMovement` (delivered to every mobile in range -- the `HandlesOnMovement` filter applies only to Items) and `Mobile.HandlesOnSpeech`/`OnSpeech` are native virtuals and are all it needs. Every `Bridge.EventsMax*` REFUSES rather than clamps, on `LeaseMaxDurationSec`'s argument from 11b: the shard's bound exists for the case where the website is wrong. `Bridge.EventsEnabled` gates all of it -- spawning is the same consent 11b introduced that switch for, not a third one. Decoration carries an `itemId`, because `Static` accounts for 5031 of the tree's decoration placements under 1992 different graphics: for that class the graphic IS the identity. Never applied to a `BaseAddon`, whose own ItemID is not what a player sees. Containers are refused outright -- teardown would delete whatever a player had left inside. `tools/scaffolding` gains `worldgone <serial>`, which deletes an object behind the registry's back. It is the one outcome the rig cannot reach by asking the bridge -- every bridge verb that removes an object also drops its row -- and it is what a player's sword does every time they kill an event creature. Verified on a real ServUO 57.4 world (206k items, 42k mobiles) against the release sidecar: all five verbs place; every ceiling refuses; a container and an unknown type refuse; one run cannot despawn another's object; the registry and its objects both survive a save and a clean restart (`pruned: 0`); a creature deleted behind the registry's back comes back `gone` rather than `removed`; and a five-second gate is collected by the shard's own deadline with `world.expired` on the wire. Refs: docs/link/v7.md, docs/website/EVENTS_PLAN.md Phase 12a Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| 63a7dc4374 |
feat(bridge): lease deadlines and the participation ledger (Phase 11b)
Protocol 6 amended in place. Two mechanisms behind one new default-off gate,
`Bridge.EventsEnabled` -- deliberately not `AdminWriteEnabled`, because enabling
the admin plane is consenting to staff moderation from a screen a human is
looking at, and this is consenting to the world being changed and watched on a
schedule, unattended.
BridgeLeases: a live config value held for a bounded time, with the deadline
honoured on the shard whether or not the website is heard from again, and a
compare-and-set restore that reports `drifted` rather than overwriting a GM's
deliberate change. Memory-only -- nothing calls Config.Save() -- so a restart is
a free restore.
BridgeParticipation: presence in a declared area plus kill credit inside it,
keyed by character serial, persisted in the world save. The Bridge's first
persisted state, because a run spans hours and an in-memory tally would regress
every attendee's score after one restart. Its snapshot is also the first handler
that DEFERS, which makes `bridge.busy` reachable for the first time.
And it immediately found a defect in 11a: BridgeIdempotency.Busy built its frame
with Begin("bridge.busy") and then appended a diagnostic `.Str("kind", ...)`, so
the object carried two `kind` fields and every JSON parser takes the last. The
sidecar answered 200 instead of 425. Renamed `busyKind`.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| 1dd490b483 |
fix(bridge): make the market sweep notice a vendor running out of gold
`BridgeMarket.Signature()` diffs shop name, owner, map, coordinates and the item/price list -- the things a LISTING is made of. Protocol 5 added a `fees` block to the frame and the change detector never learned about it. So a vendor quietly running down its gold altered nothing the sweep compared, emitted no frame, and `uo.vendor.expiring` -- the notification whose entire subject is a vendor running out of gold -- could fire only by coincidence: when somebody happened to reprice an item on a shop that was already broke. Proved on the engagement Phase 11b live rig by setting a vendor's held gold to zero and watching no frame follow. The signature carries the DERIVED values, `exempt` and `periodsRemaining`, not the raw ones. An integer division moves only when the shard's own answer to "is this vendor in danger" moves; `HoldGold` changes on every sale and `NextPayTime` on every tick, and keying on either would re-emit a fat listing frame for a shop whose listings had not changed. Emit CADENCE, not frame shape: no field added, PROTOCOL_VERSION untouched, and `overlay.toml` unchanged. The general form is worth carrying forward -- a sweep-based kind has a change detector, and a field added to the frame but not to the detector ships correct and arrives never. Also adds `tools/scaffolding/BridgeRigDriver.cs`: the shard driven from outside the game over a polled command file. A walk asserts what happened BETWEEN two steps, so the steps have to be separated by the observer rather than by a hard-coded delay -- and ServUO's console takes a fixed verb set, so `[p5probe` cannot be typed at a headless shard at all. Never deployed; `deploy.ps1` copies only `overlay/`. The README gains the two ServUO facts the walk cost a rebuild each to learn: a condemned house cannot be refreshed, and only a clean shutdown emits. Co-Authored-By: Claude <noreply@anthropic.com> |