feat(bridge): the world verbs an event owns (protocol 7, Phase 12a) #23

Merged
whitlocktech merged 1 commits from feature/events-p12a-world-verbs into edge 2026-09-07 06:58:39 +00:00
Member

Events Phase 12a, shard half. One of four: servuo-plugins, link (#—), Module-uo (#—), docs (docs#221) — all onto edge. Spec of record: docs/link/v7.md.

overlay.tomlprotocol = 7. 12b amends 7 in place rather than bumping again, so an overlay and a sidecar both declaring 7 are interchangeable only within one side of that merge — tolerable for the same single reason 6 was, and no other: nothing is released from edge.

One command family, five author verbs

world.spawn / world.despawn / world.owned, with a what discriminator (creature, boss, npc, gate, decor). Every verb ends in the same sentence — an object exists, and this run owns it — so the per-verb differences (a boss's multipliers, an oracle's lines, a gate's destination and deadline) are fields rather than kinds. One ledger shape, one teardown path, one reconcile.

The ownership registry is persisted, and that is forced rather than chosen

A spawned creature is in the world save, so unlike a crier line it survives a restart — which already rules out reconcile-by-boot-stamp on the website's side. But the record of which run owns which serial has nowhere else to live:

  • In memory: lost in the restart the creatures themselves survive. Thirty orcs stand in Britain and the website holds serials nothing will vouch for.
  • Only in the website's ledger: not held here at all, so world.despawn would delete whatever serial it was handed. "An event never touches a creature it did not create" — the sentence the boss verb is built on — would be an intention with no mechanism, and a bug in a step or a stolen sidecar token would be a delete-anything primitive.

So the Bridge gains its second persisted file, Saves/Bridge/Owned.bin, beside 11b's Participation.bin. Same mechanism, and the same rule about attaching hooks in Configure() rather than Initialize().

It cannot get out of step with the objects: the registry is written by EventSink.WorldSave, the same save that writes what it describes. That is what makes "anything not listed is gone" safe rather than hopeful.

Three answers to a despawn, and only one is a failure: removed, gone (a success — players killing event creatures is what spawning them is for), refused (not this run's; nothing is touched).

The oracle is ours, and ServUO's own dialogue engine is why

XmlSpawner2.XmlDialog already implements exactly the vocabulary this verb wants — Text plus comma-separated Keywords, a keyword-less entry as the greeting, a proximity range, a conversation lock. That is the evidence the shape is right. 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 it would leave an arbitrary-command field one step from an event author on a web form — the [set §G excludes, arriving by the back door, in a subsystem we do not own and an operator 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 all it needs. Nothing executable crosses the wire.

Ceilings, and the graphic problem

Every Bridge.EventsMax* refuses rather than clamps, on LeaseMaxDurationSec's argument from 11b unchanged. EventsMaxOwnedPerRun is the one that is not per-call: it bounds a run calling a verb in a loop, which is the shape a runaway schedule takes. 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 measured on this tree Static alone accounts for 5031 placements under 1992 different graphics: for that class the graphic is the identity, and a bare new Static() is never what the author picked. 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.

Scaffolding

tools/scaffolding/BridgeRigDriver.cs gains worldgone <serial>: delete 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, so the two never disagree — and it is exactly what a player's sword does. Same reason 11b added configset.

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 (206,557 items / 42,785 mobiles) against the release sidecar:

  • all five verbs place, and world.owned reports each with its type and deadline;
  • every ceiling refuses — 80 creatures against 30, a gate longer than 240 minutes;
  • a container (Bag) and an unknown type refuse, both 400;
  • run rig2 cannot despawn rig1's bossrefused, nothing touched;
  • the registry and its objects both survive a save + clean shutdown + restart, pruned: 0, and the world reloads with 0 errors (so BridgeOracle's serialization round-trips);
  • an unsaved run comes back with both gone, which is §3.1's claim demonstrated;
  • a creature deleted behind the registry's back comes back gone, not removed: {"removed":["0x314","0x316","0x317"],"gone":["0x315"],"refused":[]};
  • a five-second gate is collected by the shard's own deadline, with world.expired on the wire.

Not driven by the rig: the oracle's greeting and keyword reply need a connected client — the same limit 11b hit with presence. The serialization round trip is proved; the speech behaviour is not.

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Events Phase 12a, shard half. One of four: `servuo-plugins`, `link` (#—), `Module-uo` (#—), `docs` (docs#221) — all onto `edge`. Spec of record: [`docs/link/v7.md`](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/221). **`overlay.toml` → `protocol = 7`.** 12b amends 7 in place rather than bumping again, so an overlay and a sidecar both declaring 7 are interchangeable only within one side of that merge — tolerable for the same single reason 6 was, and no other: nothing is released from `edge`. ## One command family, five author verbs `world.spawn` / `world.despawn` / `world.owned`, with a `what` discriminator (creature, boss, npc, gate, decor). Every verb ends in the same sentence — *an object exists, and this run owns it* — so the per-verb differences (a boss's multipliers, an oracle's lines, a gate's destination and deadline) are **fields rather than kinds**. One ledger shape, one teardown path, one reconcile. ## The ownership registry is persisted, and that is forced rather than chosen A spawned creature is in the world save, so unlike a crier line it **survives a restart** — which already rules out reconcile-by-boot-stamp on the website's side. But the record of *which run owns which serial* has nowhere else to live: - **In memory:** lost in the restart the creatures themselves survive. Thirty orcs stand in Britain and the website holds serials nothing will vouch for. - **Only in the website's ledger:** not held here at all, so `world.despawn` would delete whatever serial it was handed. *"An event never touches a creature it did not create"* — the sentence the boss verb is built on — would be an intention with no mechanism, and a bug in a step or a stolen sidecar token would be a delete-anything primitive. So the Bridge gains its **second** persisted file, `Saves/Bridge/Owned.bin`, beside 11b's `Participation.bin`. Same mechanism, and the same rule about attaching hooks in `Configure()` rather than `Initialize()`. It **cannot get out of step with the objects**: the registry is written by `EventSink.WorldSave`, the same save that writes what it describes. That is what makes *"anything not listed is gone"* safe rather than hopeful. Three answers to a despawn, and only one is a failure: `removed`, `gone` (**a success** — players killing event creatures is what spawning them is for), `refused` (not this run's; nothing is touched). ## The oracle is ours, and ServUO's own dialogue engine is why `XmlSpawner2.XmlDialog` already implements exactly the vocabulary this verb wants — `Text` plus comma-separated `Keywords`, a keyword-less entry as the greeting, a proximity range, a conversation lock. That is the evidence the shape is right. 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 it would leave an arbitrary-command field one step from an event author on a web form — the `[set` §G excludes, arriving by the back door, in a subsystem we do not own and an operator 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 all it needs. Nothing executable crosses the wire. ## Ceilings, and the graphic problem Every `Bridge.EventsMax*` **refuses rather than clamps**, on `LeaseMaxDurationSec`'s argument from 11b unchanged. `EventsMaxOwnedPerRun` is the one that is not per-call: it bounds a run calling a verb in a loop, which is the shape a runaway schedule takes. `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 measured on this tree `Static` alone accounts for **5031 placements under 1992 different graphics**: for that class the graphic *is* the identity, and a bare `new Static()` is never what the author picked. 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. ## Scaffolding `tools/scaffolding/BridgeRigDriver.cs` gains `worldgone <serial>`: delete 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, so the two never disagree — and it is exactly what a player's sword does. Same reason 11b added `configset`. ## 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 (206,557 items / 42,785 mobiles) against the **release** sidecar: - all five verbs place, and `world.owned` reports each with its type and deadline; - **every ceiling refuses** — 80 creatures against 30, a gate longer than 240 minutes; - a **container** (`Bag`) and an **unknown type** refuse, both 400; - **run `rig2` cannot despawn `rig1`'s boss** → `refused`, nothing touched; - the registry **and its objects** both survive a `save` + clean `shutdown` + restart, `pruned: 0`, and the world reloads with 0 errors (so `BridgeOracle`'s serialization round-trips); - an unsaved run comes back with **both** gone, which is §3.1's claim demonstrated; - a creature deleted behind the registry's back comes back **`gone`**, not `removed`: `{"removed":["0x314","0x316","0x317"],"gone":["0x315"],"refused":[]}`; - a five-second gate is collected by the shard's **own** deadline, with `world.expired` on the wire. **Not driven by the rig:** the oracle's greeting and keyword reply need a connected client — the same limit 11b hit with presence. The serialization round trip is proved; the speech behaviour is not. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-07 06:54:00 +00:00
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
whitlocktech merged commit 54149fb481 into edge 2026-09-07 06:58:39 +00:00
whitlocktech deleted branch feature/events-p12a-world-verbs 2026-09-07 06:58:40 +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#23
No description provided.