Skip to content

Feature Prefab Library

Claude edited this page Oct 2, 2026 · 6 revisions

Prefab library (block-variant + loot)

Since: post-v0.76.0 — PR #108 Updated: v0.80.0 — icon-grid tooltip preview (PR #114)

Two named prefab libraries authors curate by hand and browse via two new left-side tabs in the creative inventory. Prefabs are reusable, named snippets you save once and stamp anywhere:

  • Block-variant prefabs — saved per-cell List<VariantState> snippets (the same data the V-key block-variant menu's Copy button captures into a clipboard). Right-click on a target plot cell pastes the snippet, same path as VariantClipboardItem.
  • Loot prefabs — saved per-container ContainerContentsPool (the data the C-key container-contents menu mutates per chest / barrel / dispenser). Selecting in the tab puts the actual container block in your hotbar with the saved icon; right-click anywhere places it normally with rolled loot baked into the new BlockEntity.

The libraries fill the "manage and save" gap left by the v0.56.0 block-variant menu and v0.74.0 container-contents menu — both menus could already authoritatively edit a single cell, but had no way to name a configuration and reuse it across plots / sessions / worlds. Now you save once via a button in those menus and grab it from the creative tab whenever you want to stamp it again.

When to use which

Goal Tool
Save the variant snippet at the cell I'm looking at Tap Z → block-variant menu → Save → type a name
Save the loot pool at the container I'm looking at Tap C → container-contents menu → Save → type a name
Browse my saved variant snippets Open creative inventory → click the upper left-side tab (command-block icon)
Browse my saved loot prefabs Open creative inventory → click the lower left-side tab (chest icon)
Stamp a variant snippet at a target cell Click the prefab in the tab → right-click on a cell inside an editor plot
Place a chest with rolled loot inside Click the prefab in the tab → right-click anywhere

The two libraries are independent — saving a block-variant prefab never affects loot, and vice versa.

Left-side creative tabs

Forge does not natively support left-row creative tabs (vanilla only does TOP / BOTTOM rows). The implementation registers two real CreativeModeTab objects (prefab_variants, prefab_loot) so vanilla owns all rendering — items grid, scrollbar, title, tooltips, click-to-pickup. A small mixin (CreativeModeInventoryScreenPrefabMixin) adds two vertical tab-button widgets anchored to the inventory's leftPos - 28 that, when clicked, call vanilla's private selectTab via an @Invoker accessor. The tabs themselves still appear in their default page-2 position, but the side buttons act as left-edge shortcuts and let the user tell at a glance which tab is currently selected (highlighted when active).

The first cut of this feature painted a custom panel over vanilla's items grid via overpaint — that fought every render-order edge case in vanilla (Z=0 fill behind Z=100 items, persistent vanilla title bleed-through, stale tooltip overlap). Switching to real CreativeModeTab registrations solved every issue at once because vanilla owns the render path end-to-end.

v0.1104.0 (PR #1759) adds a third left-side button, under the chest: the written-book icon opens the Dungeon Train — Narrative tab. The left-side buttons are now Stage Blocks, Containers / Loot and Narrative, top to bottom; Block Variants stays reachable through the paged tab row.

Tab entries are vanilla BlockItem stacks

Every entry in the tab is a vanilla BlockItem ItemStack (oak_planks, chest, barrel, dispenser, anything) with a discriminator NBT tag — dt_bv_prefab_id for block-variant prefabs, dt_loot_prefab_id for loot. The icon is whatever vanilla normally renders for that block. Tooltip lines are decorated with the prefab id (aqua for variants, gold for loot) via ItemTooltipEvent, so two stacks of the same source block (e.g. two oak_planks variant prefabs saved under different names) are distinguishable.

This avoids the dynamic-icon problem that custom mod-Items face — you'd need a per-stack BakedModel or a custom IClientItemExtensions.getCustomRenderer to render different blocks based on NBT, which doesn't compose with vanilla's displayItems lambda. Using vanilla BlockItem stacks directly hands the rendering off entirely.

Icon-grid tooltip preview

Hovering a prefab in the creative tab shows a 9-wide grid of texture icons below the existing text rows — one icon per VariantState (block-variant prefabs) or one stack per pool entry with vanilla count badges (loot prefabs). This makes the contents of a prefab visible at a glance, so two same-icon stacks (e.g. two oak_planks variant prefabs saved under different names) are not just distinguishable by id but also by their constituent block list.

Layout:

  • 9 icons per row, 18 px cell pitch (matches vanilla slot pitch).
  • Capped at 4 rows = 36 icons. Larger prefabs render +N more text below the grid.
  • Variant icons preserve order — duplicate icons reflect the weighted-alternates design.
  • Loot icons show vanilla count badges via ItemStack#setCount(entry.count()). Air-sentinel entries are skipped.

The implementation uses Forge 1.20.1's standard TooltipComponent + ClientTooltipComponent pair (the same pattern vanilla uses for bundle-item previews) so it composes cleanly with vanilla tooltip layout. Two event subscribers wire it in:

  • RenderTooltipEvent.GatherComponents (forge bus, client side) — reads the dt_bv_prefab_id / dt_loot_prefab_id NBT, looks up the cached contents in PrefabTabState, and appends a PrefabIconsTooltipData so vanilla layout reserves space for the grid.
  • RegisterClientTooltipComponentFactoriesEvent (mod bus, client side) — maps PrefabIconsTooltipData → PrefabIconsClientTooltipComponent.

Tooltip rendering is per-frame and cannot block on a server round-trip, so the constituent block / item lists are shipped to the client up front in the login sync packet (see Network).

A PrefabUseHandler Forge event subscriber interprets the discriminator NBT on right-click:

  • Block-variant prefab (dt_bv_prefab_id): cancels vanilla BlockItem.useOn (no oak_planks gets placed); applies the variant snippet to the targeted plot cell via BlockVariantPlot.put. OP-only; player must be in an editor plot.
  • Loot prefab (dt_loot_prefab_id): lets vanilla place the block normally (so the chest gets the right facing, lid orientation, etc.); a separate BlockEvent.EntityPlaceEvent listener then rolls the prefab's pool via ContainerContentsRoller.roll and writes the resulting Items NBT into the freshly-placed BlockEntity. Works anywhere — no editor-plot constraint.

Save flow (the "save" half of "manage and save")

A new Save toolbar cell sits in both world-space menus:

Menu Toolbar before → after
V-key block-variant Copy / Add / Lock / Remove / Clear / X (6 cells) → Copy / Save / Add / Lock / Remove / Clear / X (7 cells)
C-key container-contents Add / min / max / Clear / X (5 cells) → Add / Save / min / max / Clear / X (6 cells)

Clicking Save:

  1. Opens a modal PrefabNameScreen — a small vanilla Screen with one EditBox and Save / Cancel buttons.
  2. EditBox filter clamps input to [a-z0-9_] characters; submit is disabled until the value matches ^[a-z0-9_]{1,32}$.
  3. Pressing Save sends SaveBlockVariantPrefabPacket (or SaveLootPrefabPacket) to the server with (localPos, name).
  4. Server resolves the player's plot via BlockVariantPlot.resolveAt, reads the cell's current data, writes a JSON file, registers the id, and broadcasts an updated PrefabRegistrySyncPacket to all connected clients.
  5. Client tab content auto-refreshes via CreativeModeTabs.tryRebuildTabContents — the new entry appears in the creative tab without a reconnect.

Action-bar feedback on save:

  • New file → Saved prefab '<name>' (green)
  • Existing file overwritten → Overwrote prefab '<name>' (green)
  • Empty cell → No data at this cell (yellow)
  • Invalid name → Invalid name '<name>' (a-z, 0-9, _, 1-32 chars) (red)
  • Not in a plot → Not in an editor plot (yellow)
  • Not OP → Save requires OP (red)

Containers stay linked to the loot prefab they were saved as

Since: v0.121.0 — PR #163

Saving a container's pool as a loot prefab also links the source container to the new prefab. Subsequent C-menu Save clicks on that container skip PrefabNameScreen and write straight to the same template — no risk of accidentally forking by mistyping. Containers placed via the loot prefab's creative-tab item (inside an editor plot) are auto-linked too, and edits made through any linked container's menu propagate to every other linked container in real time.

See Container Contents Menu → Linking a container to a loot prefab for the linked-state UI, propagation behaviour, and storage schema.

Loot prefabs capture the source container block

When you save a loot prefab via the C-key menu, the server also reads the world block at the cell's position and records its block id alongside the pool. So a loot prefab saved on a minecraft:barrel cell appears in the tab as a barrel; saved on a chest, appears as a chest; on a dispenser, as a dispenser. Right-clicking the prefab places that exact block.

The schema bumps to v2 for this — files written by older code without a block field still load and fall back to minecraft:chest.

Deleting a prefab (Cmd-click, since v0.967.0)

PR #1558. Cmd-click (Ctrl on Windows/Linux) a prefab in the Block Variants, Containers / Loot or Armor Stands / Item Frames tab to open a vanilla confirm screen ("Delete loot prefab 'x'? This can't be undone."). If you choose Yes, the prefab is deleted and the registry is re-broadcast, so the slot disappears for every player. Prefabs you can delete show a grey "Cmd-click to delete" tooltip line.

  • Who can delete: you can delete prefabs saved on this install, meaning the file is in the active package's user tier (UserContentPaths.provenanceOf == USER). In editor dev mode in a checkout, the dev can also delete bundled prefabs; that removes the src/main/resources JSON and its build/resources/main twin through SourceTreeFiles.deleteWithClasspathTwin. Bundled prefabs outside dev mode, and prefabs from imported packages, never prompt. Every delete needs OP 2, the same as saving.
  • Server-authoritative: each PrefabRegistrySyncPacket entry carries a deletable flag, and the client prompt only shows when that flag is set. DeletePrefabPacket (C→S) then re-checks OP and PrefabDeletes.canDelete on the server. Protocol 90 → 91.
  • Linked chests: containers linked to a deleted loot prefab fall back to their own pool, because placers skip a missing prefab.
  • Code: editor/PrefabDeletes, net/DeletePrefabPacket, mixin/client/CreativePrefabDeleteMixin (on slotClicked HEAD).

Storage

Library Files Schema
Block-variant prefabs config/dungeontrain/user/prefabs/block_variants/<id>.json v1 — {schemaVersion, states: [...]}, where states is the same per-element shape (state, optional nbt, optional weight, optional rotation) as CarriageVariantBlocks cell sidecars
Loot prefabs config/dungeontrain/user/prefabs/loot/<id>.json v2 — {schemaVersion, block: "minecraft:chest", fillMin, fillMax, entries: [{id, count, weight}, ...]}

JSON is human-readable so you can hand-edit, copy between worlds, or commit to source control. Files dropped into the directory by hand are picked up on the next server start (or a reconnect after a save).

Block-variant prefab schema v1:

{
  "schemaVersion": 1,
  "states": [
    "minecraft:oak_planks",
    { "state": "minecraft:cobblestone", "weight": 3 }
  ]
}

Loot prefab schema v2:

{
  "schemaVersion": 2,
  "block": "minecraft:barrel",
  "fillMin": 1,
  "fillMax": 5,
  "entries": [
    { "id": "minecraft:wheat", "count": 5, "weight": 10 },
    { "id": "minecraft:bread", "count": 2, "weight": 3 },
    { "id": "minecraft:air", "count": 1, "weight": 5 }
  ]
}

fillMin / fillMax are omitted when at default (0 / -1 = "all"). The block field is omitted on schema v1 reads and falls back to minecraft:chest.

Network

Protocol bumped 9 → 11 in PR #108, then 11 → 12 in PR #114 to extend the sync payload with full constituent lists for the icon-grid tooltip.

Packet Direction Payload
PrefabRegistrySyncPacket S2C Variants: [(id, iconBlockId, committed, blockIds[])]. Loot: [(id, iconBlockId, committed, items[(itemId, count)])]. Sent on PlayerLoggedInEvent and re-broadcast after every save.
SaveBlockVariantPrefabPacket C2S (localPos, name) — server reads the cell's current states, writes the file, broadcasts sync.
SaveLootPrefabPacket C2S (localPos, name) — server reads the cell's pool + the world block at that pos, writes the file with both, broadcasts sync.

iconBlockId drives the creative tab grid icon. The full blockIds[] / items[] lists drive the icon-grid tooltip preview — shipped up front because tooltip rendering is per-frame and cannot block on a server round-trip. committed drives the uncommitted-slot tint mixin.

Authoring flow

/dt editor enter standard               # enter a carriage variant plot
[stand on a cell with a few variants]   # e.g. floor cell with 50/50 oak/cobble
Z                                       # block-variant menu opens
Click "Save"                            # PrefabNameScreen pops open
Type "wood_floor"                       # name
Enter                                   # action bar: "Saved prefab 'wood_floor'"

E                                       # open creative inventory
Click upper-left side tab               # command-block icon
[see the wood_floor entry rendered as oak_planks]
Click it                                # oak_planks (with prefab NBT) lands
                                        # in your selected hotbar slot
Esc                                     # close inventory
[walk to a different floor cell]
Right-click                             # action bar: "Pasted prefab 'wood_floor'"
                                        # → cell now has the same variants

For loot:

/dt editor contents enter barrel        # enter a contents plot
[walk to a barrel in the plot]
C                                       # container menu opens
Click "Add" (with diamonds in hand)     # diamond | x64 | w1
Click "Save"                            # PrefabNameScreen
Type "loot_diamonds"                    # name
Enter                                   # "Saved loot 'loot_diamonds'"

E                                       # open creative inventory
Click lower-left side tab               # chest icon
[see the loot_diamonds entry rendered as a barrel]
Click it                                # barrel + prefab NBT in hotbar
Esc                                     # close
[walk anywhere]
Right-click                             # places a barrel containing rolled diamonds

Files

Known limitations

  • No rename UI in the tab. Rename by editing the .json files in config/dungeontrain/user/prefabs/ and reconnecting. Deleting is in-game: see Deleting a prefab.
  • No mid-session hot reload. A reload from .json requires a server restart; clients see new entries on next reconnect (sync packet only fires on PlayerLoggedInEvent and after a Save).
  • No filtering or search inside the tabs. Vanilla's creative search bar still searches across all tabs by item name, but won't filter by prefab id.
  • Loot prefab doesn't preserve item NBT. The pool stores (itemId, count, weight) only — the same shape as the C-key menu writes. Enchantments / damage / custom NBT are not captured.
  • Stale prefab in inventory. If a player has a prefab item in their hotbar and the prefab is deleted server-side, right-clicking emits Unknown prefab '<id>' in the action bar. The stack stays in the slot but does nothing.
  • Tab visuals on Forge tab page 2. The two CreativeModeTabs also appear in their default position on page 2 of vanilla tabs (Forge places modded tabs there). The left-side side buttons are shortcuts, not relocations.

Clone this wiki locally