Feature Tunnel Generation - bh679/dungeon-train-mc GitHub Wiki
Since: v0.23.0 β PR #29 β stone-brick tunnel with stepped arched roof, stepped pyramid portals at each entrance/exit, 20-block open-trench approach cutting so portals frame against sky, and sea-lantern lamp stations every 10 blocks on the walls
v0.26.0 β PR #35 β tunnel overwrites are silent: no container spill when the generator replaces mineshaft / village / stronghold block-entities
v0.29.0 β PR #38 β tunnel geometry is now NBT-backed and editable in-world. Two StructureTemplate models (tunnel_section, tunnel_portal) replace the per-column hardcoded paint; players edit them via /dungeontrain editor enter tunnel_section|tunnel_portal and save them to config/dungeontrain/tunnels/. Procedural paint is preserved as the fallback when no template is saved, and used for partial (< 10 col) tunnel runs. Qualification samples raised from ceilingY + 1/+2 to ceilingY + 5/+6 so the train's forward-clearing AABB can't false-disqualify columns for taller carriages
v0.46.0 β PR #60 β tunnel section + portal both support multiple named variants (weighted random pick per tunnel) and per-block randomization sidecars. Authoring uses the unified track-side editor grid β see Feature-Track-Variants. Legacy tunnels/section.nbt / tunnels/portal.nbt auto-migrate to tunnels/<variant>/default.nbt on first server start.
v0.85.0 β PR #123 β tunnels are now generated entirely at chunk-gen time. Removes the v0.83.0 probe-and-confirm hybrid plus the runtime drain (TunnelChunkEvents deleted, TrainTickEvents tunnel-fill block removed, TrainTransformProvider tunnel-chunk sets removed). Single-phase placement via TunnelGenerator.placeTunnelSpaceAtWorldgen: per-column qualification probe at ceilingY+1 (lowered from +5/+6); inside an ongoing tunnel run an extended qualifier scans 4 Y levels going down with alternating Z patterns A {-3,-1,0,+1,+3} and B {-2,0,+2} so a soft spot at centerZ (cave entrance, river bed) doesn't terminate an otherwise continuous corridor. Stamp origin shifted +1 on Z (origin Z = wallMinZ + 1) to align template content with the corridor walls. New debug toggle: MIN_CARRIAGES = 0 lets numCarriages = 0 skip bootstrap auto-spawn for inspecting worldgen without carriages in the way.
Wherever the train's +X corridor runs under enough natural underground material, a proper stone-brick tunnel auto-generates: walls, arched ceiling, floor extension. Each entrance and exit gets a stepped pyramid portal on top and a 20-block open cutting on the non-tunnel side so the portal always reads against open sky instead of being buried in the hillside. Mirrors the track-gen chunk-driven async pattern β per-train cache, deferred paint, 1 chunk/tick budget, phase-offset from clearBlocksAhead and the track drain.
Since v0.29.0 the actual block placement is a StructureTemplate stamp, so the tunnel's shape is no longer hardcoded β see the editor section below.
Since v0.85.0: qualification has two modes, selected by an ext flag the generator threads through the per-chunk loop.
Default (ext=false) β single block read at (worldX, ceilingY+1, centerZ). If that block is natural underground material, the column qualifies. Otherwise it doesn't, and we move on. Used for the first qualifying column in a chunk and for chunks with no tunneled X-neighbour.
Extended (ext=true) β entered once a tunnel is already "active" upstream: either we're a column past a successful stamp earlier in the same chunk, or we entered the chunk with a tunneled neighbour at chunkXΒ±1 (tracked in an in-memory TUNNELED_CHUNKS set). On a center-probe miss we scan a coarse 3D grid before giving up: 4 Y levels going down from ceilingY+1, with Z patterns alternating between
- Pattern A (even
dy):dz β {-3,-1,0,+1,+3}(5 probes) - Pattern B (odd
dy):dz β {-2,0,+2}(3 probes)
If any of those probes hits underground material, the column qualifies. This tolerates a soft spot directly at centerZ β a cave entrance, river bed, or thin ceiling pocket β so it doesn't terminate an otherwise continuous corridor. Down-scanning 4 Y levels also keeps a tunnel from breaking when the prospective ceiling clips an overhead air pocket while the lower body of the column is still buried.
TUNNELED_CHUNKS is in-memory only (a ConcurrentHashMap.newKeySet<Long> keyed by ChunkPos.asLong); on server restart, chunks generated post-restart at the corridor boundary may not inherit ext=true from already-tunneled neighbours. Trade-off vs. SavedData synchronization across worldgen worker threads. Visible artifact: a small seam where a tunnel doesn't extend through a soft spot it would have crossed pre-restart.
The probe sits 1 block above the tunnel's interior ceiling (the arch rises 4 more rows above that, but the probe doesn't sample those). Lowered from the v0.29.0 +5/+6 two-row sample because:
- The old worldgen path was joined by the new chunk-gen path in v0.85.0, so the train-clearing concern that motivated
+5/+6(forward-slab clear sweeping up toaabb.maxY) doesn't apply: at chunk-gen time no carriages exist. - With single-probe at
centerZ, lowering the probe gets more shallow-buried columns to qualify so tunnels extend closer to the surface where they used to terminate prematurely. The arch roof containment concern is now handled by the extended mode's down-scan inside an active tunnel run.
Anchored off the existing TrackGeometry; derived in TunnelGeometry.from(g).
Templates are 10Γ14Γ13 (XΓYΓZ) β aligned with LAMP_SPACING = 10 so a single section contains exactly one lamp station on its left edge. Stamped end-to-end along a qualified run, with portals replacing the first + last sections:
| Layer | World Y | World Z | Block |
|---|---|---|---|
| Floor + track bed |
bedY (= origin.y β 2) |
[wallMinZ + 1, wallMaxZ β 1] β 11 wide |
minecraft:stone_bricks |
| Rails |
railY (= bedY + 1) |
railZMin, railZMax (2 blocks apart) |
minecraft:rail facing east-west |
| Air interior | [railY, ceilingY] |
[airMinZ, airMaxZ] β 11 wide |
minecraft:air (rails preserved) |
| Walls (Γ2) | [floorY, ceilingY] |
wallMinZ = trackZMin β 4, wallMaxZ = trackZMax + 4
|
minecraft:stone_bricks |
| Arched roof | [ceilingY + 1, ceilingY + 3] |
stepped inward 1 per tier + stair-smoothed outer edges |
minecraft:stone_bricks + minecraft:stone_brick_stairs
|
| Apex cap | ceilingY + 4 |
5-wide stone-brick | minecraft:stone_bricks |
| Corner wedges above arch |
[ceilingY + 2, ceilingY + 4] outside the arch profile |
minecraft:structure_void in the editor β stripped from NBT on save |
ceilingY = bedY + 9 β total tunnel depth is 10 blocks floor-to-wall-top, then the arch rises 4 more rows above that. Interior air column in the center is 11 wide Γ up to 11 tall (8 rectangular + 3 stepped arched). The corner wedges above the walls but outside the arch silhouette are marked STRUCTURE_VOID in the editor plot so they're stripped from the saved NBT and the stamp leaves whatever's at those positions (mountain rock) untouched β without that guard the 10Γ14Γ13 bounding-box stamp would carve rectangular gashes through the rock above the arch.
Run detection and portal stamping (v0.29.0βv0.84.x) have been removed. Each chunk does its own per-column scan:
for each X column (chunkMinX..chunkMaxX):
if !qualified(col, ext): continue # skip natural-terrain columns
if col <= lastStampEndX: continue # covered by previous stamp's footprint
if canStampSection(chunk):
placeSectionAtWorldgen(col) # stamp 10-col NBT
lastStampEndX = col + 9
ext = true # subsequent cols use extended qualifier
else:
LegacyTunnelPaint.paintTunnelColumnWorldgen(col) # 1-col fallback
ext = true
canStampSection is a single Z-window fit check: the stamp footprint spans [wallMinZ+1, wallMaxZ+1] (13 wide, including the v0.85.0 +1 Z shift) and must fall within the worldgen 3Γ3 decoration window (stampMinChunkZ β₯ chunkZ-1 && stampMaxChunkZ β€ chunkZ+1). Default carriage widths fit; very wide corridors (width > 16) exceed the envelope and fall back to legacy paint per-column for the whole chunk.
Cross-chunk alignment is per-chunk relative. Each chunk starts its tile chain at its own leftmost qualified column rather than at a world-absolute multiple of LAMP_SPACING. This can leave a visible seam at chunk boundaries inside long tunnels β accepted trade-off vs. legacy gaps with absolute alignment. Inside a single chunk, adjacent stamps tile flush.
Portals removed from worldgen-time. The v0.29.0 portal stamping (mirrored tunnel_portal at run start/end) ran off run detection, which is gone. As of v0.85.0 the worldgen path produces continuous tunnel-section content; portal placement is a separate concern and currently no entrance/exit pyramid is stamped at the corridor's start/end. Portal templates remain in the project (still authorable via /dungeontrain editor enter tunnel_portal) and TunnelTemplate.placePortalAtWorldgen is wired, just not yet called from the new path.
Stamp origin is (worldX, floorY, wallMinZ + 1) so the template's local Z=0 lands one block toward +Z relative to the corridor's βZ wall. The default tunnel section + portal NBTs ship in the resource pack with content authored to land flush against the corridor walls under this anchoring; if you author your own template, expect template-local z=1 (not z=0) to align with the βZ wall.
Each tunnel section + portal is a StructureTemplate saved to config/dungeontrain/tunnels/{section,portal}.nbt β a separate directory from config/dungeontrain/templates/ (carriages) so CarriageVariantRegistry's *.nbt scan doesn't register tunnels as custom carriages.
Two editor plots at (x=80, y=250) and (x=100, y=250) β past the carriage plots at x=0/20/40/60 so all variants are visible from one vantage point:
/dungeontrain editor enter tunnel_section # teleport to (85, 251, 6), 10Γ14Γ13 plot in bedrock cage
<edit blocks freely>
/dungeontrain editor save # writes tunnel_section.nbt (with STRUCTURE_VOID ignore filter)
/dungeontrain editor exit # return to pre-enter position
/dungeontrain editor enter tunnel_portal # separate plot at (105, 251, 6)
<edit β same model used for entrance AND exit; pyramid lives at x=0>
/dungeontrain editor save
/dungeontrain editor list # shows all carriage + tunnel variants with config/bundled status
/dungeontrain editor reset tunnel_section # deletes the saved NBT, reverts to procedural fallback
The editor plot is painted procedurally on enter including the track bed and rails, so what you see in-world is exactly what stamps at a real tunnel.
fillFromWorld(..., Blocks.STRUCTURE_VOID) strips void positions from the captured template. The editor's fillCornersWithVoid pass marks the triangular wedges above the walls (outside the arch silhouette) with void only at positions currently air β user edits in a corner override the void and are captured normally. Result: the saved NBT only covers the arch profile itself, and stamping leaves mountain rock in the corners alone. Interior air inside the arch is captured, so the stamp still carves the walkable volume when placed underground.
StructureTemplate.placeInWorld calls level.setBlock directly, which physics mixins route to the ship's voxel storage when the position lands inside an active ship. The editor path's placeSectionNamed / placePortalNamed (ServerLevel-bound, runs from /dungeontrain editor save) registers ShipFilterProcessor on its StructurePlaceSettings to drop any template block whose target world position is currently ship-managed. Without it, a stamp at the train's current chunk would wipe carriage wall/floor voxels.
The worldgen path's placeSectionAtWorldgen / placePortalAtWorldgen (WorldGenLevel-bound) skip this filter β no ships exist at chunk-gen time, so the guard would always be a no-op and would force Shipyards lookup which is awkward through WorldGenLevel.
Both TunnelTemplate.placeSectionAt and TunnelTemplate.placePortalAt try TunnelTemplateStore.get first; on miss they call into LegacyTunnelPaint.paintSection / paintPortal, which run the v0.23.0 procedural code path column-by-column. Missing or malformed NBT never leaves a hole β you get the default stone-brick tunnel unchanged.
Historical β the 20-block open trench at tunnel entrance/exit was wired into the runtime drain that v0.85.0 removed. LegacyTunnelPaint.paintApproachColumn is still in the code but is currently unreachable from the worldgen path. With portals also pulled out at v0.85.0 (see the Per-chunk per-column scan section), the approach trench has no runtime equivalent yet. Future work to re-introduce portal placement at worldgen-time should bring back the approach trench at the same time.
Baked into the default tunnel_section model β every 10-block section has a lantern pair at y = railY + 2 (walking eye height) on both wallMinZ and wallMaxZ at its leftmost column. Stamping sections end-to-end gives the original "lantern every 10 world blocks" cadence automatically. Players who save a custom section template without lanterns lose them; players who add extra lights at other Y/Z positions get those too. Portal and approach cutting get no lamps β tunnel section only.
TunnelPalette.isUndergroundMaterial(BlockState) accepts:
-
Stone family β
BlockTags.BASE_STONE_OVERWORLD(stone, granite, diorite, andesite, deepslate variants),DEEPSLATE_ORE_REPLACEABLES,STONE_ORE_REPLACEABLES -
Dirt family β
BlockTags.DIRT(dirt, coarse dirt, rooted dirt, podzol, grass-block subsoil) -
Common overworld fill β
minecraft:tuff,minecraft:gravel,minecraft:clay,minecraft:sandstone,minecraft:red_sandstone -
Clay-based biomes (via
isClayFamilyMaterialhelper) βminecraft:terracotta+ all 16 colored variants (badlands layers),minecraft:red_sand,minecraft:mud,minecraft:packed_mud(mangrove swamps) - Ores β coal / iron / copper / gold / redstone / lapis / diamond / emerald tags
Any air, fluid state, leaves, wood, plants, or un-recognised block disqualifies. Modded biomes with custom surface blocks aren't covered β a future dungeontrain:underground_material tag will let datapacks inject their own.
Removed: TunnelChunkEvents, the per-tick FIFO drain on TrainTickEvents, and the pendingTunnelChunks / tunnelFilledChunks sets on TrainTransformProvider. Tunnel placement now runs once during chunk generation, inside TrackBedFeature.place after pillars + tracks:
TrackBedFeature.place(ctx)
βββ TrackGenerator.placePillarsAtWorldgen(...)
βββ TrackGenerator.placeTracksForChunk(...)
βββ TunnelGenerator.placeTunnelSpaceAtWorldgen(...) β per-chunk per-column scan
The feature is wired by datapack: data/dungeontrain/worldgen/configured_feature/track_bed.json β placed_feature/track_bed.json β biome modifier JSONs (overworld / nether / end). Only the modifier matching the world's chosen starting dimension fires.
Cost is paid once per chunk at chunk-gen time. Already-generated chunks pre-v0.85.0 keep whatever their old runtime drain placed; new chunks use the new path. Eliminates the runtime "Can't keep up!" stalls and ConcurrentModificationException save-busy race that the v0.83.0 mitigations chased.
Per-world server config: <save>/serverconfig/dungeontrain-server.toml β generateTunnels = true|false (default true). Sibling to the existing generateTracks. Turning off stops new tunnel generation; already-placed tunnels stay in the world.
-
TunnelGenerator.javaβplaceTunnelSpaceAtWorldgen(per-chunk per-column scan),isColumnUndergroundWorldgen(default + extended qualifier),TUNNELED_CHUNKSneighbour-inheritance set -
TunnelTemplate.javaβplaceSectionAtWorldgen/placePortalAtWorldgen(WorldGenLevel-bound stamping); editor-sideplaceSectionNamed/placePortalNamed(ServerLevel-bound, runs from/dungeontrain editor save) -
TrackBedFeature.javaβ chunk-gen entry point that callsplacePillarsAtWorldgenβplaceTracksForChunkβplaceTunnelSpaceAtWorldgenin order -
TunnelTemplateStore.javaβ NBT read/write cache underconfig/dungeontrain/tunnels/ -
TunnelEditor.javaβ editor plots,enter/save/exit/plotContaining -
LegacyTunnelPaint.javaβ the v0.23.0 per-column paint, now the fallback path (NBT miss / wide-corridor) + editor preview painter -
TunnelGeometry.javaβ derived layout record (floorY,ceilingY,airMinZ/MaxZ,wallMinZ/MaxZ,centerZ,railY/ZMin/ZMax) -
TunnelPalette.javaβ block-state constants +isUndergroundMaterial+isClayFamilyMaterial -
EditorCommand.javaβ/dungeontrain editorsubtree; routestunnel_prefix toTunnelEditor -
DungeonTrainConfig.javaβgenerateTunnelstoggle,MIN_CARRIAGES = 0debug switch
Removed in v0.85.0: TunnelChunkEvents (deleted entirely), TrainTickEvents tunnel-fill block, TrainTransformProvider.tunnelFilledChunks / pendingTunnelChunks sets and getters, TunnelTemplate.placeSectionAt / placePortalAt (the ServerLevel-bound runtime variants β editor still has placeSectionNamed / placePortalNamed).
Historical, since v0.83.0 β PR #119; fully replaced by v0.85.0 β PR #123.
PR #119 introduced a hybrid where chunk-gen probed cheaply and the runtime drain confirmed by painting. PR #123 collapsed both halves into a single chunk-gen pass β there is no longer a runtime confirm step or a pendingTunnelChunks queue. See Per-chunk per-column scan above for the current path.
-
β resolved in v0.85.0. The runtime drain that raced with chunk save NBT IO is gone. Tunnel placement happens once during chunk generation, before the chunk is in the save pipeline.ConcurrentModificationExceptionon chunk save under heavy paint load -
"Can't keep up!" server-thread stallsβ resolved in v0.85.0. The runtime FIFO drain that produced multi-second TPS spikes on fresh train spawn over heavy-rock terrain is gone. Cost is paid at chunk-gen time, amortized into vanilla's normal worldgen budget. -
Cross-chunk section misalignment for runs > 56 colsβ partially resolved in v0.85.0. The 56-col qualified-window scan is gone. Cross-chunk alignment is now per-chunk relative β each chunk starts its tile chain at its own leftmost qualified column. Long-tunnel seam at chunk boundaries is still visible (accepted trade-off vs. legacy gaps with absolute alignment). -
TUNNELED_CHUNKSis in-memory only. Server restart drops the flag, so chunks generated post-restart at a corridor boundary may not inheritext=truefrom their already-tunneled neighbour. Visible artifact: a small seam where a tunnel doesn't extend through a soft spot it would have crossed pre-restart. -
Portals not yet stamped at worldgen-time. v0.29.0βv0.84.x's run-detection-driven portal placement was removed with the run system.
TunnelTemplate.placePortalAtWorldgenexists but is not yet called from the chunk-gen path; portal templates remain authorable in the editor. -
Modded biomes unsupported. Only vanilla surface blocks qualify. A modded "crystal caverns" biome with custom stone would need its blocks added to
TunnelPaletteor (future) adungeontrain:underground_materialblock tag.