Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b0fa0168a | |||
| 93c68325fe | |||
| 95a564c5e3 | |||
| f0e6187ed0 | |||
| 263b8c59d8 | |||
| a39bfee803 | |||
| 03f650ce3b | |||
| 35cbd84146 | |||
| c9f57dae9e | |||
| 83949d63b5 | |||
| 68aae5c917 | |||
| 0818fe8eab | |||
| f269cc4861 | |||
| ae183f27de | |||
| ec8f2b83cf | |||
| 28c816990a | |||
| 73f55768c3 | |||
| 48a70326d2 | |||
| 5f6a5aacdd | |||
| 8d9c04dc05 | |||
| ea9326819d | |||
| f2c9cabefe | |||
| ad9a261627 | |||
| bae44d0178 | |||
| 016985a5fc | |||
| a7f95691bb |
@@ -1,180 +1,114 @@
|
|||||||
# bnstoolkit M0 — discussion points for testing session
|
# bnstoolkit feature-complete — testing notes for 2026-06-07 evening
|
||||||
|
|
||||||
> Built 2026-06-07 (early hours UTC). Shipped via pack v0.34.0 then hotfixed via v0.34.1.
|
> Shipped: pack v0.35.0 / bnstoolkit 0.3.1.
|
||||||
> Repo: http://10.16.5.102:3000/minecraft/bnstoolkit
|
> Repo: http://10.16.5.102:3000/minecraft/bnstoolkit
|
||||||
> Jar: `bnstoolkit-0.1.1.jar` (15.6 KB)
|
> Server status: ✅ live + verified at 02:41 UTC.
|
||||||
|
|
||||||
## ⚠ Server status: needs a manual Start
|
## What's in the build
|
||||||
|
|
||||||
The first deploy (`0.1.0`) crashed the dedicated server during mod
|
### 5 screens (FTB Library widgets — matches FTB Quests look)
|
||||||
construction — see "Bug caught + fixed" below. The fixed jar (`0.1.1`)
|
|
||||||
is already staged in `/srv/fast/brass-sigil-server/server/mods/`, but
|
|
||||||
the JVM exited cleanly so systemd didn't auto-restart and I deliberately
|
|
||||||
didn't try to brute-force the admin panel credentials.
|
|
||||||
|
|
||||||
**To bring the server back: click Start in the web admin panel
|
All opened via key bindings. Rebindable in vanilla controls menu under
|
||||||
([http://glados:8080](http://glados:8080)) when you wake up.** The new
|
"Brass and Sigil" category.
|
||||||
boot should load `bnstoolkit-0.1.1` cleanly. Verification:
|
|
||||||
|
|
||||||
```
|
| Key | Screen | What it does |
|
||||||
sudo journalctl -u brass-sigil-server.service --since "2 minutes ago" \
|
|
||||||
| grep -E "bnstoolkit|Done \([0-9]+|ModLoadingException"
|
|
||||||
```
|
|
||||||
|
|
||||||
Look for `[bnstoolkit] common setup complete` AND no `ModLoadingException`.
|
|
||||||
|
|
||||||
## Bug caught + fixed (0.1.0 → 0.1.1)
|
|
||||||
|
|
||||||
```
|
|
||||||
IllegalArgumentException: class uk.sijbers.bnstoolkit.BnsToolkit
|
|
||||||
has no @SubscribeEvent methods, but register was called anyway.
|
|
||||||
```
|
|
||||||
|
|
||||||
I called `NeoForge.EVENT_BUS.register(this)` in the @Mod constructor
|
|
||||||
without yet having any `@SubscribeEvent` methods on the class. NeoForge
|
|
||||||
21.1 treats that as fatal at construct-mods time. Removed the call;
|
|
||||||
will come back together with the first real event handler in M1.
|
|
||||||
|
|
||||||
**Why local `./gradlew build` didn't catch it:** the build step only
|
|
||||||
compiles + jars. The construct-mods phase only runs when a real
|
|
||||||
NeoForge instance loads the mod. Action item for M1: add a
|
|
||||||
`gameTestServer` Gradle run config that boots the dedicated server
|
|
||||||
profile in CI-style so this class of bug fails locally next time.
|
|
||||||
|
|
||||||
## What this is
|
|
||||||
|
|
||||||
The skeleton of the custom companion mod. It loads cleanly on the client
|
|
||||||
and the dedicated server, registers nothing visible yet, and exposes the
|
|
||||||
package layout we'll fill in over M1–M4. The point of this milestone is:
|
|
||||||
|
|
||||||
1. Tooling proven end-to-end (Java 21, ModDevGradle, NeoForge 21.1.228, jar in pack)
|
|
||||||
2. Mod-id, package, and resource paths locked so future commits only ADD code
|
|
||||||
3. One C2S/S2C packet pair wired to prove the network plumbing
|
|
||||||
4. Placeholder screens in place so wiring keybinds to real UI is M1's first task
|
|
||||||
|
|
||||||
## Open decisions — please walk through these tomorrow
|
|
||||||
|
|
||||||
Each is marked with `TODO(matt …)` in the source.
|
|
||||||
|
|
||||||
### 1. Key bindings (BnsKeyBindings.java)
|
|
||||||
|
|
||||||
Defaults I picked, all unbinding is in the vanilla controls menu:
|
|
||||||
|
|
||||||
| Binding | Key | Opens |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Tier ladder | T | `TierUpgradeScreen` |
|
| **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. |
|
||||||
| Bounty board | B | `BountyBoardScreen` |
|
| **J** | Bounty Board | Today's 5-bounty pool + any active set. Detail pane: target, count, reward, progress. Accept / Cancel / Turn In buttons by state. |
|
||||||
| Plot map | P | `PlotPurchaseScreen` |
|
| **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. |
|
||||||
|
|
||||||
**Question**: any conflicts with mods you already use? `T` is vanilla's
|
### HUD overlay
|
||||||
chat-open IIRC — I should probably move it. Pick a free combo when you
|
|
||||||
playtest, I'll bump the defaults.
|
|
||||||
|
|
||||||
### 2. Screen open triggers — keybind only, or block + keybind?
|
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.
|
||||||
|
|
||||||
Each major screen has two viable open paths:
|
Auto-hides when F1 is pressed or chat is open (standard NeoForge HUD
|
||||||
|
layer behavior).
|
||||||
|
|
||||||
- **Keybind** (already drafted): bnstoolkit binds T/B/P
|
### Villager trade rebalance
|
||||||
- **In-world block**: e.g. a "Bounty Board" block placed at the spawn
|
|
||||||
guild building. Right-click opens the screen. More immersive, ties the
|
|
||||||
feature to a physical location.
|
|
||||||
|
|
||||||
Both can coexist easily — they're not exclusive. I'd default to "both for
|
Every emerald cost or result in vanilla + modded villager trades is
|
||||||
TierUpgrade + Plots, keybind-only for QuestLog, block-only for ShopBrowser
|
now Numismatics:
|
||||||
and BountyBoard". But that's a 30-second call you make tomorrow.
|
- 1..7 emeralds → spurs
|
||||||
|
- 8..63 → bevels (1 bevel = 8 spurs)
|
||||||
|
- 64+ → cogs (1 cog = 64 spurs)
|
||||||
|
|
||||||
### 3. FTB Library UI swap timing
|
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)
|
||||||
|
|
||||||
Right now the placeholder screens extend vanilla `Screen`. The
|
This was the "blocked on API" item. Built it into bnstoolkit instead
|
||||||
architecture doc commits to `dev.ftb.mods.ftblibrary.ui.BaseScreen` for
|
of pulling a third-party mod, because the cleanest Modrinth option
|
||||||
visual consistency with FTB Quests / Chunks. The swap is one line per
|
needed NeoForge 21.1.230+ and we're on 228.
|
||||||
screen — happens in M1 alongside the first real screen layout.
|
|
||||||
|
|
||||||
Reason it's deferred: the FTB maven coords need to be wired into
|
## Testing path
|
||||||
`build.gradle`, and I don't want to pin the wrong artifact name. Will
|
|
||||||
verify against the pack's installed FTB Library version (2101.1.31) before
|
|
||||||
the M1 build.
|
|
||||||
|
|
||||||
### 4. HUD overlay layout
|
|
||||||
|
|
||||||
The architecture doc shows a small panel "tier-icon | tier-name | spurs"
|
|
||||||
at top-left. Drop me a sketch when you have time and I'll lay it out for
|
|
||||||
M4. Current placeholder draws nothing — won't appear in game at all yet.
|
|
||||||
|
|
||||||
### 5. Network smoke-test packet
|
|
||||||
|
|
||||||
`RequestTierStatePacket` / `TierStateUpdatePacket` are the only wire
|
|
||||||
pair I shipped. They prove the codec + handler pattern. M1 fills in the
|
|
||||||
server-side handler that reads `player.persistentData.bnsTier` (the
|
|
||||||
KubeJS-owned NBT) and replies. **No client code yet sends the request** —
|
|
||||||
the round trip is dormant in M0.
|
|
||||||
|
|
||||||
### 6. Mod settings screen
|
|
||||||
|
|
||||||
Not addressed. Will be a small screen in M4 with toggles for the HUD,
|
|
||||||
keybind reminders, and (eventually) a colour-blind mode for tier ranks.
|
|
||||||
Low priority unless you've got a specific UX concern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files added/changed (PR for v0.34.0)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
pack/overrides/mods/bnstoolkit-0.1.0.jar NEW (15.6 KB)
|
/bns me # confirm Peasant tier 1
|
||||||
pack/pack.lock.json version 0.33.0 -> 0.34.0
|
/give @s numismatics:spur 500 # give spurs for upgrades
|
||||||
docs/bnstoolkit-discussion-points-2026-06-07.md NEW (this file)
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
The bnstoolkit source itself lives at `http://10.16.5.102:3000/minecraft/bnstoolkit`
|
## Architecture
|
||||||
— one initial commit on `main` with the full M0 scaffold. Build:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd /home/matt/dev/bnstoolkit
|
KubeJS server_scripts/economy/ ← single source of truth for state + logic
|
||||||
./gradlew build
|
↑
|
||||||
|
│ (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
|
||||||
```
|
```
|
||||||
|
|
||||||
## What you'll see in-game when you log in
|
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.
|
||||||
|
|
||||||
**Nothing yet.** That's the point of M0 — it loads cleanly with zero
|
## Known gaps for future iteration
|
||||||
visible surface. Verification path:
|
|
||||||
|
|
||||||
1. Server log: grep for `[bnstoolkit] mod entry constructed` and
|
| Gap | Where to fix |
|
||||||
`common setup complete (M0 scaffold — no surface yet)`
|
|---|---|
|
||||||
2. Client log on connect: `registered 3 key bindings under key.category.bnstoolkit`
|
| No client toast notifications — feedback comes via KubeJS chat messages | Add a dist-isolated `ClientToastDispatcher` class (0.4.0) |
|
||||||
3. Controls menu → "Brass and Sigil" category → 3 unbound-looking lines
|
| Bazaar grid doesn't tooltip the per-unit price | One `addMouseOverText` per `ItemCell` |
|
||||||
(Open Tier Ladder, Open Bounty Board, Open Plot Map) on T/B/P
|
| 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 |
|
||||||
|
|
||||||
If you bind one of those keys and press it in-game, **nothing happens**.
|
## File map
|
||||||
That's correct — the handlers are M1 work.
|
|
||||||
|
|
||||||
## Milestones — concrete next deliverables
|
| 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 |
|
||||||
|
|
||||||
| Milestone | Scope | Estimated work |
|
Source repo: http://10.16.5.102:3000/minecraft/bnstoolkit (main = 14dc69f at time of pack ship).
|
||||||
|---|---|---|
|
|
||||||
| M1 | TierUpgradeScreen + KubeJS bridge (tier/balance) + FTB Library swap | 1 evening |
|
|
||||||
| M2 | QuestLogScreen + ShopBrowserScreen | 1 evening |
|
|
||||||
| M3 | BountyBoardScreen + PlotPurchaseScreen | 1 evening |
|
|
||||||
| M4 | SpurHud, mod settings screen, polish | 1 evening |
|
|
||||||
|
|
||||||
Each milestone ends with a deployable jar + a screen you can poke in
|
|
||||||
game. None of them require server downtime — drop the new jar in
|
|
||||||
`pack/overrides/mods/`, the wrapper sync handles the rest.
|
|
||||||
|
|
||||||
## Quick recovery
|
|
||||||
|
|
||||||
If the mod misbehaves and bricks the world:
|
|
||||||
|
|
||||||
```
|
|
||||||
# 1. Stop the server
|
|
||||||
sudo systemctl stop brass-sigil-server.service
|
|
||||||
|
|
||||||
# 2. Pull the jar out of the live mods dir
|
|
||||||
sudo rm /srv/fast/brass-sigil-server/server/mods/bnstoolkit-0.1.0.jar
|
|
||||||
|
|
||||||
# 3. Restart
|
|
||||||
sudo systemctl start brass-sigil-server.service
|
|
||||||
```
|
|
||||||
|
|
||||||
That leaves `pack/overrides/mods/bnstoolkit-0.1.0.jar` in the pack repo
|
|
||||||
so the next sync would put it back — to fully revert, also `git revert`
|
|
||||||
the v0.34.0 commit on the pack.
|
|
||||||
|
|||||||
@@ -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_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" },
|
{ 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) ──
|
// ── 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_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_breeze_3', name: 'Hunt 3 Breezes', target: 'minecraft:breeze', count: 3, type: 'kill', reward: 250, 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_bogged_5', name: 'Slay 5 Bogged', target: 'minecraft:bogged', count: 5, type: 'kill', reward: 120, 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_evoker_2', name: 'Slay 2 Evokers', target: 'minecraft:evoker', count: 2, type: 'kill', reward: 350, 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" },
|
{ 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
|
// Index pool by id for O(1) lookup
|
||||||
@@ -344,4 +346,14 @@ ServerEvents.commandRegistry(event => {
|
|||||||
event.register(bountyCmd);
|
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');
|
console.info('[bns/bounty_engine] loaded — ' + BOUNTY_POOL.length + ' bounties in catalogue');
|
||||||
|
|||||||
@@ -109,19 +109,20 @@ ServerEvents.commandRegistry(event => {
|
|||||||
}
|
}
|
||||||
const nextTier = cur + 1;
|
const nextTier = cur + 1;
|
||||||
const nextCfg = bnsTier.config(nextTier);
|
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(
|
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;
|
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
|
// Promote via FTB Ranks
|
||||||
const rankId = TIER_RANK_ID[nextTier];
|
const rankId = TIER_RANK_ID[nextTier];
|
||||||
Utils.server.runCommandSilent('ftbranks add ' + player.username + ' ' + rankId);
|
Utils.server.runCommandSilent('ftbranks add ' + player.username + ' ' + rankId);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35
-24
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$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.35.0",
|
"version": "0.41.2",
|
||||||
"minecraft": "1.21.1",
|
"minecraft": "1.21.1",
|
||||||
"loader": {
|
"loader": {
|
||||||
"type": "neoforge",
|
"type": "neoforge",
|
||||||
"version": "21.1.228"
|
"version": "21.1.228"
|
||||||
},
|
},
|
||||||
"lockedAt": "2026-06-07T02:33:07Z",
|
"lockedAt": "2026-06-08T18:22:06Z",
|
||||||
"mods": [
|
"mods": [
|
||||||
{
|
{
|
||||||
"source": "modrinth",
|
"source": "modrinth",
|
||||||
@@ -130,17 +130,6 @@
|
|||||||
"size": 38629,
|
"size": 38629,
|
||||||
"side": "both"
|
"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",
|
"source": "modrinth",
|
||||||
"slug": "bellsandwhistles",
|
"slug": "bellsandwhistles",
|
||||||
@@ -1065,17 +1054,6 @@
|
|||||||
"size": 193381,
|
"size": 193381,
|
||||||
"side": "both"
|
"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",
|
"source": "modrinth",
|
||||||
"slug": "item-obliterator",
|
"slug": "item-obliterator",
|
||||||
@@ -1130,6 +1108,39 @@
|
|||||||
"sha1": "07fdfa3a75c3481770ce0118090a347d89e3a560",
|
"sha1": "07fdfa3a75c3481770ce0118090a347d89e3a560",
|
||||||
"size": 88382,
|
"size": 88382,
|
||||||
"side": "both"
|
"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": {
|
"defaultServer": {
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user