fix(events): the atlas import, and a teardown that was a no-op (Phase 16a) #33

Merged
whitlocktech merged 1 commits from fix/events-p16a-walk into edge 2026-09-09 13:47:59 +00:00
Member

Two defects the Phase 16 acceptance walk found in code already merged to edge. Both suites were green on either side of each of them.

1. The spawn atlas cannot import on a stock ServUO tree — blocking

spawnAtlasSource.js dedupes decoration types with a case-sensitive Map. shard_decor_types.type is a PRIMARY KEY under MariaDB's default ..._ai_ci collation, which folds case. Stock ServUO 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 other is a mis-cased line the shard's own loader resolves anyway.

The second row raises 1062 Duplicate entry and takes the whole import transaction down.

The blast radius is not decoration. With no atlas, every option source answers {ok: true, options: []} and no Phase 12 world verb can be authored at all — the authoring form's dropdowns are simply empty, with nothing to say why.

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". One end of the feature knew and the other did not. Folding here is the two ends agreeing rather than a new rule.

2. Teardown of every world verb was a no-op that reported success — critical

revertOwned() forwarded core's idempotencyKey as the despawn's own key. That key is the step's — the very key placeOwned() spawned under. BridgeIdempotency.Intercept keys on _byKey.TryGetValue(key, …), the key alone and not (key, command), so the despawn was recognised as a repeat and answered with the spawn's stored reply. OnDespawn never ran. Core saw ok with no refused and marked every row reverted.

Proven end to end on the rig, not inferred:

website ledger after teardown world | reverted | 21
shard at the same moment world.owned21 objects, pruned: 0 (all alive)
identical despawn, fresh key removed = all 21, owned: 0

It is the shared revert: for all five Phase 12a world verbs, so an invasion's creatures, its boss, its oracle, its gate and its decoration all stayed in the world for ever while the run console reported a clean teardown. The town-crier and news reverts are unaffected — they pass no key.

MODULE_API.md says what that key is for, and it is not this: "it will sometimes be called with the key and an EMPTY list, meaning a command went out under this key and core never learned what it did". It identifies a lost dispatch to ask about; it does not address the undo.

No key is needed on a despawn. A repeat is already safe by the handler's own three-answer design — the second pass answers gone, which §L and the module both treat as success. Dropping it also makes the documented empty-resources case work correctly, since no serials means "everything this run owns". The parameter is removed from despawnWorld's signature rather than left optional, so it cannot be handed one again.

Verification

Against a real rig — ServUO 57.4 (208k items, 42k mobiles) → cargo --release sidecar on protocol 7 → core with this module installed from a release-shaped bundle:

atlas import   309 decor types (was failing at 313 with 4 collisions),
               6,455 spawn points, 800 creatures, 558 landmarks, 387 regions
option sources all six answer
teardown       four-phase run, 21 world objects -> shard owns 0
suite          72 pass, 0 fail

Both new tests were confirmed to FAIL without their fix. The despawn test asserts the absence of the key rather than pinning the body's shape — every other stub in that file ignores the body, which is exactly why the suite stayed green while teardown did nothing.

Pairs with website#, servuo-plugins# and docs# (Phase 16a).

  • AI-assisted: written with Claude Code (Claude Opus 5).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Two defects the Phase 16 acceptance walk found in code already merged to `edge`. Both suites were green on either side of each of them. ## 1. The spawn atlas cannot import on a stock ServUO tree — **blocking** `spawnAtlasSource.js` dedupes decoration types with a case-**sensitive** `Map`. `shard_decor_types.type` is a PRIMARY KEY under MariaDB's default `..._ai_ci` collation, which folds case. Stock ServUO 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 other is a mis-cased line the shard's own loader resolves anyway. The second row raises `1062 Duplicate entry` and takes the **whole import transaction** down. **The blast radius is not decoration.** With no atlas, every option source answers `{ok: true, options: []}` and **no Phase 12 world verb can be authored at all** — the authoring form's dropdowns are simply empty, with nothing to say why. 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"*. One end of the feature knew and the other did not. Folding here is the two ends agreeing rather than a new rule. ## 2. Teardown of every world verb was a no-op that reported success — **critical** `revertOwned()` forwarded core's `idempotencyKey` as the despawn's **own** key. That key is the step's — the very key `placeOwned()` spawned under. `BridgeIdempotency.Intercept` keys on `_byKey.TryGetValue(key, …)`, the key **alone** and not (key, command), so the despawn was recognised as a repeat and answered with the **spawn's** stored reply. `OnDespawn` never ran. Core saw `ok` with no `refused` and marked every row `reverted`. Proven end to end on the rig, not inferred: | | | |---|---| | website ledger after teardown | `world \| reverted \| 21` | | shard at the same moment | `world.owned` → **21 objects, `pruned: 0`** (all alive) | | identical despawn, **fresh** key | `removed` = all 21, `owned: 0` | It is the shared `revert:` for all five Phase 12a world verbs, so an invasion's creatures, its boss, its oracle, its gate and its decoration all stayed in the world for ever while the run console reported a clean teardown. The town-crier and news reverts are unaffected — they pass no key. `MODULE_API.md` says what that key is for, and it is not this: *"it will sometimes be called with the key and an EMPTY list, meaning a command went out under this key and core never learned what it did"*. It identifies a lost dispatch to ask about; it does not address the undo. **No key is needed on a despawn.** A repeat is already safe by the handler's own three-answer design — the second pass answers `gone`, which §L and the module both treat as success. Dropping it also makes the documented empty-`resources` case work correctly, since no serials means "everything this run owns". The parameter is removed from `despawnWorld`'s signature rather than left optional, so it cannot be handed one again. ## Verification Against a real rig — ServUO 57.4 (208k items, 42k mobiles) → `cargo --release` sidecar on protocol 7 → core with this module installed from a release-shaped bundle: ``` atlas import 309 decor types (was failing at 313 with 4 collisions), 6,455 spawn points, 800 creatures, 558 landmarks, 387 regions option sources all six answer teardown four-phase run, 21 world objects -> shard owns 0 suite 72 pass, 0 fail ``` **Both new tests were confirmed to FAIL without their fix.** The despawn test asserts the **absence** of the key rather than pinning the body's shape — every other stub in that file ignores the body, which is exactly why the suite stayed green while teardown did nothing. Pairs with `website#`, `servuo-plugins#` and `docs#` (Phase 16a). - [x] AI-assisted: written with Claude Code (Claude Opus 5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-09 13:32:04 +00:00
fix(events): the atlas import, and a teardown that was a no-op (Phase 16a)
All checks were successful
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 58s
PR Checks / server-tests (pull_request) Successful in 8m27s
8def6e19f4
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
whitlocktech merged commit c73d62e93a into edge 2026-09-09 13:47:59 +00:00
whitlocktech deleted branch fix/events-p16a-walk 2026-09-09 13:48:01 +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/Module-uo#33
No description provided.