pack: KubeJS tidying + admin commands, bump to 0.20.0

Tidying pass on the KubeJS suite before plot system V1 lands. Sets up
the file structure + conventions + diagnostics so the larger plot
system has a clean foundation.

Changes:

1. NEW kubejs/README.md
   File layout doc + conventions: logging prefix [bns/<module>],
   persistent-data key naming (bns* prefix), one concern per file,
   performance discipline (O(1) lookups, no polling).

2. Split economy_policy.js -> recipes_waystones.js + recipes_numismatics.js
   Single-concern files. recipes_waystones removes ALL waystones
   crafting (welcome.js distributes the only allowed waystone instead).
   recipes_numismatics removes the 5 coin-denomination crafts.

3. NEW kubejs/server_scripts/bns_admin.js
   /bns admin subcommands for OP diagnostics:
     - info                       calling player's flags+counts
     - waystone-count <player>    read stored count
     - waystone-set <player> <n>  override stored count
     - reset-welcome <player>     clear bnsWelcomeGranted -- replays
                                  the welcome grant on next login
   Registered via ServerEvents.commandRegistry with Brigadier tree,
   permission level 2 (OP). No performance cost (operator-triggered only).

4. Logging added to waystones_policy.js + welcome.js
   Every action prints `[bns/<module>] ...` so server logs can be
   filtered with `journalctl ... | grep '\[bns/'`.

Performance discipline applied:
- All event-handler lookups use Set.has (O(1)), not Array.includes
- No periodic tick polling anywhere
- No network calls in event handlers
- console.info is cheap (no string format unless logged)

Bumps to 0.20.0. Plot system V1 follows in v0.21.x as a separate
multi-file submodule under server_scripts/plots/.
This commit is contained in:
2026-05-23 15:39:59 +00:00
parent f39205404a
commit c699dcc77b
8 changed files with 196 additions and 27 deletions
+61
View File
@@ -0,0 +1,61 @@
# Brass & Sigil KubeJS scripts
All custom server behaviour lives here. Files in this tree ship via the
modpack manifest (see `scripts/Build-Pack.ps1``kubejs/` is in
`managedRoots`), get synced to both server and client installs, and are
loaded by KubeJS on startup.
## File layout
```
kubejs/
├── README.md this file
├── server_scripts/ Loaded by the SERVER's KubeJS on world load.
│ ├── recipes_<x>.js Crafting-recipe overhauls per mod.
│ ├── waystones_policy.js Waystones placement gating (1/player + bans).
│ ├── welcome.js First-join grant: waystone + manual book.
│ ├── cc_recipes.js Full ComputerCraft recipe overhaul.
│ └── bns_admin.js /bns admin <subcommand> for OP diagnostics.
└── client_scripts/ Loaded by the CLIENT's KubeJS on game load.
└── jei_hides.js Hide specific items from the JEI ingredient list.
```
## Conventions
- **Logging prefix**: every console line uses `[bns/<module>]` so server
logs can be filtered with `journalctl -u brass-sigil-server.service |
grep '\[bns/'`. Keeps our output separate from mod output.
- **One concern per file**. Recipe overhauls live in `recipes_<modname>`.
Block/item policy goes in a `<thing>_policy.js`. Don't mix recipe
removal with event listeners in the same file.
- **Persistent player data** keys are prefixed `bns` to avoid clashing
with other mods' use of `player.persistentData`. Currently used keys:
- `bnsWaystoneCount` (int) — see `waystones_policy.js`
- `bnsWelcomeGranted` (bool) — see `welcome.js`
- **Performance**: hot-path event handlers (BlockEvents.placed/broken,
PlayerEvents.loggedIn) must stay O(1). Use `Set` for ID lookups, never
Array.includes inside an event handler. Avoid Modrinth/network calls
in event handlers — never. No periodic-tick polling unless absolutely
necessary.
- **Server commands** are registered under the `/bns` namespace
(`/bns admin ...`, future: `/bns plot ...`). See `bns_admin.js` for
the registration pattern.
## Diagnostics
When something looks wrong in-game, first port of call:
1. `/bns admin info` — prints active flags + counts for the running player
2. `/bns admin waystone-count <player>` — read stored count
3. `/bns admin reset-welcome <player>` — re-trigger the first-join grant
4. Server log: `journalctl -u brass-sigil-server.service -f | grep '\[bns/'`
## See also
- `pack/overrides/defaultconfigs/` — first-run mod configs (auto-copied
to `config/` by NeoForge on first launch only)
- `scripts/Check-Deps.ps1` — pre-flight dep check, runs before any
Deploy-Brass `-Stage Pack` deploy
- The memory file `project-mc-player-economy.md` for the broader plan
@@ -0,0 +1,89 @@
// Brass & Sigil — admin commands (/bns admin ...)
//
// Diagnostic + maintenance commands for OPs. Players without OP level
// (>= 2) get a permission-denied response from the command framework.
//
// Subcommand summary:
// /bns admin info state of the calling player
// /bns admin waystone-count <player> read stored count
// /bns admin waystone-set <player> <n> override stored count
// /bns admin reset-welcome <player> clear bnsWelcomeGranted flag
// so the welcome grant fires again
//
// Adding new subcommands: follow the same `Commands.literal(...)
// .then(...)` pattern below. Keep handlers cheap -- these are only run
// on operator action so performance isn't critical, but make them
// readable.
ServerEvents.commandRegistry(event => {
const { commands: Commands, arguments: Arguments } = event;
const bnsAdmin = Commands.literal('bns')
.then(Commands.literal('admin')
.requires(src => src.hasPermission(2))
// /bns admin info
.then(Commands.literal('info').executes(ctx => {
const player = ctx.source.player;
if (!player) {
ctx.source.sendSystemMessage(Text.red('Run from a player context.'));
return 0;
}
const data = player.persistentData;
const lines = [
`Player: ${player.username} (${player.uuid})`,
`bnsWaystoneCount = ${data.getInt('bnsWaystoneCount')}`,
`bnsWelcomeGranted = ${data.getBoolean('bnsWelcomeGranted')}`,
];
lines.forEach(line => ctx.source.sendSystemMessage(Text.aqua(line)));
console.info(`[bns/admin] info: ${player.username} -> ${JSON.stringify({
waystones: data.getInt('bnsWaystoneCount'),
welcomed: data.getBoolean('bnsWelcomeGranted'),
})}`);
return 1;
}))
// /bns admin waystone-count <player>
.then(Commands.literal('waystone-count')
.then(Commands.argument('target', Arguments.PLAYER.create(event))
.executes(ctx => {
const target = Arguments.PLAYER.getResult(ctx, 'target');
const count = target.persistentData.getInt('bnsWaystoneCount');
ctx.source.sendSystemMessage(Text.aqua(
`${target.username} has bnsWaystoneCount = ${count}`
));
return 1;
})))
// /bns admin waystone-set <player> <n>
.then(Commands.literal('waystone-set')
.then(Commands.argument('target', Arguments.PLAYER.create(event))
.then(Commands.argument('count', Arguments.INTEGER.create(event, 0, 1000))
.executes(ctx => {
const target = Arguments.PLAYER.getResult(ctx, 'target');
const count = Arguments.INTEGER.getResult(ctx, 'count');
target.persistentData.putInt('bnsWaystoneCount', count);
ctx.source.sendSystemMessage(Text.gold(
`Set ${target.username}'s bnsWaystoneCount = ${count}`
));
console.info(`[bns/admin] waystone-set: ${target.username} -> ${count}`);
return 1;
}))))
// /bns admin reset-welcome <player>
.then(Commands.literal('reset-welcome')
.then(Commands.argument('target', Arguments.PLAYER.create(event))
.executes(ctx => {
const target = Arguments.PLAYER.getResult(ctx, 'target');
target.persistentData.putBoolean('bnsWelcomeGranted', false);
ctx.source.sendSystemMessage(Text.gold(
`Cleared bnsWelcomeGranted for ${target.username}. ` +
`They'll get the welcome packet again on next login.`
));
console.info(`[bns/admin] reset-welcome: ${target.username}`);
return 1;
})))
);
event.register(bnsAdmin);
});
@@ -1,26 +0,0 @@
// Brass & Sigil — economy policy (recipe removal)
//
// Players can't craft Waystones items (they're distributed by the welcome
// grant -- see welcome.js -- and any replacements come from the bank
// vendor in the future). Players also can't manufacture currency: coins
// only enter circulation through the bank exchange (item-in / coin-out).
//
// Other Numismatics blocks (piggy banks, bank tellers, vendor blocks) stay
// craftable -- those are infrastructure, not currency.
ServerEvents.recipes(event => {
// Block ALL crafting of Waystones items. The welcome-grant script
// gives each new player exactly one regular waystone; any further
// recovery happens via OP /give or future bank vendor.
event.remove({ mod: 'waystones' });
// Block coin manufacturing only -- 5 denominations.
// Real registry IDs: spur, bevel, cog, crown, sun.
[
'numismatics:spur',
'numismatics:bevel',
'numismatics:cog',
'numismatics:crown',
'numismatics:sun',
].forEach(coin => event.remove({ output: coin }));
});
@@ -0,0 +1,24 @@
// Brass & Sigil — Numismatics coin recipe removal
//
// The 5 coin denominations cannot be crafted by players. All currency
// enters circulation exclusively through the bank exchange (item-in /
// coin-out). Initially with fixed rates admin-configured on Numismatics
// vendor blocks; future dynamic-rates layer adjusts based on trade
// volume (see project-mc-player-economy.md).
//
// Other Numismatics items (piggy banks, bank tellers, vendor blocks,
// depositors, bank cards) stay craftable -- those are infrastructure,
// not currency.
const COIN_IDS = [
'numismatics:spur',
'numismatics:bevel',
'numismatics:cog',
'numismatics:crown',
'numismatics:sun',
];
ServerEvents.recipes(event => {
COIN_IDS.forEach(coin => event.remove({ output: coin }));
console.info(`[bns/recipes_numismatics] removed crafting recipes for ${COIN_IDS.length} coin denominations`);
});
@@ -0,0 +1,16 @@
// Brass & Sigil — Waystones recipe removal
//
// All Waystones items are uncraftable. Players never see waystones,
// sharestones, portstones, warp plates, warp scrolls or warp stones in
// the recipe book or JEI search results (see also jei_hides.js on the
// client side).
//
// Regular waystones are distributed exclusively via the first-join grant
// (welcome.js). Replacement waystones for players who break theirs will
// come from the bank vendor once the economy ships -- until then OPs
// hand them out via /give.
ServerEvents.recipes(event => {
event.remove({ mod: 'waystones' });
console.info('[bns/recipes_waystones] removed all waystones recipes');
});
@@ -62,6 +62,7 @@ BlockEvents.placed(event => {
'That block is disabled. Use a regular Waystone (1 per player) ' + 'That block is disabled. Use a regular Waystone (1 per player) ' +
'and build transport between distant bases.' 'and build transport between distant bases.'
)); ));
console.info(`[bns/waystones_policy] denied ${blockId} placement by ${player.username} (banned)`);
return; return;
} }
@@ -74,9 +75,11 @@ BlockEvents.placed(event => {
`You already have ${WAYSTONE_CAP} waystone. Break your existing one ` + `You already have ${WAYSTONE_CAP} waystone. Break your existing one ` +
`to relocate, or build infrastructure to reach distant bases.` `to relocate, or build infrastructure to reach distant bases.`
)); ));
console.info(`[bns/waystones_policy] denied ${blockId} placement by ${player.username} (already at cap ${current})`);
return; return;
} }
data.putInt('bnsWaystoneCount', current + 1); data.putInt('bnsWaystoneCount', current + 1);
console.info(`[bns/waystones_policy] +1 waystone for ${player.username} (now ${current + 1})`);
} }
}); });
@@ -89,5 +92,6 @@ BlockEvents.broken(event => {
const current = data.getInt('bnsWaystoneCount'); const current = data.getInt('bnsWaystoneCount');
if (current > 0) { if (current > 0) {
data.putInt('bnsWaystoneCount', current - 1); data.putInt('bnsWaystoneCount', current - 1);
console.info(`[bns/waystones_policy] -1 waystone for ${player.username} (now ${current - 1})`);
} }
}); });
@@ -46,6 +46,7 @@ PlayerEvents.loggedIn(event => {
giveManual(player); giveManual(player);
data.putBoolean(WELCOME_FLAG, true); data.putBoolean(WELCOME_FLAG, true);
player.tell(Text.gold('Welcome to Brass & Sigil. Check your inventory for a Waystone and the server manual.')); player.tell(Text.gold('Welcome to Brass & Sigil. Check your inventory for a Waystone and the server manual.'));
console.info(`[bns/welcome] granted starter packet to ${player.username}`);
} catch (e) { } catch (e) {
// If something goes wrong, don't set the flag -- player gets another chance. // If something goes wrong, don't set the flag -- player gets another chance.
console.error(`[bns/welcome] failed for ${player.username}: ${e}`); console.error(`[bns/welcome] failed for ${player.username}: ${e}`);
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing", "$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
"name": "Brass and Sigil", "name": "Brass and Sigil",
"version": "0.19.5", "version": "0.20.0",
"minecraft": "1.21.1", "minecraft": "1.21.1",
"loader": { "loader": {
"type": "neoforge", "type": "neoforge",