feat(modules): ctx additions and the post-hook registry (API 1.1.0)
Everything the extraction needed from core that ctx did not already offer. Additions only, so minor. ctx.activity.log, because an admin action a module performs has to land in core's one audit log or the trail has a hole exactly where a module operates the game -- a module keeping its own log would be a second place to look, which in practice means a place nobody looks. Write-only; reading the log is the admin panel's job and it spans every actor. ctx.users.getById, one function for one caller: the admin.users.detail slot router needs the user its prefix names. ctx.site.baseUrl, because a module has to build absolute links and §2.7 forbids it reading core's APP_BASE_URL -- a getter, not a captured string, so it cannot go stale against the env. ctx.middleware.rateLimit is core's makeLimiter, plus accountChangeLimiter handed over whole. The split is deliberate: a module states its own window and cap because it knows what its endpoints cost, and takes the plumbing from core so there is one express-rate-limit in the process and one place a breach is logged. accountChangeLimiter is shared policy -- core's /auth/me and /player/account sit behind the same counter -- so a module's account-change route has to land IN it rather than beside it. marketLimiter was UO policy living in core's file and leaves with the route it guards. registerPostHook is the fourth registry, and the last thing binding core to the module. Core's post controller called newsGump.syncPost directly: core's CMS naming a UO file. It now publishes what it already knows and a subscriber decides what to do with it. Not folded into registerAnnounceLeg, which fires on the same transition, because a leg is a one-shot DELIVERY with retry and classification while a post hook maintains idempotent STATE, runs on delete as well as save, and refreshes silently on an edit. Also fixes a real loader defect the extraction exposed: schema table names were matched against the RAW file, so a fragment whose header says "every CREATE TABLE carries IF NOT EXISTS" was rejected for a prefix violation on a table called `carries`. module-uo's fragment hit exactly that. Both scans now read split statements, which strip comments -- the same class of bug as a boundary check failing on its own documentation. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
//
|
||||
// Refresh the spawn atlas from a ServUO tree, from the command line.
|
||||
//
|
||||
// npm run atlas:import # use the configured path
|
||||
// npm run atlas:import -- --servuo <path> # override it for this run
|
||||
// npm run atlas:import -- --force # reimport even if unchanged
|
||||
// npm run atlas:import -- --approve # apply a staged refresh
|
||||
// npm run atlas:import -- --status # report without changing anything
|
||||
//
|
||||
// The server does this itself on every boot (see `shardAtlas.refreshOnBoot`), so
|
||||
// this is for operators who want to apply a map change without a restart, and
|
||||
// for approving a refresh that was staged because it would remove a facet.
|
||||
//
|
||||
// All the logic lives in `src/model/shardAtlas/shardAtlas.model.js`; this file
|
||||
// is argument parsing and output formatting.
|
||||
|
||||
const db = () => require('../src/utils/db')
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {}
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const flag = argv[i]
|
||||
if (flag === '--servuo') args.servuo = argv[++i]
|
||||
else if (flag === '--force') args.force = true
|
||||
else if (flag === '--approve') args.approve = true
|
||||
else if (flag === '--reject') args.reject = true
|
||||
else if (flag === '--status') args.status = true
|
||||
else if (flag === '--help' || flag === '-h') args.help = true
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
const USAGE = `
|
||||
Refresh the spawn atlas from a ServUO tree.
|
||||
|
||||
node scripts/importSpawnAtlas.js [options]
|
||||
|
||||
--servuo <path> Use this tree for this run instead of the configured path.
|
||||
--force Reimport even when the source files are unchanged.
|
||||
--approve Apply a refresh that was staged for removing a facet.
|
||||
--reject Keep the current atlas and dismiss the staged refresh.
|
||||
--status Report atlas and source state; change nothing.
|
||||
|
||||
With no options this imports only if the tree differs from what is loaded.
|
||||
`
|
||||
|
||||
function describe(result) {
|
||||
switch (result.status) {
|
||||
case 'skipped':
|
||||
return (
|
||||
'No ServUO path configured — nothing to import.\n' +
|
||||
'Set one with SERVUO_PATH, the admin panel, or --servuo <path>.\n'
|
||||
)
|
||||
case 'unavailable':
|
||||
return `ServUO tree unavailable: ${result.reason}\n`
|
||||
case 'unchanged':
|
||||
return `Atlas is already up to date${result.reason ? ` (${result.reason})` : ''}.\n`
|
||||
case 'needsReview': {
|
||||
return (
|
||||
'Refresh NOT applied — it would remove ' +
|
||||
`${result.removedFacets.length} facet(s): ${result.removedFacets.join(', ')}.\n` +
|
||||
'This is what a half-copied or mid-update tree looks like, so it has been\n' +
|
||||
'staged for review. The current atlas is unchanged.\n' +
|
||||
'Apply it with --approve, or dismiss it with --reject.\n'
|
||||
)
|
||||
}
|
||||
case 'imported': {
|
||||
const c = result.counts
|
||||
const added = result.addedFacets?.length ? ` Added facets: ${result.addedFacets.join(', ')}.` : ''
|
||||
const removed = result.removedFacets?.length
|
||||
? ` Removed facets: ${result.removedFacets.join(', ')}.`
|
||||
: ''
|
||||
return (
|
||||
`Atlas imported: ${c.points} points, ${c.creatures} creatures, ` +
|
||||
`${c.pointTypes} point/type rows, ${c.regions} regions, ` +
|
||||
`${c.landmarks} landmarks, ${c.champions} champion altars.${added}${removed}\n`
|
||||
)
|
||||
}
|
||||
case 'failed':
|
||||
return `Atlas refresh failed: ${result.reason}\n`
|
||||
default:
|
||||
return `${JSON.stringify(result, null, 2)}\n`
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
if (args.help) {
|
||||
process.stdout.write(USAGE)
|
||||
return
|
||||
}
|
||||
|
||||
const shardAtlas = require('../src/model/shardAtlas/shardAtlas.model')
|
||||
|
||||
// `--servuo` is a per-run override and deliberately does NOT persist to the
|
||||
// configured path; changing where the atlas permanently reads from is an
|
||||
// admin action, not a side effect of a one-off import.
|
||||
const override = { path: args.servuo ?? '' }
|
||||
|
||||
if (args.status) {
|
||||
process.stdout.write(`${JSON.stringify(await shardAtlas.status(override), null, 2)}\n`)
|
||||
return
|
||||
}
|
||||
if (args.reject) {
|
||||
process.stdout.write(`${JSON.stringify(await shardAtlas.rejectPending(), null, 2)}\n`)
|
||||
return
|
||||
}
|
||||
|
||||
const result = args.approve
|
||||
? await shardAtlas.approvePending(override)
|
||||
: await shardAtlas.refresh({ ...override, force: Boolean(args.force) })
|
||||
|
||||
process.stdout.write(describe(result))
|
||||
if (result.status === 'failed') process.exitCode = 1
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main()
|
||||
.catch((err) => {
|
||||
process.stderr.write(`atlas:import failed: ${err.message}\n`)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(() => db().close())
|
||||
}
|
||||
|
||||
module.exports = { describe, parseArgs }
|
||||
Reference in New Issue
Block a user