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
This commit is contained in:
@@ -39,6 +39,21 @@ function writeTree(root, { facets = ['Sosaria'], includeChampions = true } = {})
|
||||
fs.mkdirSync(path.join(root, 'Data', 'Locations'), { recursive: true })
|
||||
fs.mkdirSync(path.join(root, 'Config'), { recursive: true })
|
||||
|
||||
// Decoration, NESTED, because the real tree nests two deep in places
|
||||
// (`Magincia/Trammel`, `Stygian Abyss/Ter Mur`) and a flat read would index a
|
||||
// fraction of it while looking like it worked.
|
||||
fs.mkdirSync(path.join(root, 'Data', 'Decoration', 'Deep', 'Deeper'), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'Data', 'Decoration', 'top.cfg'),
|
||||
'# a brazier\nBrazier 0x0E31\n100 100 0\n200 200 -5\n\nStatic 0x108F\n300 300 0\n',
|
||||
'utf8',
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'Data', 'Decoration', 'Deep', 'Deeper', 'nested.cfg'),
|
||||
'Brazier 0x0E31\n400 400 0\nLargeCrate 0x0E3C\n500 500 0\n',
|
||||
'utf8',
|
||||
)
|
||||
|
||||
for (const facet of facets) {
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'Spawns', `${facet}.xml`),
|
||||
@@ -397,3 +412,45 @@ test('refresh: an explicit path overrides the configured one', async () => {
|
||||
assert.equal(result.status, 'imported')
|
||||
assert.deepEqual(result.addedFacets, ['Override'])
|
||||
})
|
||||
|
||||
// ── The decoration index (Phase 12a) ────────────────────────
|
||||
|
||||
test('decoration is read recursively and rolled up per type', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-decor-'))
|
||||
try {
|
||||
writeTree(root)
|
||||
const atlas = buildAtlas(root)
|
||||
|
||||
// Sorted by type, and `uses` counts every header line across the whole tree
|
||||
// — the nested file's Brazier is the second use of the same type, not a
|
||||
// second type.
|
||||
assert.deepEqual(atlas.decor, [
|
||||
{ type: 'Brazier', itemId: 0x0e31, uses: 2 },
|
||||
{ type: 'LargeCrate', itemId: 0x0e3c, uses: 1 },
|
||||
{ type: 'Static', itemId: 0x108f, uses: 1 },
|
||||
])
|
||||
assert.equal(atlas.meta.counts.decor, 3)
|
||||
|
||||
// Every decoration file is fingerprinted like every other source, so an
|
||||
// operator editing one is a tree change the boot path notices.
|
||||
const labels = Object.keys(atlas.meta.source).filter((l) => l.startsWith('Data/Decoration/'))
|
||||
assert.deepEqual(labels.sort(), ['Data/Decoration/Deep/Deeper/nested.cfg', 'Data/Decoration/top.cfg'])
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('a tree with no decoration at all still builds', () => {
|
||||
// Optional, like the champion file. A shard that has stripped its decoration
|
||||
// has a perfectly good atlas; the decoration verb simply has nothing to offer.
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-nodecor-'))
|
||||
try {
|
||||
writeTree(root)
|
||||
fs.rmSync(path.join(root, 'Data', 'Decoration'), { recursive: true, force: true })
|
||||
const atlas = buildAtlas(root)
|
||||
assert.deepEqual(atlas.decor, [])
|
||||
assert.equal(atlas.meta.counts.decor, 0)
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user