Mods added (Phase 0a substrate for the bnstoolkit economy work): - FTB Quests 2101.1.25 (progression quest UI for Phase 5+) - FTB XMod Compat 21.1.8 (KubeJS hooks for FTB Chunks/Quests events) - FTB Ranks 2101.1.3 (tier system via permission nodes) Design revision: restrictive travel. This is a Create Aeronautics pack — the journey is the point. Removed all instant-teleport perks from the tier ladder. Players get one starter Waystone and use it for spawn↔base; further travel means building airships, trains, vehicles. Pressure on real movement is intentional. Removed perks: sethomes, /back after death. Removed system: the full sethome subsystem (commands, SavedData, cooldowns) from the bnstoolkit mod scope. Tier table reduced from 10 columns to 8: cost, wilderness chunks, plot slots, daily bounties, shop fee, chat colour, join broadcast. Mod phasing collapsed from 7 phases to 6 (M2 sethome system removed, later phases shift up). Future perk dimensions list updated with two waystone-related candidates (slot cap, XP cost reduction) as the natural travel- progression hooks for later tier-perk revisions. Verified live: all 3 mods discovered + loaded, port 25565 binds, no FATAL/Crashed/Exception lines in startup log. Server resumed to 0.32.0 cleanly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
26 KiB
bnstoolkit — Architecture & Perk Specification
Status: design draft (v0.1). Not yet implemented. Owner: matt Last updated: 2026-06-06
bnstoolkit is the custom companion mod for the Brass and Sigil economy
system. It provides the UI layer (GUI screens, HUD), the FTB Chunks
map mixins for spawn plot integration, and the data models the economy
needs. All gameplay logic (bounty rotation, state machine, villager
trades, Numismatics integration, etc.) lives in KubeJS and is called
into by the mod via network packets.
This doc is the source of truth for the mod design. It must be reviewed before any Java is written.
See also: economy-system.md for the gameplay
side of the design.
1. Purpose & scope
In scope
- GUI screens built on FTB Library's UI framework (matches FTB Quests / FTB Chunks visual language)
- Mixins into FTB Chunks' map UI for spawn-plot cost overlay, hover tooltips, and click-to-purchase popups
- Custom HUD render layer for quest progress (replaces stacked vanilla bossbars)
- Data persistence for plot ownership (server-side, saved with world)
- Network packets for client↔server state sync
- Admin commands:
/bnstoolkit reload, debugging commands - Config-driven tier perks (
tiers.json) and plot definitions (plots.json)
Out of scope
- Bounty quest engine (lives in KubeJS)
- Villager trade rebalance (lives in KubeJS)
- Numismatics direct integration (lives in KubeJS — mod calls KubeJS commands to debit/credit)
- FTB Quests integration (FTB Quests handles its own UI for the progression campaign)
- FTB Ranks admin (FTB Ranks handles permissions directly; mod just reads them)
- Sethome /
/back/ fast-travel features — explicitly removed. The pack uses Waystones as the only travel mechanic. Restrictive travel is a design feature for a Create Aeronautics pack: airships, trains, and vehicles are the intended way to cover distance.
2. High-level architecture
┌──────────────────────┐
│ Player (client) │
└──────────┬───────────┘
│ keybind / NPC click / chunk click
▼
┌───────────────────────────────────┐
│ bnstoolkit (client) │
│ • FTB Library UI screens │
│ • FTB Chunks map mixin │
│ • Quest progress HUD │
└───────────────┬───────────────────┘
│ network packets
▼
┌───────────────────────────────────┐
│ bnstoolkit (server) │
│ • Plot ownership data │
│ • Sethome data │
│ • Network packet handlers │
│ • /bnstoolkit reload command │
└─────┬───────────────────────┬─────┘
│ │
reads │ │ calls KubeJS commands /
FTB Ranks / │ │ fires events for KubeJS
FTB Chunks / │ │ to react to
Numismatics ▼ ▼
┌───────────────┐ ┌──────────────────────┐
│ FTB / NeoForge │ │ KubeJS scripts │
│ stack │ │ • Bounty engine │
└───────────────┘ │ • Quest state │
│ • Numismatics calls │
│ • Villager trades │
│ • Tier promotion │
└──────────────────────┘
Key invariant: the mod owns UI + plot data + sethomes. KubeJS owns gameplay logic. They communicate via network packets and KubeJS commands. The mod never directly mutates Numismatics balances or quest state — it asks KubeJS to do that via commands.
3. Package layout
bnstoolkit/
├── build.gradle NeoForge 1.21.1 + FTB Library dep
├── src/main/java/uk/sijbers/bnstoolkit/
│ ├── BnsToolkit.java Mod entry, lifecycle
│ ├── client/
│ │ ├── ClientEvents.java Keybinds, client setup
│ │ ├── screen/
│ │ │ ├── QuestLogScreen.java
│ │ │ ├── BountyBoardScreen.java
│ │ │ ├── TierUpgradeScreen.java
│ │ │ ├── PlotPurchaseScreen.java
│ │ │ ├── ShopBrowserScreen.java
│ │ │ └── widgets/ (subclass FTB Library widgets)
│ │ ├── hud/
│ │ │ └── QuestProgressHud.java IGuiOverlay
│ │ └── mixin/
│ │ ├── ChunkScreenPanelMixin.java
│ │ └── ChunkButtonMixin.java
│ ├── server/
│ │ ├── command/
│ │ │ └── BnsToolkitCommand.java /bnstoolkit reload, debug
│ │ ├── data/
│ │ │ ├── PlotRegistry.java loads plots.json
│ │ │ └── PlotOwnership.java SavedData, plot owner UUIDs
│ │ ├── event/
│ │ │ ├── PlayerJoinHandler.java tier-based join broadcast
│ │ │ └── ChatColourHandler.java tier-based chat name colouring
│ │ └── net/
│ │ └── (packet implementations — see §6)
│ ├── common/
│ │ ├── data/
│ │ │ ├── TierConfig.java loads tiers.json
│ │ │ ├── BountyDef.java DTO mirrored from KubeJS
│ │ │ ├── QuestStateView.java snapshot for UI display
│ │ │ └── PlotDef.java plot definition (region, price)
│ │ └── net/
│ │ └── BnsPacket.java network channel + payload base
│ └── resources/
│ ├── META-INF/neoforge.mods.toml
│ ├── bnstoolkit.mixins.json
│ └── assets/bnstoolkit/lang/en_us.json
└── src/main/resources/
└── data-config/ packed defaults, copied on first run
├── tiers.json
└── plots.json
4. Screen specifications
All screens extend FTB Library's BaseScreen. They share a common
header (player name, current tier, spur balance) so navigation feels
consistent.
4.1 Quest Log
Opened by: keybind (default Q+B?) or right-clicking a Quest Log block placed at any guild building.
Layout: three tabs across the top — Active / Available / Completed.
- Active tab: list of currently-accepted bounties with progress
bars. Each row clickable; click expands a detail panel with
description, source guild, reward, and a
Cancel questbutton. Cancel triggers a confirmation modal warning about cooldown. - Available tab: list of bounties currently offered (rotated daily
by the KubeJS engine). Each row shows the bounty source, reward,
cooldown remaining (if any). Click → detail panel with
Acceptbutton. - Completed tab: historical log of completed bounties this week, for satisfaction value. No interaction beyond viewing.
Network calls:
- On open: client requests current quest state from server (
S2CQuestSnapshot) - On accept/cancel click:
C2SAcceptBounty(id)/C2SCancelQuest(id)
4.2 Bounty Board
Opened by: right-clicking the Adventurer's Guild Quartermaster
NPC (Easy NPC dialog action runs /bnstoolkit openboard adventurers_guild)
OR clicking an in-world bnstoolkit:bounty_board block.
Layout: poster-board style — the day's available bounties as visual "cards" laid out in a grid, each with mob icon, name, description, reward, accept button. Friendly to non-tech users.
Network calls:
- On open:
S2CBountyBoardSnapshot(guild_id)returns today's bounty list - On accept: same
C2SAcceptBounty(id)as Quest Log
4.3 Tier Upgrade Screen
Opened by: right-clicking the Civic Office Magistrate NPC.
Layout: full ladder shown as a vertical or horizontal track. Your
current tier highlighted (glowing badge). Next tier shows cost +
delta perks (e.g. "+50 chunks, +1 plot slot, +2 sethomes, +1 bounty,
-2% shop fee, chat colour: yellow"). Big Upgrade to <Tier> button
at the bottom, greyed out if you don't have the spurs.
Hovering any future tier shows its full perk list in a side panel.
Network calls:
- On open:
S2CTierSnapshotreturns current tier + balance - On upgrade click:
C2SRequestTierUpgrade→ server verifies balance, calls KubeJS/bnstoolkit_internal upgrade_rank <uuid>which charges spurs and runsftbranks add <player> <tier>
4.4 Plot Purchase Popup
Opened by: clicking an unowned plot region in the FTB Chunks map
UI (via the mod's mixin into ChunkScreenPanel).
Layout: modal overlay on top of the FTB Chunks map. Shows: plot
ID, plot type (standard/premium), region size in chunks, owner (none),
price. Your current spur balance. Your remaining plot slots (e.g.
"2 of 3 used"). Purchase and Cancel buttons. If you're at your
slot cap or don't have enough spurs, Purchase is greyed out with
the reason shown.
Network calls:
- On purchase:
C2SBuyPlot(plot_id)→ server checks slot capacity, asks KubeJS to charge spurs, on success records ownership inPlotOwnershipSavedData.
4.5 Shop Browser
Opened by: right-clicking the Bazaar Master NPC at the Merchant's Bazaar (provides a curated view of all currently-listed shop blocks
- NPC sell shop).
Layout: searchable item list with current prices, sort by category. Click an item to see who's selling it / where the shop block is located. The NPC sell shop (diamonds, netherite etc.) shows as a special "Official Bazaar" category with fixed prices and the current diminishing-return multiplier per item.
Note: individual Numismatics shop blocks keep their native UI when right-clicked — this Bazaar Master is the curated central view, not a replacement for shop block interaction.
Network calls:
- On open:
S2CShopBrowserSnapshot(asks KubeJS for current listings) - Selecting an item →
S2CShopItemDetail(item_id)for full info
4.6 Quest Progress HUD
Render layer: IGuiOverlay, drawn over the vanilla HUD.
Layout: small panel in top-right (configurable position) showing each active quest as a row — quest name + progress bar + N/M numeric. Fades on inactive (no progress in last 30s) so it doesn't visually clutter when you're not actively bounty-hunting. Re-appears the moment any quest progresses.
State source: KubeJS pushes updates via S2CHudQuestUpdate packet
when any quest's progress changes (debounced to once per second
maximum per quest). The HUD reads from a client-side cache.
5. Mixin targets
Minimal — we don't fork FTB Chunks, we inject only what we need.
| Target | Method | Purpose | Mixin file |
|---|---|---|---|
ChunkScreenPanel |
drawBackground (TAIL) |
Overlay drawing — cost preview for the currently-hovered or selected chunk, "Your plot slots: N of M" text in a corner | ChunkScreenPanelMixin.java |
ChunkScreenPanel$ChunkButton |
addMouseOverText (TAIL) |
Append "Cost: N spurs" or "Plot: " to the per-chunk hover tooltip | ChunkButtonMixin.java |
ChunkScreenPanel$ChunkButton |
onClicked (HEAD, with cancellation if applicable) |
If the clicked chunk is a plot region, open PlotPurchaseScreen instead of triggering a regular claim |
ChunkButtonMixin.java |
That's it. Three injection points across two mixin files. No modification of FTB Library, FTB Quests, FTB Ranks, or FTB Teams.
6. Network packet contract
NeoForge PayloadRegistrar. All packets versioned via a single
PROTOCOL_VERSION constant; bump on schema change.
Server → Client
| Packet | Payload | When sent |
|---|---|---|
S2CQuestSnapshot |
List<QuestStateView> |
On Quest Log open, on any quest-state change |
S2CBountyBoardSnapshot |
List<BountyDef> (5-10 entries) |
On Bounty Board open |
S2CTierSnapshot |
(currentTier, balance, perksByTier) |
On Tier Upgrade Screen open |
S2CShopBrowserSnapshot |
List<ShopListing> |
On Shop Browser open |
S2CHudQuestUpdate |
(questId, progress, target) |
On quest progress (debounced 1s) |
S2CPlotPurchaseResult |
(success, message) |
After C2SBuyPlot resolves |
S2CTierUpgradeResult |
(success, message) |
After C2SRequestTierUpgrade resolves |
Client → Server
| Packet | Payload | What server does |
|---|---|---|
C2SAcceptBounty |
questId |
Call /bnstoolkit_internal accept_bounty <uuid> <questId> (KubeJS) |
C2SCancelQuest |
questId |
Call /bnstoolkit_internal cancel_quest <uuid> <questId> (KubeJS) |
C2SBuyPlot |
plotId |
Verify slot capacity, ask KubeJS to debit spurs, record ownership |
C2SRequestTierUpgrade |
(none — server knows player) | Verify balance, ask KubeJS to debit + promote |
C2SOpenBoard |
guildId (optional context) |
Reply with appropriate snapshot |
All C2S packets include the player implicitly (NeoForge passes
the ServerPlayer in the handler).
7. Data persistence
| Data | Storage | Lifetime |
|---|---|---|
| Plot ownership | bnstoolkit:plots — vanilla NeoForge SavedData attached to overworld |
Persists with world. Authoritative source. |
| Quest state | KubeJS-managed JSON at world/data/bns_economy/quests/<uuid>.json |
Not the mod's concern — the mod only reads via packets |
| Tier configuration | config/bnstoolkit/tiers.json (hot-reloadable) |
Until edited |
| Plot definitions | config/bnstoolkit/plots.json (hot-reloadable via /bnstoolkit reload) |
Until edited |
Plot ownership is server-authoritative. Clients only get views via packets — they never write to these stores directly.
8. Configuration files
config/bnstoolkit/tiers.json
Defines the 13-tier ladder. Hot-reloadable.
{
"tiers": [
{
"id": "peasant",
"displayName": "Peasant",
"cost": 0,
"perks": {
"wildernessChunks": 9,
"plotSlots": 0,
"dailyBountySlots": 3,
"shopFeeDiscount": 0,
"chatColour": "grey",
"joinBroadcast": "none"
}
},
/* ... 12 more tiers ... */
]
}
config/bnstoolkit/plots.json
Defines spawn plot regions. Each plot has a unique ID, region bounds (chunk coordinates), type (standard/premium), and price.
{
"plots": [
{
"id": "bazaar_stall_01",
"displayName": "Bazaar Stall 1",
"type": "standard",
"price": 2000,
"chunks": [[10, 20], [10, 21]], // chunk coords, inclusive
"world": "minecraft:overworld"
},
{
"id": "bazaar_corner_premium",
"displayName": "Premium Corner Plot",
"type": "premium",
"price": 5000,
"chunks": [[15, 25], [15, 26], [16, 25], [16, 26]],
"world": "minecraft:overworld"
}
]
}
9. KubeJS integration surface
The mod calls into KubeJS via custom commands registered by KubeJS
(ServerEvents.commandRegistry). All have the bnstoolkit_internal
prefix to mark them as not-for-player-use:
| Command | Purpose |
|---|---|
/bnstoolkit_internal accept_bounty <uuid> <questId> |
Mark quest as ACTIVE in KubeJS state |
/bnstoolkit_internal cancel_quest <uuid> <questId> |
Cancel quest, apply cooldown |
/bnstoolkit_internal complete_quest <uuid> <questId> |
Mark quest READY (for testing/manual) |
/bnstoolkit_internal turn_in_quest <uuid> <questId> |
Pay reward via Numismatics |
/bnstoolkit_internal debit <uuid> <amount> <reason> |
Charge spurs (used by plot purchase, tier upgrade) |
/bnstoolkit_internal credit <uuid> <amount> <reason> |
Pay spurs |
/bnstoolkit_internal upgrade_rank <uuid> <tierId> |
Promote via FTB Ranks |
/bnstoolkit_internal get_balance <uuid> |
Returns current spur balance (used for screen header) |
All commands return a JSON result via the command output that the
mod parses. KubeJS handles the actual Numismatics + FTB Ranks calls
(it already has the Java.loadClass helpers from the economy KubeJS
scripts).
KubeJS in turn pushes events to the mod via custom payload packets when quest state changes (so the HUD can update without polling).
10. Build & release
- Repo: new private Gitea repo
minecraft/bnstoolkit(separate from the modpack repo for clean release-versioning, but mirrored to the modpack'spack/overrides/mods/on each release) - Build: standard NeoForge 1.21.1 ModDevGradle template
- Dependencies: Architectury API (provided), FTB Library (provided), Numismatics (compileOnly), FTB Chunks (compileOnly)
- CI: Gitea Actions runs
./gradlew buildon push to main, uploads the jar as a release artifact - Modpack integration: the modpack's bump-version flow includes
pulling the latest bnstoolkit release jar and updating
pack.lock.json/ manifest like any other mod - Versioning: semantic —
1.0.0for V1 ship,1.x.yfor patches,2.0.0if we ever change the network protocol or data format
11. Perk specifications (detailed)
Every perk listed in the 13-tier ladder, expanded with mechanics, implementation, and edge cases.
11.1 Wilderness chunks
What it does: caps how many wilderness chunks (outside spawn) a player can claim via the FTB Chunks map. Claiming within the cap is free; over the cap is refused with an in-game message.
Implementation: FTB Ranks permission node
ftbchunks.max_claimed_chunks set per rank in the FTB Ranks config
(written by the mod on first launch from tiers.json). FTB Chunks
reads this permission natively — no mod-side enforcement needed.
Edge case: if a player downgrades (which they can't normally, but might happen via op command), they keep already-claimed chunks over the new cap, but can't claim more until they free some.
11.2 Plot slots
What it does: caps how many spawn plots (predefined regions at
spawn, see plots.json) a player can own concurrently. Independent
of wilderness chunks.
Implementation: stored in PlotOwnership SavedData. On purchase,
mod checks playerOwnedPlots.size() < tier.plotSlots. If at cap,
the Plot Purchase Popup shows the slot count and disables the
Purchase button.
Releasing a plot: players can sell their plot back via a "Release" button in the plot-detail screen (opens when they click their own plot in the map). Releases the slot. Refund: 50% of purchase price.
11.3 Daily bounty slots
What it does: how many bounties a player can have ACTIVE simultaneously. Once the slot is freed (by completion or cancel), the player can accept another from any guild board.
Implementation: enforced in KubeJS bounty engine. The mod's Bounty Board and Quest Log screens read the slot count via packet and grey out the Accept button when at cap.
Edge case: a quest in READY state still counts against your active slots until you turn it in. This is intentional — pushes players to actually visit the NPC to claim.
11.4 Shop fee discount
What it does: reduces the "merchant cut" deducted when you sell items at the Bazaar's NPC sell shop. The Bazaar charges a base 25% fee on all sales; your tier provides a discount that reduces this toward zero.
Worked example: you sell a diamond worth 10 spurs at the Bazaar.
- Peasant (0% discount): 25% fee → you receive 7.5 spurs (rounded down to 7).
- Knight (-5% discount): 20% fee → 8 spurs.
- Sovereign (-25% discount): 0% fee → full 10 spurs.
Implementation: KubeJS sell-shop handler reads the seller's tier and computes effective payout. Rounded down to nearest integer.
Edge case: player-to-player Numismatics shop blocks do NOT apply this fee — those are direct peer transactions, no Bazaar middleman. The fee only applies to the official Bazaar sell shop.
11.5 Chat colour
What it does: the player's name in chat appears in this colour, visible to everyone. Visual rank marker.
Implementation: KubeJS PlayerEvents.chat event modifies the
chat component for the player's display name based on their tier
permission (read from FTB Ranks). The mod also colours the player's
nameplate above their head (via a small client packet on tier change
that other clients use to set the display name colour locally).
Colour ladder:
- 1-2 Peasant/Farmer: grey
- 3 Citizen: white
- 4 Merchant: yellow
- 5 Knight: green
- 6 Baron: cyan
- 7 Viscount: blue
- 8 Earl: purple
- 9 Marquess: gold
- 10 Duke: orange
- 11 Archduke: red
- 12 Grand Duke: bright red
- 13 Sovereign: rainbow (animated via colour-cycling text component)
Edge case: the Founder cosmetic [Founder] prefix sits before
the coloured name, in a distinct colour (probably bright yellow or
white-bold). Sovereign + Founder is visually busy but acceptable —
only one player on the server will have both.
11.6 Join broadcast
What it does: when the player logs in, this controls what message goes to everyone else on the server.
Levels:
- none: standard vanilla
<player> joined the game. - simple: rank-prefixed message in their chat colour. Example:
[Knight] Matt has logged in. - fancy: multi-line, with a separator. Example:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [Viscount] Matt has arrived ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - epic: multi-line, with a sound effect (e.g. trumpet fanfare, configurable per-tier). Larger ASCII separator. Possibly an ASCII art banner of the rank.
- epic + particles: as epic, plus a 5-second particle effect at the player's spawn location (firework spark, end-rod sparkle, etc.) for everyone nearby to see.
- epic + spectacular: as epic + particles, plus a server-wide effect — actionbar message visible to everyone, brief light-up of the sky if Sovereign joins.
Implementation: KubeJS PlayerEvents.loggedIn reads tier and
dispatches the appropriate message. Particle/sound effects are
server-side commands (/particle, /playsound).
11.7 Future perk dimensions (not in V1, but easy to add later)
Architecture supports adding new perk columns in tiers.json
without code changes (the schema is open-ended). Candidates for
later expansion:
- Waystone slot cap — limit how many waystones a player can place and/or own. Starter waystone is granted; higher tiers unlock 2nd, 3rd, etc. Cleanest in-theme perk for a Create Aeronautics pack where restrictive travel is intentional.
- Waystone XP cost reduction — vanilla Waystones charges XP per teleport. Tier-based discount on this cost (or eventual exemption for Sovereign).
- Particle trail — high tiers leave a visible particle wake when walking
- Custom join sound — different sound effect per tier on login
- Custom title prefix — text added before chat name beyond the
colour (e.g.
⚔ Knight Matt) - Priority bounty access — higher tiers see a larger pool of daily bounties to pick from
- Tier-gated vendor stock — specific Numismatics shops only visible to Baron+ etc.
- Daily login bonus — small spur reward for logging in, scaling by tier
- Death keep-inventory chance — RNG % to keep inventory on death (controversial, gate carefully)
12. Open implementation questions
These can be deferred until they become blocking, but worth flagging:
- Sovereign tier rainbow chat name — Minecraft text components don't natively support gradient text. Implementation options: (a) cycle through colours character by character, or (b) animate via packet on a tick interval. Choose during Phase 0c scaffolding.
- Plot release refund percentage — proposed 50%. Tune via playtest.
- Sethome cooldown values per tier — proposed scaling from 60s
(low tiers) to 5s (Sovereign). Confirm in
tiers.jsonschema. /backinteraction with dimension changes — does dying in the End and/backing return you to the End or to overworld? Default: returns to wherever you died including the dimension.- Founder cosmetic prefix colour — bright yellow? Bold white? Animated? Settle during implementation.
- Sovereign spectacular join effect — exact effects need design
pass (sky flash? Server-wide actionbar text?). Tune in
tiers.json.
13. Implementation phases (mod-specific)
Aligned with the economy doc's Phase 0c (mod scaffolding):
| Mod phase | Deliverable |
|---|---|
| M0 | Gradle project, mod metadata, FTB Library dep wired, network channel registered, empty placeholder for each screen class. Builds + loads cleanly into the running pack. |
| M1 | Tier system: TierConfig loads from tiers.json, TierUpgradeScreen works, perks applied via FTB Ranks promotion command. Founder cosmetic prefix. Chat colour. Join broadcast at all tiers. |
| M2 | Plot system: PlotRegistry + PlotOwnership, FTB Chunks mixins, PlotPurchaseScreen. KubeJS integration for spur debit. |
| M3 | Quest UI: QuestLogScreen, BountyBoardScreen, QuestProgressHud. Network packets between KubeJS bounty engine and the mod. |
| M4 | Shop browser: ShopBrowserScreen. |
| M5 | Polish — animations, sound effects, particle effects for high tiers, theming pass on all screens. |
Each phase is independently shippable as a 1.x.0 release of the
mod jar.
See also
economy-system.md— gameplay-side design (currency, quest mechanics, villager rebalance, etc.)MEMORY.mdentries — KubeJS Rhino gotchas, defaultconfigs pattern