Compare commits

...

11 Commits

Author SHA1 Message Date
Matt d24f7f03a3 kubejs: drop jei_hides.js (wrong API binding), 0.21.1 -> 0.21.2
Two consecutive client_scripts errors traced to jei_hides.js:
1. Array-spread syntax not supported by Rhino (fixed in 0.21.1)
2. JEIEvents global is not defined in KubeJS 2101.7 -- the actual JEI
   event API uses classes like JEIRemoveEntriesKubeEvent but the binding
   name is something different we haven't identified yet.

Rather than ship more guesses, drop the script entirely. Functional
impact is minimal: recipes for waystones + coins are still removed
server-side (recipes_waystones.js, recipes_numismatics.js), so players
cannot craft these items even if they appear in JEI search. The only
loss is some JEI clutter.

When we identify the correct JEI hide API in KubeJS 2101.x, can add
back as kubejs/client_scripts/jei_hides.js with the right binding.
2026-05-23 16:22:01 +00:00
Matt 28a02d0ced Merge pull request 'jei_hides: Rhino-compatible (no array spread)' (#26) from fix/jei-hides-rhino-spread into main 2026-05-23 16:18:51 +00:00
Matt d5fd2c97e9 jei_hides: use .concat instead of array-spread (Rhino compat). 0.21.0 -> 0.21.1.
KubeJS uses Rhino as its JS engine, which doesn't support array-spread
syntax (`[...a, ...b]`). The line `[...WAYSTONE_ITEMS, ...COIN_ITEMS]
.forEach(...)` threw EvaluatorException: syntax error at jei_hides.js:46
on client launch, surfacing the KubeJS client script errors dialog.

Fix: use Array.prototype.concat() which is universal JS. Also extracted
the hide-quietly helper into a named function for readability.
2026-05-23 16:18:50 +00:00
Matt ae061a5035 Merge pull request 'v0.21.0: Plot system V1' (#25) from feature/plots-v1 into main 2026-05-23 16:02:05 +00:00
Matt 6264785f2c plots: v1 spawn-commercial-plots system, bump to 0.21.0
/bns plot list|info|buy|release|reload. Single ~316-line file with
clear sections. Hardcoded PLOT_DEFS Map (3 example plots), ownership
state in server.persistentData.bnsPlots, best-effort Numismatics +
FTB Chunks integration (will be verified in playtest). See file
header for full design notes.
2026-05-23 16:01:47 +00:00
Matt 998ec21bb5 Merge pull request 'Fix bns_admin command registration' (#24) from fix/bns-admin-int-range into main 2026-05-23 15:41:34 +00:00
Matt aa881bfc65 bns_admin: drop INTEGER range args (KubeJS wrapper doesn't expose them), validate in handler instead. 0.20.0 -> 0.20.1. 2026-05-23 15:41:34 +00:00
Matt 3cec2cb69b Merge pull request 'v0.20.0: KubeJS tidying + admin commands' (#23) from feature/v0.20.0-tidying into main 2026-05-23 15:40:00 +00:00
Matt c699dcc77b 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/.
2026-05-23 15:39:59 +00:00
Matt f39205404a Merge pull request 'RPO: default-enable CC Recreated pack' (#22) from feature/rpo-default-config into main 2026-05-23 15:32:49 +00:00
Matt b3d46931b2 rpo: ship default-enable config for ComputerCraft Recreated, bump 0.19.5
Resource Pack Overrides config field names extracted directly from the
mod's class files (default_packs + default_overrides confirmed via grep
on the .class strings). Config ships through defaultconfigs/ so it
seeds the player's local config on first run and respects later edits.

The 'default_position: TOP' override puts the ReCreated pack at the top
of the enabled-packs list so it takes precedence over the vanilla
ComputerCraft textures.

If the filename or schema turns out to be off when tested, it's a
one-line follow-up to fix.
2026-05-23 15:32:49 +00:00
11 changed files with 531 additions and 80 deletions
@@ -0,0 +1,11 @@
{
"default_packs": [
"file/Computer Craft Recreated v1.2.zip"
],
"default_overrides": [
{
"id": "file/Computer Craft Recreated v1.2.zip",
"default_position": "TOP"
}
]
}
+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
@@ -1,53 +0,0 @@
// Brass & Sigil — JEI hides
//
// Hides all Waystones items + all Numismatics coin items from the JEI
// item list. Combined with the server-side recipe removal in
// economy_policy.js, players don't see crafting recipes OR the item
// icons in the JEI ingredient list -- they only ever encounter these
// items through the welcome grant or the bank exchange.
const COLORS = [
'white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime',
'pink', 'gray', 'light_gray', 'cyan', 'purple', 'blue',
'brown', 'green', 'red', 'black',
];
const WAYSTONE_ITEMS = [
// regular waystone variants
'waystones:waystone',
'waystones:mossy_waystone',
'waystones:sandy_waystone',
'waystones:blackstone_waystone',
'waystones:deepslate_waystone',
'waystones:end_stone_waystone',
// handheld + plate
'waystones:warp_stone',
'waystones:warp_scroll',
'waystones:bound_scroll',
'waystones:return_scroll',
'waystones:warp_plate',
// sharestones (16 colors) + shards
'waystones:sharestone', // base id if registered
];
COLORS.forEach(c => {
WAYSTONE_ITEMS.push(`waystones:${c}_sharestone`);
WAYSTONE_ITEMS.push(`waystones:${c}_portstone`);
});
const COIN_ITEMS = [
'numismatics:spur',
'numismatics:bevel',
'numismatics:cog',
'numismatics:crown',
'numismatics:sun',
];
JEIEvents.hideItems(event => {
[...WAYSTONE_ITEMS, ...COIN_ITEMS].forEach(id => {
try { event.hide(id); } catch (e) {
// Some IDs may not exist (e.g. base 'waystones:sharestone' isn't
// registered if only colored variants are). Swallow per-item
// failures so one bad id doesn't break the whole pass.
}
});
});
@@ -0,0 +1,97 @@
// 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>
// (Argument-bounds aren't exposed via KubeJS's wrapper, so we
// validate the range inside the handler instead.)
.then(Commands.literal('waystone-set')
.then(Commands.argument('target', Arguments.PLAYER.create(event))
.then(Commands.argument('count', Arguments.INTEGER.create(event))
.executes(ctx => {
const target = Arguments.PLAYER.getResult(ctx, 'target');
const count = Arguments.INTEGER.getResult(ctx, 'count');
if (count < 0 || count > 1000) {
ctx.source.sendSystemMessage(Text.red(
`count must be 0..1000 (got ${count})`
));
return 0;
}
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,316 @@
// Brass & Sigil — Plot system V1
//
// SPAWN COMMERCIAL PLOTS (not bases). Players' homes go out in the world
// with normal FTB Chunks claims. A "plot" is a small commercial chunk-
// allocation at spawn for shops. 1 plot per player; release-and-rebuy to
// change size.
//
// Player commands (/bns plot ...):
// list show all plots + availability + price
// info <id> detailed info for one plot
// buy <id> purchase if eligible (no current plot, has coin)
// release release current plot (50% refund)
// Admin commands (OP, level 2):
// reload re-load plot definitions (currently in-script)
//
// Storage:
// Plot definitions: hardcoded `PLOT_DEFS` below; admin edits and reloads.
// Path-to-disk loading deferred to V1.1 (KubeJS file
// I/O is sandboxed).
// Ownership state: server.persistentData.bnsPlots -- compound NBT
// keyed by plot id, value = player UUID string. Single
// source of truth.
//
// Integrations (best-effort; verify in playtest):
// - Numismatics coin handling: inventory-based. Counts coins by denomination
// value, takes greedily by largest first. Assumed values:
// spur=1, bevel=8, cog=64, crown=512, sun=4096
// If Numismatics uses different ratios, adjust COIN_VALUES below.
// - FTB Chunks claim transfer: runs `/ftbchunks claim <x> <z>` as the player
// for each chunk in the plot, prefixed by `execute in <dim> as <player>`.
// Release runs `/ftbchunks unclaim`. May need adjusting if FTBC command
// syntax differs in this version.
//
// Performance:
// - All event-handler-style work is bounded by player action (command).
// No periodic ticks. No event hot-path scans.
// - Plot lookup is O(1) via Map.
// - Coin inventory scan is O(36) per purchase -- once per player action.
// ─── Plot definitions ─────────────────────────────────────────────────
// V1 hardcoded. Replace with admin-defined plots once the spawn marketplace
// is built. Chunks are [chunkX, chunkZ] pairs in the overworld.
const PLOT_DEFS = new Map([
['p001', {
id: 'p001', name: 'Market Stall 1', size: 'small',
chunks: [[10, 10]], // 1x1
price: 100, // in spurs
dimension: 'minecraft:overworld',
}],
['p002', {
id: 'p002', name: 'Corner Lot', size: 'medium',
chunks: [[12, 10], [12, 11]], // 1x2
price: 250,
dimension: 'minecraft:overworld',
}],
['p003', {
id: 'p003', name: 'Anchor Plot', size: 'large',
chunks: [[14, 10], [14, 11], [15, 10], [15, 11]], // 2x2
price: 600,
dimension: 'minecraft:overworld',
}],
]);
const REFUND_FRACTION = 0.5; // released plots refund 50% of purchase price
// ─── Numismatics coin handling ────────────────────────────────────────
const COIN_VALUES = {
'numismatics:spur': 1,
'numismatics:bevel': 8,
'numismatics:cog': 64,
'numismatics:crown': 512,
'numismatics:sun': 4096,
};
const DENOMS_DESC = ['numismatics:sun', 'numismatics:crown', 'numismatics:cog',
'numismatics:bevel', 'numismatics:spur'];
function countCoins(player) {
let total = 0;
const inv = player.inventory;
for (let i = 0; i < inv.containerSize; i++) {
const stack = inv.getItem(i);
const v = COIN_VALUES[stack.id];
if (v) total += v * stack.count;
}
return total;
}
function takeCoins(player, amount) {
let remaining = amount;
const inv = player.inventory;
// Pass 1: take from largest denomination downward (minimises change).
for (const id of DENOMS_DESC) {
const value = COIN_VALUES[id];
if (remaining < value) continue;
for (let i = 0; i < inv.containerSize && remaining >= value; i++) {
const stack = inv.getItem(i);
if (stack.id !== id) continue;
const take = Math.min(stack.count, Math.floor(remaining / value));
stack.shrink(take);
remaining -= take * value;
}
}
return amount - remaining; // returns how many spurs actually taken
}
function giveCoins(player, amount) {
// Give back as largest denomination possible to minimise inventory clutter.
let remaining = amount;
for (const id of DENOMS_DESC) {
const value = COIN_VALUES[id];
if (remaining < value) continue;
const count = Math.floor(remaining / value);
if (count > 0) {
player.give(Item.of(id, count));
remaining -= count * value;
}
}
}
// ─── Ownership storage (server.persistentData.bnsPlots compound) ──────
function getOwnerMap() {
// Lazily ensure the compound exists. The compound is a plot_id -> uuid map.
const root = Utils.server.persistentData;
if (!root.contains('bnsPlots')) {
root.put('bnsPlots', {});
}
return root.getCompound('bnsPlots');
}
function ownerOf(plotId) {
const map = getOwnerMap();
return map.contains(plotId) ? map.getString(plotId) : null;
}
function plotOwnedBy(playerUuid) {
const map = getOwnerMap();
for (const key of map.getAllKeys()) {
if (map.getString(key) === playerUuid) return key;
}
return null;
}
function recordPurchase(plotId, playerUuid) {
getOwnerMap().putString(plotId, playerUuid);
console.info(`[bns/plots] purchase recorded: ${plotId} -> ${playerUuid}`);
}
function clearOwner(plotId) {
getOwnerMap().remove(plotId);
console.info(`[bns/plots] ownership cleared: ${plotId}`);
}
// ─── FTB Chunks integration (best-effort) ────────────────────────────
// FTB Chunks doesn't expose a KubeJS API for "claim chunk X for player Y",
// but its commands (/ftbchunks claim, /ftbchunks unclaim) work as
// console-as-player. Issue them with `execute as <player>` so they claim
// to that player's team.
function claimChunksForPlayer(plot, player) {
for (const [cx, cz] of plot.chunks) {
// /ftbchunks claim claims the chunk the executor stands in by default,
// but accepts X Z args in most versions. Try with explicit coords first;
// if that fails the syntax may need a `position` subcommand variant.
const cmd = `execute as ${player.username} in ${plot.dimension} run ftbchunks claim ${cx} ${cz}`;
Utils.server.runCommandSilent(cmd);
console.info(`[bns/plots] claim: ${cmd}`);
}
}
function unclaimChunksForPlayer(plot, player) {
for (const [cx, cz] of plot.chunks) {
const cmd = `execute as ${player.username} in ${plot.dimension} run ftbchunks unclaim ${cx} ${cz}`;
Utils.server.runCommandSilent(cmd);
console.info(`[bns/plots] unclaim: ${cmd}`);
}
}
// ─── Helpers ──────────────────────────────────────────────────────────
function formatPlotLine(plot) {
const owner = ownerOf(plot.id);
const status = owner ? `OWNED (${owner.substring(0,8)}...)` : 'AVAILABLE';
const cn = plot.chunks.length;
return `${plot.id} "${plot.name}" ${plot.size} ${cn} chunk${cn>1?'s':''} ${plot.price} spurs [${status}]`;
}
function sendMulti(source, lines) {
for (const line of lines) source.sendSystemMessage(Text.of(line));
}
// ─── Command tree ─────────────────────────────────────────────────────
ServerEvents.commandRegistry(event => {
const { commands: Commands, arguments: Arguments } = event;
const plotIdArg = () => Commands.argument('id', Arguments.STRING.create(event));
const tree = Commands.literal('bns')
.then(Commands.literal('plot')
// /bns plot list
.then(Commands.literal('list').executes(ctx => {
const lines = ['§7--- Spawn plots ---'];
for (const plot of PLOT_DEFS.values()) {
lines.push(formatPlotLine(plot));
}
lines.push('§7Run §f/bns plot info <id>§7 for details or §f/bns plot buy <id>§7 to purchase.');
sendMulti(ctx.source, lines);
return 1;
}))
// /bns plot info <id>
.then(Commands.literal('info').then(plotIdArg().executes(ctx => {
const id = Arguments.STRING.getResult(ctx, 'id');
const plot = PLOT_DEFS.get(id);
if (!plot) {
ctx.source.sendSystemMessage(Text.red(`No such plot: ${id}`));
return 0;
}
const owner = ownerOf(id);
const chunkStr = plot.chunks.map(([x,z]) => `(${x},${z})`).join(', ');
sendMulti(ctx.source, [
`§6Plot ${id}: §f${plot.name}`,
`§7 Size: §f${plot.size} (${plot.chunks.length} chunk${plot.chunks.length>1?'s':''})`,
`§7 Chunks: §f${chunkStr}`,
`§7 Dimension: §f${plot.dimension}`,
`§7 Price: §f${plot.price} spurs`,
`§7 Owner: §f${owner || 'AVAILABLE'}`,
]);
return 1;
})))
// /bns plot buy <id>
.then(Commands.literal('buy').then(plotIdArg().executes(ctx => {
const player = ctx.source.player;
if (!player) {
ctx.source.sendSystemMessage(Text.red('Run from a player context.'));
return 0;
}
const id = Arguments.STRING.getResult(ctx, 'id');
const plot = PLOT_DEFS.get(id);
if (!plot) {
player.tell(Text.red(`No such plot: ${id}`));
return 0;
}
if (ownerOf(id)) {
player.tell(Text.red(`Plot ${id} is already owned.`));
return 0;
}
const existing = plotOwnedBy(player.uuid.toString());
if (existing) {
player.tell(Text.red(
`You already own plot ${existing}. Release it first with /bns plot release.`
));
return 0;
}
const have = countCoins(player);
if (have < plot.price) {
player.tell(Text.red(
`You need ${plot.price} spurs (have ${have}). Earn coins at the bank exchange.`
));
return 0;
}
const took = takeCoins(player, plot.price);
if (took < plot.price) {
// Shouldn't happen if countCoins agreed, but be safe.
giveCoins(player, took);
player.tell(Text.red(`Coin removal failed -- refunded. Try again.`));
return 0;
}
recordPurchase(id, player.uuid.toString());
claimChunksForPlayer(plot, player);
player.tell(Text.gold(
`Plot ${id} (${plot.name}) is now yours. Paid ${plot.price} spurs.`
));
console.info(`[bns/plots] ${player.username} bought ${id} for ${plot.price} spurs`);
return 1;
})))
// /bns plot release
.then(Commands.literal('release').executes(ctx => {
const player = ctx.source.player;
if (!player) {
ctx.source.sendSystemMessage(Text.red('Run from a player context.'));
return 0;
}
const id = plotOwnedBy(player.uuid.toString());
if (!id) {
player.tell(Text.red(`You don't own a plot.`));
return 0;
}
const plot = PLOT_DEFS.get(id);
const refund = Math.floor(plot.price * REFUND_FRACTION);
unclaimChunksForPlayer(plot, player);
clearOwner(id);
giveCoins(player, refund);
player.tell(Text.gold(
`Released plot ${id}. Refunded ${refund} spurs (${Math.round(REFUND_FRACTION*100)}% of ${plot.price}).`
));
console.info(`[bns/plots] ${player.username} released ${id}, refunded ${refund} spurs`);
return 1;
}))
// /bns plot reload (OP)
.then(Commands.literal('reload').requires(src => src.hasPermission(2))
.executes(ctx => {
// V1: PLOT_DEFS is hardcoded; reload is a no-op until V1.1
// (JSON-from-disk loading). Logged so admins know it ran.
ctx.source.sendSystemMessage(Text.gold(
`Plot defs are hardcoded in plots.js (V1). Edit the file + restart to change.`
));
console.info(`[bns/plots] reload called (no-op in V1)`);
return 1;
}))
);
event.register(tree);
console.info('[bns/plots] commands registered: list, info, buy, release, reload');
});
@@ -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.4", "version": "0.21.2",
"minecraft": "1.21.1", "minecraft": "1.21.1",
"loader": { "loader": {
"type": "neoforge", "type": "neoforge",