feat(shard): read clilocs from a source SET so shard items get names
Shards edit items and add new ones, and those carry cliloc ids no stock client
table has. Reading exactly one converted file meant an operator had to
re-export 5 MB every time they added one item — friction enough that the table
would simply go stale, which is the failure the spawn atlas was redesigned to
avoid in the first place.
So this mirrors spawnAtlasSource.readSources(): a BASE (the converted client
table) plus every operator-maintained overlay under `custom/`, all re-read on
every boot and hash-gated as a SET. Later sources win, so an overlay both adds
ids the client never had and overrides stock ones the shard re-purposed.
Adding, editing or removing any overlay counts as drift.
`custom/` is the one convention here that is ours rather than the shard's, and
deliberately so: ServUO has no server-side notion of a custom cliloc — they
live in the patched client a shard distributes, and nothing in the tree
declares them. There is nothing to discover. (An operator who does patch their
client cliloc needs no overlay: convert the patched file and the edits are in
the base.) Scale, measured on the live shard: its script tree references 16,434
cliloc ids and only 37 are absent from stock — tens against a 67k base, which
is why this is an overlay and not a second table.
The set brings back a hazard a single file did not have, and it gets the
atlas's answer. A corrupt source fails the parse loudly, but a source that has
VANISHED parses perfectly and imports a table quietly missing everything it
contributed — an unmounted volume is indistinguishable from a deliberate
deletion. So it is staged, not applied (`needsReview`), reported by both the
import and status(), and accepted with `{approve:true}`. That is a flag rather
than the atlas's approve/reject pair because the atlas stores a pending
decision SO THAT approving re-parses; here nothing is stored, so re-reading at
approval time is automatic.
Also reports a per-source breakdown (entries/added/overrode) on import and in
status, which is how an operator confirms an overlay took effect — "overrode: 0"
on a file meant to re-label stock items says it did not.
Two bugs this surfaced, both found by running a shard-style overlay rather than
by another stock-table fixture:
- displayText tidied punctuation unconditionally, so a custom
"Runic Gateway Sigil (v2)" rendered as "(v2". Stripping leftover brackets is
right after a placeholder is removed and wrong otherwise — the same condition
the `%` rule already had.
- CANDIDATE_NAMES did not include `clilocs.plain`, which is the exact filename
CLILOCS.md and the export tool's README tell operators to write. Pointing at
the directory they were told to create failed with NO_FILE.
Verified end to end against the live MariaDB and a real server boot: base-only
import, overlay adding one id and overriding another (per-source breakdown
correct), unchanged set as a no-op, an edited overlay re-importing and
withdrawing its override, a vanished overlay refused with the table intact,
status reporting missingSources, approve applying it, and a file-path
configuration still finding overlays beside it. All three resolve correctly
through the running server: shard-added, overridden and stock. 646 server tests
pass (16 new in clilocSource.test.js, 3 new in clilocParse.test.js); swagger,
routes.manifest.json and routes.guards.json regenerated.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -209,6 +209,18 @@ test('displayText: ordinary names pass through untouched', () => {
|
||||
assert.equal(displayText(' spiked collar '), 'spiked collar')
|
||||
})
|
||||
|
||||
test('displayText: punctuation is only tidied when a placeholder was removed', () => {
|
||||
// A shard's custom "Runic Gateway Sigil (v2)" came back as "(v2" while the
|
||||
// bracket trim was unconditional. A string with no placeholder has no debris
|
||||
// to clean, so it is left alone apart from whitespace.
|
||||
assert.equal(displayText('Runic Gateway Sigil (v2)'), 'Runic Gateway Sigil (v2)')
|
||||
assert.equal(displayText('scroll of power - greater'), 'scroll of power - greater')
|
||||
assert.equal(displayText('[Companion] Great Dane'), '[Companion] Great Dane')
|
||||
// …but the debris a placeholder leaves behind is still cleaned.
|
||||
assert.equal(displayText('[~1_stuff~]'), '')
|
||||
assert.equal(displayText('cold damage ~1_val~%'), 'cold damage')
|
||||
})
|
||||
|
||||
test('displayText: null and undefined are empty, not "null"', () => {
|
||||
assert.equal(displayText(null), '')
|
||||
assert.equal(displayText(undefined), '')
|
||||
|
||||
Reference in New Issue
Block a user