docs(link): protocol 7 part b -- what an event borrows, and the one-shots
`link/v7.md` gains §11-§14: the two targeted lease planes, the two one-shots, the
routes, and what the build found in already-merged code. `EVENTS.md` §G's five
part-b rows are marked built, three of them carrying a correction. `MODULE_API.md`
records the three contract members 12b amends into 1.10.0. `EVENTS_PLAN.md` has
Phase 12b as built.
THE DESIGN POINT, WRITTEN DOWN
A borrowed value whose home is the world save does not come back on a restart the
way a config value does. 11b's fail-safe -- a lease that never reaches disk makes
a restart a free restore -- depends entirely on the leased value being
memory-only too, and for a spawner property or a seasonal status it is not: a
restart preserves the CHANGE and destroys only the timer that would have undone
it. So those two planes' holds are persisted and their deadlines re-armed, and
the config plane's still are not. The same argument, applied where its premise is
false.
FIVE CORRECTIONS TO EVENTS.md
- `Spawner.Amount` does not exist. The property is `MaxCount`, and
`MinDelay`/`MaxDelay` are TimeSpans, so the wire carries seconds.
- The seasonal toggle is not "small and safe". Safe, yes -- ServUO does it to
itself from a staff gump -- but `OnStatusChange()` generates or removes world
content for six of the eight permitted types.
- It is a THREE-value enum over nine named events, not a nine-value enum.
(Caught in 12a's survey; the row is corrected here now it is built.)
- `TreasuresOfTokuno` is excluded, because `IsActive()` reads its own `DropEra`
rather than `Status`. A lease on it applies cleanly, reads back, restores
cleanly and changes nothing -- §N10's "capability that lies", and the one
instance no runtime probe can catch.
- The grant row said failure aborts rather than retries. Protocol 6 changed that:
an idempotency key means a repeat is answered by the original reply, so a
retried grant cannot be one winner receiving two.
And the config-lease row is closed at one key. Counted on ServUO 57.4: 156
non-Bridge `Config.Get` call sites, 82 outside a field declaration, all but four
of those inside a `Configure()` or static constructor and cached at boot anyway.
The self-check ships regardless -- it exists for the operator whose OWN scripts
read config live.
MODULE_API 1.10.0, AMENDED IN PLACE
`target` on a lease declaration, `values` on a string lease, and `searchable` +
`{ q }` on option sources. Amended rather than bumped for the reason every phase
since P10 has: 1.10.0 has never reached `main`, so no deployment can tell the
difference, and the cutover is what publishes the whole of it.
The `target` entry records why this extends `core.lease` rather than giving the
module a lease verb of its own: §F settled that in Phase 8, and half its
objection no longer holds (the two-events-one-target refusal comes from the
ledger's unique index whichever verb reserves the row) while the other half --
`maxDurationMs` re-implemented per module -- still does.
§14 records the protocol-pin defect 11a and 12a both shipped, and why the test
that guards it passed anyway: it asserts the three declarations agree with each
other, which all three being equally stale satisfies.
CHECKS
`docs` has no CI. Every relative link in the new sections was resolved by hand;
the files are CRLF in the working tree and each diff is content-sized
(`--numstat` matches the real change), so nothing carries the `\r\r\n` full-file
rewrite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -103,6 +103,12 @@ ctx.events.reconcile()
|
||||
api.registerEventOptionSources([{
|
||||
id: 'uo.options.creatures', label: 'Creatures',
|
||||
async resolve() { return [{ value: 'Orc', label: 'Orc', group: 'Humanoid' }] },
|
||||
}, {
|
||||
// A catalog bigger than a dropdown holds. Core passes `q` to EVERY source and
|
||||
// requires it of none, so a resolver that ignores it is unchanged; `searchable`
|
||||
// is what tells the authoring form to render a typeahead rather than a select.
|
||||
id: 'uo.options.spawners', label: 'Spawners', searchable: true,
|
||||
async resolve({ q } = {}) { return search(q).map((r) => ({ value: r.id, label: r.name })) },
|
||||
}])
|
||||
|
||||
// A value a run may borrow. The module ships the three callables; the VERB an
|
||||
@@ -115,6 +121,28 @@ api.registerEventLeases([{
|
||||
async apply(v, until) { return { ok: true } },
|
||||
async restore(baseline, { expected }) { return { ok: true } },
|
||||
async inForce() { return { ok: true, held: true } }, // optional
|
||||
}, {
|
||||
// A TARGETED lease: one capability over many things. Core adds the target to
|
||||
// the reservation ref (`<lease id>#<target>`) so two runs may hold the same key
|
||||
// on two different objects, and hands it to all four callables.
|
||||
id: 'uo.spawner.maxcount', label: 'Spawner: how many at once',
|
||||
type: 'int', min: 0, max: 100, maxDurationMs: 43200000,
|
||||
target: { label: 'Which spawner', source: 'uo.options.spawners' },
|
||||
async read({ target }) { return { ok: true, value: '3' } },
|
||||
async apply(v, until, { target }) { return { ok: true } },
|
||||
async restore(baseline, { expected, target }) { return { ok: true } },
|
||||
async inForce({ target }) { return { ok: true, held: true } },
|
||||
}, {
|
||||
// A string lease may close its value set. `min`/`max` bound the numeric types
|
||||
// and nothing bounded `string`, so without this the only check on the value is
|
||||
// the game side's -- a refusal arriving unattended, mid-run, rather than on the
|
||||
// authoring form.
|
||||
id: 'uo.seasonal.status', label: 'Seasonal event status',
|
||||
type: 'string', values: ['Inactive', 'Active', 'Seasonal'], maxDurationMs: 43200000,
|
||||
target: { label: 'Which seasonal event', source: 'uo.options.seasonal' },
|
||||
async read({ target }) { return { ok: true, value: 'Inactive' } },
|
||||
async apply(v, until, { target }) { return { ok: true } },
|
||||
async restore(baseline, { expected, target }) { return { ok: true } },
|
||||
}])
|
||||
```
|
||||
|
||||
@@ -133,6 +161,23 @@ field:
|
||||
- **`inForce()` is a fourth question, not a fourth spelling of `read()`.** Optional, and answering
|
||||
`{ ok: true, held: false }` is the only thing that takes a lease's ledger row out — everything
|
||||
else, including a throw and a lease that declares no `inForce()` at all, leaves the row alone.
|
||||
- **A lease that declares a `target` is a family of values, and core changes what it reserves.**
|
||||
Without one, the lease id *is* the target and the ledger reserves it alone — which is right for a
|
||||
config key and wrong for a property, because `Spawner.MaxCount` is one capability over thousands
|
||||
of spawners and reserving the id would let one run turning up one spawner refuse every other run
|
||||
every other spawner. With one, the ref is `<lease id>#<target>`, the two-events-one-target index
|
||||
bites at the granularity the world actually has, and the target reaches all four callables.
|
||||
**Core refuses a targeted lease with no target and an untargeted one with a target**, both
|
||||
`retry: false`: the second attempt has the same params. `target.source` names an option source
|
||||
for the authoring form, and is not resolved by core at registration — a source registered by a
|
||||
module that boots later must not make this one throw.
|
||||
- **`values` closes a `string` lease's set, and belongs to no other type.** `min`/`max` bound the
|
||||
numeric types; a set on an int lease would be a second bound beside them with no rule about
|
||||
which wins, so it is refused.
|
||||
- **A source is passed `{ q }` and may ignore it.** Additive: a resolver written before this
|
||||
existed behaves identically. Declare `searchable: true` when the term actually narrows the
|
||||
answer — the form reads that to decide between a typeahead and a select, and inferring it from a
|
||||
truncated list would read correctly right up until a small deployment's list happened to fit.
|
||||
Core needs it because a reconcile after an outage asks *"does the game side still have any record
|
||||
of this hold?"*, and none of the other three answers that: a value that DIFFERS from what the run
|
||||
applied is drift, which `restore()` reports so the row lands `drifted` with the current value
|
||||
@@ -315,6 +360,38 @@ Two members joined it in Phase 10, both on an envelope:
|
||||
envelope rather than in `events/` because "this particular firing is narrower than the kind
|
||||
usually is" is a fact any emitter can have.
|
||||
|
||||
Three more joined it in **Phase 12b**, all on declarations rather than envelopes, and all amended
|
||||
into 1.10.0 in place for the reason the two above were: 1.10.0 has never reached `main`, so there
|
||||
is no deployment that could tell the difference, and the events cutover is what publishes the
|
||||
whole of it. `module-uo`'s `coreApi` is unaffected; the integration kit is already red on purpose
|
||||
and stays so until the cutover re-pins `ci/core-ref.json`.
|
||||
|
||||
- **`target` on a lease declaration** (`EVENTS.md` §F, `link/v7.md` §11). A lease with one is a
|
||||
FAMILY of values rather than a single value, and core reserves `<lease id>#<target>` rather than
|
||||
the id — so two runs may hold the same key on two different objects while two runs holding one
|
||||
object still collide on the unique index. The target reaches `read`, `apply`, `restore` and
|
||||
`inForce`. Every lease before this named one value, so the id *was* the target and none of the
|
||||
four needed an argument; a property does not have that shape.
|
||||
|
||||
**The verb stays core's**, which is the whole reason this is an extension rather than a lease
|
||||
verb of the module's own. §F settled that in Phase 8: a lease verb per module would
|
||||
re-implement `maxDurationMs` and the conflict check once per module, advisory everywhere and
|
||||
wrong in the first one that forgot. Half of that objection no longer holds — the
|
||||
two-events-one-target refusal comes from the ledger's unique index whichever verb reserves the
|
||||
row — and the other half still does.
|
||||
- **`values` on a `string` lease.** The closed set an author may choose from, checked by
|
||||
`core.lease` at authoring time. `min`/`max` bound the numeric types and nothing bounded
|
||||
`string`, so the only check on a string lease's value was the game side's — a refusal arriving
|
||||
unattended, mid-run, from a step nobody is watching. Refused on any other type: a set beside
|
||||
`min`/`max` would be a second bound with no rule about which wins.
|
||||
- **`searchable` on an option source, and `{ q }` passed to every `resolve()`.** A source whose
|
||||
catalog is larger than a dropdown can hold narrows its answer by the term; one that ignores the
|
||||
argument answers exactly as it did before this existed, which is what makes it additive. The
|
||||
first source that needed it is `module-uo`'s spawner target — 6,707 spawn points against the
|
||||
2,000-entry bound — and a truncated list is not an answer: it drops most of the world and says
|
||||
nothing about which part. `searchable` is declared rather than inferred, because inferring it
|
||||
from a truncated answer reads correctly right up until a small deployment's list happens to fit.
|
||||
|
||||
**1.7.0 — the engagement contract** (`website/ENGAGEMENT.md` Phase 2). Four additions, no removals
|
||||
and no changed signature, so minor; `module-uo`'s `coreApi: "^1.3.0"` still resolves.
|
||||
`api.registerEventTriggers([...])`, `api.registerAudiences([...])` and
|
||||
@@ -664,8 +741,8 @@ api.registerAudiences([{ id, label, params, ceiling, resolve }]) //
|
||||
api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0
|
||||
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile }]) // 1.10.0
|
||||
api.registerEventBudgets([{ id, label, unit }]) // 1.10.0
|
||||
api.registerEventLeases([{ id, label, type, min, max, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0
|
||||
api.registerEventOptionSources([{ id, label, resolve }]) // 1.10.0
|
||||
api.registerEventLeases([{ id, label, type, min, max, values, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0
|
||||
api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0
|
||||
api.onBoot(async (ctx) => {})
|
||||
api.onShutdown(async () => {})
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user