docs(link): protocol 7 -- what an event owns, and the split that carries it

`link/v7.md` is the spec of record: one command family for five verbs, the
persisted ownership registry and why it is forced rather than chosen, the oracle
that reuses ServUO's own dialogue vocabulary without its command-scripting
field, and the decoration index.

`EVENTS_PLAN.md` splits Phase 12 into 12a and 12b (org lead, 2026-09-07), on the
line section G already draws between what an event OWNS and what it BORROWS. Ten
verbs, four repos and a protocol bump is one review and one walk too many, and
the halves prove different things. One protocol version across both, the 11a/11b
shape.

Three corrections the survey forced on `EVENTS.md`:

The config lease catalog does NOT grow in 12b, and the plan's promise of "the
rest of the allowlist" was written before anyone counted. Measured on ServUO
57.4: 156 non-Bridge `Config.Get` call sites; 82 sit outside a field
declaration, but all but four of those are inside a `Configure()` or a static
constructor and are cached at boot exactly as the field initialisers are. The
live, event-useful reads are `PlayerCaps.SkillCap` (11b shipped it) and vendor
bribe decay. So 12b's lease work is object-property leases.

Section G calls the seasonal toggle "a nine-value enum". `EventStatus` has THREE
values; it is `EventType` that has nine entries. The verb is a three-state
toggle over nine named events, which is a different form to author and to cap.

Section G costed the reopening at one protocol bump. It is two: the idempotency
guarantee had to land before the verbs that depend on it, which is v6's own
argument.

`README.md` gains rows for v6 and v7. v6's was missing -- the index has been one
protocol behind since 11a.

Refs: EVENTS_PLAN.md Phase 12a

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-07 01:52:34 -05:00
parent 5fe4305502
commit c53cf9fce5
4 changed files with 415 additions and 20 deletions

View File

@@ -1423,26 +1423,122 @@ reachable.
### Phase 12 — UO wave 2: the world verbs (`servuo-plugins` + `link` + `module-uo` + `docs`)
**§N1 answered 2026-09-01 — no longer gated**, and taken in full, so the item grant row below
stands. The capability set the two UO programs demonstrate, each cap-bounded, ledgered, and
either owned or borrowed:
stands.
**Split into 12a and 12b (org lead, 2026-09-07)**, on the line §G already draws: what an event
**owns** and what it **borrows**. Ten verbs, four repos and a protocol bump is one review and one
walk too many, and the two halves prove different things — 12a proves a run can put things in the
world and get all of them back, 12b proves it can change something it did not create and give that
back unchanged. They land as **one protocol version**: 12a bumps to **7**, 12b amends 7 **in place**
on `edge`. That is the 11a/11b shape, and it carries the same hazard §7 of `v6.md` states — an
overlay and a sidecar both declaring 7 are interchangeable only within one side of the 12b merge —
tolerable for the same single reason and no other: nothing is released from `edge`, so the bundle CI
never sees two meanings of 7.
`installer` is not in this phase, for the reason Phase 11 records: no protocol version is hardcoded
anywhere in it.
#### Phase 12a — what an event OWNS
Five verbs that put something in the world, ledger its serial, and delete it at teardown.
| Verb | Cap dimension | Author's action |
| --- | --- | --- |
| Named, hued creatures from the atlas's ~800 constructible types | `uo.creatures` | `uo.creature.spawn` |
| "Simple" boss variants, as event-owned creature templates | `uo.bosses` | `uo.boss.spawn` |
| Oracle NPCs with scripted dialogue | `uo.npcs` | `uo.npc.place` |
| Temporary gates | `uo.gate.minutes` | `uo.gate.open` |
| Temporary decoration | `uo.decor` | `uo.decor.place` |
**One command family, five author verbs** (org lead, 2026-09-07). Every row above ends in "an object
exists and this run owns it", so the wire carries `world.spawn` / `world.despawn` / `world.owned`
with a `what` discriminator, and the per-verb differences — a boss's stat multipliers, an oracle's
lines, a gate's target and deadline — are **fields rather than kinds**. One ledger shape, one
teardown path, one reconcile, instead of five near-identical ones in three repos. An *author* still
sees five verbs, because five is what they are: the discriminator is a wire detail, and a dropdown of
`what` would be a worse form than five clearly-named steps.
**The caps are the module's, never core's** (org lead, 2026-09-07). `uo.creatures` and the rest are
declared by `module-uo` through `registerEventBudgets`, exactly as `uo.broadcasts` already is; core
meters whatever dimensions a module declares and holds no UO knowledge — which is the whole of what
§F means by game-agnostic. The shard additionally carries its own `Bridge.EventsMax*` ceilings and
**refuses rather than clamps**, on `Bridge.LeaseMaxDurationSec`'s argument from 11b unchanged: the
shard's bound exists for the case where the website is wrong, and being loud about it is its value.
**Ownership is persisted, and that is forced rather than chosen.** A spawned creature lives in the
world save, so unlike a crier line it *survives* a restart — which already means
`reconcileByBootId` is wrong here, for the reason it was wrong for the participation ledger, and
reconcile has to ask. But the record of *which run owns which serial* has nowhere else to live. Held
in memory it is lost in the restart the creatures survive, orphaning them. Held only in the website's
ledger it is not held on the shard at all, so `world.despawn` would delete whatever serial it was
handed — and "an event never touches a creature it did not create" is the sentence the boss verb is
built on. So the Bridge gains its **second** persisted file, beside `Participation.bin`.
**The oracle is our own, and ServUO's own dialogue engine is the reason for both halves of that.**
`XmlSpawner2.XmlDialog` already implements exactly the vocabulary this verb wants — `Text` plus a
comma-separated `Keywords` list, an entry with no keywords being the greeting, a proximity range, a
per-player conversation lock — which is evidence the shape is right rather than invented. It is
also the reason not to build **on** it: `SpeechEntry` carries an `Action` string, XmlSpawner's
command-scripting language, and routing authored dialogue through XmlDialog would leave an
arbitrary-command field one step from an event author. That is the `[set` §G excludes, arriving by
the back door, in a subsystem we do not own and a shard can switch off. `Mobile.OnMovement`
(delivered to **every** mobile in range — the `HandlesOnMovement` filter applies only to Items,
`Server/Mobile.cs:3369` against `:3375`) and `Mobile.HandlesOnSpeech`/`OnSpeech` are native virtuals
and are the whole of what the verb needs.
**Decoration comes from the shard's own decoration files.** `Data/Decoration/**/*.cfg` names every
item type the shard already uses as decoration, with its item id (`LargeCrate 0x0E3C`). The atlas
build indexes them, so the dropdown is derived from the operator's own tree and resolves with the
shard down — and the list is "decoration" by the shard's own definition rather than by our taste.
The plugin validates the type independently, because it cannot trust the website.
**Ships:** an event can populate a venue — creatures, a boss, an oracle, a gate to reach it and
decoration around it — every piece cap-bounded, ledgered by serial, and gone at teardown.
**Verify:** the whole rig. A run that spawns one of each of the five; a restart mid-run proving the
ownership registry survives it and reconcile still answers; a creature killed by a player proving
"gone" is an ordinary teardown outcome and not a failure; and a despawn **refused** for a serial the
run does not own.
#### Phase 12b — what it BORROWS, and the one-shots
| Verb | Owned or borrowed | Cap dimension |
| --- | --- | --- |
| Named, hued creatures from the atlas's ~800 constructible types | owned — deleted by serial | `uo.creatures` |
| "Simple" boss variants, as event-owned creature templates | owned | `uo.bosses` |
| Oracle NPCs with scripted dialogue | owned | `uo.npcs` |
| Temporary gates | owned, with a deadline | `uo.gate.minutes` |
| Temporary decoration lockdown | owned | `uo.decor` |
| A live config value | **borrowed** — a lease | — |
| A property on an existing object | **borrowed** — a lease | — |
| A live config value | **borrowed** — a lease | — |
| Seasonal-event toggle | borrowed | — |
| World save | neither — a one-shot | — |
| Item grant | owned, `reversible: 'none'` | `uo.rewards` |
**The lease allowlist ships with a boot-time self-check.** 258 `Config.Get` call sites in ServUO split
between live reads and values cached at type initialisation, and a lease on the second kind applies
cleanly and does nothing. Each key sets, reads back and restores at boot, and drops itself from the
advertised catalog if it does not take — a capability that disappears loudly beats one that lies.
**The config lease catalog does not grow, and the promise of "the rest of the allowlist" was written
before anyone counted.** Measured on ServUO 57.4: 156 non-Bridge `Config.Get` call sites in
`Scripts/`; 82 sit outside a field declaration, but all but four of *those* are inside a
`Configure()` or a static constructor, and so are cached at boot exactly as the field initialisers
are. The genuinely live, event-useful reads are `PlayerCaps.SkillCap` — which 11b already shipped
— and `Vendors.BribeDecayMinTime`/`MaxTime`, which is vendor bribe decay and which no event would
plausibly lease. (`Staff.*` in `GMbody.cs` is live, and is staff-body cosmetics.) So **12b's lease
work is object-property leases**, and the config half of the catalog is finished at one key.
**The boot-time self-check ships anyway** (org lead, 2026-09-07). On a stock shard it guards a
one-key catalog, which is not why it exists: it exists for the operator whose *own* scripts read
config live, and it is the mechanism that keeps a capability that lies out of the advertised catalog.
A key that sets, reads back and restores at boot stays; one that does not drops itself and says so.
§D and §G of `EVENTS.md` carry the measurement, so nobody re-plans against "258 call sites, two
patterns".
**An object-property lease names its target by an allowlist of (type, property) pairs, addressed by
serial** (org lead, 2026-09-07). The plugin ships the catalog — `Spawner.Amount` / `MinDelay` /
`MaxDelay` to start — and refuses any serial whose type is not in it. That mirrors the config
catalog exactly, and keeps §G's "a curated allowlist the plugin ships" true of both halves of the
lease plane; the alternative, a property name and a serial taken on trust, is `[set` with extra
steps. It also finally puts compare-and-set in front of a real hand: a spawner is trivially drifted
with `[props`, which is the test 11b could run only with scaffolding, because `Config.Set` has one
caller in the whole tree.
**§G describes the seasonal toggle wrongly, and 12b corrects it.** It calls
`SeasonalEventSystem.GetEntry(type).Status` "a nine-value enum". `EventStatus` has **three** values
`Inactive`, `Active`, `Seasonal` — and it is `EventType` that has nine entries. The verb is a
three-state toggle over nine named events, which is a different form to author and a different one
to cap.
**Ships:** the invasion.
**Verify:** the whole rig, running a real multi-phase event with spawns, a lease, and a full teardown