Feature Template Refactor - bh679/dungeon-train-mc GitHub Wiki

Template OOP refactor

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.

Why this exists

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.

Phase 1 (v0.108.0 — additive)

  • 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) implement TemplateType so Template.type() returns a uniform handle regardless of which kind owns the type.
  • Capability metadata (isBuiltin(), canPromote()) is informational only — SaveCommand still uses its existing instanceof dispatch.
  • Existing EditorModel deleted; all 6 records move to Template.*Model.

Phase 2 (v0.110.0 — load-bearing collapse)

Phase 2 makes the Phase 1 metadata load-bearing:

New abstractions

  • 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-editor SaveResult records, all 6 of which had identical shape).
  • template/Stores — static helper hiding the unchecked cast at the dispatch site (Stores.save(player, t) forwards to t.store().save(player, t)).

Template.store() and Template.registry() accessors

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.

SaveCommand collapse

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.

Parts plot dispatch

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 (v0.114.0 — renames + dispatch finish)

Phase 3 is purely internal architecture; the diff is invisible end-to-end.

Step 1 — Static placer renames

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.

Step 2 — Identifier records (storage-layer surface)

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.

Step 3 — Bare-named records

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

Step 4 — Dispatch collapse (the win)

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 a noBundledTierMessage switch helper that mirrors SaveCommand.promoteUnavailableMessage to 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)

Phase 4 (deferred)

Documented in the Phase 3 plan, not yet executed:

  • Adopt the identifier records (*TemplateId) at broader call sites: CarriagePartEditor.save / .saveAs signatures, TrackGenerator placement, command-layer argument parsing in EditorCommand.
  • Add the /dt save all parts arm — Phase 3 deferred adding parts to EditorCategory.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 *Placer helpers 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.

Verification

  • 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.
⚠️ **GitHub.com Fallback** ⚠️