Compare commits

...

37 Commits

Author SHA1 Message Date
Matt 7949c2a3bb Merge pull request 'pack: v0.41.3 — Journal becomes the 'what's available + where to go' view' (#85) from feature/journal-town-board into main 2026-06-08 18:30:29 +00:00
Matt 9a3b3985ee pack: v0.41.3 — Journal becomes the "what's available" view
bnstoolkit 0.8.2 -> 0.8.3.

Adventurer's Journal rebuilt as the read-only town-board view:
- Available Bounties (today's pool of 5) with rewards
- Plots For Sale (every plot with state)
- Exchange Rates (each wealth-token with effective rate)
- Civic Tier (current + next cost)
- Your Status (active bounty progress, owned plots, wealth)

Each section heads with "Visit X Clerk at Y" so players know which
spawn building to walk to. The Journal is still read-only; all
transactions happen at the NPCs there.

Content scrolls via PanelScrollBar on the right.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:30:28 +00:00
Matt 1b0fa0168a Merge pull request 'pack: v0.41.2 — Exchange rename + click-refresh fix' (#84) from hotfix/exchange-rename into main 2026-06-08 18:22:48 +00:00
Matt 93c68325fe pack: v0.41.2 — rename Counting House to Exchange + fix click-refresh bug
bnstoolkit 0.8.1 -> 0.8.2.

Counting House -> Exchange (more intuitive naming).
- /bns open counthouse -> /bns open exchange
- screen title shown as "Exchange"
- Lang + docs updated

Click-doesn't-refresh-detail bug: ItemCell / BountyRow / PlotRow now
call getGui().refreshWidgets() after updating their static `selected`
field, so the detail sub-panel re-renders with the new selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:22:47 +00:00
Matt 95a564c5e3 Merge pull request 'pack: v0.41.1 — UI pass + spawn-building setup doc' (#83) from hotfix/ui-pass into main 2026-06-08 18:14:51 +00:00
Matt f0e6187ed0 pack: v0.41.1 — UI pass + spawn-building-setup doc
bnstoolkit 0.8.0 -> 0.8.1.

UI pass on every transactional screen:
- Counting House grid now 4-cell (one per wealth-token), with
  inventory count badge on each cell
- Detail pane shows on-hand count + friendly display name
- Sell buttons grey out when you don't have enough
- Bottom-row Refresh replaced with smart Back/Close — fixes the
  button overlap with sub-panel internals
- Panel heights reduced (height-30 -> height-50) so internal
  buttons don't clash with the screen-level action row

Naming cleanup:
- Dropped "shop" alias (Counting House is the only correct name)
- Added "journal" as the user-facing screen key for Adventurer's
  Journal

New docs/spawn-building-setup.md covers:
- /bns admin spawn-init flow for FTB Teams init
- Per-building Easy NPC dialogue setup (Counting House Clerk,
  Bounty Officer, Plot Clerk, Civic Registrar)
- Pre-wipe testing checklist for every feature

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:14:50 +00:00
Matt 263b8c59d8 Merge pull request 'pack: v0.41.0 — plot config JSON + FTB Chunks integration' (#82) from feature/v0.41.0-plot-config-ftb-integration into main 2026-06-08 17:53:35 +00:00
Matt a39bfee803 pack: v0.41.0 — plot system JSON config + FTB Chunks integration
bnstoolkit 0.7.0 -> 0.8.0.

Plot system migrated from static Java + KubeJS to a runtime
configurable, FTB-Chunks-integrated system.

* Plot definitions now live at world/serverconfig/bnstoolkit/plots.json.
  Default file ships with 3 starter plots; edit + /bns admin plot reload.
* /bns admin plot define <id> <name> <price>: add a plot in-game by
  standing in the chunk. Re-run with same id to add multi-chunk plots.
* /bns admin plot list / remove / reload + /bns admin spawn-init.
* /bns admin spawn-init creates Spawn (cyan #00BFFF) + Plots (gold
  #FFD700) server teams via FTB Teams + claims all defined plots for
  the Plots team. Idempotent.
* /bns plot buy now: pays via combined cash+bank, unclaims chunks
  from Plots team, claims for buyer's personal FTB Teams team.
* /bns plot release: reverse — chunks go back to Plots team, 50% refund.
* Plot chunk guard: ClaimedChunkEvent.BEFORE_UNCLAIM subscriber blocks
  manual unclaim of plot chunks (player gets chat hint redirecting
  to /bns plot release).
* KubeJS plots.js retired (.retired suffix in repo as documentation).

On the map: cyan zones = Spawn team protection (manually claimed),
gold zones = Plots team (for sale), other team colors = owned plots.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 17:53:34 +00:00
Matt 03f650ce3b Merge pull request 'pack: v0.40.0 — drop Alex's Mobs + bounty board fix + Numismatics polish' (#81) from feature/v0.40.0-drop-am-polish into main 2026-06-08 17:20:10 +00:00
Matt 35cbd84146 pack: v0.40.0 — drop Alex's Mobs + bounty board fix + Numismatics polish
Drops Alex's Mobs from the pack (105 -> 103 mods total). The 4 AM-mob
bounty entries are replaced with vanilla 1.21.x mob bounties (Breeze,
Bogged, Evoker, Vindicator, Piglin Brute, Ravager).

Bounty board bug fix: KubeJS PlayerEvents.loggedIn now ensures the
daily pool is rolled on first login. Previously the pool only rolled
when a player ran /bns bounty list, so the bounty board screen showed
empty for anyone who'd never typed that command.

/bns tier upgrade now uses combined cash+bank deduction via
NumismaticsBridge.spend (Java). Players who've sold a fortune into
their bank can now spend it on tier upgrades directly.

Every player gets a bound white_card on first login — no more "I need
to craft + bind a card before I can spend at a Vendor block" friction.

bnstoolkit 0.6.2 -> 0.7.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 17:20:09 +00:00
Matt c9f57dae9e Merge pull request 'pack: v0.39.2 — Adventurer's Journal + smart Back button' (#80) from hotfix/adventurers-journal into main 2026-06-08 16:20:03 +00:00
Matt 83949d63b5 pack: v0.39.2 — Adventurer's Journal + smart Back button
bnstoolkit 0.6.1 -> 0.6.2.

Journal (formerly Quest Log) reworked to be useful: active bounties
with progress bars, current Counting House rates per wealth-token,
owned plots. FTB Quests stays as a separate system (footer line
points players to their Quest Book item).

Back button: opens-from-Hub uses FTB Library's previous-screen
mechanism so Close becomes Back and returns to Hub. NPC-opened
journals (no previous screen) get a regular Close.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 16:20:03 +00:00
Matt 68aae5c917 Merge pull request 'pack: v0.39.1 — bnstoolkit 0.6.1 sub-panel sizing hotfix' (#79) from hotfix/sub-panel-sizing into main 2026-06-08 15:51:32 +00:00
Matt 0818fe8eab pack: v0.39.1 — bnstoolkit 0.6.1 sub-panel sizing hotfix
Hub screen showed blank content area: title bar + 3 buttons rendered,
but the status panel in between was empty. Same latent bug affected
every screen with a sub-panel (Tier, Counting House, Bounty, Plot,
Quest Log).

Root cause: FTB Library's Panel.refreshWidgets order is
addWidgets → recurse-into-child-panels.refreshWidgets → alignWidgets.
So when the parent's alignWidgets sets a sub-panel's size, the
sub-panel's children have already aligned themselves using the
sub-panel's pre-size dimensions (0x0). Every Row got setSize(0, 12)
→ invisible.

Fix: size sub-panels BEFORE add() in the parent's addWidgets so the
recursive refreshWidgets sees correct dimensions. Parent's
alignWidgets now only positions the top-level action buttons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 15:50:51 +00:00
Matt f269cc4861 Merge pull request 'pack: v0.39.0 — Counting House + remove Create: Encased' (#78) from feature/v0.39.0-counting-house into main 2026-06-08 15:34:48 +00:00
Matt ae183f27de pack: v0.39.0 — Counting House refactor + remove Create: Encased
Major economy / spawn-design pivot.

## Removed: Create: Encased
The mod's `chorium_ingot` → `creative_casing` chain unlocked 60+
infinite-output creative-tier blocks via fully renewable inputs (rose
quartz + chorus fruit + lava). Single creative_mechanical_drill in a
player base would have produced enough diamonds to trivialize the
civic ladder. Removing the mod was simpler than KubeJS recipe overrides
since none of the mod's other content (wood-variant encased shafts,
coloured casings) was load-bearing for our pack.

Parallel audit of 14 other Create-family mods confirmed none have
similar exploits — only Create: Encased was the breaker.

## Counting House refactor
What used to be "Bazaar" became "Counting House" — a wealth exchange,
not a general shop. Conceptual model:
- Players accumulate diamonds / netherite / ancient debris through
  adventure and mining (the wealth-store axis)
- Counting House converts these to spurs, credited directly to the
  player's Numismatics bank account
- Iron / gold / coal / lapis / redstone are no longer here — those
  belong on player-run Numismatics Vendor blocks at market prices

SellCatalogue cut from 9 items to 4 (diamond, diamond_block,
netherite_ingot, ancient_debris). Diminishing-returns floor tightened
50% -> 30%.

## Numismatics integration
NumismaticsBridge (reflection — no hard dep) wires up:
- ensureAccount on PlayerLoggedInEvent (every player gets a personal
  account on first login)
- balanceOf / deposit / deduct for bank operations
- SpurBalance.totalSpurs = inventory coins + own bank balance, no card
  lookups required
- HUD overlay now shows three lines: tier / cash on hand / bank
  balance — total wealth visible regardless of cards

## /bns commands migrated to Java
`/bns sell` and `/bns wallet` are now Java-handled
(CountingHouseCommands.java). Bank credit happens atomically with
inventory pull. KubeJS `02_sell_shop.js` retired (.retired suffix
preserved in the repo as documentation).

## Hub status-only
Removed the Civic Tier / Bazaar / Bounty / Plot routing buttons.
Transactional screens are now only reachable via NPC dialogue
(`/bns open <screen>` from an Easy NPC COMMAND action). The hub
shows tier + cash + bank + total + slot caps; footer reads "Visit
spawn NPCs for trades, bounties, plots, tier upgrades."

This locks the spawn-as-civic-centre design: players have to travel.

## bnstoolkit jar
0.5.1 -> 0.6.0 (network version 0.4 -> 0.5; TierStateUpdatePacket
now carries cashOnHand + bankBalance separately).

## Pack manifest
102 -> 104 mods after Encased removal. Manifest republished on glados
(publish-pack.py) before restart so the wrapper's sync sees the right
target state. Without that step, the sync was reverting our changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 15:34:25 +00:00
Matt ec8f2b83cf Merge pull request 'pack: v0.38.1 — bnstoolkit 0.5.1 UI coords hotfix' (#77) from hotfix/bnstoolkit-ui-coords into main 2026-06-08 14:43:10 +00:00
Matt 28c816990a pack: v0.38.1 — bnstoolkit 0.5.1 UI coords hotfix
Every screen positioned its child widgets with setPos(getX() + offset,
getY() + offset) under the assumption that FTB Library uses absolute
screen coordinates. It uses PARENT-RELATIVE coordinates — the panel's
own position is added back at render time — so all widgets rendered
at panel-position * 2 + offset, shoving them off the right edge.

Fix: drop the getX()/getY() from every setPos. Now using plain
parent-relative offsets, which is what FTB Library docs + FTB Quests/
Chunks source actually do.

Affects: HubScreen, TierUpgradeScreen, BountyBoardScreen,
PlotPurchaseScreen, ShopBrowserScreen, QuestLogScreen.
Also fixed BaseBnsScreen.contentTop()/contentBottom() to return
relative offsets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 14:42:41 +00:00
Matt 73f55768c3 scripts: add publish-pack.py — Linux port of Build-Pack.ps1 + deploy
Regenerates /srv/services/sites/bns/pack/manifest.json from
pack.lock.json + pack/overrides/, mirrors changed override files into
the Caddy-served web root, and atomically swaps in the new manifest.

Resolves a class of bug where the published manifest gets stuck at the
last Windows-side build and the launcher errors with "invalid hash for
<file>" on its next sync. Caught today because the manifest had been
serving v0.33.0 since v0.34.0 shipped — five missed publishes.

Run after every pack PR with sudo on glados:
    sudo -n python3 scripts/publish-pack.py

Idempotent. Skips files whose hash already matches the served copy.

Special-cases bnstoolkit-*.jar (side: both — has client UI) so the
launcher doesn't refuse to install it on clients; brassandsigil_tweaks
stays side: server as the existing convention dictates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 14:35:54 +00:00
Matt 48a70326d2 Merge pull request 'pack: v0.38.0 — Create: Tracks+ (tank tread addon)' (#76) from feature/v0.38.0-create-tracks into main 2026-06-07 22:30:37 +00:00
Matt 5f6a5aacdd pack: v0.38.0 — Create: Tracks+ for tank-tread contraptions
Create: Tracks+ 1.0.5 (Modrinth slug create-tracks+) adds Trackwork-
style caterpillar/tank tracks to Create contraptions. Fork of qwxon's
original Create:Tracks, the only 1.21.1 NeoForge port. Required deps
(Create, Create Aeronautics, Sable) are all already in the pack.

Use case: tracked vehicles / tanks built with Create — caterpillar
treads for moving contraptions over uneven terrain, military
aesthetic, or just heavy industrial movers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:30:08 +00:00
Matt 8d9c04dc05 Merge pull request 'pack: v0.37.0 — village supply/demand + Structory structure mods' (#75) from feature/v0.37.0-villages-and-structures into main 2026-06-07 16:27:53 +00:00
Matt ea9326819d pack: v0.37.0 — Tier 3 village supply/demand + Structory structures
Pre-wipe consolidation. After this, the seed pick is unblocked.

bnstoolkit 0.4.1 → 0.5.0 — village supply/demand throttle
- 64×64 block tiles each have a finite demand pool per cost item
- Spur-paying trades decrement their tile's pool; pool refills over 1
  hour real-time (lazy refresh, no scheduled tick, no perf overhead)
- Saturated trades show vanilla "out of stock" in the UI until the
  villager restocks AND the tile's demand has refilled
- ~150 spurs/hour cap per tile in a fully-stocked multi-profession hall
- 50-librarian-hall exploit dies; players who want more income must
  build halls in distant tiles (overworld logistics) OR diversify
  professions (each item type has its own pool)
- Anyone with /bns admin can inspect a tile's state — full picture
  lives in server.persistentData.bnsVillageDemand

New worldgen mods (Modrinth — both NeoForge 1.21.1):
- Structory 1.3.16 — 15 small thematic ruins: chapels, graveyards,
  fire-watch towers, abandoned camps, boats, outcast huts
- Structory: Towers 1.0.16 — 21 mid/rare-tier towers: lighthouses,
  engineer's tower, ocean pillars, wizard towers, end towers

Density assessment (per earlier audit): pack goes from ~52 to ~88
generating structure types. Towers are mostly rare-tier (40-80 chunk
spacing) so they read as discoveries, not clutter. Loot is in line
with vanilla bastion/mansion tier — finite chest pool that the
diminishing-returns sell shop and village supply system already
balance against.

Pack mods: 102 → 104.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 16:27:25 +00:00
Matt f2c9cabefe Merge pull request 'pack: v0.36.0 — bnstoolkit 0.4.1 (review fixes + hub UI + NPC integration)' (#74) from feature/v0.36.0-bnstoolkit-0.4.1 into main 2026-06-07 09:28:20 +00:00
Matt ad9a261627 pack: v0.36.0 — bnstoolkit 0.4.1 (NBT fix + hub UI + NPC integration + hardening)
Post-review consolidation PR. All four open issues from the parallel
code/security/hotkey/NPC audits resolved in one ship.

Key user-facing changes:

UI
- Single keybind: B opens a hub screen with buttons to the 5 sub-screens.
- K/J/N/H/M direct shortcuts default to UNBOUND so they no longer
  compete with the ~100 other mods. Rebindable in vanilla controls menu.
- M used to clash with vanilla Social Interactions + FTB Chunks
  fullscreen map; fully resolved.

NPC dialogue + scripting integration
- New /bns open <screen> command. Easy NPC dialogue COMMAND actions
  can now open any bnstoolkit screen on the clicking player.
- FTB Quests reward commands, command blocks, and /trigger objectives
  can all route into bnstoolkit screens via the same command.
- config/easy_npc/security.cfg ships with executeAsUserCommandAllowList
  .ALL=bns so dialogue-triggered /bns commands actually run.

Bug fix (this is the load-bearing item)
- Bounty board + plot office screens were reading NBT keys that don't
  exist in KubeJS, so they always showed "no active bounties" and "all
  plots free" no matter what was actually in the world. Fixed by reading
  the real per-player keys (bnsBountyPool/Active/Completed) and the
  server-scope bnsPlots compound via KubeJS' MinecraftServer mixin
  (reflective, no compileOnly dep on KubeJS).

Hardening (no behavior change, defense-in-depth)
- Per-player token bucket on C2S packets (10/s sustain, 20 burst).
- All STRING_UTF8 codec fields capped at sane sizes (≤80 chars).
- Verb whitelist replaced with structured parse + per-verb regex
  validation + rebuilt command. No prefix-startsWith surprises.
- Tier-upgrade in-flight guard closes the concurrent-packet
  double-spend race window.
- Log lines sanitised to prevent control-char injection.

Plus the nits caught by review:
- Per-player tick counter for HUD push (was a shared static)
- VillagerTradeRebalance try/catch + null-cost-A passthrough +
  round-to-nearest denom split (was floor — ~10% silent price haircut)
- SpurHud component cache (was allocating per frame)
- BountyBoardScreen dead branch removed

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 09:27:47 +00:00
Matt bae44d0178 Merge pull request 'docs: rewrite bnstoolkit handoff for feature-complete state' (#73) from docs/bnstoolkit-feature-complete into main 2026-06-07 02:43:56 +00:00
Matt 016985a5fc docs: rewrite bnstoolkit handoff for feature-complete state
The original handoff doc described an empty M0 scaffold with placeholder
screens to "discuss tomorrow". 0.3.1 actually ships the full UI plus
villager trade rebalance, so the doc now is a testing guide for the
working features rather than a discussion-points list.

Includes the architecture diagram for how Java + KubeJS interact, the
key bindings table, a step-by-step testing path, and a known-gaps list
for future iteration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:43:42 +00:00
Matt a7f95691bb Merge pull request 'pack: v0.35.0 — bnstoolkit feature-complete UI + villager rebalance' (#72) from feature/v0.35.0-bnstoolkit-ui-full into main 2026-06-07 02:42:43 +00:00
Matt 23efdee804 pack: actually include bnstoolkit-0.3.1.jar in v0.35.0
The previous commit deleted 0.1.1 from the gitignored mods dir but
git swallowed the add for 0.3.1 because pack/overrides/mods is
ignored. Re-adding with -f.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:42:06 +00:00
Matt 1859613f1f pack: v0.35.0 — bnstoolkit 0.3.1 (feature-complete UI + villager rebalance)
bnstoolkit jumps from M0 (empty scaffold) straight to feature complete:

UI screens (all FTB Library widgets, opens via keybinds):
  K  Civic tier ladder + upgrade
  J  Bounty board (accept/cancel/turn-in)
  N  Plot office (buy/release)
  H  Bazaar sell shop (1/16/64 quick-sell)
  M  Quest log (status overview)

HUD: top-left tier + spurs balance overlay, refreshed 1Hz server-side.

Villager trade rebalance (NEW): the mod now hooks VillagerTradesEvent
+ WandererTradesEvent and swaps every emerald cost/result for the
Numismatics equivalent (1..7 emeralds -> spurs, 8..63 -> bevels, 64+
-> cogs). This was the blocked Modrinth dep — the third-party mod
options either required a NeoForge bump (numismatics-villager-currency
1.1.0 wants 21.1.230+, we're on 21.1.228) or hand-written JSON for
every trade (data-trades). The in-tree wrapper is cleaner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:41:49 +00:00
Matt a41d8cb531 Merge pull request 'docs: bnstoolkit postmortem' (#71) from docs/bnstoolkit-discussion-postmortem into main 2026-06-07 00:19:21 +00:00
Matt e770cb55cb docs: add bnstoolkit 0.1.0 crash postmortem + manual-restart note
Surfaces the server-needs-Start state and the fix details up top so
when matt wakes up he doesn't have to scroll past the M1+ planning
to find out the box is down.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 00:19:06 +00:00
Matt 338617e855 Merge pull request 'pack: v0.34.1 — bnstoolkit hotfix 0.1.1' (#70) from hotfix/bnstoolkit-0.1.1 into main 2026-06-07 00:18:38 +00:00
Matt ecd5933f9f pack: v0.34.1 — bnstoolkit hotfix 0.1.1 (fixes mod-load crash)
0.1.0 brought the dedicated server down during construct-mods because
the @Mod constructor called NeoForge.EVENT_BUS.register(this) without
any @SubscribeEvent methods on the class — which NeoForge 21.1 treats
as a fatal IllegalArgumentException.

Caught only after live-deploy (gradle build doesn't simulate the
construct-mods step). 0.1.1 removes the bogus register call. The
@SubscribeEvent listeners will come back in M1 alongside the first
real event handler.

Server has been brought down by the crash and is waiting for a manual
start via the web admin panel. The fixed jar is staged in
/srv/fast/brass-sigil-server/server/mods/ already.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 00:18:17 +00:00
Matt 38fbf80bc1 Merge pull request 'pack: v0.34.0 — bnstoolkit M0 scaffold' (#69) from feature/v0.34.0-bnstoolkit-m0 into main 2026-06-07 00:13:28 +00:00
Matt 61dd3d3754 pack: v0.34.0 — bnstoolkit M0 scaffold
Ships the empty bnstoolkit companion mod (0.1.0) into the pack. The mod
loads cleanly on client and server with zero visible surface — it just
proves the deploy pipeline end-to-end so M1-M4 only have to add code,
not figure out tooling.

Per-milestone scope locked in docs/bnstoolkit-discussion-points-2026-06-07.md.

Source: http://10.16.5.102:3000/minecraft/bnstoolkit

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 00:13:05 +00:00
Matt 61a923cbef Merge pull request 'docs: economy V1 overnight handoff' (#68) from docs/overnight-handoff into main 2026-06-06 23:57:24 +00:00
13 changed files with 539 additions and 39 deletions
@@ -0,0 +1,114 @@
# bnstoolkit feature-complete — testing notes for 2026-06-07 evening
> Shipped: pack v0.35.0 / bnstoolkit 0.3.1.
> Repo: http://10.16.5.102:3000/minecraft/bnstoolkit
> Server status: ✅ live + verified at 02:41 UTC.
## What's in the build
### 5 screens (FTB Library widgets — matches FTB Quests look)
All opened via key bindings. Rebindable in vanilla controls menu under
"Brass and Sigil" category.
| Key | Screen | What it does |
|---|---|---|
| **K** | Civic Tier ladder | 13 rows, current tier highlighted. Right pane shows next tier's cost + perks + Upgrade button. Disabled if you can't afford it or are at Sovereign. |
| **J** | Bounty Board | Today's 5-bounty pool + any active set. Detail pane: target, count, reward, progress. Accept / Cancel / Turn In buttons by state. |
| **N** | Plot Office | Plot list with ownership state-coding (yours / taken / free). Detail pane: size, chunks, price. Buy / Release buttons. |
| **H** | Bazaar shop | 9-cell sell item grid. Click an item, see DR-aware effective price for current tier. Sell 1 / 16 / 64 quick buttons. |
| **M** | Quest Log | At-a-glance status: tier + balance + active bounties (with progress bars) + owned plots. |
### HUD overlay
Top-left of the screen, always visible: `<tier-colour><tier-name>` on
one line, `<balance> spurs` on the second. Refreshes 1Hz from server
push — picks up coin pickups immediately, no inventory polling.
Auto-hides when F1 is pressed or chat is open (standard NeoForge HUD
layer behavior).
### Villager trade rebalance
Every emerald cost or result in vanilla + modded villager trades is
now Numismatics:
- 1..7 emeralds → spurs
- 8..63 → bevels (1 bevel = 8 spurs)
- 64+ → cogs (1 cog = 64 spurs)
Works automatically on:
- All 14 vanilla villager professions
- Wandering trader (generic + rare)
- Any modded villager professions (we wrap the event's list, no enum
of professions needed)
This was the "blocked on API" item. Built it into bnstoolkit instead
of pulling a third-party mod, because the cleanest Modrinth option
needed NeoForge 21.1.230+ and we're on 228.
## Testing path
```
/bns me # confirm Peasant tier 1
/give @s numismatics:spur 500 # give spurs for upgrades
K # open tier ladder
# click Upgrade — should pay 200 and bump you to Farmer
/bns admin tier set <you> 5 # jump to Knight for plot testing
K # confirm new tier visible
H # open bazaar
# click diamond, click Sell 1 (you'll need a diamond in inventory)
J # open bounty board, accept one
# kill the target mobs — progress shows in chat AND the screen
# turn in via the J screen
N # open plot office, buy p001
M # quest log shows everything
# Test villager trades:
/summon villager ~ ~ ~
# Right-click the villager once it picks a profession (place a workstation)
# Trades should show spurs/bevels/cogs instead of emeralds
```
## Architecture
```
KubeJS server_scripts/economy/ ← single source of truth for state + logic
│ (chat commands via server.getCommands)
ServerEconomyBridge.java ← reads NBT, sums Numismatics coins,
routes write verbs through /bns
↑↓
4 snapshot + 2 command packets
↑↓
ClientBnsState.java ← single static state holder
Screens read this each frame
```
Source of truth stays in KubeJS. Java is just the UI layer and a thin
write-path proxy. If you want to rebalance prices or add bounties, you
do it in KubeJS — Java auto-reflects.
## Known gaps for future iteration
| Gap | Where to fix |
|---|---|
| No client toast notifications — feedback comes via KubeJS chat messages | Add a dist-isolated `ClientToastDispatcher` class (0.4.0) |
| Bazaar grid doesn't tooltip the per-unit price | One `addMouseOverText` per `ItemCell` |
| Plot office doesn't show a minimap | Render bluemap PNG snippet as background or use FTB Chunks' map widget |
| Quest log doesn't show FTB Quests progression | Hook into FTB Quests' API once first chapter ships |
| Numismatics coins as bunch — no fractional change | Bazaar pays in highest denom that fits; player converts at the Bank |
| Wanderer rare-trade rebalance is greedy (every rare item costs are converted) | Profession-aware exclusion list if anything weird sneaks through |
## File map
| File | Purpose |
|---|---|
| `pack/overrides/mods/bnstoolkit-0.3.1.jar` | The mod itself (95 KB) |
| `pack/overrides/kubejs/server_scripts/economy/*.js` | State + business logic (unchanged from v0.33.0) |
| `pack/overrides/world/serverconfig/ftbranks/ranks.snbt` | 13-tier rank config (unchanged) |
| `docs/bnstoolkit-architecture.md` | Original architecture spec |
Source repo: http://10.16.5.102:3000/minecraft/bnstoolkit (main = 14dc69f at time of pack ship).
+143
View File
@@ -0,0 +1,143 @@
# Spawn building setup — wiring NPCs to bnstoolkit screens
How to give players access to the Exchange / Bounty Board / Plot
Office / Civic Registrar via Easy NPC dialogue at spawn.
## Prerequisites (one-time, post-wipe)
```
/setworldspawn ~ ~ ~ # set the canonical world spawn
/bns admin spawn-init # creates Spawn (cyan) + Plots (gold)
# FTB Teams and claims existing plots
```
If you also want spawn protection: switch to the Spawn team and claim
your spawn town chunks via the FTB Chunks map (M key, left-click).
## The five screens you can expose
| Screen | Open command | What it does |
|---|---|---|
| Hub | `/bns open hub` | Status overview (tier, balance, bounties, plots) |
| Exchange | `/bns open exchange` | Wealth exchange — sell diamonds/netherite/ancient debris for spurs |
| Bounty Board | `/bns open bounty` | Today's 5 bounties + accept/cancel/turn-in |
| Plot Office | `/bns open plot` | Buy / release plots |
| Civic Tier | `/bns open tier` | Pay spurs to upgrade tier |
| Adventurer's Journal | `/bns open journal` | Status overview + active bounties + exchange rates |
`hub` and `journal` are status-only (read). The other four are
transactional — they should be gated behind an NPC at the relevant
spawn building.
## Per-building setup (Exchange example)
Repeat for each of the 4 transactional buildings, varying just the
screen name and NPC name.
### 1. Spawn the NPC
In creative mode, get an Easy NPC spawn egg:
```
/give @s easy_npc:custom_egg
```
Right-click the ground where you want the NPC. It spawns a customisable
NPC.
### 2. Configure appearance + name
Right-click the NPC → **Configuration** (or use the wrench).
- **Skin**: pick a fitting villager / human skin
- **Name**: "Exchange Clerk" (or whatever fits the building)
- **Profession**: optional — purely cosmetic
### 3. Configure dialog
In the NPC config → **Dialog** tab.
Create a new STANDARD dialog. Suggested text:
> Welcome to the Exchange. We exchange diamonds, netherite, and
> ancient debris for spurs deposited directly to your bank account.
> Care to make a trade?
Add a **Button** to this dialog:
- **Label**: "Open the exchange"
- **Type**: ACTION
- **Action**: COMMAND
- **Command**: `bns open exchange`
- **Execute as user**: ✅ (must be enabled)
- **Permission level**: 0 (no special permission needed)
- **Trailing action**: CLOSE_DIALOG (optional — closes the dialog after click)
Add another button "Goodbye" → CLOSE_DIALOG for the polite exit.
### 4. Configure trigger
In the NPC config → **Actions** tab:
- Event: **ON_INTERACTION**
- Action: **OPEN_DEFAULT_DIALOG**
Now when a player right-clicks the NPC, the dialog opens. Clicking
"Open the exchange" runs `/bns open exchange` as that player and
the Exchange screen pops up.
## Building-to-screen mapping
| Building | NPC name suggestion | Open command |
|---|---|---|
| Exchange | Exchange Clerk | `bns open exchange` |
| Adventurer's Guild | Bounty Officer | `bns open bounty` |
| Plot Office | Plot Clerk | `bns open plot` |
| Civic Hall / Town Hall | Civic Registrar | `bns open tier` |
| (anywhere — optional) | Quartermaster | `bns open hub` |
| (anywhere — optional) | Cartographer | `bns open journal` |
## Security gate (one-time, already shipped)
The Easy NPC config at `/srv/fast/brass-sigil-server/server/config/
easy_npc/security.cfg` has `executeAsUserCommandAllowList.ALL=bns`,
so dialogue actions can run `/bns ...` on the player's behalf. This
ships with the pack as of v0.36.0 — no manual edit required after
fresh installs.
## Testing your setup
1. Build the building
2. Place the NPC
3. Configure the dialog as above
4. Switch to survival, walk up, right-click the NPC
5. Click the action button
6. The screen should open on your client immediately
If nothing happens:
- Check chat for errors ("Blocked execute-as-user command root ...")
- If you see that, the security.cfg allowlist is missing `bns` — see
the security gate section above
- Otherwise: check the server log for `[bnstoolkit] /bns open <screen>`
to confirm the command actually fired
## What if I just want to test as op (no NPC)
You can always run `/bns open <screen>` directly from chat or console
to open a screen for yourself. The op-level requirement is none — the
command works for any player. NPCs are just the in-world surface.
## Pre-wipe testing checklist (current world)
Each feature, where you'd manually test it before the wipe:
| Feature | How to test |
|---|---|
| Hub | Press B → see status panel with tier/cash/bank/total/bounties/plots |
| Exchange | `/bns open exchange` → 4 items in grid (diamond, diamond_block, netherite_ingot, ancient_debris), inventory count badge bottom-right of each cell, fee + per-unit price in detail pane, Sell 1/16/64 buttons grey out when you don't have enough |
| Sell flow | `/give @s minecraft:diamond 64` → click diamond → Sell 16 → bank balance ticks up, sell button updates |
| Bounty Board | `/bns open bounty` → see today's pool, click a bounty for detail, accept, kill the targets, turn in |
| Plot Office | `/bns open plot` → see 3 default plots, buy p001 (tier 3+ required), check FTB Chunks map M — chunk should turn your team colour |
| Plot unclaim guard | After buying a plot, try to unclaim its chunk via FTB Chunks UI → should reject with redirect message |
| Civic Tier | `/give @s numismatics:spur 500` or `/bns wallet deposit ...``/bns open tier` → click Upgrade — pays from cash + bank combined |
| Wallet | `/bns wallet`, `/bns wallet deposit 50`, `/bns wallet withdraw 50` |
| Auto-bound card | Look in inventory — should already have a white_card bound to your account |
| Villager rebalance | Trade with a villager — emerald-side replaced with spurs/bevels/cogs |
| Quest Log auto-open population | Press B → click Journal → should show active bounties, rates, plots |
If any of these break, ping with the details. Pre-wipe is the time to find these bugs.
@@ -0,0 +1,50 @@
#Easy NPC Security Configuration
#
#Permission values use enum names: ALL, MODERATORS, GAMEMASTERS, ADMINS, OWNERS.
#Legacy numeric Minecraft command permission levels are accepted and rewritten to enum names.
#
#normalPlayerCommandLevel: Max command level for normal player-owned imports (default: ALL)
#creativePlayerCommandLevel: Max command level for creative non-admin imports (default: MODERATORS)
#maxAdminImportedCommandLevel: Max command level for admin imports (default: ADMINS)
#serverTrustedCommandLevel: Max command level for server-side trusted imports without player context (default: ADMINS)
#blockUnsafeNpcCommands: Blocks critical server management commands from NPC command execution (default: true)
#npcSpawnRateLimitCreative: Max new NPCs a creative (non-admin) player may spawn via browser per minute (default: 5)
#npcSpawnRateLimitAdmin: Max new NPCs an admin player may spawn via browser per minute (default: 20)
#Feature values use enum names: NORMAL_PLAYER, CREATIVE_PLAYER, ADMIN, SERVER_TRUSTED.
#feature.SPAWN_NPC: Minimum role required to spawn new NPCs from the preset browser (default: CREATIVE_PLAYER)
#feature.WORLD_PRESET: Minimum role required to import/export world presets (default: CREATIVE_PLAYER)
#feature.CUSTOM_PRESET: Minimum role required to import/export custom server presets (default: CREATIVE_PLAYER)
#feature.LOCAL_PRESET: Minimum role required to export presets locally to the client (default: NORMAL_PLAYER)
#feature.DEFAULT_PRESET_IMPORT: Minimum role required to import built-in default presets (default: CREATIVE_PLAYER)
#feature.URL_RESOURCE: Minimum role required to use URL-based skin loading (default: CREATIVE_PLAYER)
#Sat Jun 06 23:34:43 UTC 2026
blockUnsafeNpcCommands=true
creativePlayerCommandLevel=MODERATORS
executeAsUserCommandAllowList.ADMINS=
executeAsUserCommandAllowList.ALL=bns
executeAsUserCommandAllowList.GAMEMASTERS=
executeAsUserCommandAllowList.MODERATORS=
executeAsUserCommandAllowList.OWNERS=
feature.BASE_ATTRIBUTE=CREATIVE_PLAYER
feature.COMBAT_ATTRIBUTE=CREATIVE_PLAYER
feature.COMMAND_ACTION=CREATIVE_PLAYER
feature.CUSTOM_PRESET=CREATIVE_PLAYER
feature.DEFAULT_PRESET_IMPORT=CREATIVE_PLAYER
feature.DIALOG=NORMAL_PLAYER
feature.INTERACT_BLOCK_ACTION=CREATIVE_PLAYER
feature.LOCAL_PRESET=NORMAL_PLAYER
feature.MOVEMENT=CREATIVE_PLAYER
feature.OBJECTIVE=CREATIVE_PLAYER
feature.OPEN_TRADING_ACTION=CREATIVE_PLAYER
feature.POSITION=CREATIVE_PLAYER
feature.SCOREBOARD_ACTION=CREATIVE_PLAYER
feature.SPAWN_NPC=CREATIVE_PLAYER
feature.TRADING=CREATIVE_PLAYER
feature.URL_RESOURCE=CREATIVE_PLAYER
feature.WORLD_PRESET=CREATIVE_PLAYER
maxAdminImportedCommandLevel=ADMINS
normalPlayerCommandLevel=ALL
npcSpawnRateLimitAdmin=20
npcSpawnRateLimitCreative=5
serverTrustedCommandLevel=ADMINS
unsafeNpcCommands=ban,ban-ip,banlist,debug,deop,difficulty,forceload,function,gamerule,kick,op,pardon,reload,save-all,save-off,save-on,schedule,setidletimeout,setworldspawn,spawnpoint,spreadplayers,stop,whitelist
@@ -59,12 +59,14 @@ const BOUNTY_POOL = [
{ id: 'b_wither_skel_3', name: 'Slay 3 Wither Skeletons', target: 'minecraft:wither_skeleton', count: 3, type: 'kill', reward: 150, source: "Adventurer's Guild" },
{ id: 'b_shulker_3', name: 'Slay 3 Shulkers', target: 'minecraft:shulker', count: 3, type: 'kill', reward: 200, source: "Adventurer's Guild" },
// ── Boss tier (rare picks) ──
{ id: 'b_warden_1', name: 'Slay the Warden', target: 'minecraft:warden', count: 1, type: 'kill', reward: 800, source: "Adventurer's Guild" },
{ id: 'b_voidworm_1', name: 'Destroy a Void Worm', target: 'alexsmobs:void_worm', count: 1, type: 'kill', reward: 1200, source: "Adventurer's Guild" },
{ id: 'b_warped_mosco_1', name: 'Slay a Warped Mosco', target: 'alexsmobs:warped_mosco', count: 1, type: 'kill', reward: 700, source: "Adventurer's Guild" },
{ id: 'b_farseer_1', name: 'Slay the Farseer', target: 'alexsmobs:farseer', count: 1, type: 'kill', reward: 900, source: "Adventurer's Guild" },
{ id: 'b_skreecher_1', name: 'Slay a Skreecher', target: 'alexsmobs:skreecher', count: 1, type: 'kill', reward: 400, source: "Adventurer's Guild" },
// ── Boss tier (rare picks) — all vanilla 1.21.x ──
{ id: 'b_warden_1', name: 'Slay the Warden', target: 'minecraft:warden', count: 1, type: 'kill', reward: 800, source: "Adventurer's Guild" },
{ id: 'b_breeze_3', name: 'Hunt 3 Breezes', target: 'minecraft:breeze', count: 3, type: 'kill', reward: 250, source: "Adventurer's Guild" },
{ id: 'b_bogged_5', name: 'Slay 5 Bogged', target: 'minecraft:bogged', count: 5, type: 'kill', reward: 120, source: "Adventurer's Guild" },
{ id: 'b_evoker_2', name: 'Slay 2 Evokers', target: 'minecraft:evoker', count: 2, type: 'kill', reward: 350, source: "Adventurer's Guild" },
{ id: 'b_vindicator_4', name: 'Slay 4 Vindicators', target: 'minecraft:vindicator', count: 4, type: 'kill', reward: 180, source: "Adventurer's Guild" },
{ id: 'b_piglin_brute_2', name: 'Slay 2 Piglin Brutes', target: 'minecraft:piglin_brute', count: 2, type: 'kill', reward: 400, source: "Adventurer's Guild" },
{ id: 'b_ravager_1', name: 'Hunt down a Ravager', target: 'minecraft:ravager', count: 1, type: 'kill', reward: 500, source: "Adventurer's Guild" },
];
// Index pool by id for O(1) lookup
@@ -344,4 +346,14 @@ ServerEvents.commandRegistry(event => {
event.register(bountyCmd);
});
// ─── First-login pool roll ─────────────────────────────────────────
// Without this, new players see an empty Bounty Board until they type
// /bns bounty list (which triggers ensurePoolFresh). The board reads
// bnsBountyPool from NBT; bnsBountyPool stays empty until rollDailyPool
// runs. This hook closes the gap so the screen is populated the first
// time it's opened.
PlayerEvents.loggedIn(event => {
ensurePoolFresh(event.player);
});
console.info('[bns/bounty_engine] loaded — ' + BOUNTY_POOL.length + ' bounties in catalogue');
@@ -109,19 +109,20 @@ ServerEvents.commandRegistry(event => {
}
const nextTier = cur + 1;
const nextCfg = bnsTier.config(nextTier);
const balance = countCoins(player);
if (balance < nextCfg.cost) {
// Combined cash+bank deduction via bnstoolkit Java bridge.
// Pulls inventory coins first, then bank for the remainder.
const Bridge = Java.loadClass('uk.sijbers.bnstoolkit.economy.NumismaticsBridge');
const ok = Bridge.spend(player, nextCfg.cost);
if (!ok) {
const cash = countCoins(player);
const bank = Bridge.balanceOf(player);
player.tell(Text.red(
'Upgrade to ' + nextCfg.name + ' costs ' + nextCfg.cost + ' spurs. You have ' + balance + '.'
'Upgrade to ' + nextCfg.name + ' costs ' + nextCfg.cost + ' spurs. ' +
'You have ' + cash + ' cash + ' + bank + ' bank = ' + (cash + bank) + ' total.'
));
return 0;
}
const took = takeCoins(player, nextCfg.cost);
if (took < nextCfg.cost) {
giveCoins(player, took);
player.tell(Text.red('Coin removal failed — refunded. Try again.'));
return 0;
}
// Promote via FTB Ranks
const rankId = TIER_RANK_ID[nextTier];
Utils.server.runCommandSilent('ftbranks add ' + player.username + ' ' + rankId);
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35 -24
View File
@@ -1,13 +1,13 @@
{
"$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.33.0",
"version": "0.41.3",
"minecraft": "1.21.1",
"loader": {
"type": "neoforge",
"version": "21.1.228"
},
"lockedAt": "2026-06-06T23:34:21Z",
"lockedAt": "2026-06-08T18:30:00Z",
"mods": [
{
"source": "modrinth",
@@ -130,17 +130,6 @@
"size": 38629,
"side": "both"
},
{
"source": "modrinth",
"slug": "create-encased",
"versionId": "3sUW27Ho",
"version": "1.8.1-ht1",
"path": "mods/Create Encased-1.21.1-1.8.1-ht1.jar",
"url": "https://cdn.modrinth.com/data/hSSqdyU1/versions/3sUW27Ho/Create%20Encased-1.21.1-1.8.1-ht1.jar",
"sha1": "5ca3d7674676dda5dd472b0433505a54d5e920d0",
"size": 2574837,
"side": "both"
},
{
"source": "modrinth",
"slug": "bellsandwhistles",
@@ -1065,17 +1054,6 @@
"size": 193381,
"side": "both"
},
{
"source": "modrinth",
"slug": "alexs-mobs(1.21.1)",
"versionId": "KSgki4uc",
"version": "1.22.17",
"path": "mods/alexsmobs-1.22.17.jar",
"url": "https://cdn.modrinth.com/data/EmNhnNnt/versions/KSgki4uc/alexsmobs-1.22.17.jar",
"sha1": "5070e8070f60650d773bfd9f1d031496f8f50259",
"size": 26620546,
"side": "both"
},
{
"source": "modrinth",
"slug": "item-obliterator",
@@ -1130,6 +1108,39 @@
"sha1": "07fdfa3a75c3481770ce0118090a347d89e3a560",
"size": 88382,
"side": "both"
},
{
"source": "modrinth",
"slug": "structory",
"versionId": "Nvh2sSPX",
"version": "1.3.16",
"path": "mods/Structory_26.1_v1.3.16.jar",
"url": "https://cdn.modrinth.com/data/aKCwCJlY/versions/Nvh2sSPX/Structory_26.1_v1.3.16.jar",
"sha1": "ee48f145a3a09fdb3f492576cbc21ed26abc74bb",
"size": 1261336,
"side": "both"
},
{
"source": "modrinth",
"slug": "structory-towers",
"versionId": "ioKwCTuD",
"version": "1.0.16",
"path": "mods/Structory_Towers_26.1_v1.0.16.jar",
"url": "https://cdn.modrinth.com/data/j3FONRYr/versions/ioKwCTuD/Structory_Towers_26.1_v1.0.16.jar",
"sha1": "ed22fe659cb3020f2e86b7619ce721714b9546be",
"size": 491545,
"side": "both"
},
{
"source": "modrinth",
"slug": "create-tracks+",
"versionId": "cju2ayQC",
"version": "1.0.5",
"path": "mods/tracks_plus-1.0.5.jar",
"url": "https://cdn.modrinth.com/data/E8eHF2Yl/versions/cju2ayQC/tracks_plus-1.0.5.jar",
"sha1": "87f72a49f53d7cf4202a11e4fc68f9bd18ab2c38",
"size": 610174,
"side": "both"
}
],
"defaultServer": {
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Linux-side port of Build-Pack.ps1 + Deploy-Brass.ps1 (publish step).
Reads pack/pack.lock.json + walks pack/overrides/, rebuilds manifest.json,
mirrors every override file to the live web root, and republishes the
manifest. The launcher will then see fresh hashes on its next sync.
Run on glados with passwordless sudo. Idempotent.
Web root: /srv/services/sites/bns (mounted into Caddy as /srv/sites/bns).
"""
import hashlib
import json
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
REPO = Path("/home/matt/dev/brass-and-sigil")
LOCK_PATH = REPO / "pack" / "pack.lock.json"
OVERRIDES = REPO / "pack" / "overrides"
WEB_ROOT = Path("/srv/services/sites/bns/pack")
WEB_FILES = WEB_ROOT / "files"
WEB_MANIFEST = WEB_ROOT / "manifest.json"
SELF_HOST_BASE = "https://bns.sijbers.uk/pack/files"
MANAGED_ROOTS = ["mods", "config", "resourcepacks", "shaderpacks", "kubejs", "defaultconfigs", "patchouli_books"]
# bnstoolkit ships client+server surface (screens, HUD, network bridge),
# so it must NOT inherit the default "server-only" tag that Build-Pack.ps1
# stamps on every pack/overrides/mods/*.jar. brassandsigil_tweaks is
# data-only (worldgen modifiers / recipe overrides) so server-side is
# correct for it.
CLIENT_AND_SERVER_LOCAL_MODS = {"bnstoolkit"}
def sha1_of(p: Path) -> str:
h = hashlib.sha1()
with p.open("rb") as f:
for chunk in iter(lambda: f.read(1 << 16), b""):
h.update(chunk)
return h.hexdigest()
def main():
with LOCK_PATH.open() as f:
lock = json.load(f)
files = []
total_bytes = 0
# 1. Mods from the lockfile (Modrinth-hosted)
for m in lock["mods"]:
entry = {
"path": m["path"],
"url": m["url"],
"sha1": m["sha1"],
"size": m["size"],
}
side = m.get("side")
if side and side != "both":
entry["side"] = side
files.append(entry)
total_bytes += m["size"]
# 2. Local overrides
for root in MANAGED_ROOTS:
root_dir = OVERRIDES / root
if not root_dir.exists():
continue
for p in sorted(root_dir.rglob("*")):
if not p.is_file():
continue
rel = p.relative_to(OVERRIDES).as_posix()
sha1 = sha1_of(p)
size = p.stat().st_size
entry = {
"path": rel,
"url": f"{SELF_HOST_BASE}/{rel}",
"sha1": sha1,
"size": size,
}
if root == "mods":
# See CLIENT_AND_SERVER_LOCAL_MODS — bnstoolkit must be loaded
# on both sides; the other locally-built mods are server-only.
stem = p.name.split("-")[0].split("_")[0] # before first - or _
if not any(p.name.startswith(prefix) for prefix in CLIENT_AND_SERVER_LOCAL_MODS):
entry["side"] = "server"
files.append(entry)
total_bytes += size
# 3. Compose manifest matching the existing schema
manifest = {
"name": lock["name"],
"version": lock["version"],
"minecraft": {"version": lock["minecraft"]},
"loader": {"type": lock["loader"]["type"], "version": lock["loader"]["version"]},
"files": files,
}
for opt in ("defaultServer", "defaultShader", "panelUrl"):
if opt in lock:
manifest[opt] = lock[opt]
# Carry the launcher metadata from the existing published manifest
# (we can't read a Windows .exe's FileVersion from Linux easily, and
# the launcher version isn't related to pack content).
try:
with WEB_MANIFEST.open() as f:
existing = json.load(f)
for k in ("launcherVersion", "launcherUrl"):
if k in existing:
manifest[k] = existing[k]
except Exception:
pass
# 4. Mirror override files to web root (uses sudo + cp -a). Only update
# files whose hash differs from what's already published to keep the
# rsync footprint tiny.
print(f"\nMirroring {sum(1 for r in MANAGED_ROOTS if (OVERRIDES / r).exists())} override roots to {WEB_FILES}\n")
changed = 0
skipped = 0
for root in MANAGED_ROOTS:
root_dir = OVERRIDES / root
if not root_dir.exists():
continue
target_root = WEB_FILES / root
for p in root_dir.rglob("*"):
if not p.is_file():
continue
rel = p.relative_to(OVERRIDES)
target = WEB_FILES / rel
try:
if target.exists() and sha1_of(target) == sha1_of(p):
skipped += 1
continue
except PermissionError:
# Need sudo to read
pass
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(p, target)
changed += 1
print(f" changed: {changed}, skipped (matching hash): {skipped}\n")
# 5. Write the manifest atomically (write to .new, then move)
new_manifest = WEB_MANIFEST.with_suffix(".json.new")
bak_manifest = WEB_MANIFEST.with_suffix(".json.bak")
if WEB_MANIFEST.exists():
shutil.copy2(WEB_MANIFEST, bak_manifest)
with new_manifest.open("w") as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
f.write("\n")
new_manifest.replace(WEB_MANIFEST)
print(f"Manifest written: {WEB_MANIFEST}")
print(f" Version: {lock['version']}")
print(f" Files: {len(files)}")
print(f" Total: {total_bytes / 1024 / 1024:.1f} MB download")
print()
# Quick verify
rel = "kubejs/server_scripts/economy/00_tier_system.js"
entry = next((f for f in files if f["path"] == rel), None)
if entry:
print(f" {rel} -> sha1={entry['sha1'][:12]}... size={entry['size']}")
if __name__ == "__main__":
main()