chore(cutover): sync main into edge before the Asset Bridge cutover
`edge` was BEHIND `main` by two commits — Module-uo#35 (the atlas keeps its `UniqueId`, and a landmark option value names one landmark) and the Event System's core re-pin — so merging `edge` into `main` as the Asset Bridge cutover would have REVERTED a released fix. Phase 9a's walk measured it: 0 of 6,455 spawners carried a `UniqueId` on an `edge` rig even after the column existed. ## The one conflict, and why the number had to move Both sides bumped `PARSER_VERSION` 4 -> 5, for different reasons, and main's 5 is RELEASED in v1.2.2: "a point keeps its `UniqueId`". `edge`'s 5 was phase 7's canonical label order. Keeping 5 would have made phase 7's change unreachable. `sameSources` gates on the tree hash and `currentParser` on the stored number; an install that imported under v1.2.2 already stores 5, so a phase-7 build declaring 5 would be called current and would never re-read. That is precisely the trap this constant exists to defeat, so the merged file carries BOTH notes: 5 is main's released meaning, 6 is phase 7's, with the renumbering explained in place. Everything else merged clean and keeps #35's files verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
@@ -58,7 +58,8 @@ function writeTree(root, { facets = ['Sosaria'], includeChampions = true } = {})
|
||||
fs.writeFileSync(
|
||||
path.join(root, 'Spawns', `${facet}.xml`),
|
||||
`<Spawns>
|
||||
<Points><Name>${facet}A</Name><Map>${facet}</Map><X>1100</X><Y>1100</Y>
|
||||
<Points><Name>${facet}A</Name><UniqueId>uid-${facet}-A</UniqueId>
|
||||
<Map>${facet}</Map><X>1100</X><Y>1100</Y>
|
||||
<MaxCount>3</MaxCount><IsRunning>True</IsRunning>
|
||||
<Objects2>Lizardman:MX=3:SB=0:OBJ=Orc:MX=1:SB=0</Objects2></Points>
|
||||
<Points><Name>${facet}B</Name><Map>${facet}</Map><X>9000</X><Y>9000</Y>
|
||||
@@ -112,6 +113,29 @@ function tempTree(options) {
|
||||
|
||||
// ── buildAtlas against a custom-facet tree ─────────────────────────────────
|
||||
|
||||
test('buildAtlas: a point keeps the UniqueId a property lease targets', () => {
|
||||
// The field is asserted on the AGGREGATOR's output, not the parser's, which is
|
||||
// the whole point of this test. `parsePoints` produced it from Phase 12b
|
||||
// onwards and `PARSER_VERSION`'s own note said a point kept it, while the
|
||||
// mapping in `buildAtlas` rebuilt each point from an explicit field list that
|
||||
// omitted it — so `shard_spawn_points.unique_id` was NULL on every row, and
|
||||
// `listSpawners`, whose WHERE is `unique_id IS NOT NULL`, answered empty. That
|
||||
// left `uo.options.spawners` an empty dropdown and every Phase 12b
|
||||
// object-property lease unauthorable. Found by the Phase 16b released-artefact
|
||||
// walk, against a real tree whose files carry ~6,400 of these.
|
||||
//
|
||||
// The fixture above had no <UniqueId> at all until this test, which is exactly
|
||||
// why a green suite said nothing about it.
|
||||
const root = tempTree({ facets: ['Sosaria'] })
|
||||
const atlas = buildAtlas(root)
|
||||
const named = atlas.points.find((p) => p.name === 'SosariaA')
|
||||
assert.equal(named.uniqueId, 'uid-Sosaria-A')
|
||||
// And a point whose file names none is absent rather than empty-string, so the
|
||||
// DB layer's `unique_id IS NOT NULL AND <> ''` reads it the same way either way.
|
||||
const unnamed = atlas.points.find((p) => p.name === 'SosariaB')
|
||||
assert.ok(!unnamed.uniqueId)
|
||||
})
|
||||
|
||||
test('buildAtlas: works entirely on facets that do not exist in stock UO', () => {
|
||||
const root = tempTree({ facets: ['Sosaria', 'Underdark'] })
|
||||
const atlas = buildAtlas(root)
|
||||
|
||||
@@ -563,6 +563,78 @@ test('an atlas larger than the dropdown bound is truncated and said so', async (
|
||||
assert.ok(warned, 'a truncated source must leave a log line naming itself')
|
||||
})
|
||||
|
||||
// ── A landmark option value names ONE landmark (Phase 16b) ────────────────
|
||||
|
||||
test('two landmarks sharing a name are two different options, and both resolve', async () => {
|
||||
// A stock 57.4 tree has 558 landmarks under 320 distinct `facet/name` pairs:
|
||||
// `Trammel/Entrance` is 23 different dungeons. The source emitted `facet/name`
|
||||
// and `landmarkPoint` resolved with `.find()`, so 22 of the 23 were unreachable
|
||||
// — an author who picked "Entrance — Destard" got Blighted Grove, with a
|
||||
// successful run and no warning. The group was already the disambiguator and it
|
||||
// was shown to the eye while being left out of the value.
|
||||
//
|
||||
// Asserted as an INEQUALITY between two resolved points rather than against a
|
||||
// literal value string, so it survives someone changing the value's format
|
||||
// again as long as the two options still address two places.
|
||||
shardAtlas.listLandmarks = async () => [
|
||||
{ facet: 'Felucca', name: 'Entrance', group: 'Blighted Grove', x: 586, y: 1643, z: 0 },
|
||||
{ facet: 'Felucca', name: 'Entrance', group: 'Destard', x: 1176, y: 2637, z: 0 },
|
||||
]
|
||||
|
||||
const source = actions.OPTION_SOURCES.find((s) => s.id === 'uo.options.landmarks')
|
||||
const options = await source.resolve({})
|
||||
assert.equal(options.length, 2)
|
||||
assert.equal(new Set(options.map((o) => o.value)).size, 2, 'both options must be addressable')
|
||||
|
||||
const points = []
|
||||
for (const option of options) {
|
||||
const result = await byId('uo.creature.spawn').perform({
|
||||
runId: 41,
|
||||
idempotencyKey: `L${option.value}`.padEnd(40, 'x'),
|
||||
params: { place: option.value, creature: 'Orc', count: 1 },
|
||||
verify: true,
|
||||
})
|
||||
assert.equal(result.ok, true, `${option.value} must resolve`)
|
||||
points.push(option.value)
|
||||
}
|
||||
assert.notEqual(points[0], points[1])
|
||||
})
|
||||
|
||||
test('a place published before the group was carried still resolves', async () => {
|
||||
// Every event published before the fix stores `facet/name`, and a published
|
||||
// version is immutable — so a parse that stopped understanding the two-part
|
||||
// form would break those runs rather than correct them. It keeps the old
|
||||
// first-match read, which is imprecise in exactly the way it always was.
|
||||
shardAtlas.listLandmarks = async () => [
|
||||
{ facet: 'Felucca', name: 'Entrance', group: 'Blighted Grove', x: 586, y: 1643, z: 0 },
|
||||
{ facet: 'Felucca', name: 'Entrance', group: 'Destard', x: 1176, y: 2637, z: 0 },
|
||||
// A name carrying a slash reads as three parts too; the two-part read is what
|
||||
// resolves it, which is why the three-part attempt must not answer for it.
|
||||
{ facet: 'Felucca', name: 'Odd/Name', group: null, x: 10, y: 20, z: 0 },
|
||||
]
|
||||
|
||||
for (const place of ['Felucca/Entrance', 'Felucca/Odd/Name']) {
|
||||
const result = await byId('uo.creature.spawn').perform({
|
||||
runId: 42,
|
||||
idempotencyKey: `P${place}`.padEnd(40, 'x'),
|
||||
params: { place, creature: 'Orc', count: 1 },
|
||||
verify: true,
|
||||
})
|
||||
assert.equal(result.ok, true, `${place} must still resolve`)
|
||||
}
|
||||
|
||||
// And a three-part value whose group is gone REFUSES rather than silently
|
||||
// landing somewhere else. That is the honest answer: it asked for one place.
|
||||
const gone = await byId('uo.creature.spawn').perform({
|
||||
runId: 42,
|
||||
idempotencyKey: 'G'.repeat(40),
|
||||
params: { place: 'Felucca/Renamed/Entrance', creature: 'Orc', count: 1 },
|
||||
verify: true,
|
||||
})
|
||||
assert.equal(gone.ok, false)
|
||||
assert.match(gone.error, /no landmark called/)
|
||||
})
|
||||
|
||||
// ── The world verbs (Phase 12a) ───────────────────────────────
|
||||
|
||||
test('a spawn files one ledger row per serial, not one per call', async () => {
|
||||
|
||||
Reference in New Issue
Block a user