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
This commit is contained in:
@@ -336,11 +336,29 @@ function buildAtlas(root, options = {}) {
|
||||
|
||||
// Decoration: what this shard already calls scenery, which is what makes the
|
||||
// authoring dropdown the operator's own vocabulary rather than our taste.
|
||||
//
|
||||
// **Keyed case-INSENSITIVELY, because the decoration files disagree with
|
||||
// themselves about casing.** Stock 57.4 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 a mis-cased line
|
||||
// the shard's own loader resolves anyway. A case-sensitive Map keeps both, and
|
||||
// then `shard_decor_types.type` (a PRIMARY KEY under MariaDB's default
|
||||
// `..._ai_ci` collation, which folds case) rejects the second row and takes the
|
||||
// WHOLE import transaction down with it. That is not a decoration bug: with no
|
||||
// atlas, every option source answers empty and no world verb can be authored at
|
||||
// all. The shard end of this feature already knew — `BridgeWorld.cs` resolves a
|
||||
// decor type with `FindTypeByName(name, ignoreCase: true)` and says why — so
|
||||
// folding here is the two ends agreeing rather than a new rule.
|
||||
//
|
||||
// The first spelling seen wins, exactly as the first item id does. Either
|
||||
// spelling resolves on the shard, so which one survives is cosmetic.
|
||||
const decorUses = new Map()
|
||||
for (const file of files) {
|
||||
if (!file.label.startsWith('Data/Decoration/')) continue
|
||||
for (const entry of parseDecoration(file.text)) {
|
||||
const seen = decorUses.get(entry.type)
|
||||
const key = entry.type.toLowerCase()
|
||||
const seen = decorUses.get(key)
|
||||
if (seen) {
|
||||
seen.uses += 1
|
||||
continue
|
||||
@@ -348,7 +366,7 @@ function buildAtlas(root, options = {}) {
|
||||
// The FIRST item id wins, and it is only a preview: a type appears under
|
||||
// as many ids as it has facings or variants, and picking one arbitrarily
|
||||
// is honest in a way that picking "the most used" would not be.
|
||||
decorUses.set(entry.type, { type: entry.type, itemId: entry.itemId, uses: 1 })
|
||||
decorUses.set(key, { type: entry.type, itemId: entry.itemId, uses: 1 })
|
||||
}
|
||||
}
|
||||
const decor = [...decorUses.values()].sort((a, b) => a.type.localeCompare(b.type))
|
||||
|
||||
Reference in New Issue
Block a user