feat(bridge): what an event borrows, and the two one-shots (Phase 12b) #24

Merged
whitlocktech merged 1 commits from feature/events-p12b-borrowed-and-oneshots into edge 2026-09-07 16:23:00 +00:00
Member

Events Phase 12b, shard half. One of five: website (website#194), servuo-plugins, link (#—), Module-uo (#—), docs (docs#222). Spec of record: docs/link/v7.md §11–§14.

overlay.toml stays at protocol = 7 — 12b amends 7 in place, tolerable for the single reason 6 and 7 already are: nothing is released from edge.

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. That is the exact failure the lease framing exists to make impossible.

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.

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 droppedAddItem failing on a full backpack would otherwise leave it in the world 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 (dotnet build Scripts.csproj -c Release: 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 Inactive → Active;
  • 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.

Scaffolding

BridgeRigDriver.cs gains spawnerlist, propset, propread and seasonlist. propset is the one that matters: 11b added configset because Config.Set has one caller in the whole tree, so nothing could drift a config lease. A spawner is the opposite — a GM drifts one with [props in four seconds — but the rig has no client, so it needs the same door.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Events Phase 12b, shard half. One of **five**: `website` (website#194), `servuo-plugins`, `link` (#—), `Module-uo` (#—), `docs` (docs#222). Spec of record: [`docs/link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/222) §11–§14. **`overlay.toml` stays at `protocol = 7`** — 12b amends 7 in place, tolerable for the single reason 6 and 7 already are: nothing is released from `edge`. ## 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. That is the exact failure the lease framing exists to make impossible. 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. `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 `TimeSpan`s, 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 in the world 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 (`dotnet build Scripts.csproj -c Release`: 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 `Inactive → Active`; - 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. ## Scaffolding `BridgeRigDriver.cs` gains `spawnerlist`, `propset`, `propread` and `seasonlist`. `propset` is the one that matters: 11b added `configset` because `Config.Set` has one caller in the whole tree, so nothing could drift a config lease. A spawner is the opposite — a GM drifts one with `[props` in four seconds — but the rig has no client, so it needs the same door. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-07 13:10:50 +00:00
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
whitlocktech merged commit 41ac2ccaaa into edge 2026-09-07 16:23:00 +00:00
whitlocktech deleted branch feature/events-p12b-borrowed-and-oneshots 2026-09-07 16:23:01 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/servuo-plugins#24
No description provided.