Feature Carriage Contents - bh679/dungeon-train-mc GitHub Wiki
Since: v0.36.0 โ PR #47
v0.44.0 โ PR #57 โ contents-editor variants: shift-right-click inside a contents plot now appends to a per-contents <id>.variants.json sidecar, identical authoring flow to the shell + parts editors. Runtime overlays the picks on top of the base contents stamp using the same (seed, carriageIndex, localPos) deterministic mixer. v2 schema means BE NBT (sign text, banner patterns, lectern books) and directional placement (stairs, doors, pistons) round-trip. Also bundles 13 author-content templates (2ndlevel, barrel, books, chest, craft, enchant, ender, kitchen, maze, sand, smelt, upgrade, vase) with the contents/customs.json manifest that registers them at server start (closing the followup item that prevented the existing books.nbt from loading).
v0.50.0 โ PR #64 โ variant-place hotkey moved from sneak to a rebindable Forge KeyMapping (default Z). The contents-plot variant authoring flow described below now uses Hold Z + right-click instead of sneak + right-click; sneak no longer triggers variant placement so authors can move-while-clicking inside the editor.
v0.51.0 โ PR #65 โ weighted contents pick replaces the old uniform-modulo selection. New CarriageContentsWeights storage at config/dungeontrain/contents/weights.json (mirror of carriage weights); weight 0 excludes a contents from the spawn pool. The editor command menu now shows a [-] Weight (N) [+] Triple row over contents plots, and a new CLI verb /dungeontrain editor contents weight <id> <inc|dec|0..100> parallels the existing carriage editor weight subcommand. Behaviour change: existing worlds will see different concrete contents at the same (seed, carriageIndex) than before this version even with all weights at the default 1, because the seeded RNG draw shape changed.
v0.63.0 โ PR #79 โ refreshed author-content NBTs: contents/2ndlevel, contents/books, contents/enchant, contents/vase. Same ids and registry behaviour โ only the bundled geometry changed, so existing worlds keep the same (seed, carriageIndex) โ id mapping and just render the updated layouts.
v0.66.3 โ PR #88 โ recovers vase, books, enchant, 2ndlevel, and barrel variant sidecars that were authored alongside the v0.44.0/v0.63.0 NBT updates but never made it into main. Root cause: CarriageContentsEditor.save() (and saveAs()) promoted the NBT template to source in dev mode but not the .variants.json sidecar โ so vase floor cells stamped as bare command_block placeholders (the empty-air sentinel) instead of randomised decorated_pots. PR #88 wires sidecar.saveToSource(contents) into both editor save paths and into all 4 BlockVariantPlot.save() impls so shift-right-click edits in any plot type now write through to source. Same defect-class as PR #75 for part-assignments and PR #73 for weights โ both already auto-promoted.
v0.92.0 โ PR #129 โ per-carriage Contents allow-list. New Contents drilldown in the X-menu (carriage templates only) lists every registered content with a per-row red/green Toggle โ OFF excludes that content from this carriage's spawn pool. Stored as a <variant-id>.contents-allow.json sidecar next to the carriage NBT (mirrors the <id>.parts.json pattern). Spawn-time pick filters the pool through the allow-list; safety-net falls back to DEFAULT if the author excludes everything. Contents added to the registry later are automatically allowed for every existing carriage (sidecar stores only excluded ids โ empty/absent file = all allowed). Protocol bumped 13 โ 14.
v0.95.0 โ PR #132 โ three new bundled author-contents โ cows, pigs, sheep โ and the first bundled allow-list sidecars to go with them: the new pen carriage only spawns farm interiors (excludes the 12 loot-style contents); standard and windowed exclude cows/pigs/sheep so the farm interiors don't bleed into the workshop carriages. The shipped <id>.contents-allow.json files are the bundled-tier override of the allow-list loader โ players authoring their own carriages get them via the in-game X-menu Contents drilldown, players using the built-ins get the curated mix without authoring.
v0.97.0 โ PR #136 โ three more bundled author-contents โ library, seating, shop โ and dev-mode write-through for the allow-list sidecar. The X-menu Contents toggle previously only wrote to config/dungeontrain/templates/<id>.contents-allow.json; the source-tree mirror that already wrapped <id>.parts.json saves was missing for allow-list saves, so authored bundled-tier exclusions never made it into the repo without a manual copy. PR #136 adds the same trySaveToSource pattern to CarriageVariantContentsAllowStore so toggles in ./gradlew runClient now ship in the next build alongside parts and weights edits. Also tunes the standard carriage's bundled allow-list โ excludes chest, ender, library (theme-fit with the workshop look). Wall-part side: a new window2 wall joins the windowed carriage's pool (weight 1, sideMode either, alongside slotwindow and smallwindows).
v0.132.0 โ PR #172 โ contents groups & sub-variants. A contents id can now be a group parent with weighted sub-variant members. At spawn time the parent's pick resolves to one of [parent self, ...members] by weight, so the parent's own .nbt is always the "default" sub-variant in rotation. Editor adds a new Sub-Variants companion menu next to the per-plot label (mirrors the existing type-row companion) with a + New button that prompts for a name and atomically creates the child, joins it to the group, and teleports the player to its plot. Plots laid out as 2D โ parents in the +X row at z = PLOT_Z, sub-variants in a +Z column from their parent (2 air blocks between cages). Allow-list operates at parent level only; sub-variants follow their parent's allowance. Bundles cows as the first group, with chickens as its first sub-variant (pigs/sheep were removed in the same change as standalone contents).
v0.134.0 โ PR #175 โ Sub-Variants weight editor: clicking a รN weight cell in the Sub-Variants companion menu now bumps the per-member weight in the parent's .group.json sidecar (+1; shift-click โ1; clamped 0โ100). Previously the click fired the top-level editor contents weight command which the spawn pipeline ignored for group members. New subcommand editor contents group set-weight <parent> <child> <inc|dec|value> routes the click through CarriageContentsGroup.withMember / withSelfWeight. The parent's (default) row is now editable too โ CarriageContentsGroup schema extended with an optional selfWeight field (default 1; backwards compatible โ pre-v1.1 sidecars without the field load with the prior hardcoded weight). Four new animal contents shipped with this PR โ armadillo, pigs, rabbits, sheep โ wired into the cows group (replacing the legacy standalone pigs/sheep entries from v0.95.0); craft group rebalanced with selfWeight: 5 and kitchen/smelt weights bumped to 5.
v0.137.0 โ PR #178 โ bundled-content reshape: three new groupings move existing leaves under parents so the editor's CONTENTS row surfaces them as inline sub-variant chips instead of cluttering the top-level list.
v0.1060.0 โ PR #1702 โ contents sizes: contents now come in Room, Half and Full sizes, each its own type in the editor, and a carriage only takes contents of its own size. Adds the long Full carriage and default_full Full contents; Full carriages on the train are off by default.
v0.141.0 โ PR #182 โ seven new animal sub-variants under the cows group: horse, donkey, mule, bee, fox, goat, frog. Each ships an .nbt template plus a .variants.json sidecar โ bee, donkey, fox, horse, mule carry substantive variant data (mob displays + barrels); frog and goat ship as minimal stubs ready for future variant authoring. horse/donkey/mule barrel cells exercise both loot systems in parallel: the primary draw is a fixed inline-NBT barrel (wheat + saddle), with a weighted alternative that rolls the new prefabs/loot/horse.json weighted table (lead/hay/wheat/apple/golden_apple/saddle/chest, weight 1 each) via "lootPrefab": "horse". The third weighted option per cell is the empty command_block placeholder so the cell can also vanish entirely. Group-weight retune in weights.json: cows 8โ15, ender 2โ1, added default: 1. Within the cows group: selfWeight 1โ2, chickens/pigs/sheep 1โ2, rabbits 5โ2, all new mob members at weight 1. Pure data PR โ zero Java changes, no save-format break; existing worlds load normally and just see a wider rotation of cow-group interiors.
v0.151.0 โ PR #193 โ promotes the user-authored barrles dtpack to shipped resources. Three new bundled loot prefabs โ prefabs/loot/flowers.json (12 vanilla flowers incl. torchflower / lily_of_the_valley / cornflower), prefabs/loot/mushrooms.json (brown / red / crimson fungus / warped fungus), prefabs/loot/saplings.json (every vanilla sapling incl. mangrove_propagule / cherry / azalea). barrels.variants.json rewires 35 barrel-slot rolls to add the new flower / mushroom / sapling lootPrefabs (command_block-placeholder weights re-tuned 40 โ 60 in several slots); chest.contents gains three new content links (3,0,2 โ meat, 4,0,2 โ mushrooms, 5,0,2 โ flowers); contents_barrels.contents.json bumps schemaVersion 2 โ 3 for the new saplings pool at 1,0,2. prefabs/loot/meat.json count tuning: 5/3 โ 2/1 (items, weights, schema unchanged). Loot prefab registry count: 22 โ 25 at boot. Pure data PR โ zero Java changes; existing worlds keep already-rolled barrel/chest contents, only newly-generated dungeon rooms roll the new pools.
v0.139.0 โ PR #180 โ fix: animals, villagers, and armor stands no longer disappear from newly spawned carriages. The per-tick killEntitiesAhead sweep was discarding our own contents entities because shipyard-absolute coords failed the train_aabb.contains() spare-interior filter โ armor stands died at ageTicks=0 (same tick as spawn), Mobs survived briefly then died later as the train moved. Every contents-spawned entity is now tagged dungeontrain_contents_pidx_<N> and the kill-ahead filter exempts them. Defensive companions: entity spawn deferred until the placement-collision tracker confirms placedSuccessfully (60 clean ticks), and Mob instances get PersistenceRequired=true. Diagnostics added: opt-in /dungeontrain debug contents-entities on toggles per-entity JOIN/LEAVE logs with stack-trace at LEAVE. See Compatibility ยง Carriage-contents entities live at shipyard-absolute coords for the full bisect. library is now a sub-variant of shop; seating is now a sub-variant of books (moved out of library, which can't be both child and parent under the flat one-level group model); sand is now a sub-variant of dirt (weight 0 โ manual-spawn only). First hostile-mob interiors ship as a new enemies group bundling brute, cagedzombie, and piglin. Top-level weights.json retuned (armor=2, books=4, cows=8, craft=8, dirt=2, ender=2, maze=2, sand=0, shop=2, vase=2). Also promotes pending user-tier edits to dirt.nbt/.variants.json, pen.nbt/.variants.json (schema v5 โ v6), black.contents-allow.json, plus a new prefabs/loot/loot.json loot prefab. Pure data PR โ zero Java changes, no save-format break; existing worlds load normally and just route spawn-time picks through the new group parents. Authoring follow-up: the user/ โ src/ auto-promotion problem (writes landing in config/dungeontrain/user/... instead of src/main/resources/... when EditorDevMode is off) remains gated by the session-only enabled flag โ fix deferred to a separate PR.
v0.150.0 โ PR #192 โ three new sub-variants under the books group: dining, seating_thin, center_dining (each with .nbt + .variants.json shipped in data/dungeontrain/contents/). books.variants.json upgrades position 6,0,4 to a weighted three-prefab chest (starter + loot_irongold + loot) โ bookshelf weight 240, chiseled_bookshelf weight 30, three chest variants stacked underneath. seating.variants.json adds a loot_irongold chest at positions 6,0,4 and 6,0,0 alongside the existing starter chest; command_block placeholder weight raised to 50 to dominate when no chest rolls. Seven loot prefabs rebalanced (copper, fish, fruit_veg, horse, loot_irongold, meat, starter) โ notable: horse wheat weight 1โ70 with count 1โ3, loot_irongold iron_nugget 20โ60 and gold_nugget 10โ50. All loot entries also persist the v3 randDur / durChance / randEnch / enchChance fields the parser was already defaulting (no behaviour change from the field-persistence alone). Bundled alongside the collide โ move-together โ collide placement lock โ a separate Java fix in the same PR.
v0.827.0 โ PR #1344 โ new traps group (mild_1, dangerous_1 displayed as "tntbarrel", mild_3, mild_4), gated in weights.json to the VOID, UPSIDE_DOWN and CHUNCKS phases. Same PR: the Contents editor overlay no longer rebuilds the whole registry per plot lookup โ standing at a group parent cost 85โ180 ms/tick, now ~3 ms (see Development ยง Editor tick-cost profiling).
v0.885.0 โ PR #1415 โ fix: player heads now draw their death pool from the carriage group anchor (DeathHeadSkins.poolIndexFor), so every carriage โ not just every third โ shows a real fallen player.
v0.1048.0 โ PR #1688 โ new contents templates are opt-in. A newly created top-level contents template (editor contents new, Save As copy, Train Builder save, relay install) now starts OFF in every carriage's and portal room's Contents list, and only spawns where someone switches it on. Existing templates and sub-variants are unchanged. The Contents drilldown also now shows each carriage's real state; before this it showed every row ON regardless. Protocol bumped 102 โ 103.
Every carriage shell spawned before v0.36.0 was hollow โ the interior (length-2) ร (height-2) ร (width-2) volume was pure air. Contents variants add a second catalogue inside that volume: a global list of interior layouts authored through a shell-style editor plot, saved as structure NBT, and picked deterministically per spawned carriage from the same (world seed, carriage index) pair that drives shell selection. The default contents is a single stone_pressure_plate at the floor centre โ every existing world immediately gets that minimal default without any authoring work.
Contents are independent of shells โ any shell variant can pair with any contents variant. Picks happen independently, so a rolling train of 10 carriages might show standard/bedroom, windowed/maze, open/workspace, โฆ each combination rolled from world seed + carriage index.
/dungeontrain editor contents enter <name> [shell_variant] # teleport to plot (default shell: standard)
/dungeontrain editor contents save [new_name] # capture interior as NBT; with new_name, also rename
/dungeontrain editor contents list # show contents kind + config/bundled status
/dungeontrain editor contents reset <name> # delete the config copy; for customs also unregister
/dungeontrain editor contents new <name> [source] # new custom contents seeded from source (default: default)
/dungeontrain editor exit # return to pre-enter position (shared session map)
The /dt alias works for all of the above (/dt editor contents enter <name>, etc.) per the v0.36.0 editor categories refactor.
- Requires permission level 2 (same as other editor subcommands).
-
nametab-completes against every registered contents id โdefaultplus any customs currently on disk. -
shell_variant(optional 2nd arg toenter) tab-completes against every registered carriage variant. Defaults tostandard. -
namefornew/save <new_name>must match^[a-z0-9_]{1,32}$and cannot collide with the reserveddefaultname.
Plots sit at y=250 along +X, z=80 so they're offset from the shell editor row (z=0, tunnels at xโฅ80) and the pillar row (z=40). The default built-in comes first; customs follow in alphabetical order:
| Contents | Plot origin |
|---|---|
default |
(0, 250, 80) |
| first custom (alphabetically) | (20, 250, 80) |
| second custom | (40, 250, 80) |
| โฆ | โฆ |
On enter, the mod:
- Clears the full carriage footprint at the plot origin.
- Stamps the chosen shell (parameter or default
standard) as non-editable context โ floor, walls, ceiling so the author can see how the contents fit inside. - Stamps the current contents template on top of the interior air.
- Wraps the whole plot in a bedrock cage (always visible; unbreakable in survival). (v0.40.0 โ previously invisible barrier blocks.)
- Teleports the player inside.
Shell blocks are not protected from edits โ if the author breaks a wall mid-session it doesn't affect the saved template. Save captures only the interior volume (dx 1..length-2, dy 1..height-2, dz 1..width-2), so shell blocks are naturally excluded.
Re-entering re-stamps the shell from scratch, so any accidental wall damage is repaired. The contents editor also discards any non-player entities in the interior volume before re-stamping so armor stands / item frames don't accumulate across edit sessions.
Contents selection at spawn time uses the same mixing scheme as variant blocks, but at the contents-variant granularity rather than per-block. Since v0.51.0 the pick is weighted rather than uniform:
// CarriageContentsRegistry.pick (v0.51.0+)
int total = 0;
int[] cumulative = new int[allContents().size()];
for (int i = 0; i < allContents().size(); i++) {
total += CarriageContentsWeights.current().weightFor(allContents().get(i).id());
cumulative[i] = total;
}
long seed = worldSeed ^ ((long) carriageIndex * 0x9E3779B97F4A7C15L);
int r = new Random(seed).nextInt(total);
// linear scan: return allContents().get(i) where r < cumulative[i]Same world + same carriage index + same weights โ same contents on reload. Different indices along the train vary independently. Walking back over a previously-visited stretch shows the same mix because the pick is pure-function of (seed, index, weights) โ nothing is stored.
The carriage index is the train-relative number from TrainAssembler / TrainWindowManager, so the contents mix shifts consistently as the rolling window extends forward or backward.
Before v0.51.0 the pick was a uniform Math.floorMod(rng.nextInt(), n) over allContents(). v0.51.0 reshapes it to a cumulative-weight pick (mirroring CarriageTemplate.weightedSeededPick), with one once-per-server warning and a uniform fallback if every contents has weight 0.
Behaviour change: even with every weight at the default 1, the seeded RNG draw shape differs (nextInt(n) vs nextInt(total) followed by a cumulative scan), so existing worlds upgrading past v0.51.0 will see different concrete contents at the same (seed, carriageIndex) than they did on v0.50.x. The mix is just as deterministic going forward โ same (seed, index, weights) always yields the same id โ but the absolute mapping shifts.
/dungeontrain editor contents weight <id> <n> # 0..100; 0 excludes the contents, 1 is the default
/dungeontrain editor contents weight <id> inc # +1 (clamped to 100)
/dungeontrain editor contents weight <id> dec # -1 (clamped to 0)
Tab-completes against every registered contents id. Writes the full weights map to config/dungeontrain/contents/weights.json (sorted, pretty-printed) and reloads the in-memory map immediately. Existing carriages in the rolling window keep their contents until they scroll out.
The editor command menu renders the same [-] Weight (N) [+] Triple row over contents plots that it does over carriage plots โ side cells run inc / dec and stay open, middle cell drops into typing mode for an exact value.
Same schema as carriage weights โ non-negative integers clamped to [0, 100], missing entries default to 1, malformed entries fall back to 1 with a warn log. After a hand-edit, restart the server so ServerStartingEvent reloads the overlay.
If every contents has weight 0, CarriageContentsRegistry.pick falls back to a uniform pick over the full registry and emits one warn log per server session. Prevents an empty interior from a misconfigured weights file.
Author-side flag for "this shell should never spawn lava_pool interiors, only loot rooms." Each carriage variant carries a per-template list of excluded content ids; at spawn time CarriageContentsRegistry.pick filters those out before the weighted draw, so (world seed, carriage index, variant) resolves to a content that the variant actually permits.
Stand inside any carriage editor plot (e.g. /dt editor carriages standard), press X, click Contents. The drilldown lists every registered content (built-ins + customs) as a red/green Toggle row:
default [ON] โ green, currently allowed
loot_room [ON]
lava_pool [OFF] โ red, excluded from this carriage
< Back
Click a row to flip its state. The menu stays open so you can flip several in a row. Server saves the result to config/dungeontrain/templates/<variant-id>.contents-allow.json immediately and pushes a fresh EditorStatusPacket so the toggle's render flips on the next rebuild โ same round-trip the existing "Part Variant Menu" toggle uses. Since v0.97.0 (PR #136) the save also write-throughs to src/main/resources/data/dungeontrain/templates/<id>.contents-allow.json when running from a ./gradlew runClient checkout, mirroring the parts-store dev-mode promote โ so authored allow-list edits ship in the next build instead of staying in the player's config/.
The Contents row only appears under the carriages branch of the X-menu Editor screen โ it's hidden in parts plots, contents plots, and tracks plots.
Lives next to the carriage NBT and <id>.parts.json:
// config/dungeontrain/templates/standard.contents-allow.json
{
"schemaVersion": 1,
"excluded": ["lava_pool"]
}The sidecar stores the author's explicit decisions: excluded (switched off here) and, since v0.1048.0, included (switched on here, schemaVersion 2). Anything with no decision falls to the template's own default. See Opt-in new templates. Before v0.1048.0 the file stored only excluded, and every content added later was automatically allowed everywhere. Those schema-1 files still read unchanged. The .contents-allow.json suffix keeps CarriageVariantRegistry's *.nbt-only scan from picking these up as custom carriages.
// CarriageContentsRegistry.pick (v0.92.0+)
CarriageContentsAllowList allow = (variant == null)
? CarriageContentsAllowList.EMPTY
: CarriageVariantContentsAllowStore.get(variant).orElse(CarriageContentsAllowList.EMPTY);
List<CarriageContents> pool = allContents().stream()
.filter(c -> allow.isAllowed(c.id()))
.toList();
if (pool.isEmpty()) return CarriageContents.of(ContentsType.DEFAULT); // safety net
// ... same weighted cumulative scan as v0.51.0+, but over `pool` instead of `allContents()`Three call paths into pick:
| Overload | Used by |
|---|---|
pick(seed, idx) |
Legacy callers / tests โ delegates with EMPTY allow-list |
pick(seed, idx, CarriageVariant) |
CarriageTemplate.applyContents at spawn time โ loads the sidecar |
pick(seed, idx, CarriageContentsAllowList) |
Pure overload for unit tests โ no filesystem access |
Determinism is preserved under filtering: same (seed, idx, allow) always picks the same content. All-excluded fallback: if the author excludes every registered content (rare but possible), the pick returns the DEFAULT builtin so spawning still produces a coherent interior instead of an empty volume.
The variant is threaded through applyContents from CarriageTemplate.placeAt at line 248 โ already in scope, no new plumbing.
The Toggle rows can't read the sidecar client-side, and the ON/OFF state now depends on more than the sidecar (the template's opt-in mark lives in contents/weights.json). Since v0.1048.0 the screen asks the server: ContentsAllowRequestPacket (op-only) is answered with a ContentsAllowSyncPacket carrying every top-level contents id and the ones that are effectively off. The client caches it in ClientContentsAllowState, and the screen shows "Loading..." until the answer arrives. Each carriage-contents / portal-room-contents toggle pushes a fresh answer. (The original v0.92.0 route through EditorStatusPacket.excludedContents had stopped being sent, so the screen had been showing every row ON.)
A contents template carrying "optIn": true in contents/weights.json is off in every carriage and portal room unless that Contents list switches it on. Every new top-level template gets the mark when it is created: editor contents new (blank or from a source), Save As copy, Train Builder save and relay install. Sub-variants are never marked, because the allow-list only ever consults top-level ids.
| Decision in the sidecar | Opt-out template (all pre-v0.1048.0 ones) | Opt-in template (new ones) |
|---|---|---|
in excluded
|
off | off |
in included
|
on | on |
| neither | on | off |
A template that existed before v0.1048.0 has no mark, so every carriage keeps exactly the contents assignments it had.
-
CarriageContentsAllowList.javaโ immutable record;withAllowed,withExcluded,toggle, JSON codec -
CarriageVariantContentsAllowStore.javaโ three-tier sidecar load/save (config โ bundled โ empty), per-id cache (mirrorsCarriageVariantPartsStore) -
CarriageContentsAllowScreen.javaโ X-menu DrillIn screen; one Toggle row per content + Back row
Every contents template has a size, and a carriage only ever picks contents of its own size.
| Size | Box (default dims) | Interior | Who uses it |
|---|---|---|---|
| Room | one carriage, 9ร7ร7 | 7ร5ร5 | every ordinary carriage; portal_short and its sub-variants |
| Half | the long portal corridor, 13ร7ร7 | 11ร5ร5 | the portal carriage: portal and its sub-variants |
| Full | a whole group as one carriage, 27ร7ร7 (group size 3) | 25ร5ร5 | Full carriages such as long
|
-
Declared, not measured. A contents template's size lives in
contents/sizes.jsonand a carriage's intemplates/sizes.json({"schemaVersion":1,"sizes":{"<id>":"room|half|full"}}). Anything absent is Room. User tiers are<user>/contents/sizes.jsonand<user>/template-sizes.json. A sub-variant always takes its group root's size. - Full needs room to fit. Full = group size ร carriage length. Past the 32-block cap (e.g. 4 ร 9) Full is off in that world.
-
Picking.
- The train's contents pick, the Contents allow list (X menu), the editor plot's shell and Test the Carriage are all filtered to the carriage's size.
- Carriages whose template has no blocks are never used as a shell.
- Types. Room, Half and Full are separate types: tabs in the floating Contents menu and groups in the Templates screen.
- One size at a time. They share the plot origin, so only one size is stamped at a time. Picking another size's tab or template erases the standing plots and repopulates that size's. The resident size is saved with the world.
-
New. "New" offers only the selected size:
- a blank of it
- a copy of the template you stand in
- that size's Standard:
default(Room),portal(Half),default_full(Full)
-
Command.
editor contents new <name> blank_half|blank_fullmakes a blank of that size. -
New carriages. "New carriage" offers Blank Room and Blank Full (
editor new <name> blank_full). - Test the Carriage shows exactly the template you test. Cmd/Ctrl+R only re-seeds its variants and flip.
FullCarriageSelection can replace a group with one Full carriage furnished with Full contents.
- Off by default. It's a seeded one-in-N group lottery, and portal and whole groups win collisions.
-
Turning it on. Set it with
/dungeontrain whole full every <n>|off, ortemplates/full-settings.json({"every": 0}). -
Testing.
/dungeontrain whole full force <n>|offsets a session-only cadence. - Requirements. A Full carriage needs a built Full shell with some allowed Full contents.
Mental model: a contents id (e.g. cows) can be a group parent with weighted sub-variant members (e.g. chickens). At spawn time the parent's pick resolves to one of [parent self, ...members] by weight โ so the parent's own .nbt is always the "default" sub-variant alongside any siblings authored under it.
Authors who never set up a group keep the existing flat behaviour: every contents is a leaf with its own .nbt and is picked directly. The feature is purely additive โ adding a sub-variant to a parent doesn't break or hide the parent's own interior; it just becomes one of N options.
Two entry points โ pick whichever fits your workflow:
In-world (per-plot menu). Enter a contents plot for the parent (e.g. /dt editor contents enter cows). A new Sub-Variants companion menu floats next to the per-plot label panel, listing <parent> (default) plus any existing members. Click the bottom + New row โ keyboard prompt asks for a name โ child is created blank, joined to the group, and the player auto-teleports to its plot one row +Z from the parent.
Chat commands.
/dungeontrain editor contents group new <parent> <name> # atomic create + add + teleport
/dungeontrain editor contents group add <parent> <child> [w] # add an existing contents to the group (default weight = 1)
/dungeontrain editor contents group remove <parent> <child> # remove a member; deletes the group file if empty
/dungeontrain editor contents group list <parent> # print members + weights
/dungeontrain editor contents group clear <parent> # drop the whole group; parent reverts to a leaf
Validation at write time rejects:
- Built-in parents (the
defaultbuilt-in has hardcoded fallback that conflicts with no-.nbtsemantics) - Self-membership (
group add cows cowsโ rejected) - Nested groups (a child can't itself be a group parent)
- Cycles (a parent that's already a member of another group can't host its own group)
The contents editor row is now 2D:
| Axis | Used for | Step |
|---|---|---|
| +X | Top-level (parent / leaf) variants |
dims.length() + EditorLayout.GAP (3 air blocks) |
| +Z (from parent) | Sub-variants of that parent |
dims.width() + EditorLayout.SUB_VARIANT_GAP (2 air blocks) |
So a parent at (x, 250, 80) has its first sub-variant at (x, 250, 80 + width + 4), the second at (x, 250, 80 + 2*(width + 4)), and so on. Top-level variants in the +X row do NOT shift when a sibling parent gains a sub-variant โ children are excluded from the top-level row indexing.
EditorLayout.CONTENTS_VIEW_MAX_Z reserves space for up to MAX_SUB_VARIANTS_PER_PARENT = 8 rows per group, so the TRACKS view (at TRACKS_FIRST_Z) doesn't collide with sub-variant columns at typical group sizes. Soft cap โ the 9th+ sub-variant just visually overflows into the TRACKS region; no command-side enforcement.
At spawn time, the registry first does a top-level weighted pick (per weights.json, filtered by the variant allow-list) over parents and leaves only โ group members are excluded from the top-level pool. If the picked id has a <id>.group.json sidecar, the registry then runs a second weighted draw over [synthetic-self(weight=SELF_WEIGHT=1), ...members] and returns the chosen entry.
// CarriageContentsRegistry.resolveGroup (v0.132.0+)
List<PoolEntry> pool = new ArrayList<>(group.members().size() + 1);
pool.add(new PoolEntry(SELF_TOKEN, SELF_WEIGHT)); // parent's own .nbt
for (var m : group.members()) {
pool.add(new PoolEntry(m.id(), m.weight())); // explicit members
}
// weighted scan over pool using a child RNG derived from parentRng.nextLong()The child RNG is derived from the parent's RNG via new Random(parentRng.nextLong()) so the two streams are independent โ adding members shifts the group's internal distribution without leaking back into the top-level pick.
The per-carriage allow-list operates at parent level only. Once a parent passes the allow-list and is picked, its full sub-variant pool runs unfiltered โ including all explicit members regardless of whether they're listed individually in the allow-list.
UI surface:
- The X-menu Contents drilldown lists only parents (and leaves with no group). Sub-variants don't get a toggle row โ they follow their parent's allowance.
- The
/dt editor carriage-contents <variant> <contents> on|offtab-completion suggests parents only. If the player manually types a sub-variant id, the command rejects with'<id>' is a sub-variant of '<parent>' โ toggle the parent in the allow-list instead.
Lives next to the contents NBT:
| Tier | Path |
|---|---|
| Per-install override | `config/dungeontrain/<user |
| Bundled with the mod |
data/dungeontrain/contents/<parent-id>.group.json (in jar / source tree) |
// data/dungeontrain/contents/cows.group.json
{
"schemaVersion": 1,
"variants": [
{ "id": "chickens", "weight": 1 }
]
}Field naming reflects the user's mental model: a group's members are themselves variants of contents, so the array key is variants (not subVariants). Weights clamp to [0, 100] (same range as global weights). Empty / missing list โ the parent is effectively a leaf; the synthetic-self path still runs but with no other candidates.
The store is three-tier (config โ bundled โ empty), mirroring the allow-list. Dev-mode runClient mirrors saves to the source tree, so authoring a group via + New ships in the next build.
PR #172 also ships the first bundled group: cows is now a group parent with chickens as its first sub-variant. The legacy pigs.nbt and sheep.nbt standalone contents were removed in the same change โ they were redundant once cows could host an arbitrary list of farm-animal interiors, and the user can re-author them as sub-variants if they want. customs.json was updated to add chickens and remove pigs/sheep.
-
CarriageContentsGroup.javaโ immutable record with weightedMemberentries, JSON codec, dedupe semantics (first occurrence wins position, last wins value) -
CarriageContentsGroupStore.javaโ three-tier sidecar store + reverse parent lookup (findParentOf) + child-id index (allChildIds) + dev-mode source mirror -
CarriageContentsRegistry.javaโresolveGroupwith synthetic-self injection,.group.jsonscan in both bundled + config dirs, post-reload validation pass -
CarriageContentsEditor.javaโ 2DplotOrigin(top-level row in +X, sub-variants in +Z column from parent) -
EditorLayout.javaโSUB_VARIANT_GAP,MAX_SUB_VARIANTS_PER_PARENT, expandedCONTENTS_VIEW_MAX_Z -
VariantOverlayRenderer.javaโappendSubVariantsCompanionbuilds the per-plot Sub-Variants menu -
EditorTypeMenuRenderer.java/EditorTypeMenuRaycast.java/EditorTypeMenuInputHandler.javaโcompanionShiftXsequencing for multiple companions ++ Newrouting into the picker -
NewSourcePickerScreen.javaโCategory.CONTENTS_SUB_VARIANTmode (single-row name TypeArg dispatchingcontents group new <parent> <name>)
Since v0.613.0 (PR #1040) every generated player_head in a carriage is dressed, after the contents NBT lands, with the skin and name of a real player whose run ended in that carriage โ CarriageContentsPlacer.applyHeadSkins โ DeathHeadSkins โ relay GET /deaths/skins?carriage=N. Mining the head keeps the face and the "'s Head" title (vanilla copy_components). Heads an author placed with a face already are left alone. Falls back to PlayerMob's bundled skins where nobody has died yet, and the fetch never blocks generation.
Pool granularity is the carriage group, not the carriage (v0.885.0, PR #1415). A death is recorded at the group anchor the boarding scan reports (floorDiv(pIdx, groupSize) * groupSize โ carriages are placed groupSize at a time on one Sable body), so the relay only ever holds pools at 0, 3, 6, โฆ. Until v0.885.0 the placer asked for the exact index and two of every three carriages silently fell back to stock faces. DeathHeadSkins.poolIndexFor(carriage, groupSize) now keys the cache and the fetch on the anchor (absolute, so backward carriages find the pool their deaths were filed under); the per-cell deterministic roll still uses the real carriage index, so neighbouring carriages wear different faces from one pool. Prefetch is one group ahead โ one relay request per group.
The flatbed built-in shell skips the contents pick entirely โ flatbeds are floor-only, so any interior content would render as floating blocks visible from every side. CarriageTemplate.applyContents checks for CarriageVariant.Builtin(FLATBED) at the top and returns before calling CarriageContentsRegistry.pick. Flatbeds stay empty as train separators between enclosed carriages.
This check is built into the spawn path, not the registry, so the flatbed constraint applies regardless of which contents are registered.
CarriageContentsTemplate.captureTemplate passes includeEntities=true to StructureTemplate.fillFromWorld, and stampTemplate uses StructurePlaceSettings.setIgnoreEntities(false) on placeInWorld. So decorative entities placed during authoring (armor stands, paintings, item frames, glow item frames) are saved in the NBT and re-spawn when the contents stamps at an editor plot.
The mod also ships four data/minecraft/vs_entities/*.json handler files pairing those entity types with VS's valkyrienskies:shipyard handler, so they're registered to ride ships when VS's rendering pipeline supports them.
However: those entities do NOT render correctly on moving VS ships as of VS 2.4.11. They spawn at the correct shipyard coordinates server-side and sync to clients, but the client-side render pipeline doesn't apply the ship's transform matrix to them โ they appear stuck in world space even though VS mirrors their positions through the shipyard. Manually placing an armor stand in a moving carriage hits the same limitation, so this is a VS constraint, not a mod bug. See Compatibility ยง Non-registered entity types don't render with ship transforms for the full writeup.
Contents that consist only of blocks (pressure plates, chests, carpets, beds, furnaces, etc.) work perfectly on moving ships.
Contents NBTs are resolved via the same three-tier fallback as shell templates, but in a separate directory:
| Tier | Location | Applies to | Who writes here |
|---|---|---|---|
| 1 โ per-install override | config/dungeontrain/contents/<id>.nbt |
default + customs |
/editor contents save (always), /editor contents new (seeds), /editor contents reset (deletes) |
| 2 โ bundled mod default |
/data/dungeontrain/contents/<id>.nbt (inside the jar) |
default + manifest-listed customs |
/editor contents save while devmode on, OR CarriageContentsStore.promote(...)
|
| 3 โ hardcoded fallback | CarriageContentsTemplate.legacyPlaceDefault |
default only |
The mod itself โ writes one stone pressure plate at floor centre when both above miss |
The cache invalidates on ServerStoppedEvent so a world switch with different CarriageDims doesn't return stale templates.
CarriageTemplate.applyContents runs at the end of every 6-arg placeAt call โ both initial train assembly and every rolling-window addition. It:
- Checks the variant isn't flatbed โ early return if so.
- Calls
CarriageContentsRegistry.pick(config.seed(), carriageIndex, variant)โ a deterministicCarriageContents, filtered through the variant's allow-list (since v0.92.0). - Calls
CarriageContentsTemplate.discardEntitiesAtto clean up leftover entities from the previous carriage at this shipyard position (so decorative entities don't pile up each rolling-window cycle). - Calls
CarriageContentsTemplate.placeAtโ stamps the interior NBT (or the hardcoded fallback fordefault).
The whole applyContents call is wrapped in try/catch so a contents-load failure can't abort the spawn โ worst case the interior is empty with a log warning.
Contents blocks are picked up by collectFootprint after placement, so they're part of the Set<BlockPos> passed to ShipAssembler.assembleToShip โ they ride the VS ship like the shell blocks do.
Inside any contents plot, hold the variant-place key (default Z) and right-click a placed block with a different block in your main hand. The clicked block becomes a multi-candidate variant cell โ at spawn time the runtime rolls one of the candidates deterministically per (world seed, carriage index, local pos), identical to the shell variant blocks and the part-template authoring flow. The first hold-Z + right-click seeds the list with [base, added] so you don't lose the block you already placed.
A common authoring pattern: place a chest, hold Z + right-click a barrel onto it, hold Z + right-click a hopper onto it โ every contents plot now picks one of chest | barrel | hopper per spawned carriage.
Because v2 carries block-entity NBT, you can hold Z + right-click a held sign with text โ the text comes along โ or a banner with patterns, or a lectern with a book inside. Capture also reads the held BlockItem's placement context, so directional blocks (stairs, doors, pistons, mushroom blocks) face the way you were looking.
v0.50.0 โ the variant-place hotkey moved from sneak to a rebindable Forge
KeyMapping(default Z). Sneak-right-click no longer triggers variant placement; rebind via Esc โ Options โ Controls โ Dungeon Train.
The sidecar lives next to the contents NBT:
| Tier | Path |
|---|---|
| Per-install override | config/dungeontrain/contents/<id>.variants.json |
| Bundled with the mod |
data/dungeontrain/contents/<id>.variants.json (in jar / source tree) |
/dt editor variant list and /dt editor variant clear work inside contents plots; entries with BE NBT print a +nbt annotation. Runtime call site is CarriageContentsTemplate.placeAt(level, origin, contents, dims, seed, carriageIndex) โ the 6-arg overload added in v0.44.0; CarriageTemplate.applyContents threads through the seed and index already in scope.
The Sub-Variants companion menu floats beside the per-plot panel in every contents editor plot. Its รN weight cell on each row is now a direct +/โ control over the parent's group sidecar:
| Cell | Click | Shift-click |
|---|---|---|
Member row รN
|
+1 to that member's weight in <parent>.group.json
|
โ1 (floored at 0) |
(default) row รN
|
+1 to the parent's selfWeight in <parent>.group.json
|
โ1 |
Weights are clamped to [0, 100] via CarriageContentsGroup.clampWeight. The default row's weight cell is hidden when the group has no explicit members โ selfWeight has no behavioural effect without competing members.
Each click runs dungeontrain editor contents group set-weight <parent> <child> <inc|dec|value>. When parent == child, the executor calls CarriageContentsGroup.withSelfWeight; otherwise it calls withMember(new Member(child, newWeight)). Either path persists through CarriageContentsGroupStore.save and propagates to source via the dev-mode write-through.
// config/dungeontrain/contents/<parent>.group.json
{
"schemaVersion": 1,
"selfWeight": 1,
"variants": [
{ "id": "container_wooden", "weight": 5 },
{ "id": "container_metal", "weight": 2 }
]
}The selfWeight field controls how often the parent's own .nbt is drawn from its group pool โ competing with the explicit members listed in variants. Default 1 matches the pre-v0.134.0 hardcoded behaviour. Backwards compatible: pre-v1.1 sidecars without the field still load (the reader defaults missing selfWeight to 1 and CarriageContentsRegistry.SELF_WEIGHT aliases to CarriageContentsGroup.DEFAULT_SELF_WEIGHT so legacy callers and tests keep their semantics).
/dungeontrain editor contents group set-weight <parent> <child> <n> # 0..100 โ set
/dungeontrain editor contents group set-weight <parent> <child> inc # +1
/dungeontrain editor contents group set-weight <parent> <child> dec # -1
Tab-completes <parent> + <child> against every registered contents id. Pass <parent> as both args to edit the parent's selfWeight.
Every contents template may be randomly mirrored when it is stamped, so one authored interior reads several ways as a player walks the train. Enabling an axis is permission for a flip, not a flip: each carriage rolls the enabled axes independently, so the template still reads as authored about half the time.
| Axis | Mirrors | Default |
|---|---|---|
z |
left <-> right (carriage width) | on |
x |
front <-> back (carriage length, along the direction of travel) | off |
y |
up <-> down | off |
rooms |
(scope flag, not an axis) also flip when this template furnishes a portal room | off |
Z is the default because mirroring across the train's width leaves the direction of travel alone, which is the flip an authored interior is most likely to survive intact. Y is best-effort - Minecraft has no vertical block mirror, so doors, beds and tall plants cannot survive one.
Authoring - a Flip quad (X / Y / Z / Rooms) in the editor menu's Current tab, or:
/dungeontrain editor contents flip <id> <x|y|z|rooms> <on|off>
Persisted as a flip block on the template's contents/weights.json entry:
{ "maze": { "weight": 4, "flip": { "z": false, "x": true } } }An absent block means the default, so every template that predates the option picks up the Z flip and untouched files keep their bare-int form. Each field falls back to its own default, so writing one flag never silently resets the others.
Editor plots never flip - an author looking at a plot always sees what they authored. The roll is
pure in (contents id, world seed, carriage index), which is what lets the block pass and the
deferred entity pass agree without sharing state. The F3+4 debug panel shows the result as a
Flip: line (none, Z, X+Z, ...) recorded at placement.
Horizontal flips ride vanilla's own Mirror/Rotation (X -> FRONT_BACK, Z -> LEFT_RIGHT, both ->
CLOCKWISE_180) with the placement origin shifted by size-1 on each flipped axis - vanilla mirrors
about the origin, and an integer rotation pivot could never centre an even-length box. The vertical
axis has no vanilla equivalent, so it rebuilds the template from its NBT using
EditorMirror's
state maths. Everything keyed by an authored cell - the variant sidecar, container pools, head skins,
the entity list - moves with the stamp, and keeps seeding its own rolls on the authored position, so
a flipped carriage draws the same blocks as an unflipped one, mirrored.
-
CarriageContents.javaโ sealedBuiltin(DEFAULT) | Custom(name)identifier -
CarriageContentsRegistry.javaโ built-ins + customs, scansconfig/dungeontrain/contents/on server start, deterministicpick(seed, index) -
CarriageContentsTemplate.javaโplaceAt/captureTemplate/eraseAt/discardEntitiesAt/ hardcodedlegacyPlaceDefault -
ContentsFlip.java- the per-stamp flip roll, the shared local-position mapping, and the vertical rebuild -
FlipOptions.java- which axes a template may flip along, plus the portal-room scope flag -
CarriageContentsStore.javaโ three-tier loader, NBT I/O, per-id cache -
CarriageContentsEditor.javaโ enter / save / exit / duplicate / saveAs, plot layout at z=80, shell-as-context stamp -
EditorCommand.javaโ/dungeontrain editor contentsBrigadier sub-tree, rootsavedispatch that routes contents-plot sessions here -
CarriageTemplate.javaโapplyContentsin the 6-argplaceAt, with the flatbed guard -
src/main/resources/data/minecraft/vs_entities/โ four{"handler": "valkyrienskies:shipyard"}files forarmor_stand,painting,item_frame,glow_item_frame
- Decorative entities don't visually ride VS ships. Save/load through the editor plot works; placement at shipyard coords works; render transform doesn't. See Compatibility ยง Non-registered entity types don't render with ship transforms. Block-only contents work fine.
-
Customs live in
config/only. They cannot be promoted into the bundled jar resource tier via a command โ to ship a custom with the mod, save it once indevmode onso it writes to the source tree, then commit the resulting.nbt. Since v0.59.0 (PR #74) the bundled set is auto-discovered byBundledNbtScannerwalking/data/dungeontrain/contents/*.nbton the classpath โ nocustoms.jsonmanifest update required. The legacycustoms.jsonis still read as a transitional drift cross-check (a WARN fires if it disagrees with the scan); it will be deleted once a few releases confirm clean drift logs. Pre-v0.59.0, the bundled set was registered via/data/dungeontrain/contents/customs.jsonand any bundled.nbtnot listed in that manifest was inert (the v0.44.0 ship-fix added 13 IDs to the manifest; v0.59.0 makes the manifest itself optional). -
Footprint is per-world. Contents templates must match the interior of the world's
CarriageDims. A contents template authored against a 9ร7ร7 carriage (interior 7ร5ร5) won't stamp in a world running 5ร5ร5 dims โ the loader rejects size mismatches and falls through to the next tier. - Block-entity NBT. Chests preserve inventory contents through save/load. Signs preserve text. Command blocks: untested โ avoid if possible. Pistons with in-flight state: untested.
-
Connected-block side effects. Same caveat as shell templates โ water, redstone, and pistons can propagate beyond the captured region when
placeInWorldstamps them at spawn. Stick to inert decorative blocks for reliable results.