Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0e6187ed0 | |||
| 263b8c59d8 | |||
| a39bfee803 | |||
| 03f650ce3b | |||
| 35cbd84146 | |||
| c9f57dae9e | |||
| 83949d63b5 | |||
| 68aae5c917 | |||
| 0818fe8eab | |||
| f269cc4861 | |||
| ae183f27de | |||
| ec8f2b83cf | |||
| 28c816990a | |||
| 73f55768c3 |
@@ -0,0 +1,143 @@
|
||||
# Spawn building setup — wiring NPCs to bnstoolkit screens
|
||||
|
||||
How to give players access to the Counting House / 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) |
|
||||
| Counting House | `/bns open counthouse` | 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 (Counting House 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**: "Counting House 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 Counting House. 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 counthouse`
|
||||
- **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 counthouse` as that player and
|
||||
the Counting House screen pops up.
|
||||
|
||||
## Building-to-screen mapping
|
||||
|
||||
| Building | NPC name suggestion | Open command |
|
||||
|---|---|---|
|
||||
| Counting House | Counting House Clerk | `bns open counthouse` |
|
||||
| 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 |
|
||||
| Counting House | `/bns open counthouse` → 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.
|
||||
@@ -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.
+2
-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",
|
||||
"name": "Brass and Sigil",
|
||||
"version": "0.38.0",
|
||||
"version": "0.41.1",
|
||||
"minecraft": "1.21.1",
|
||||
"loader": {
|
||||
"type": "neoforge",
|
||||
"version": "21.1.228"
|
||||
},
|
||||
"lockedAt": "2026-06-07T22:29:48Z",
|
||||
"lockedAt": "2026-06-08T18:14:17Z",
|
||||
"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",
|
||||
|
||||
@@ -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