Feature Template Refactor - bh679/dungeon-train-mc GitHub Wiki
v0.108.0 — PR #148 — Phase 1: introduces the sealed template.Template interface with 7 record permittees (CarriageModel, ContentsModel, PartModel, TrackModel, PillarModel, AdjunctModel, TunnelModel) and capability metadata (kind(), type(), isBuiltin(), canPromote()). Replaces the per-subsystem EditorModel shape that grew organically over the prior 18 months.
v0.110.0 — PR #150 — Phase 2: adds the generic TemplateRegistry<T> and TemplateStore<T> interfaces, migrates the 5 per-kind registries and 6 per-kind stores onto them via adapter() factories, and collapses SaveCommand's 7-arm + 12-arm instanceof Template.X dispatch onto template.store().save / .promote (498 → 339 lines). Also wires EditorCategory.locate() to recognise carriage-part plots so /dt save from a part plot stops crashing on "Unknown model type".
v0.114.0 — PR #155 — Phase 3: finishes the renames + dispatch consolidation deferred from Phases 1 and 2. Renames the static placement utilities to *Placer (CarriagePlacer, CarriagePartPlacer, CarriageContentsPlacer, TunnelPlacer, TrackPlacer) — 5 file moves + ~50 caller files swept. Frees the namespace so the Template.*Model records can take their natural bare names (Template.Carriage, Contents, Part, Track, Pillar, Adjunct, Tunnel). Introduces 4 identifier records (PillarTemplateId, StairsTemplateId, TunnelTemplateId, CarriagePartTemplateId) at the storage-layer surface only. Collapses the two remaining 12-arm instanceof chains onto seven new per-record methods on Template — weight(), variantName(), restampPlot(), hasBundledTier(), bundled(), editorPlotOrigin(), eraseEditorPlot(). Net ~110 lines deleted from VariantOverlayRenderer + ResetCommand.
Templates / variants / registries grew organically per-subsystem in Dungeon Train. Before Phase 1 there were seven "saveable, editable, in-game placeable thing" subsystems with seven slightly different shapes — each with its own identifier (sealed Builtin/Custom for carriages, String for tracks, (kind, name) tuples for pillars/parts/tunnels), its own type-discriminator enum living in different files, and its own per-kind dispatch arms in SaveCommand, ResetCommand, and the editor flow.
The unified Template hierarchy lets commands like /dt save, /dt save default, and /dt save all treat all kinds the same — the dispatch lives on the template's store() accessor, not on a switch statement that has to be updated every time a new kind is added.
- New
template/package:Template.java(sealed interface),TemplateKind.java(enum),TemplateType.java(marker interface). - The 6 type-discriminator enums (
CarriageType,ContentsType,CarriagePartKind,PillarSection,PillarAdjunct,TunnelVariant) implementTemplateTypesoTemplate.type()returns a uniform handle regardless of which kind owns the type. - Capability metadata (
isBuiltin(),canPromote()) is informational only —SaveCommandstill uses its existinginstanceofdispatch. - Existing
EditorModeldeleted; all 6 records move toTemplate.*Model.
Phase 2 makes the Phase 1 metadata load-bearing:
-
template/TemplateRegistry<T extends Template>—all()/builtins()/customs()/find(id)/reload()/clear(). -
template/TemplateStore<T extends Template>—save(player, template)/canPromote(template)/promote(template). -
template/SaveResult— uniform record (mirrors the per-editorSaveResultrecords, all 6 of which had identical shape). -
template/Stores— static helper hiding the unchecked cast at the dispatch site (Stores.save(player, t)forwards tot.store().save(player, t)).
Each of the 7 record permittees exposes typed store() and registry() accessors that resolve to per-discriminator singletons cached in EnumMaps on the corresponding backing class:
| Record |
store() adapter |
registry() adapter |
|---|---|---|
CarriageModel |
CarriageTemplateStore.adapter() |
CarriageVariantRegistry.adapter() |
ContentsModel |
CarriageContentsStore.adapter() |
CarriageContentsRegistry.adapter() |
PartModel |
CarriagePartTemplateStore.adapter(partKind) |
CarriagePartRegistry.adapter(partKind) |
TrackModel |
TrackTemplateStore.adapter() |
TrackVariantRegistry.adapterForTrack() |
PillarModel |
PillarTemplateStore.adapter(section) |
TrackVariantRegistry.adapterForPillar(section) |
AdjunctModel |
PillarTemplateStore.adapterForAdjunct(adjunct) |
TrackVariantRegistry.adapterForAdjunct(adjunct) |
TunnelModel |
TunnelTemplateStore.adapter(variant) |
TrackVariantRegistry.adapterForTunnel(variant) |
13 store-adapter instances + 13 registry-adapter instances, all cached at class-load via EnumMaps. Existing static methods on the backing classes are unchanged — the adapters wrap them.
saveOne (115 lines, 7 instanceof arms) becomes 25 lines. promoteOne + promoteOneSilent (137 lines combined, 12 instanceof arms) become 31 lines. Per-kind chat strings are preserved verbatim via small switch helpers (savedMessage, bundledWriteMessage, bundledWriteFailMessage, promoteUnavailableMessage) keyed on model.kind() — player-visible output is byte-identical to v0.108.0.
EditorCategory.locate() gained a parts arm between the carriage and contents checks (parts grid sits inside the CARRIAGES Z range). Before v0.110.0, walking into a part plot and running /dt save threw "Unknown model type" because Phase 1's PartModel record had no dispatch site. Now it routes through CarriagePartTemplateStore.adapter(kind) to the existing CarriagePartEditor.save(player, kind, name).
Phase 3 is purely internal architecture; the diff is invisible end-to-end.
Five static placement-utility files renamed to free the *Template namespace for the records. Inner enums kept their simple names and stay implementing TemplateType:
| Before | After | Inner enum |
|---|---|---|
train/CarriageTemplate |
train/CarriagePlacer |
CarriagePlacer.CarriageType |
train/CarriagePartTemplate |
train/CarriagePartPlacer |
(none) |
train/CarriageContentsTemplate |
train/CarriageContentsPlacer |
(none) |
tunnel/TunnelTemplate |
tunnel/TunnelPlacer |
TunnelPlacer.TunnelVariant |
track/TrackTemplate |
track/TrackPlacer |
(none) |
CarriageTemplateTest → CarriagePlacerTest. ~50 caller files swept (mechanical import + reference updates). The *TemplateStore classes (carriage, part, tunnel, track) intentionally stay — they belong to a separate persistence-layer naming family and aren't renamed in this PR.
Four new records lift the bare (discriminator, name) tuples passed through Phase 2's adapter() factories into typed handles:
template/PillarTemplateId(PillarSection section, String name)-
template/StairsTemplateId(String name)— single-discriminator since STAIRS is the only adjunct kind today template/TunnelTemplateId(TunnelVariant variant, String name)template/CarriagePartTemplateId(CarriagePartKind kind, String name)
Adoption surface is intentionally narrow — the Template.*.store() / .registry() accessors construct id records and call new adapter(<id-record>) overloads on the Phase-2 stores. Underlying EnumMap cache keys stay the bare enums; the id record is a callsite shape only. Broader call sites (CarriagePartEditor.save / .saveAs, TrackGenerator placement) keep (kind, name) parameters — Phase 4 will sweep them.
With the *Template static namespace freed in step 1, the seven sealed permittees take their natural names:
| Before | After |
|---|---|
Template.CarriageModel |
Template.Carriage |
Template.ContentsModel |
Template.Contents |
Template.PartModel |
Template.Part |
Template.TrackModel |
Template.Track |
Template.PillarModel |
Template.Pillar |
Template.AdjunctModel |
Template.Adjunct |
Template.TunnelModel |
Template.Tunnel |
Two remaining 12-arm instanceof chains lift onto seven new per-record methods on the sealed Template interface. Each record carries its own delegate to the right per-editor or per-store helper, so the call sites shrink to one-liners.
New method on Template
|
Default body | Used by |
|---|---|---|
int weight() |
abstract | VariantOverlayRenderer.weightFor |
String variantName() |
id() |
VariantOverlayRenderer.modelNameFor |
void restampPlot(level, dims) |
no-op | ResetCommand.resetToSaved |
boolean hasBundledTier() |
canPromote() |
ResetCommand.resetToDefault (read-side gate) |
Optional<StructureTemplate> bundled(level, dims) |
abstract | ResetCommand.resetToDefault |
BlockPos editorPlotOrigin(level, dims) |
abstract | ResetCommand.resetToDefault |
void eraseEditorPlot(level, origin, dims) |
no-op |
ResetCommand.resetToDefault (carriage overrides for legacy-fallback erase) |
Collapsed sites:
-
VariantOverlayRenderer.weightFor— 24 lines →return model.weight(); -
VariantOverlayRenderer.modelNameFor— 22 lines →return model.variantName(); -
ResetCommand.resetToSaved— 26 lines →model.restampPlot(overworld, dims); -
ResetCommand.resetToDefault— 90 lines → ~25 lines (plus anoBundledTierMessageswitch helper that mirrorsSaveCommand.promoteUnavailableMessageto preserve the contents/tunnel kind-specific failure strings byte-identical).
Per-record weight() overrides:
| Record | Returns |
|---|---|
Carriage |
CarriageWeights.current().weightFor(variant.id()) |
Contents |
CarriageContentsWeights.current().weightFor(contents.id()) |
Track |
TrackVariantWeights.weightFor(TrackKind.TILE, name) |
Pillar |
TrackVariantWeights.weightFor(TrackPlotLocator.pillarKind(section), name) |
Adjunct |
TrackVariantWeights.weightFor(PillarTemplateStore.adjunctKind(adjunct), name) |
Tunnel |
TrackVariantWeights.weightFor(TrackPlotLocator.tunnelKind(variant), name) |
Part |
EditorStatusPacket.NO_WEIGHT (parts have no weight pool today) |
Documented in the Phase 3 plan, not yet executed:
- Adopt the identifier records (
*TemplateId) at broader call sites:CarriagePartEditor.save / .saveAssignatures,TrackGeneratorplacement, command-layer argument parsing inEditorCommand. - Add the
/dt save all partsarm — Phase 3 deferred adding parts toEditorCategory.models()because the iteration UX needed a decision on row order and(kind, name)addressing. - Lift placement methods onto
Template(e.g.Template.placeAt(level, origin, dims)) — Phase 2/3 kept placement on the static*Placerhelpers because signatures vary too much per kind, but worth revisiting once identifier records are adopted in step 1. - Resolve the pre-existing
/dt save all"must be inside every plot at once" bug — file under a separate session; not made trivial by the dispatch collapse.
- Save formats unchanged. NBT layouts unchanged. Command syntax unchanged.
- HUD strings, chat messages, and editor menu text are byte-identical across v0.108.0 → v0.110.0 → v0.114.0.
- Worlds saved on v0.108.0 load identically on v0.114.0.
- Test count grew 21 → 222 → 233 across the three phases as the new dispatch surface picked up coverage; all pass on every release.