f9bbc7a90ddd6ed49789ec5c5b4cece5def9c515
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d6346996d3 |
fix(atlas): keep the UniqueId, and make a landmark value name one landmark
Two defects the Phase 16b re-verify found in the released v1.2.1 bundle, both of which make a shipped feature unusable and neither of which any test saw. ## The aggregator discarded the UniqueId `shard_spawn_points.unique_id` was NULL on all 6,455 rows of a stock 57.4 tree. `listSpawners` filters `unique_id IS NOT NULL`, so `uo.options.spawners` was an empty dropdown -- and it is the ONLY option source for the Phase 12b object-property leases, so no `Spawner.MaxCount` / `MinDelay` / `MaxDelay` lease could be authored at all, with nothing on the form to say why. Every part of the path was already right except one line. The spawn files carry `<UniqueId>` (~6,374 of them), `parsePoints` returns it, the column exists and the insert passes `p.uniqueId || null`. `buildAtlas` rebuilds each point from an explicit field list and `uniqueId` was not on it -- the word appears nowhere in that file. `PARSER_VERSION = 4`'s own note says "a spawn point keeps its UniqueId, which is what a property lease targets", so the intent shipped as a comment while the code dropped the field one function later. `PARSER_VERSION` goes to 5 because the bump is the only thing that re-reads an already-imported tree: `sameSources` compares the tree's hashes, which have not changed -- only what is kept from them. Confirmed on the rig, where the boot after the fix logged `spawn atlas refreshed` on an unchanged tree and the manual import then correctly answered `unchanged`. ## A landmark option value named 23 places at once A stock tree has 558 landmarks under 320 distinct `facet/name` pairs. `Trammel/Entrance` is 23 different dungeons -- Blighted Grove, Covetous, Deceit, Despise, Destard and so on -- and `landmarkPoint` resolved with `.find()`, so 22 of the 23 were unreachable. An author who picked "Entrance - Destard" got Blighted Grove, and the run succeeded with no warning. The group was already the disambiguator: it was shown in the dropdown and left out of the value. The value is now `facet/group/name`, which is distinct across all 558. `landmarkPoint` tries that form first and keeps the two-part read as a fallback, because every event published before this fix stores `facet/name` and a published version is immutable -- refusing to parse those would break runs rather than correct them. The fallback keeps the old first-match behaviour deliberately: it is imprecise in exactly the way it always was, and silently relocating a live event's spawn point is worse than repeating a known imprecision. A three-part value whose group is gone REFUSES rather than falling back to the name, because it asked for one particular place. ## Verification On the released-artefact rig (installer -> bundle 2026.09.10 -> stock 57.4 tree -> protocol-7 sidecar -> core at main with this module): spawn points 6455 rows, 6364 with a unique_id (was 0) uo.options.spawners 100 options, and `?q=orc` searches them (was 0) uo.options.landmarks 558 options, 558 distinct values (was 320) suite 625 pass, 0 fail Each new test was confirmed to FAIL without its fix. The atlas one asserts the field on the AGGREGATOR's output rather than the parser's, which is the whole point of it -- and the test fixture had no `<UniqueId>` at all until now, which is exactly why a green suite said nothing. The landmark one asserts an INEQUALITY between two resolved points rather than a literal value string, so it survives another change of format as long as two options still address two places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| 8def6e19f4 |
fix(events): the atlas import, and a teardown that was a no-op (Phase 16a)
Two defects the acceptance walk found in shipped code, both invisible to the suites that were green on either side of them. **The spawn atlas cannot import on a stock ServUO tree.** `spawnAtlasSource.js` dedupes decoration types with a case-SENSITIVE `Map`, but `shard_decor_types.type` is a PRIMARY KEY under MariaDB's default `..._ai_ci` collation, which folds case. Stock 57.4's own `Data/Decoration/` names four types under two spellings each (CheckerBoard/Checkerboard, ChessBoard/Chessboard, MetalChest/Metalchest, SpinningWheelEastAddon/SpinningwheelEastAddon), and in every pair exactly one is a real class. The second row raised `1062 Duplicate entry` and took the WHOLE import transaction down. The blast radius is not decoration: with no atlas, EVERY option source answers empty and no Phase 12 world verb can be authored at all. The shard end already knew — `BridgeWorld.cs` resolves a decor type with `FindTypeByName(name, ignoreCase: true)` and its comment says the atlas and the decoration files disagree about casing. Folding here is the two ends agreeing. **Teardown of every world verb was a no-op that reported success.** `revertOwned` forwarded core's `idempotencyKey` as the despawn's OWN key — and core's key is the step's, the one `placeOwned` spawned under. `BridgeIdempotency` keys on the key alone, so the despawn was taken for a repeat and answered with the SPAWN's stored reply; `OnDespawn` never ran. Core read `ok` with no `refused` and marked every row `reverted` while the shard still held every object. Measured on the rig: ledger `world | reverted | 21`, shard `world.owned` 21 alive with `pruned: 0`, and the identical despawn re-sent with a fresh key removed all 21. It affected all five world verbs, so an invasion's creatures, boss, oracle, gate and decoration stayed in the world for ever while the console reported a clean teardown. `MODULE_API.md` says what that key is for and it is not this: it identifies a dispatch core never learned the outcome of, so the module can ask about it. No key is needed on a despawn — a repeat answers `gone`, which both ends already treat as success — and dropping it also makes the documented empty-`resources` case work, since no serials means "everything this run owns". The parameter is removed from `despawnWorld`'s signature rather than left optional. Both fixes are verified end to end against a real ServUO + sidecar + website rig: the import now yields 309 decor types (was failing at 313 with 4 collisions), 6,455 spawn points, 800 creatures, 558 landmarks; and a full four-phase run's teardown left the shard owning 0 objects. Each new test was confirmed to FAIL without its fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 |
|||
| 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 |
|||
| 6b99d7e220 |
test(server): port core's UO suite onto the ctx harness
22 test files moved from core, plus the two that were split out of files core keeps. 351 tests pass. One change runs through every moved test, and it is the boundary rather than a chore: core internals can no longer be stubbed by requiring them, because there are none to require. `../utils/db` and `../model/settings` do not exist here. What a test controls instead is the ctx core would have handed over, installed once by test/_setup.js -- which is a better seam anyway, since it is exactly the surface the contract promises and nothing wider. The ctx _setup installs is deliberately unfrozen. Core freezes what it hands a module and entry.test.js still asserts against a frozen one; but a test that needs settings.get to return a path has to be able to say so. Two tests changed SHAPE, and that is the boundary too. fromShardEvent used to assert through publish() into pushDevices and a captured fetch -- which endpoints were hit, how many requests went out. None of that is this module's any more: publish is ctx.push.publish, and the device registry and the relay are behind it. Reaching for them from here would be reaching past ctx. What remains is what the module owns and is the part worth guarding: a game account resolves to a website user, a personal target that resolves to nobody is dropped rather than published, and a sensitive kind never reaches publish at all. Co-Authored-By: Claude <noreply@anthropic.com> |