0b142adb81b34c8ec455472c0e6a14880b6f14f7
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 89be9d6a4e |
feat(events): the five world verbs an author sees (Phase 12a)
`uo.creature.spawn`, `uo.boss.spawn`, `uo.npc.place`, `uo.gate.open` and `uo.decor.place`, over protocol 7's one command family. Five actions because five is what an author has; one `perform`/`revert`/`reconcile` because on the wire they are one thing. Five new budget dimensions -- `uo.creatures`, `uo.bosses`, `uo.npcs`, `uo.decor`, `uo.gate.minutes` -- all declared by THIS MODULE (org lead, 2026-09-07). Core meters whatever dimensions a module declares and holds no UO knowledge, which is the whole of what MODULE_API means by game-agnostic. A gate is priced in minutes rather than in gates: one standing all day and twelve standing five minutes each are not the same imposition on a world. `reconcile()` ASKS the shard, and is the one place in this file that must not use `reconcileByBootId`. A crier line lives in shard memory, so a changed `bootId` IS proof it is gone; a spawned creature is in the world SAVE and survives the restart the stamp would report it lost by. Anything `world.owned` does not list is gone -- safe only because the shard's registry and the objects it describes are written by the same save. Teardown reports `gone` as success and `refused` as failed. A creature a player killed is the point of having spawned it, and a run that ended `incomplete` because its event worked would be a report nobody could read. `refused` means the shard denies this run ever owned the serial, so nothing will delete it through this path and the row must land unresolved with a reason. The atlas gains a decoration index, parsed from the shard's own `Data/Decoration/**/*.cfg` -- 120 files, read RECURSIVELY because the real tree nests two deep and a flat read would index a fraction of it while looking like it worked. 313 distinct types. The decor verb resolves through it rather than passing a type name through, which keeps the verb to this shard's own decoration vocabulary AND fetches the item id: `Static` alone accounts for 5031 placements under 1992 different graphics, so a bare type name places the wrong thing. `PARSER_VERSION` -> 3, so an already-imported tree is re-read. Two things the build found in code that had already shipped: `uo.options.creatures` answered with the atlas SLUG -- unique, stable, and not something the shard can build, because a creature is constructed from a ServUO class name and `orc-brute` is not one. The atlas's `name` is the raw type token from the spawn files, so the fix was to stop discarding the half that works. Safe to change because Phase 12a is the source's first consumer; the file said so when it shipped. `uo.npc.place` could not be performed from its own required params. Both ends refuse an oracle with neither a greeting nor a line, but both fields were optional -- so a cross-field rule sat where no authoring form could render it. The greeting is now `required`, which says the same thing in the contract itself. Caught by the existing dry-run sweep, which is a better argument for that test than anything written about it when it shipped. 605 tests pass. `swagger-fragment.json` is stale on `edge` already and this phase adds no route, so it is left alone. 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 |
|||
| 88bfe9310e |
feat(events): one lease and the participation verbs (Phase 11b)
The UO half of protocol 6 part b. No route added, no schema change, no
MODULE_API bump.
`uo.playercaps.skillcap` is the one lease, and the catalog is short because
ServUO made it short: of the 158 non-Bridge `Config.Get` call sites in
`Scripts/`, roughly eight are read live. This one is read inside
`CharacterCreation.cs`'s per-character path, so it is both live and observable --
which is what "proven" has to mean, since the failure an allowlist exists to
prevent is a key that applies cleanly and changes nothing.
Its `apply()` sends a DURATION rather than the deadline: an absolute time
computed here and honoured there is measured against two clocks, and a shard
running ten minutes fast would restore a ten-minute lease the instant it took it.
Its `restore()` turns `lease.drifted` into `{ drifted: true, current }` rather
than an error, because core records drift as a distinct successful outcome and an
error would put the row on the retry ladder. Its `inForce()` asks whether the
shard still HOLDS the lease, never whether the value still matches -- see the
core PR.
`uo.participation.open` / `.collect` count who took part and file them on the
success envelope. `open` is the one resource in this module that must NOT
reconcile by boot stamp: every other resource here lives in shard memory, so a
changed bootId IS the proof it is gone, while the participation ledger is written
into the world save precisely so it survives that restart. It asks instead.
Co-Authored-By: Claude <noreply@anthropic.com>
|
|||
| dc13515927 |
feat(events): send the idempotency key, and declare champ.boss.killed (Phase 11a)
The website's half of protocol 6. Every event-driven write now carries the step's idempotency key, and `uo.broadcast` stops being un-retryable. Phase 9 shipped it answering `retry: false` to everything including a 503 from a shard that was merely restarting, with a comment naming the line that would change when the wire could refuse a repeat. This is that line: it defers to `sidecarFailure`, the same helper its two siblings already used, so the hand-rolled variant that forced every outcome terminal is gone rather than re-tuned. One verb was less idempotent than its own id made it look. Both keyed verbs post under a run-scoped id and a repeat replaces — but `news.add` with `announce: true` makes the criers proclaim the title on every post, so a retry replaced the article silently and proclaimed it again. The key stops the second proclamation. `champ.boss.killed` is mapped to the `champs` feature (rule 2 would otherwise fail it closed to admin), with `damagers` a nested `staff` field rule: the kill is public because a champion falling is what the board is for, the ranked roll of who was strong enough to fell it is not. `uo.champ.boss_killed` is declared as a trigger — which is what makes it usable as an event PHASE CONDITION, since a condition is written over a trigger firing — and it carries `damagerCount`, never a damager name, because a trigger variable reaches mail an operator may address to every subscriber. Its seeded rule is its own group, `champ-boss-killed-v1`: `triggers-v1` is stamped once under a settings guard, so appending a 27th entry would have reached fresh installs and nothing else. It also ships email+inapp and NOT push, and the comment says why — no trigger in this module is also a registered stream, so no engagement rule here can push. That is pre-existing in twenty rules and flagged rather than fixed; this one declines to be the twenty-first. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| 57419111e6 |
feat(events): UO wave 1 — the verbs that need no protocol change (Phase 9)
module-uo registers its first event actions: `uo.broadcast`, `uo.towncrier.post` and `uo.news.post`, plus the `uo.broadcasts` budget dimension and the three spawn-atlas option sources. The write plane they use has existed since protocol 2.1; what is new is the declaration that lets the event engine drive it unattended. Three things the tree corrected about the plan: - The plan's `on_failure: 'skip'` for `uo.broadcast` is already the default for `risk: 'notify'`, and `on_failure` is what happens AFTER the retries. The lever a module actually has is the failure envelope, so the action answers `retry: false` to everything — and every action declares `budgetMs: 15000`, because core's 10s default deadline fires before `uoLinkClient`'s 12s timeout and `classify()` answers `retry` for a timeout without asking the module. Without the budget the retry refusal is unreachable. - `reconcile()` needs no protocol work. A shard restart wipes both the crier lines and an event's news article, so `perform()` stamps the shard `bootId` into the resource payload and `reconcile()` reports in force exactly the rows whose stamp still matches — correct for the module's own trigger and for core's boot sweep alike. `shardIngest` fires `ctx.events.reconcile()` on a changed `bootId`, after `recordStatus` so the comparison reads the new boot. - Event articles post under `evt-<idempotencyKey>`, because `newsGump.js` uses the bare website post id and re-pushes that set on every reconnect. `ci/core-ref.json` moves to a website `edge` sha for the length of this workstream: `registerEventActions` exists only from MODULE_API 1.10.0, so under the old `main` pin the module does not load at all. Verified locally — the frozen-manifest rig passes against the new pin. Co-Authored-By: Claude <noreply@anthropic.com> |
|||
| fe3251a543 |
feat(server): port the UO models, utils and schema fragment
The data half of the extraction: 8 model directories, 13 utils, the shard
stream catalog and the 27-table schema fragment with its purge.
server/core.js is what makes the port a one-line import change per file rather
than a signature change per function. Ported code requires its dependencies at
file scope -- `const { query } = require('../../core')` -- which runs before
register() has been called and before any ctx exists. So every member is a
stable function that resolves ctx when CALLED, and nothing may be destructured
off ctx at init either, because core is free to hand over a getter.
Two helpers are vendored rather than taken from ctx, and the line between them
is the point. utils/excerpt.js is core's deriveExcerpt -- nine lines of pure
text handling. Core's sanitiser next to it was NOT copied: a second copy of a
security control diverges silently the moment either is fixed. announceLinks.js
vendors legError and articleUrl the same way, but baseUrl could not be: core's
reads APP_BASE_URL, and §2.7 forbids a module reading core's environment, so it
comes off ctx.site.baseUrl.
The schema fragment is core's 27 shard_*/uo_link_* statements, verbs CREATE,
ALTER and UPDATE only, every CREATE TABLE guarded. Two of its tables carry a
foreign key INTO users, which is allowed and is why the replay order matters --
core's schema is in place before this runs. The reverse never occurs and must
not: it would make core unable to boot without a module installed.
One real port bug caught by the integration run, not by tests: the atlas art
map resolved `../../../db/data`, which pointed at core's tree when this file
lived there and points outside server/ now. A path that happens to resolve is
exactly what survives a green suite, because the absent-file branch returns {}
and looks like the normal case.
Co-Authored-By: Claude <noreply@anthropic.com>
|