Compare commits

...

9 Commits

Author SHA1 Message Date
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
8 changed files with 254 additions and 2 deletions
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+35 -2
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.35.0",
"version": "0.38.1",
"minecraft": "1.21.1",
"loader": {
"type": "neoforge",
"version": "21.1.228"
},
"lockedAt": "2026-06-07T02:33:07Z",
"lockedAt": "2026-06-08T14:42:41Z",
"mods": [
{
"source": "modrinth",
@@ -1130,6 +1130,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()