diff --git a/pack/overrides/kubejs/README.md b/pack/overrides/kubejs/README.md new file mode 100644 index 0000000..2372b12 --- /dev/null +++ b/pack/overrides/kubejs/README.md @@ -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_.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 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/]` 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_`. + Block/item policy goes in a `_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 ` — read stored count +3. `/bns admin reset-welcome ` — 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 diff --git a/pack/overrides/kubejs/server_scripts/bns_admin.js b/pack/overrides/kubejs/server_scripts/bns_admin.js new file mode 100644 index 0000000..50d9501 --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/bns_admin.js @@ -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 read stored count +// /bns admin waystone-set override stored count +// /bns admin reset-welcome 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 + .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 + .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 + .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); +}); diff --git a/pack/overrides/kubejs/server_scripts/economy_policy.js b/pack/overrides/kubejs/server_scripts/economy_policy.js deleted file mode 100644 index cb58465..0000000 --- a/pack/overrides/kubejs/server_scripts/economy_policy.js +++ /dev/null @@ -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 })); -}); diff --git a/pack/overrides/kubejs/server_scripts/recipes_numismatics.js b/pack/overrides/kubejs/server_scripts/recipes_numismatics.js new file mode 100644 index 0000000..d8f34a3 --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/recipes_numismatics.js @@ -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`); +}); diff --git a/pack/overrides/kubejs/server_scripts/recipes_waystones.js b/pack/overrides/kubejs/server_scripts/recipes_waystones.js new file mode 100644 index 0000000..7b77ecf --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/recipes_waystones.js @@ -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'); +}); diff --git a/pack/overrides/kubejs/server_scripts/waystones_policy.js b/pack/overrides/kubejs/server_scripts/waystones_policy.js index 21e2ba4..acebf0a 100644 --- a/pack/overrides/kubejs/server_scripts/waystones_policy.js +++ b/pack/overrides/kubejs/server_scripts/waystones_policy.js @@ -62,6 +62,7 @@ BlockEvents.placed(event => { 'That block is disabled. Use a regular Waystone (1 per player) ' + 'and build transport between distant bases.' )); + console.info(`[bns/waystones_policy] denied ${blockId} placement by ${player.username} (banned)`); return; } @@ -74,9 +75,11 @@ BlockEvents.placed(event => { `You already have ${WAYSTONE_CAP} waystone. Break your existing one ` + `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; } 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'); if (current > 0) { data.putInt('bnsWaystoneCount', current - 1); + console.info(`[bns/waystones_policy] -1 waystone for ${player.username} (now ${current - 1})`); } }); diff --git a/pack/overrides/kubejs/server_scripts/welcome.js b/pack/overrides/kubejs/server_scripts/welcome.js index d70f357..ce8f709 100644 --- a/pack/overrides/kubejs/server_scripts/welcome.js +++ b/pack/overrides/kubejs/server_scripts/welcome.js @@ -46,6 +46,7 @@ PlayerEvents.loggedIn(event => { giveManual(player); data.putBoolean(WELCOME_FLAG, true); 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) { // If something goes wrong, don't set the flag -- player gets another chance. console.error(`[bns/welcome] failed for ${player.username}: ${e}`); diff --git a/pack/pack.lock.json b/pack/pack.lock.json index 2abb157..9779f16 100644 --- a/pack/pack.lock.json +++ b/pack/pack.lock.json @@ -1,7 +1,7 @@ { "$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing", "name": "Brass and Sigil", - "version": "0.19.5", + "version": "0.20.0", "minecraft": "1.21.1", "loader": { "type": "neoforge",