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:
@@ -1122,22 +1122,24 @@ a capability exists.
|
||||
| Name landmarks, regions, creatures for authoring | ✅ | The spawn atlas — and it answers the "meeting location" field every EM listing carries. |
|
||||
| Detect a boss defeated | ✅ **built (protocol 6)** | `champ.boss.killed`, fired from `EventSink.CreatureDeath` and detected by type, with the altar attributed from the sweep. The inference this replaces was more fragile than "slightly": `bossUp` also drops when a GM resets a spawn, when a boss despawns, and after a sidecar reconnect clears the diff cache. And it was silent about who fought — the new kind carries the damage table, which exists at the death and nowhere else. |
|
||||
| **Participation attribution** | ✅ built | Protocol 6 part b. Presence in a declared area plus kill credit inside it, keyed by character serial, **persisted in the world save** so a restart mid-event does not lose it. The area is a map, a point and a radius rather than a region name — the most specific region containing an event is routinely anonymous. Kill credit goes to every damager standing in the area, not to the killer: a last hit is a poor description of who fought something. |
|
||||
| Oracle NPC with scripted dialogue | 🔧 📡 | PEC caps this at 5 NPCs × 5 lines. **This is literally a web form** — arguably a better fit for browser authoring than spawning is, and it is how most story events actually work. |
|
||||
| Temporary gate to a venue | 🔧 📡 | PEC caps at 4 hours and forbids cross-facet gating to restricted areas. Inherently temporary, so it maps onto a run's lifetime and the ledger with no friction. |
|
||||
| Temporary decoration lockdown | 🔧 📡 | Permanent decoration prohibited in the program and should be prohibited here. Ledgered and reverted like anything else. |
|
||||
| Named, hued creatures from an allowlist | 🔧 📡 | PEC's core capability, and its cap is the useful part: common creatures, custom name and hue, **capped at 30**. A bounded one-shot spawn with each serial ledgered — **not a spawner**, which PEC withholds precisely because it is unbounded over time. |
|
||||
| "Simple" boss variants | 🔧 📡 | An enhanced regular mob, capped at 2–4. The defensible form is an **event-owned creature template** — the event declares what it spawns, stats included, and never touches a creature it did not create. |
|
||||
| Oracle NPC with scripted dialogue | ✅ **built (protocol 7)** | PEC caps this at 5 NPCs × 5 lines. **This is literally a web form** — arguably a better fit for browser authoring than spawning is, and it is how most story events actually work. Phase 12a: a greeting on approach plus keyword rows, on `Mobile.OnMovement` and `Mobile.OnSpeech`. Built rather than layered on `XmlSpawner2.XmlDialog`, which implements exactly this vocabulary **and an `Action` command-scripting field** — the `[set` this table excludes, one field away from an author. |
|
||||
| Temporary gate to a venue | ✅ **built (protocol 7)** | PEC caps at 4 hours and forbids cross-facet gating to restricted areas. Inherently temporary, so it maps onto a run's lifetime and the ledger with no friction. Phase 12a; the deadline is the SHARD's, so a gate closes whether or not the website is heard from again — and it crosses as a duration, never an absolute time. |
|
||||
| Temporary decoration lockdown | ✅ **built (protocol 7)** | Permanent decoration prohibited in the program and should be prohibited here. Ledgered and reverted like anything else. Phase 12a places it from the shard's OWN `Data/Decoration/**/*.cfg` vocabulary, carrying the item id: `Static` alone accounts for 5031 placements under **1992 different graphics**, so a bare type name places the wrong thing. Containers are refused — teardown would delete what a player left inside. |
|
||||
| Named, hued creatures from an allowlist | ✅ **built (protocol 7)** | PEC's core capability, and its cap is the useful part: common creatures, custom name and hue, **capped at 30**. A bounded one-shot spawn with each serial ledgered — **not a spawner**, which PEC withholds precisely because it is unbounded over time. Phase 12a; the ledger is a PERSISTED shard-side registry, because a spawned creature survives the restart that proves a crier line gone. |
|
||||
| "Simple" boss variants | ✅ **built (protocol 7)** | An enhanced regular mob, capped at 2–4. The defensible form is an **event-owned creature template** — the event declares what it spawns, stats included, and never touches a creature it did not create. Phase 12a: an allowlisted type plus hits/damage/stat multipliers, each bounded. |
|
||||
| **Lease a live config value** — rates, toggles, caps | ✅ built (one key) | Protocol 6 part b: the registry, the deadline timer, compare-and-set restore and `lease.list`, proved end to end against one verified live-read key. **The allowlist is far shorter than this table assumed** — of the 158 non-Bridge `Config.Get` call sites in `Scripts/`, roughly *eight* are read live, so the split below is nearer 95/5 than half and half. Phase 12 adds the rest with the boot-time self-check. |
|
||||
| Lease a property on an existing object | 🔧 📡 | Practical, and an earlier revision was wrong to rule it out. The before-image lives in the website's database and survives a shard restart; a save just persists current state; a deleted target makes restore a no-op. The one real hazard — a GM editing the same property mid-event — is answered by compare-and-set restore and the `drifted` state. |
|
||||
| Grant an event item | 🔧 📡 | An ordinary action, not a special contract member. Admin-gated and capped like any other, and `reversible: 'none'` *for UO specifically* — an object in a backpack cannot be recalled. The constructible allowlist is the plugin's; every label and icon comes from `shard_clilocs` and `item_id`. Failure aborts rather than retries: a retried grant is one winner receiving two. |
|
||||
| Toggle a ServUO seasonal event | 🔧 📡 | Small and safe: `SeasonalEventSystem.GetEntry(type).Status` over a nine-value enum, already persisted across saves. |
|
||||
| Toggle a ServUO seasonal event | 🔧 📡 | Small and safe: `SeasonalEventSystem.GetEntry(type).Status`, already persisted across saves. **Corrected in Phase 12a's survey:** `EventStatus` is a **three**-value enum (`Inactive`, `Active`, `Seasonal`); it is `EventType` that has nine entries. So the verb is a three-state toggle over nine named events — a different form to author and to cap than one nine-way choice. Phase 12b. |
|
||||
| Trigger a world save | 🔧 📡 | Catalogued Tier B in `ADMIN_CONTROLS.md` §3.6, never built. `AutoSave.Save()`; emits the `world.save.*` events already streamed. Useful as a phase boundary. |
|
||||
| Idempotent command application | ✅ **built (protocol 6)** | Phase 11a, and deliberately ahead of every verb below it rather than alongside one. A key is executed at most once; a repeat gets the original reply. Its immediate dividend is that `uo.broadcast` stopped being un-retryable — Phase 9 had to answer `retry: false` even to a 503 from a restarting shard, because a lost ack and a command that never applied were the same event. |
|
||||
| Loot-table changes | ⛔ | Per-creature-type and shard-wide, so it cannot be scoped to one run at all — the one thing on this list a lease genuinely cannot express. Attach loot to an event-owned creature template instead. |
|
||||
| Arbitrary `[set` / `[get` / `[add` | ⛔ | `ADMIN_CONTROLS.md` §8's exclusion of these should survive the reopening unchanged. Sharp, privilege-escalating, and expressible only as "trust the caller" — which is precisely a change with no baseline, no cap and no ledger entry. |
|
||||
|
||||
**What the reopening actually costs.** Nine new plugin verbs, one protocol bump carrying an
|
||||
idempotency key and a lease deadline, and a participation ledger. Bounded work, and every verb is
|
||||
**What the reopening actually costs.** Nine new plugin verbs and **two** protocol bumps: 6
|
||||
for the idempotency key, the lease deadline and the participation ledger, and 7 for the world
|
||||
verbs themselves. (One bump was the estimate; the guarantee had to land before the verbs that
|
||||
depend on it, which is [`../link/v6.md`](../link/v6.md) §1's whole argument.) Bounded work, and every verb is
|
||||
cap-bounded, ledgered and attributable by construction. **None of it is `[add`.** The distinction that
|
||||
makes it defensible: an event may **own** what it creates and **borrow** what it changes — and a
|
||||
borrowed value carries a deadline the game itself enforces. What it may never do is change something
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user