Rename
------
Counting House → Exchange everywhere. "Exchange" reads as intuitive
("an exchange where I trade items for spurs") where Counting House
was historically accurate but obscure. /bns open counthouse →
/bns open exchange. Class CountingHouseScreen → ExchangeScreen.
Lang key counting_house → exchange. NPC name suggestion in docs:
"Exchange Clerk".
Click-doesn't-refresh bug
-------------------------
ItemCell/BountyRow/PlotRow's mousePressed updated the static
`selected` field, but the detail sub-panel was already built with
the old value at refreshWidgets time. Result: clicking a different
item updated the highlight but the right-side detail pane never
changed.
Fix: call getGui().refreshWidgets() after updating selection. FTB
Library's recursive refresh re-runs SellDetail.addWidgets/
BountyDetail.addWidgets/PlotDetail.addWidgets with the new selected
value. Standard FTB Library pattern for "selection changed → redraw".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Counting House screen:
- 4-cell grid (was 5-col 5x2 wasted slots) with item icons centered
- Inventory count badge bottom-right of each item cell
- Detail pane: friendly display name, item id, on-hand count,
list price, lifetime sold, effective fee, payout per unit
- Sell 1/16/64 buttons grey out (and no-op) when you don't have
enough of the selected item in inventory
* Bottom action row on all transactional screens: replaced "Refresh"
with a single smart Back/Close button that returns to the Hub when
opened from there. Fixes the overlap with sub-panel internal buttons
that were caused by Refresh sitting at bottom-right.
* Panel heights reduced from (height - 30) to (height - 50) so the
internal sell/action buttons don't clash with the screen-level
action row.
* Same Back/Close pattern applied to TierUpgrade, Bounty, Plot, Journal.
* Removed "shop" alias from OpenScreenDispatcher — Counting House
responds only to "counthouse" now. (No NPCs exist yet so this is safe.)
* Added "journal" as the canonical screen name for the Adventurer's
Journal (replaces "questlog" — both keys still accepted in the
command suggestion list for now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plot system migrated from static Java + KubeJS to a runtime
configurable, FTB-Chunks-integrated system.
* Plot definitions now live in JSON at
<world>/serverconfig/bnstoolkit/plots.json. Default file written on
first server start with the three v0.33 plots. Editable in-place
+ reloadable via /bns admin plot reload.
* New static Plot.REGISTRY replaces Plot.ALL. Loaded on ServerStartedEvent.
Plot.all() returns the current snapshot.
* New FtbBridge.java — compileOnly deps on ftb-teams + ftb-chunks
(always in the pack). Wraps team lookup, chunk claim/unclaim, and
the BEFORE_UNCLAIM event.
* /bns admin spawn-init creates the Spawn (cyan) + Plots (gold) server
teams using configured UUIDs/colors and claims every defined plot
for the Plots team. Idempotent.
* /bns admin plot define <id> <name> <price> — adds the chunk you're
standing in to the plot. Re-run with the same id (different chunks)
to make multi-chunk plots. Auto-claims for the Plots team and
persists to JSON.
* /bns admin plot list / remove / reload commands.
* /bns plot buy <id> — pays via NumismaticsBridge.spend (cash+bank
combined), unclaims chunks from Plots team, claims for the buyer's
personal FTB Teams team. Records ownership in server NBT and the
player's bnsOwnedPlots list.
* /bns plot release <id> — reverse flow, refunds 50% to bank.
* Plot chunk guard: ClaimedChunkEvent.BEFORE_UNCLAIM subscriber rejects
any unclaim of a registered plot chunk unless a thread-local
SYSTEM_UNCLAIMING flag is set (only set during the release/transfer
flows). Players who try to unclaim via the FTB Chunks UI get a chat
message redirecting them to /bns plot release.
* KubeJS plots.js retired (.retired suffix kept). All logic now in
Java; KubeJS no longer participates in plot ownership state.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two polish features that complete the "one number" wealth UX:
1. NumismaticsBridge.spend(player, amount): combined deduction. Pulls
inventory coins first (most immediate), then bank for the remainder.
Atomic — if total is short, nothing is removed. Used by /bns tier
upgrade (KubeJS calls Java.loadClass). Future plot/bounty spend
operations should call this too.
2. NumismaticsBridge.ensureBoundCard: on first login, the player gets a
white_card pre-bound to their own bank account via the
numismatics:card_account_id DataComponent. Means a player visiting
a Numismatics Vendor block can pay from their bank without manually
crafting + binding a card first. If they die and lose it, the next
login re-issues.
Bounty pool: dropped 4 Alex's Mobs entries; added 6 vanilla replacements
(Breeze, Bogged, Evoker, Vindicator, Piglin Brute, Ravager). All
high-tier targets are vanilla 1.21.x mobs now.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
QuestLogScreen reworked from a generic status summary into a proper
adventure journal. Three sections, top to bottom:
- Active Bounties (N / cap) with progress bars + reward preview
- Counting House Rates — every wealth-token's effective per-unit spur
payout for the player's tier fee + lifetime DR. Lets players check
rates from the field instead of travelling to the Counting House.
- Owned Plots (N / cap)
- Footer: "FTB Quests is a separate mod — open your Quest Book item"
Lang title changed to "Adventurer's Journal" so it's distinct from
the FTB Quests system. Internal class + key stay QuestLog* for
backward-compat of /bns open questlog.
BaseBnsScreen.makeBackOrClose(screen) — labels the close button "Back"
with a back-arrow when FTB Library's previous-screen is set, "Close"
otherwise. Hub now passes the current screen wrapper as the previous
screen when opening the Journal, so close-from-Journal returns to Hub.
Bug fix: sub-panel children now pass `this` correctly to their parent
(was missing — same FTB Library latent bug).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FTB Library's Panel.refreshWidgets runs in this order:
1. clearWidgets
2. addWidgets (parent creates sub-panels, default size 0x0)
3. for each Panel child: refreshWidgets() ← recurses HERE
4. alignWidgets (parent sizes children, but it's too late)
Step 3 means sub-panels' own addWidgets/alignWidgets run with
width=height=0. Children sized via setSize(width, 12) end up 0 wide,
visible content disappears.
Fix is mechanical: every screen now sizes its sub-panels in
addWidgets() BEFORE the add() call. By the time the recursive
refreshWidgets hits the sub-panel, its dimensions are correct.
Caught when HubScreen rendered blank (status panel was set up to fill
the body but its Row children were 0px wide). Same latent issue in
TierUpgrade, CountingHouse, Bounty, Plot, Quest Log — all fixed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Major economic-design pivot per design conversation:
* Bazaar renamed to "Counting House" — wealth exchange, not general shop
* Players bring wealth-tokens (diamond, netherite_ingot, ancient_debris,
diamond_block); transactions credit their Numismatics bank account
directly (not inventory)
* SellCatalogue cut from 9 items to 4; iron/gold/coal/lapis/redstone/
emerald removed (those belong on player-run Numismatics Vendor blocks)
* Diminishing-returns floor tightened 50% -> 30% so deep-grinders feel
the friction
Numismatics integration via reflection (NumismaticsBridge):
* Auto-creates personal bank account on PlayerLoggedInEvent
* SpurBalance.totalSpurs() now sums inventory coins + bank balance
* TierStateUpdatePacket carries cashOnHand + bankBalance separately so
the hub can break them out
* HUD overlay shows three lines: tier / cash / bank — no card lookup,
always shows total wealth regardless of what's on the player
* /bns sell handled in Java (CountingHouseCommands); KubeJS 02_sell_
shop.js retired to .retired
* /bns wallet (+ deposit/withdraw) for managing cash<->bank flow
* Wire-version bumped 0.4 -> 0.5
Hub becomes status-only:
* Removed Civic Tier / Bazaar / Bounty / Plot transactional buttons
* Kept Quest Log, Refresh, Close
* Status panel shows tier, cash on hand, bank balance, total wealth,
fee%, plot slots, bounty slots
* Footer: "Visit spawn NPCs for trades, bounties, plots, tier upgrades"
Screen rename: ShopBrowserScreen -> CountingHouseScreen.
OpenScreenDispatcher accepts both "counthouse" and "shop" keys
(latter as backward-compat alias).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FTB Library's Widget.setPos(x, y) takes coords RELATIVE to the parent
panel. My code passed (getX() + offset, getY() + offset), so widgets
ended up at panel-pos * 2 + offset, far off-screen to the bottom-right.
Fix: drop getX()/getY() everywhere in alignWidgets. Refresh button,
close button, header bar, sub-panels — all relative now. Plus
BaseBnsScreen.contentTop()/contentBottom() are now plain offsets
(20 and getHeight() - 24) instead of getY()-prefixed.
Visual bug surfaced with user screenshot of HubScreen showing all
buttons stacked on the right edge of the screen with empty dark panel
on the left.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Anti-farm system that limits how much income players can extract from
villager trading in one location. Designed to make 50-librarian halls
diminish into futility without disabling villager trading altogether.
Model:
- World is partitioned into 64×64 block tiles (no POI lookups,
villager-position rounded by integer division)
- Each tile has a CompoundTag of {item_id -> remaining_demand} at
server.persistentData.bnsVillageDemand[dim:tileX:tileZ]
- Demand caps per item are set in VillageDemand.java; paper=20,
wheat=25, rotten flesh=15, iron=6, diamond=1, etc.
- Demand refills linearly: full cap restored over 1 hour of real time
(no scheduled tick — refresh computed lazily on each query from
lastUpdated timestamp)
- A 1-tile multi-profession hall earns ~150 spurs/hour at saturation.
Scaling income requires building halls in distant tiles — that's
overworld logistics, not micro-exploit.
Implementation:
- VillageDemand.java: per-item cap config
- VillageDemandTracker.java: lazy-refresh + atomic decrement against
the KubeJS server-scope NBT (existing reflection accessor)
- VillageTrackedMerchantOffer.java: MerchantOffer subclass that
overrides increaseUses() to decrement demand on trade completion
- VillagerTradeRebalance.java: at offer-build time, queries current
tile demand for the cost item. If saturated (<1 left), the offer
comes back with uses=maxUses (vanilla "out of stock" UI). Otherwise
wrapped in VillageTrackedMerchantOffer so post-trade decrement fires.
- Wandering trader: uses whatever tile he's wandered into. No special
case — it's just whichever tile he stands in at trade time.
Wandering traders, modded merchants (entity is not Villager): same
tracking, same caps. Anything that produces a spur as a trade result
is throttled by the tile.
Hooks: zero mixins. MerchantOffer.increaseUses() is vanilla-public
and fires server-side after the trade settles, so a subclass override
is the right hook point.
Perf: lazy refresh = no scheduled ticks. Per-trade overhead is one
NBT lookup + a few timestamp ops, microseconds. Idle servers do zero
work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Post-review fixes from the parallel code/security/hotkey/NPC audits.
## #35 — NBT keys (CRITICAL: screens were showing fictional data)
ServerEconomyBridge.pushBountySnapshot now reads the actual KubeJS keys:
- bnsBountyPool (compound id->1) — was bnsBounties.pool
- bnsBountyActive (compound id->{progress,target,reward,...}) — was bnsBounties.active
- bnsBountyCompleted (compound id->long cooldown-end-ms) — was bnsBounties.completed
- Filters completed by `cooldown-end > now` so old cooldowns drop off
ServerEconomyBridge.pushPlotSnapshot now reads server-scope state via
KubeJS's MinecraftServer mixin (kjs$getPersistentData) using reflection,
no compileOnly dep on KubeJS. Falls back to empty CompoundTag if the
method isn't there. Resolves owner names from PlayerList for online
players, GameProfileCache for offline.
## #36 — Hub UI + key rebind
Single primary key: B opens HubScreen. Direct shortcut keys (K/J/N/H/M)
all default to UNBOUND so they no longer compete with the ~100 other
pack mods. Audit found M = vanilla Social Interactions + FTB Chunks
fullscreen map (hard conflict, removed); H/N collided with Voice Chat
toggles (minor, removed); K/J were safe (kept rebindable for power users).
New HubScreen routes to the 5 sub-screens via labelled buttons + shows
tier/balance/fee at a glance in the header.
## #37 — /bns open <screen>
Java-registered Brigadier subcommand at RegisterCommandsEvent time,
merges with KubeJS's /bns root. New OpenScreenPacket (S2C) pushes a
screen open to the calling player. Screens: hub, tier, bounty, plot,
shop, questlog. Suggests valid screens via tab-complete.
This unlocks:
- Easy NPC dialogue COMMAND actions calling /bns open shop, etc.
- FTB Quests reward commands
- Command blocks / /trigger objectives
- Future scripted intros / tutorials
Also: patched /srv/fast/brass-sigil-server/server/config/easy_npc/
security.cfg to allow `bns` in executeAsUserCommandAllowList.ALL.
## #38 — Network hardening
- Per-player token bucket on C2S packets (10/s sustain, 20 burst).
- All STRING_UTF8 codecs capped: kind=8, args=80, which=16.
- Verb whitelist replaced with structured parse: args -> tokens ->
per-verb regex validators -> rebuilt command. No more prefix-match
surprises if /bns sell syntax expands.
- Per-player in-flight guard on tier upgrades (500ms) closes the
double-spend race window.
- Bridge logs sanitised (control-char-stripped) so a hostile client
can't inject newlines/ANSI into server logs.
## Review-driven nits also folded in
- Per-player tickCount (was shared static — N-player skew)
- VillagerTradeRebalance: try/catch around inner.getOffer; null cost-A
passthrough; round-to-nearest denom split (was floor, ~10% price haircut)
- SpurHud caches Component refs (avoid 120/s allocation)
- ClientBnsState.completedIds now HashSet (O(1) per-frame lookups)
- BountyBoardScreen: deleted the empty `if (isActive)` dead branch
- Wire-format version bumped 0.2 -> 0.4 (added OpenScreenPacket)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ToastPacket imported net.minecraft.client.gui.components.toasts.* and
net.minecraft.client.Minecraft at class scope. When BnsNetwork.onRegister
Payloads referenced ToastPacket::handle on the dedicated server,
NoClassDefFoundError fired (the client-only classes don't exist on
the dedicated server side).
Toast feedback wasn't load-bearing — KubeJS already chat-prints the
result of every /bns verb. Players see "Sold 8x diamond for 60 spurs"
in the chat as before, and the screens re-render off the snapshot
the bridge pushes after each routed verb.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hooks NeoForge's VillagerTradesEvent + WandererTradesEvent at data-pack
reload time and wraps every existing ItemListing so the resulting
MerchantOffer has its emerald-cost-or-result items swapped to
Numismatics coin equivalents:
1..7 emeralds -> spurs (1:1)
8..63 emeralds -> bevels (1 bevel = 8 spurs)
64+ emeralds -> cogs (1 cog = 64 spurs)
Lives in bnstoolkit instead of a third-party mod because the obvious
choice — Modrinth's 'numismatics-villager-currency' — requires NeoForge
21.1.230+ and we're on 21.1.228. The two-page Java wrapper here is
simpler than the alternatives (writing every trade override JSON via
'data-trades') AND doesn't push us into a runtime upgrade.
Includes wanderer trades. Modded villager professions are picked up
automatically since we just wrap the event's trade list as-is.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All five screens implemented with FTB Library widgets (Panel/Widget/
SimpleTextButton). Layout matches FTB Quests/Chunks for visual
consistency with the rest of the pack.
Screens:
- TierUpgradeScreen — 13-row ladder, current tier highlighted,
Upgrade button to next tier with cost/affordability check
- ShopBrowserScreen — 9-cell item grid + detail pane, quick-sell
buttons (1/16/64) with live DR-aware price preview
- BountyBoardScreen — daily pool + active set, accept/cancel/turn-in
per state, slot-cap indicator
- PlotPurchaseScreen — plot list + detail pane, buy/release with
ownership colour-coding and tier-gated buy
- QuestLogScreen — at-a-glance tier+balance summary, active
bounties with progress bars, owned plots
HUD overlay: top-left tier name + spurs balance, registered as a
NeoForge GUI layer above the experience bar. 1Hz server push keeps
the balance current without polling client-side inventory.
Network: replaces single smoke-test packet with 7-packet contract:
- C2S: RequestEconomySnapshot, RunBnsCommand (whitelisted)
- S2C: TierStateUpdate, SellSnapshot, BountySnapshot, PlotSnapshot, Toast
Server-side bridge (ServerEconomyBridge) reads player NBT and
Numismatics-by-item-id-string for balance, routes write verbs
through the existing KubeJS /bns chat commands so KubeJS remains the
source of truth for economy logic.
Key bindings (rebindable in vanilla controls menu):
- K Civic tier ladder
- J Bounty board
- N Plot office
- H Bazaar shop
- M Quest log
FTB Library + Architectury maven repos wired in build.gradle as
compileOnly; the jar stays small since FTB Library is already in pack.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The empty-class registration was rejected by NeoForge 21.1 at mod-loading
time with:
IllegalArgumentException: class uk.sijbers.bnstoolkit.BnsToolkit
has no @SubscribeEvent methods, but register was called anyway.
That tanked the dedicated server during construct-mods. Caught only after
deploying 0.1.0 to the live server — the local /gradlew build doesn't
exercise mod-loading. Will add a gameTestServer run later to catch this
class of bug pre-deploy.
When the first @SubscribeEvent listener lands (likely PlayerLoggedInEvent
in M1), the register call comes back together with it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>