77 KiB
Java migration plan and audit findings
Status: living document
Initial audit: 2026-08-14
Audited revision: f10595d (master)
Decision summary
Converting ForgeMultipart to Java is realistic. It should be treated as a staged compatibility migration, not as an automatic source conversion or a ground-up rewrite.
The main difficulty is preserving behavior and compatibility, not translating Scala syntax. In particular, ForgeMultipart dynamically builds TileMultipart subclasses from combinations of part capabilities, and its published bytecode exposes Scala types and Scala compiler artifacts to other mods.
The current direction is:
- Preserve observable behavior unless a divergence is an intentional, documented bug fix or performance fix.
- Deliver a complete, documented Java API for GTNH consumer integrations, reusing the existing Java surface.
- Keep compatibility bridges for the old Scala-facing API while consumers migrate; deprecate each entry only with a usable replacement. Remove FMP's remaining Scala implementation and dependencies after the consumer and internal removal gates pass.
- Use the Hodgepodge/GTNH convention for any new Sponge Mixins: UniMixins plus explicit normal, early, and late registration where the target lifecycle requires it.
- Allow GTNHLib as a dependency. Use fastutil selectively for measured primitive-collection or allocation problems, not as a blanket collection replacement.
- Keep ForgeMultipart's runtime composite-tile generator separate from the Sponge Mixin decision until it is proven replaceable. They solve different problems.
No trustworthy tool will produce a maintainable Java port automatically. A decompiler can help recover control flow or compare generated bytecode, but its output should be used as a reference rather than committed as the implementation.
Current status
Work continues on algent/java. The low-coupling queue, core part/tile types, registries, handlers, factories,
placement/render helpers, both generators and all built-in tile traits are Java. Common, face, corner, edge, post,
hollow and occlusion microblock traits retain Scala inheritance/state/super declarations over Java behavior,
including client bounds/mask updates. StackAnalyser initialization and constant-type classification delegate to
Java; retain its coordinated Scala class/companion/models. FieldMixin.accessName also delegates to Java while
retaining its case class. MixinInfo.linearise and ScalaSignature.Bytes.section delegate traversal and array-copy
behavior to Java. TypeRef.jName, TypeRef.jDesc and TMethodType.jDesc delegate name/descriptor conversion and
method assembly while preserving virtual lookup and evaluation order. ClassSymbolRef.jInterfaces delegates
ordered interface-name mapping while retaining its Scala List contract. Class and method symbol formatting also
delegate to Java. The active priority is completing the consumer-facing Java API and migration documentation in
Phases 2, 9 and 10. Further compiler/model extraction must unblock that milestone or address a demonstrated issue.
Java-trait rewriting and compiler startup also delegate to Java helpers. Abstract Java mixins and Java-path
side-only filtering are complete; multiple Scala-trait inheritance still needs its metadata. Retain trait
state/accessor/super bridges and the compiler/signature/analyser model shells.
Start with JAVA_MIGRATION_HANDOFF.md for the exact source/test baseline, workflow and Java-source limitations. Read both the ABI inventory and consumer audit before changing compatibility surfaces. Completed-target evidence is in the history; the divergence ledger records only effective compatibility differences.
The source-only consumer guards, ordered tile lifecycle/NBT/packet fixtures and generated Scala/Java trait fixtures are in place. Schematica's private registry-map view is restored. The tile equality/callback regressions and the generated client-render dispatch crash are fixed; the user confirmed placement no longer crashes. Broader manual client checks and full-pack profiling remain release gates.
Focused traversal/redstone allocation improvements are recorded in the profile. GTNHLib/UniMixins remain conditional on concrete need. Removing Scala from FMP remains the final target, gated by released consumer migrations and retirement of FMP's own Scala-dependent implementation.
Remaining work overall
Current checkpoint: the core runtime implementation is Java; all ten Phase 9.1 API-table entries and Phase 9.2's internal-boundary documentation are complete. Nine Scala files / 747 nonblank lines remain, plus Scala-dependent storage and compatibility references in Java. A source-file count is not a measure of the remaining migration effort. The phase checklists include recurring verification rules, historical work and conditional ideas; this is the active queue.
| Workstream | Remaining deliverables | Gate |
|---|---|---|
| Complete the supported Java surface | Physical-client validation of the documented illuminated microblock example; keep the completed audited reflection/legacy-use map current as consumer revisions change | Direct typed calls and documented override behavior are available for the identified FMP-side gaps; physical-client evidence remains |
| Migrate consumers and adopt releases | Patch GuideNH/Schematica to their documented APIs; migrate ProjectRed's external trait and other Scala/helper users, including OpenComputers, ProjectBlue and ForgeRelocationFMP; direct Galacticraft integration; Et Futurum/Iguana migrations; UtilitiesInExcess enumeration and mat/material fix; complete the full adoption ledger beyond these hotspots |
Source patches, released versions and actual target-pack jars must all be recorded; reference checkouts are not migrated |
| Measure and improve performance | Fresh realistic profiles, ranked candidates, bounded improvements and repeated paired measurements; distinguish FMP implementation gains from migrated-consumer gains and report variability/regressions | Phase 4b starts when API/extension workloads are stable; it can overlap consumer adoption |
| Validate releases | Complete client rendering/particles/lighting/interaction and integration checklist; old-world, multiplayer, movement and preview checks; packaged/obfuscated artifacts on supported runtimes; optional-mod absent/present loading; actual patched-world MCPC hook where supported | Automated Forge tests do not replace physical-client/full-pack evidence; repeat affected checks after consumer migration and Scala removal |
| Prepare the repository for merge | Move Java sources where joint compilation permits; reassess forced Scala compilation/version freshness; refresh README; organize durable migration docs; expand modern Java syntax only across verified compilation boundaries | Preserve packaged Java 8 compatibility and document any deferred source/toolchain constraints |
| Remove FMP's Scala requirement | Replace remaining trait/model shells, Scala storage and internal users; retire eligible legacy bridges/signature decoding; remove Scala compiler/runtime dependencies; scan shipped/generated classes and validate migrated consumers | Last milestone: consumer releases/adoption and removal policy first, then internal cleanup and final release validation |
Extra Utilities remains an active supported consumer. UtilitiesInExcess is the intended replacement, but changing the support target awaits approval and actual pack adoption. Keep Extra Utilities compatibility until then; also verify that the replacement has migrated the relevant FMP contracts before removing them.
GTNHLib/fastutil, UniMixins, a new generator design and further mechanical Scala-shell extraction are conditional work, not mandatory tasks to add without a concrete need. The existing generator is retained. See the detailed phases below, consumer adoption ledger, manual checklist and performance protocol.
Scope and compatibility target
“Behave the same” means preserving the following unless a change is explicitly entered in a divergence log:
- Multipart placement, removal, occlusion, collision, selection, ticking, redstone, rendering, and scheduled-tick behavior.
- Client/server separation and Forge/FML lifecycle timing.
- Part and tile registration order and lookup behavior.
- Part identifiers, NBT keys and structures, saved-world loading, and conversion of existing multipart tiles.
- Packet identifiers, field order, encoding, dispatch timing, and client update behavior.
- Dynamically generated tile interfaces and capabilities expected by parts and integrations.
- Public class names, method descriptors, static entry points, and reflective lookups that supported downstream mods use.
- Behavior in both development and obfuscated production environments.
Allowed divergences are:
- Confirmed bug fixes.
- Removal of avoidable allocation, boxing, exception-based control flow, or other performance problems.
- Better failure messages where success-path behavior is unchanged.
Each divergence should record the old behavior, new behavior, reason, compatibility impact, and how it was manually verified. Unintentional behavior changes are regressions even if the new implementation appears cleaner.
Java runtime policy
The main GTNH release targets modern Java across the Java 17–26 range. A Java 8 artifact must remain available for legacy launchers, with the same functional behavior even if it cannot match the modern runtime's performance.
- Prefer modern Java syntax where it makes the code easier to read and the compilation path supports it. Java 8 artifact compatibility does not require Java 8 source syntax everywhere.
- Reuse the scoped JVM Downgrader path established in
5f0e329b; extend it per source unit when justified by a concrete readability improvement, with normal and clean-build verification. Do not enable modern compilation globally while it breaks retained Scala compilation. - Java 8 startup, serialization, networking, registration, and gameplay semantics are compatibility requirements. Equal throughput or allocation behavior between Java 8 and modern Java is not required.
- Validate routinely in the Java 8 deobfuscated Forge runner. Validate the packaged release in a representative modern-Java server before release, because the current GTNH deobfuscated
runServer17/runServer21/runServer25tasks do not provide a working dedicated-server path in this checkout. - Avoid modern runtime APIs whose downgrader stubs are unavailable or whose semantics materially differ on Java 8, especially in compatibility-critical and hot code.
Modern Java readability policy
Commit 5f0e329b (build: support scoped modern Java compilation) is an implemented precedent, not just a proposed
experiment. It replaced the instruction-type dispatch in StackAnalyserLogic.visitInsn with a Java 21 pattern
switch. A JDK 25 task compiles the helper with --release 21; JVM Downgrader supplies Java 8 bytecode to tests,
Forge and both packaged jars. Scala 2.11.5 and joint Scala/Java compilation stay on Java 8. The checkpoint passed
normal/clean builds, frozen consumers and Forge tests with unchanged retained Scala classes and generated tile
dumps. See JVM_DOWNGRADER_HANDOFF.md for the arrangement, evidence and limits.
- Prefer clearer pattern matching, switch expressions and local type inference where supported and appropriate; retain explicit types or ordinary control flow when they communicate the intent better.
- Apply readability improvements during relevant work or in focused follow-ups. They need not wait for consumer migration or complete Scala removal, but a broad syntax rewrite is not a prerequisite for delivering the Java API.
- Preserve behavior, virtual dispatch, evaluation order, public descriptors and generated-class contracts. Modern declarations such as records/sealed types are not automatic replacements for compatibility-sensitive models.
- Distinguish syntax from library APIs: syntax may downgrade without a runtime dependency, whereas a newer API can require runtime stubs or change behavior. Verify that separately before adopting it.
- When the old Scala parser, circular compilation, a later compilation stage, or downgrader/runtime support blocks a change, keep the readable working form and defer that specific modernization. Record the concrete blocker and what would unblock it in the handoff; do not introduce fragile compilation workarounds solely to change syntax.
- Keep
enableModernJavaSyntax = falsewhile global mode moves Scala 2.11.5 onto an incompatible toolchain. Expand the explicit modern source set only after declaration visibility, compile order and downgraded output are verified.
For a new modern source unit or syntax feature, use the existing characterization/compatibility checks and verify the clean compilation path, packaged Java 8 output and relevant Forge behavior. This is a readability preference with a compatibility gate, not an obligation to modernize every file immediately.
Audit findings
Codebase size
At the audited revision:
| Area | Scala files | Approximate Scala lines |
|---|---|---|
| Multipart core | 38 | 4,076 |
| Multipart ASM | 8 | 1,933 |
| Microblocks | 28 | 3,013 |
| Total | 74 | 9,022 |
There are also 19 Java source files with approximately 1,148 lines. The existing README explicitly describes the API as Java-friendly, and the bundled Minecraft multipart implementation already serves as a Java consumer of it.
The current Scala source compiles successfully with Scala 2.11.5. This gives the migration a working reference implementation rather than requiring recovery of a broken codebase.
Reference classfile inventory
The compiled mod contains 635 class files and is approximately 1.64 MiB before its Scala runtime dependency. Compiler-generated artifacts include:
- 236
anonfunclosure classes, approximately 448 KiB. - 39 trait helper
$classfiles, approximately 98 KiB. - 121 object/companion-related class files, approximately 415 KiB.
These figures support a maintainability and allocation argument for Java, but not the claim that Scala wraps most methods in try/catch.
A scan of 5,126 Scala methods found 52 methods with exception tables and 53 total entries. The important groups were:
- 33 catch-all/finally handlers, mostly compiler machinery for synchronization and lazy initialization.
- 9
scala.runtime.NonLocalReturnControlhandlers caused byreturnfrom inside closures. - 11 ordinary exception handlers covering
Exception,ClassCastException,IllegalStateException, andIOException.
The 222 methods emitted from the Java sources had no exception-table entries in the same scan.
Exception-table metadata does not impose a meaningful steady-state cost merely by existing. The nine non-local-return sites are different: when the early return is taken, Scala implements it by throwing and catching NonLocalReturnControl. Those sites are worth removing, but they do not justify assuming exception overhead across the whole mod.
More credible performance risks
The source and bytecode audit identified these allocation patterns; the named ports have now resolved them:
- The earlier Java
TileMultipart.updateEntity()traversed throughoperate(Function1), whoseparts()helper materialized anArrayList, backing array, Scala iterator and Java-conversion wrappers per call. - The former Scala
TRedstoneTilequeries created closures andscala.runtime.IntRefboxes; the Java trait now walks the normal immutable list without allocation while preserving an arbitrary-Seqfallback. - Several Scala 2.11 collection, range, and
JavaConversionspaths create closure objects, boxed primitives, iterators, or temporary collections. - The former
TSlottedTile.canAddPartandMicroRecipescans used exception-based non-local returns; both are now ordinary Java loops. - Rendering code in
TileMultipartalready uses cached arrays andwhileloops in important paths. It should be preserved as evidence of deliberate optimization, not mechanically rewritten into streams or higher-level collection pipelines.
The focused Forge/JFR baseline confirmed both risks. With eight parts, updateEntity and operate allocated about 184
bytes per call, while a three-query redstone iteration allocated 80.5 bytes. The completed ports reduce the normal
paths to effectively zero measured allocation; these remain focused measurements, not whole-pack TPS. See
JAVA_MIGRATION_PROFILE.md.
Runtime code generation is architectural, not incidental
MultipartGenerator selects a tile class from the interfaces required by the parts in a block. ASMMixinFactory caches generated constructors by a trait bit set, so class generation is primarily a registration/first-combination cost rather than a per-tick cost.
ASMMixinCompiler already supports two inputs:
registerScalaTrait, which reads Scala signature data and understands Scala trait output.registerJavaTrait, which transforms a concrete or abstract Java mixin class into the interface/static-helper form used by the generator.
The Java path rejects Java interfaces, inner classes, static initializers, and constructors with arguments. Abstract inputs retain their abstract methods as interface contracts while later mixins provide the implementation. Those restrictions must be accounted for when converting built-in tile traits, but the path proves that Java implementations can participate in the current composition system.
MultipartGenerator.registerPassThroughInterface also explicitly supports interfaces that generated tiles must expose. This is part of the compatibility surface and must not be lost in a static subclass rewrite.
The least risky plan is therefore to keep the current composition model during the first Java migration. Port or redesign its ASM implementation only after the rest of the behavior is stable.
Public ABI exposes Scala
Several public descriptors contain Scala runtime types. Examples include:
TileMultipart.partListexposingscala.collection.Seqthrough generated accessors.TileMultipart.operateacceptingscala.Function1.- Occlusion methods accepting Scala
Seqvalues. - Tile conversion helpers returning
scala.Tuple2. MultiPartRegistryoverloads accepting ScalaFunction2orSeq, and returningSeq.- Scala object singletons (
MODULE$), companion/static forwarders, and trait$classhelpers.
Changing these descriptors directly to java.util or java.util.function types can cause NoSuchMethodError, NoClassDefFoundError, or reflective integration failures in already compiled mods. Source compatibility alone is not sufficient.
This means “all source is Java” and “the Scala runtime is gone” are separate milestones. Deprecated bridges may still require Scala library types even after the implementation is Java.
API migration design
Complete the existing Java API with equivalent capabilities, then make legacy entry points adapt to the shared Java implementation. Avoid two independently maintained implementations. Where an old entry is an overridable hook, preserve its virtual dispatch while it remains supported: moving an algorithm behind a new Java method must not bypass old subclass overrides. Define and test the dispatch path for each such pair, without recursive forwarding.
Direct consumer access
Supported integration tasks must be possible through direct, statically typed public calls, without reflection, private-field accessor mixins or Scala companion lookup. Reuse existing methods first; expose a missing capability only when an audited consumer needs it. Do not turn unrelated implementation details into public contracts. Optional integrations should isolate FMP-typed code behind mod-presence and supported-version checks; optionality alone is not a reason to require reflection. Validate both absent and present mod loading in consumer adoption. Existing reflective integrations remain supported during migration. Reflection snippets in the guides describe legacy interoperability options, not the intended completion point; consumer patches should use the typed examples.
Consumer API milestone and retained Scala stopping point
The first release milestone is a complete, documented Java API that every supported GTNH integration can migrate to, while the existing supported binaries continue to work. Counting remaining Scala declarations is not an exit criterion. A Java API may initially run over Scala-backed storage and retained compatibility shells.
- Reuse existing Java classes, interfaces and entry points; add only the missing capabilities. No wholesale new facade or separate API artifact is required unless a concrete compilation or packaging constraint calls for it.
- Cover extension contracts as well as calls: part/material subclasses, lifecycle and super dispatch, generated tile capabilities, microblock trait registration, and supported replacements for reflection into internals.
- Document collection ownership and mutation, ordering, nulls, callback/reentrancy behavior, override hooks, registration timing, client/server restrictions, and persistence/network responsibilities where applicable.
- Provide buildable Java examples using only the supported surface, with Forge fixtures for generated extensions. Use ProjectRed's illuminated microblocks to prove the Java extension path end to end; a Java-shaped signature alone does not establish a usable replacement. Check emitted consumer bytecode for legacy FMP references.
- Scala-authored consumers can use this Java API without converting their whole mods to Java. The migration target is their dependency on FMP's Scala-facing contracts, including compiler-emitted trait helper calls.
Pause mechanical extraction of retained Scala models, accessors and trait declarations once their compatibility role is understood and tested, unless an extraction enables the Java API, fixes a demonstrated defect, or provides a measured benefit. Keep those shells supported during consumer migration. Resume their final replacement/removal when the relevant consumer gates pass; an internal dependency may still need implementation work at that point.
Execution order is complete and document the Java API → migrate and release consumers → adopt those releases in the target pack → retire FMP's Scala dependencies. Work can overlap by capability, but the removal gate cannot be skipped. Phases 9 and 10 now drive the remaining work; Phase 8 is the final gated milestone.
After the Java API and representative extension workloads are stable, run the focused performance pass in Phase 4b. It can overlap consumer migration and must not wait for final Scala removal. Attribute implementation gains and consumer migration gains separately; performance work does not relax any compatibility or adoption gate.
Candidate mappings, with final names and mutability contracts to be decided from existing behavior:
| Existing Scala-facing shape | Java-first equivalent |
|---|---|
Seq<TMultiPart> |
List<TMultiPart> or a purpose-specific read-only view |
Function1<TMultiPart, Unit> |
Consumer<TMultiPart> or direct indexed traversal methods |
Function2 factory callbacks |
A named Java functional interface with domain-specific arguments and return type |
Tuple2<TileMultipart, Boolean> |
A small immutable Java result class with named accessors |
| Scala collection overloads in registries | Collection, List, arrays, or Java Iterable, according to actual ordering and mutation semantics |
Bridge rules:
- Preserve old JVM descriptors, not merely old source names.
- Mark source-visible legacy APIs deprecated and point their documentation to the Java replacement.
- Preserve ordering, nullability, mutability, exception, and callback timing semantics.
- Keep legacy singleton/static entry points when downstream bytecode may reference them.
- Do not leak new Scala types into the Java API.
- Do not remove Scala library dependencies until an ABI inventory shows that no retained bridge needs them.
- Use
javapplus an API/ABI comparison tool such as japicmp or Revapi against the reference dev jar at every public-API phase.
Before implementing bridges, inventory real consumers in the target modpack. Some Scala compiler artifacts may be technically public but unused; others may be load-bearing despite looking internal.
Mixin strategy
Follow the pattern currently used by Hodgepodge for fixed class transformations:
- Set the GTNH build convention's
usesMixins = true, which supplies UniMixins integration and declares UniMixins as a required dependency. - Use a normal/pre-init mixin configuration for transformations that do not require special early or optional-mod timing.
- Use an
IEarlyMixinLoaderconfiguration only for targets that must be selected before ordinary mod loading. - Use an
@LateMixin/ILateMixinLoaderconfiguration for optional-mod targets whose presence is known later. - Centralize selection through the GTNHMixins builder pattern, including side, loaded-coremod, loaded-mod, and configuration gates.
- Keep early and late lists minimal. A mixin should not be early or late merely because those mechanisms are available.
- Prefer Sponge Mixins for fixed transformations of known classes. Retain raw ASM only where a mixin cannot express the required behavior safely.
The inspected Hodgepodge reference uses:
HodgepodgeCore implements IFMLLoadingPlugin, IEarlyMixinLoaderand delegates early selection toIMixins.getEarlyMixins(...).HodgepodgeLateMixins, annotated with@LateMixin, implementingILateMixinLoaderand delegating late selection toIMixins.getLateMixins(...).- Separate normal, early, and late JSON configurations.
- Explicit dependencies on GTNHMixins and UniMixins.
Important distinction: Sponge/UniMixins modifies known classes during class loading. ForgeMultipart's current generator creates different tile classes for runtime combinations of part interfaces. The Hodgepodge registration pattern should be used for ordinary patches introduced by this port, but UniMixins is not assumed to replace the composite-tile generator.
GTNHLib and fastutil
GTNHLib is an acceptable dependency for the port. The inspected GTNHLib checkout declares fastutil 8.5.18 as compileOnlyApi and uses its dependency-loader/GTNHExtLib arrangement for external runtime libraries. Hodgepodge consumes GTNHLib as an API dependency and imports fastutil through that environment.
Integration requirements:
- Select the GTNHLib version aligned with the target pack/release train rather than copying Hodgepodge's current version blindly.
- Verify both compile and production runtime resolution in ForgeMultipart's actual build after adding the dependency.
- Add the Forge mod dependency declaration with a compatible version range.
- Use primitive fastutil collections where they remove boxing or object-key allocation in a demonstrated hot path.
- Prefer JDK collections for small, cold, or API-facing collections unless fastutil provides a concrete benefit.
- Do not expose fastutil implementation classes from the stable public API without a strong reason; ordinary Java interfaces keep consumers decoupled.
Candidate areas to measure include redstone query state, coordinate-keyed caches, scheduler structures, registry lookups, and render/tile scratch collections. A primitive map is only useful if the key can be represented without first allocating a wrapper such as BlockCoord.
GTNHLib's ASM utilities may also reduce low-level boilerplate when the runtime generator is eventually ported. Evaluate them against the generator's exact needs instead of changing ASM APIs during the earlier source migration.
Migration phases
Every implementation phase follows the same test-first gate:
- Write characterization tests against the current Scala implementation for the area about to change.
- Run them on the untouched implementation and retain any golden fixtures they produce.
- Commit the tests separately from the port so the baseline is reviewable.
- Replace the implementation while leaving the tests unchanged.
- Require the same tests to pass before moving to the next area.
If a characterization test captures a confirmed bug that the port intentionally fixes, record it in the divergence log and change that expectation in a separate, explicit bug-fix step. This prevents accidental changes from being presented as fixes.
Phase 0 — Freeze and measure the reference behavior
- Produce and retain a reference dev jar from
f10595dor the selected migration base. - Dump the public/protected JVM API and important generated tile class shapes.
- Inventory downstream mods that compile against or reflect into ForgeMultipart. See
JAVA_MIGRATION_ABI_INVENTORY.mdandJAVA_MIGRATION_CONSUMER_AUDIT.md. - Record source-level consumers of part IDs, NBT, packets, registration order, reflection, and lifecycle behavior.
- Freeze representative part IDs, NBT trees, packet bytes, registration order, and lifecycle behavior as tests.
- Add a Java-8-compatible JUnit/Jupiter setup to the existing Gradle
testtask. - Add a separate Forge integration-test source set/runner for behavior that cannot execute in a plain JVM.
- Write the first characterization suite against the untouched Scala implementation.
- Create a remaining manual compatibility checklist for rendering, input, and other behavior that cannot be asserted reliably by the automated harness. See
JAVA_MIGRATION_MANUAL_CHECKS.md. - Capture representative CPU and allocation profiles before optimization work. See
JAVA_MIGRATION_PROFILE.md. - Start a divergence log.
Exit condition: there is a reproducible behavior and ABI baseline, the test layers run in CI or an equivalent repeatable command, and the initial suite passes against the Scala implementation.
Phase 1 — Add infrastructure only when first needed
- Add GTNHLib with a pack-aligned version when the first Java migration or data-structure change uses it.
- Enable UniMixins when the first fixed class transformation is introduced.
- Copy the Hodgepodge loader/config structure selectively: normal by default, early/late only for demonstrated lifecycle needs.
- Confirm client and dedicated-server startup after each dependency or loader change.
Exit condition: dependencies and loaders are present with no unintended gameplay behavior change.
Phase 2 — Establish the Java-first API and compatibility bridges
- Inventory every public Scala descriptor and compiler-generated entry point used by downstream code.
- Add ABI snapshots and compiled consumer fixtures before changing public descriptors.
- Define Java collection, callback, and result types with documented behavior equivalent to the current API.
- Implement the canonical behavior once in Java.
- Keep deprecated Scala signatures and singleton/static entry points as thin adapters.
- Compare the produced jar with the reference API and verify representative Java and Scala consumers can load.
Status: implementation conversion alone does not close the two open items. Complete the missing consumer entry
points, documented contracts and extension examples in Phase 9, then map every legacy use to a supported replacement
in Phase 10. Every changed public boundary remains diffed against the reference dev jar. For example, getIdMap
keeps its scala.Tuple2 array while Extra Utilities still uses it; the new API must offer an alternative alongside it.
Exit condition: new code can use a clean Java API while existing supported binaries still link through the deprecated surface.
Phase 3 — Convert low-coupling code
- Characterize each related leaf group before converting it.
- Convert the identified constants, value holders, simple utilities, registries, packet data helpers, and leaf classes.
- Replace
JavaConversionsin converted files with explicit Java collections or controlled adapters. - Preserve iteration order and null behavior.
- Keep mixed Scala/Java compilation working throughout the phase.
Status: complete. The low-risk queue is empty. All eight load-bearing $class helpers are Java with their bridges
retained, as are both registries, the central part/tile types, scheduler/helper utilities, BlockMultipart, and
MultipartRenderer. Per-type evidence is retained in docs/migration/HISTORY.md; the handoff lists the remaining source units.
Exit condition: leaf code is Java and no longer generates avoidable Scala closures or collection adapters.
Immediate compatibility gate — complete
Do this before another source conversion:
- Restore Schematica's reflective
MultiPartRegistry$type-map view with its exact field name and Scala mutable-map shape, backed by the canonical Java registry state; add a regression test that performs Schematica's actual lookup. - Add exact member-level guards for GuideNH's
BlockMicroMaterial.block/metamixin fields, Et Futurum's mutableButtonPart.metaSideMap/sideMetaMap, Iguana'sItemSaw.harvestLevel, and Galacticraft's name-onlyregisterMateriallookup. - Freeze
TileMultipartorder and lifecycle observations:parts/idNBT,partList/jPartList,partMap, slot rebinding, add/remove/replace callback order, moving the live tile, andonMoved. - Add compact mixed-part NBT and description-packet fixtures. The existing torch/button parts provide the needed distinct IDs, order, slots and payload without adding downstream mods or a test-only part registry.
- Freeze one generated Scala trait, one Java trait, and representative pass-through interfaces before changing built-in tile traits or either generator.
Exit condition: every source-only constraint that can silently fail has an automated structural/behavioral guard, and the already-ported registry again supports Schematica.
Phase 4 — Convert measured hot paths
- Characterize result ordering, callback timing, early exits, and edge cases before changing traversal code.
- Rewrite
TileMultipartupdate/operate traversal without per-call collection allocation. - Rewrite redstone queries without closure allocation or
IntRefboxing. - Replace non-local returns in recipe scans with ordinary Java control flow.
- Replace the remaining non-local return in the generated
TSlottedTileslot scan. - Keep fastutil out of these paths because profiling did not justify another dependency or data structure.
- Re-profile the same scenarios and record both improvements and regressions.
Status: complete for the measured targets. operate now preserves its captured-sequence mutation semantics without
collection materialization,
and the six-interface IRedstonePart/RedstoneInteractions$ unit is ported with its class list and descriptors intact.
The comparison proved its allocation was owned by the generated TRedstoneTile, which is now Java and allocation-free
on the normal immutable-list path.
MicroRecipe is also Java: all five recipe forms and precedence are characterized, its published Scala façade remains,
and its closure-backed scans and non-local returns are gone. TPartialOcclusionTile completed the simple no-field
Java-trait pilot, and TSlottedTile completed the stateful field/lifecycle checkpoint while removing the remaining
slot-scan closures and exception-backed return. The paired TRedstoneTile run removed 80.5 B per three-query iteration
and improved throughput by 20.3% with an identical checksum.
Exit condition: identified steady-state Scala allocation sites are removed with compatible results.
Phase 4b — Measured performance pass
Status: planned. Phase 4's focused historical results are complete; they do not establish current whole-pack performance. Re-profile the corrected Java branch and representative consumer integrations, then improve the measured costs. The goal is useful gains from both the port and adoption of its API, not a faster language label.
Prioritize hot paths by CPU/allocation evidence and user impact, while also checking intermittent and retained costs:
| Representative scenario | Costs to investigate |
|---|---|
| Typical and dense mixed multipart bases with ticking parts, ProjectRed wiring and redstone activity | Main-thread CPU, tick-time median/tails, allocation and GC pauses |
| Placement/replacement, occlusion previews and changing OpenComputers print shapes | Query/interaction latency, callback and snapshot costs, allocations |
| Chunk load/unload, old-world reconstruction, schematic/GuideNH previews and frame moves | Load/transition latency, NBT work, cache churn and peak/retained memory |
| Client views of ordinary and dense microblocks, including illuminated ProjectRed parts | Frame-time distributions, chunk rebuilds, render CPU and GPU limits |
| Description updates and multiplayer chunk watches | Serialization time, bytes/packets, burst allocation and tick-time spikes |
| Fresh startup, first trait combinations and repeated combinations | Startup/first-use latency, class generation, cache hit behavior and retained heap |
These are coverage candidates, not assumed bottlenecks. Include a modest/common scene, a representative busy scene and an explicitly labeled stress case. Reuse the existing Forge/JFR harness for attribution; add a workload only when it represents an observed consumer path. Use actual client/pack runs to validate user-facing claims.
- Capture a fresh baseline with exact FMP/consumer revisions, runtime, pack/config/world and repeatable actions; record CPU, allocation and memory profiles before selecting targets.
- Rank candidates by measured cost and expected user impact, including worthwhile startup/memory improvements outside steady-state hot paths. Set a measurable acceptance criterion before each edit.
- Compare unchanged consumers on baseline versus optimized FMP to isolate implementation gains. On the same FMP artifact, compare a consumer's old calls with its migrated calls to isolate migration benefits and bridge overhead. Include a compatible Scala/reference artifact as context where feasible; report non-comparable cases explicitly.
- Make one bounded improvement per commit, with characterization/regression checks and paired measurements. Preserve ordering, snapshots, callback/override behavior, packets, NBT and generated-extension compatibility. Do not remove Scala shells or weaken an API contract just to improve a benchmark.
- Publish absolute and relative before/after results, run variability, unchanged controls and regressions using the measurement protocol. Validate CPU wins against allocation/GC, retained memory, startup and client/server tails rather than trading one cost silently for another.
- Repeat the representative pack scenarios after integrating the changes and relevant consumer migrations. Retain only justified improvements; record below-noise results and rejected candidates without claiming a speedup.
Exit condition: representative scenarios have repeatable before/after evidence, selected improvements meet their declared criteria without unresolved correctness or material performance regressions, and consumer migration benefits are separately demonstrated where present. There is no preset percentage target or promised whole-pack TPS gain.
Phase 5 — Convert built-in tile traits through the existing Java path
- Characterize the direct class shape, generated interface, super dispatch, behavior, and class caching of a no-field pilot.
- Port one simple built-in trait as a pilot using
registerJavaTrait. - Verify generated field accessors, initialization, copying, lifecycle callbacks, super bridges, and class caching
on the stateful
TSlottedTilecheckpoint. - Characterize ProjectRed/Extra Utilities redstone calls and port
TRedstoneTilewith an immediate paired profile. - Convert remaining traits in small related groups.
- Document or deliberately relax the Java trait restrictions only when a real trait requires it.
- Preserve pass-through interface behavior.
- Keep the completed OpenComputers
TSlottedTile.v_partMapmutation/rebinding case green and cover ProjectRed/ Extra Utilities behavior beforeTRedstoneTile. - Explicitly cover AE2's
TIInventoryTile.rebuildSlotMapbefore the inventory trait.
Exit condition: built-in behavior is implemented in Java while the established runtime composition mechanism remains stable.
Status: complete. All built-in tile traits are Java. The client pair required narrowly extending registerJavaTrait
for a registered Java trait extending another registered Java trait, explicit Java field accessors, and transient
runtime-only fields that must not participate in generated copyFrom. The generated runtime interfaces and class
cache remain reference-identical; pass-through-interface coverage remains green.
Phase 6 — Convert multipart core and microblocks
Status: runtime behavior for core code, factories, ordinary helpers and common/face/corner/edge/post/hollow/occlusion microblock traits is Java. The remaining Scala trait declarations retain inheritance, state/accessor and super-call contracts. Their final replacement belongs behind the consumer-adoption gate; abstract Java mixins and side filtering alone do not replace multiple-trait inheritance metadata. The checklist below records per-conversion obligations, not an instruction to repeat completed ports; completed per-type evidence is in the history.
- Add characterization coverage for each subsystem immediately before its conversion.
- Convert the central tile, part, registry, placement, rendering, networking, scheduler, and microblock code in dependency order.
- Treat NBT, packets, registration, client/server separation, and rendering state as compatibility-critical.
- Keep recipe scan order and output semantics stable while replacing exception-based early exits.
- Check source and binary compatibility at each public boundary.
Exit condition: normal runtime implementation is Java; any remaining Scala is isolated to compatibility or ASM support.
Phase 7 — Port the ASM subsystem last
- Freeze structural and behavioral fixtures for generated classes before changing the compiler.
- Translate the existing compiler/generator into readable Java while preserving emitted class shapes and cache keys.
- Evaluate GTNHLib ASM helpers where they reduce boilerplate without altering semantics.
- Keep Scala-trait registration while deprecated bridges or supported external Scala traits require it.
- Allow abstract Java mixins whose base type is abstract, skipping their abstract members instead of rejecting the
class. Required before any Java mixin over
Microblockis possible. See Phase 10. - Implement
@SideOnlymember stripping on the Java mixin path, matching whatlistSideOnlydoes for Scala signatures. Required before a client-only Java mixin method is safe on a dedicated server. See Phase 10. - Run ProjectRed's
LightMicroblockexternal Scala-trait fixture and pass-through-interface fixtures against the ported generator. - Decide separately whether a future generator design can avoid custom bytecode generation. Do not couple that experiment to the source-language port.
Exit condition: the composition subsystem is maintainable Java with behavior and generated ABI verified against the reference.
Pre-merge cleanup
- Relocate Java sources from
src/main/scalatosrc/main/javaonce their remaining Scala dependencies no longer require joint compilation; document any compatibility/ASM Java sources that must stay in the joint source set. - Revisit
compileScala.scalaCompileOptions.force = trueafter relocation and keep it only if generated-version or remaining joint-compilation inputs still require it. - Refresh
README.mdfor the Java-first codebase while retaining Scala IDE guidance for any sources that remain. - Move the durable migration documents under
docs/migration/and decide whether the historical running plan is archived or removed before merge. - Complete and record the relevant checks in
JAVA_MIGRATION_MANUAL_CHECKS.mdagainst a representative client and GTNH pack before release.
Exit condition: source layout, contributor guidance and durable migration documentation match the codebase being merged, and all release-required manual checks have recorded results.
Phase 8 — Remove FMP's Scala implementation and dependencies after consumer migration
Status: planned, gated by consumer adoption. The inventory showed shipping mods linking against trait helpers, companion singletons and Scala descriptors, plus ProjectRed registering an external Scala trait. The first release therefore delivers the Java API with compatibility retained. Complete Scala removal from FMP follows migration of those integrations and adoption of their released versions in the target GTNH pack.
This removes FMP's own Scala requirement. Other mods may still use Scala internally; they need not be converted to Java. Consumer migration also does not remove FMP's internal dependencies automatically: retained built-in trait inputs, storage, models and compiler support must be replaced or retired before their dependency can be dropped.
- Measure actual downstream use of Scala descriptors, Scala traits, object singletons, and
$classhelpers. - Close every legacy-use row in the Phase 10 adoption ledger against the actual target-pack release jars, including reflection, mixin targets and compiler-emitted helper calls. Retire or replace unmodifiable consumers.
- Publish and complete a deprecation window if compatibility policy permits removal.
- Replace or retire FMP's remaining Scala storage, trait inputs and model shells; remove bridges and signature decoding only after their external and internal users are gone, in a release allowed to break the old ABI.
- Remove FMP's Scala compilation and compiler/runtime dependencies; verify no FMP-owned Scala references remain in shipped classes, descriptors, generated classes or runtime loading paths.
- Validate the resulting artifact with the migrated consumer jars, old-world loading, packets, client rendering and dedicated-server behavior before release.
Exit condition: FMP builds and runs without its own Scala requirement, and the supported target-pack integrations pass against that artifact. Any unfinished gate keeps the corresponding compatibility support in place.
Phase 9 — Deprecate the Scala-shaped API and mark the internal boundary
Status: in progress: all ten Phase 9.1 table entries and Phase 9.2 are complete; broader extension/API guidance remains.
This phase does not redesign the API. A surface audit against all 28 consumer checkouts found the
published API is already largely Java-shaped: TMultiPart is an ordinary abstract Java class with conventional
lifecycle, render, NBT, and packet methods, and IPartFactory2, IPartConverter, IMicroMaterial, PartMap,
RedstoneInteractions, and the occlusion interfaces need no replacement. The remaining problems are a short list of
signatures that still carry Scala types, and a boundary problem that is larger than it looks.
The governing rule is: deprecate only where a replacement already exists or is added in the same change. A deprecation with nowhere to go is noise a consumer cannot act on. Bridge lifetime follows consumer adoption and the removal policy. Adding the Java sibling and marking the Scala-shaped entry deprecated is one change, not two.
Apply siblings, deprecations and internal-marker javadoc together when the relevant API is being changed. Most implementations are already Java; remaining work is at the specific boundaries below. Keep unrelated API changes separate from mechanical compiler extraction, and do not add a deprecation without its replacement.
9.1 — Scala-typed signatures
Each row keeps its existing descriptor for binary compatibility and gains a Java-shaped sibling. The registry's
existing Java registerParts overload still required Scala during javac resolution; registerPartFactory avoids it.
| Deprecate | Java-shaped replacement | Notes |
|---|---|---|
TileMultipart.partList(): scala.collection.Seq |
jPartList(): java.util.List |
Getter deprecated; captured-sequence view documented in the guide and compiling example |
MultiPartRegistry.registerParts(IPartFactory2, scala.collection.Seq) and companion entry |
registerPartFactory(IPartFactory2, String...) |
Implemented over existing Java registration; distinct name compiles without Scala. Guide and example also cover the retained Boolean/function adapters and their migration |
MicroMaterialRegistry.getIdMap(): scala.Tuple2[] |
materialCount(): int plus existing materialName(int) and getMaterial(int) |
Implemented with static/companion deprecation; guide and compiling example. Consumer release/adoption remains pending |
TileMultipart.operate(scala.Function1<TMultiPart, BoxedUnit>) |
forEachPart(java.util.function.Consumer<TMultiPart>) |
Implemented through the legacy virtual hook; preserves captured traversal, detached-part filtering and callback failures. Lifecycle still calls operate |
TileMultipart.occlusionTest(scala.collection.Seq, TMultiPart) |
testOcclusion(Collection<? extends TMultiPart>, TMultiPart) |
Implemented with an input snapshot and legacy virtual dispatch, including generated partial occlusion; guide. Distinct Java name avoids Scala overload resolution |
NormalOcclusionTest.apply(scala.collection.Traversable<Cuboid6>, scala.collection.Traversable<Cuboid6>) and companion entry |
testBoxes(Iterable<? extends Cuboid6>, Iterable<? extends Cuboid6>) |
Implemented over the existing Java loop; both inputs snapshotted before geometry tests. Guide and compiling example. ForgeRelocationFMP/OpenComputers adoption remains pending |
TileMultipart.loadParts(scala.collection.Iterable) |
loadPartList(Collection<TMultiPart>) |
Implemented through the legacy virtual hook; binding, world notifications, input iteration and partial failure retained. Loading guide |
TileMultipart.partList_$eq(scala.collection.Seq) |
setPartList(List<TMultiPart>) |
Implemented through the legacy setter; Java list copied without binding, null sentinel supported. GuideNH's old reflective name remains supported |
TileMultipart.renderID() / renderID_$eq(int) and companion entries |
getRenderID() / setRenderID(int) |
Implemented over the same global value; four legacy entries deprecated. Guide and compiling example. Reading does not initialize the client renderer |
TileMultipart.getOrConvertTile2(): scala.Tuple2<TileMultipart, Object> and companion entry |
getOrConvertTileResult(World, BlockCoord): TileConversionResult with getTile() / isConverted() |
Implemented over the retained tuple path; guide and compiling example distinguish existing tiles, uninstalled placeholders and failed conversion. No audited direct tuple caller |
Explicitly not renamed: TMultiPart.world(), x(), y(), z(), and tile(). These carry Scala accessor naming
but no Scala type, so renaming is cosmetic churn across 27 consumers with no compatibility or performance payoff.
tile() is additionally unsafe to rename toward getTile(), which already exists with a different return type; a
same-name overload there invites a silent wrong-overload bind.
- Add the ten table rows' Java-shaped siblings over shared behavior, preserving legacy override dispatch and avoiding recursive forwarding as required by the API migration design.
- Add and document
materialCount()with existing indexed lookups; retain and deprecate bothgetIdMap()entries. - Document
jPartList()and addforEachPart(Consumer); deprecate the legacy getter/callback entries while retaining their override dispatch. JVM and generated-tile Forge cases cover the documented contract. - Add Java loading and storage assignment, with legacy override/reflection coverage, explicit ownership and lifecycle contracts, and generated server/client tile checks; guide, API index.
- Add
testOcclusion(Collection, candidate)with snapshot ownership, subtype inputs, legacy hook dispatch and generated partial-occlusion coverage; guide and compiling example. - Add direct
testBoxes(Iterable, Iterable)with eager input snapshots, ordered intersection callbacks and retained static/companion bridges; migration guide. - Add
registerPartFactory(IPartFactory2, String...), deprecate the remaining Scala sequence entries, and document initialization/factory/payload contracts with a compiling example and real Forge initialization coverage. - Add Java render-ID accessors with shared global-state/sentinel checks, retained static/companion entries and explicit client initialization versus assignment semantics; guide.
- Add the named tile conversion result, preserving both tuple descriptors and documenting placeholder versus installed-tile lifecycle; guide. All ten table entries now have replacements.
- For these table siblings, compile external Java examples with Scala excluded from the compile classpath before
finalizing names. The proposed
loadParts(Collection)overload requiredscala.collection.Iterableduring javac overload resolution; the implementedloadPartList(Collection)avoids that dependency. Apply this gate to the remaining APIs as well as checking the emitted consumer bytecode. Registration hit the same constraint withscala.collection.Seqandscala.Function2;registerPartFactorypasses this gate. - Mark the legacy table entries above
@Deprecatedwith javadoc naming a working replacement. - Confirm every original descriptor still exists in the ABI fixture after these table changes.
- Document converter registration, candidate ownership and committed lifecycle with a compiling Java example and registry/Forge characterization; guide.
- Characterize raw Java tile-trait linkage failures and document stable interface/base access with a compiling example; guide.
- Add a supported
refreshPartSlotsoperation for OpenComputers' live slot-array mutation, preserving equality, virtual bind dispatch, storage/ownership and caller-controlled notification behavior; guide. - Document custom Java tile-trait authoring with a compiling marker/trait/capability example, registration and state/lifecycle contracts, and generated server/client Forge coverage; guide.
- Document the supported API with compiling usage examples and an old-to-new migration guide. Validate Java subclasses and generated extensions on the actual Forge path, including both sides where relevant.
9.2 — Mark the internal boundary
Scala's private[multipart] compiles to public. As a result the published surface currently advertises implementation
hooks as though they were API, and a consumer author cannot distinguish addPart from addPart_do. This is a more
practical API defect than the Scala types above, and the fix is javadoc only: no rename, no descriptor change, no
deprecation, and no new dependency for a marker annotation.
Completed with a fresh 2026-09-05 consumer audit covering 28 source checkouts and active Extra Utilities compatibility:
no external calls to these FMP members were found. The installed +719 binary scan retains the same baseline.
Javadocs on the declarations below and the five matching material-registry companion bridges mark them internal;
see the API boundary guide.
TileMultipart.addPart_impl, addPart_do, remPart_impl, writeAddPart, partAdded, partRemoved, from,
copyFrom, loadFrom, setValid; MicroMaterialRegistry.setupIDMap,
calcMaxCuttingStrength, loadIcons, writeIDMap, readIDMap.
TileMultipart.operate also has no audited external callers, but remains a supported legacy override hook. It is
deprecated for callers in favor of forEachPart; lifecycle dispatch still uses it. Keep that explicit contract rather
than applying the internal-only marker to it. The new convenience method does not replace the lifecycle override hook.
getOrConvertTile2 also has no audited external callers. Its retained static/companion entries are deprecated for
callers in favor of the supported getOrConvertTileResult API; keep the explicit placeholder contract instead of
marking that lookup internal. Internal placement tuple users, including MicroblockPlacement.gtile(), still need
their own migration before Scala removal. Completing the table does not finish the broader extension API audit.
Two similar-looking members are externally load-bearing and must not be marked internal:
| Member | Consumer | Call site |
|---|---|---|
TileMultipart.bindPart |
OpenComputers | li/cil/oc/integration/fmp/PrintPart.scala:171 |
TileMultipart.internalPartChange |
ProjectRed | mrtjp/projectred/integration/gatepartrs.scala:74 |
- Add an internal-marker javadoc line to each zero-caller member listed above, including registry companion bridges.
- Leave
bindPartandinternalPartChangedocumented as supported API and add them to the consumer audit's cross-cutting map. - Do not add an annotations dependency for this; javadoc is sufficient and changes no descriptor.
The registry-wide loadIcons dispatcher is internal; IMicroMaterial.loadIcons remains a supported material
extension callback. Internal tile lifecycle hooks still participate in trait overrides and generated state copying.
This slice changes documentation only, and does not complete the broader extension-example exit condition below.
Exit condition: every Scala-typed public entry has a documented Java-shaped replacement and a deprecation pointing at it, supported extension paths have buildable examples and Forge coverage, implementation-hook members are documented as internal, and the retained ABI fixture is unchanged.
Phase 10 — Upstream consumer cleanup
Status: planned, required for final Scala removal. Individual migrations can start as soon as their supported Java replacements exist. They need not wait for all internal source conversion; their release and pack adoption block removal of the corresponding legacy surface in Phase 8.
Several consumers reach FMP through private fields, Scala-mangled names, name-only reflection, or third-party Scala traits. Each dependency requires FMP to preserve the corresponding compatibility contract during migration. Editable GTNH consumer projects can adopt the supported Java API upstream. Extra Utilities remains an active, supported consumer; an upstream rewrite is not part of this work. UtilitiesInExcess is the intended replacement, but switching the support target still needs approval and actual pack adoption. Until then, preserve Extra Utilities compatibility. Approval alone does not authorize dropping contracts still used by the supported pack's jars.
Apply the following sequence per capability:
- FMP adds a public, supported equivalent. Additive, no ABI break, safe to land at any time.
- The consumer is patched to use it and released.
- The target pack adopts the released consumer; scan its actual jar and validate the integration.
- FMP can retire the old shape once every supported user has migrated and the removal policy permits it. Feeds Phase 8.
Maintain an adoption ledger in the consumer audit: legacy member/extension/reflection use, supported Java replacement, consumer source revision, first released version using it, version actually included in the target pack, and verification evidence. Cover all audited consumers, including OpenComputers, ProjectBlue and ForgeRelocationFMP; the cleanup table below is a list of hotspots, not the complete removal checklist. A source patch or merged PR alone does not close a row, and scans must distinguish a consumer's own Scala code from its legacy FMP dependencies.
Cleanup targets
| Consumer | What FMP is currently forced to preserve | Upstream fix | Can start |
|---|---|---|---|
| ProjectRed | Runtime Scala-signature decoding and external trait registration, reached through MicroblockGenerator.registerTrait(classOf[LightMicroblock]) |
Rewrite the ~60-line LightMicroblock trait as a Java mixin registered through the registerJavaTrait path |
FMP compiler prerequisites complete; consumer rewrite and release remain |
| Schematica | Private Scala-mangled field codechicken$multipart$MultiPartRegistry$$typeMap, cast to scala.collection.mutable.Map |
MultiPartRegistry.getPartFactory(String) is implemented; guide and example. Schematica replaces map/Option lookup with the public method and retains its microblock creation/rejection policy |
FMP lookup ready; consumer patch/release/adoption pending |
| GuideNH | Companion MultipartGenerator$.MODULE$.generateCompositeTile and MicroblockGenerator$.create; partList_$eq(scala.collection.Seq); mixin into private BlockMicroMaterial.block and .meta |
Direct Java tile generation/loading, microblock creation and existing block() / meta() access are documented/tested; typed material query. Remove reflection/accessor mixins when migrating |
These FMP paths ready; broader consumer migration/release/adoption pending |
| Et Futurum Requiem | Mutable static int[] ButtonPart.metaSideMap and sideMetaMap must stay public and mutable |
ButtonPart.setOrientation(int, ForgeDirection) is implemented and documented; Et Futurum adopts it |
FMP API ready; consumer patch/release/adoption pending |
| IguanaTweaksTConstruct | Private ItemSaw.harvestLevel field name and type |
ItemSaw.setHarvestLevel(int) is implemented and documented; Iguana uses it before ForgeMicroblock post-init |
FMP API ready; consumer lifecycle/source patch, release and adoption pending |
| Galacticraft | Selects the first public method named registerMaterial without checking its signature, so FMP cannot add any overload of that name |
Call registerMaterial directly from gated compatibility code; exact-signature reflection is an interim hardening option |
Existing public API; consumer migration can start |
| UtilitiesInExcess | MicroMaterialRegistry.getIdMap(): scala.Tuple2[] |
Both call sites read only the name, so materialCount() from Phase 9.1 plus the existing materialName(int) covers them; no material object is needed |
After Phase 9.1, before it enters the pack |
| UtilitiesInExcess | Not an FMP constraint, but a shipping hazard: the server factory reads NBT key material while MaterialBasedPart.save writes mat |
Fix on the UtilitiesInExcess side; FMP must not special-case it | Immediately; no FMP change needed |
Galacticraft needs no FMP change for direct registration. As interim hardening, its reflection can check exact
parameter types; the current wrong-overload failure is silent because it sits inside an empty catch (Exception).
ProjectRed's LightMicroblock is the audited external constraint on retiring runtime Scala-signature decoding.
Its Java rewrite removes that external dependency only after the updated consumer is released into the pack;
decoder removal also requires retiring internal Scala trait inputs and resolving retained model bridges.
The rewrite has two Phase 7 prerequisites:
- Abstract Java mixins over an abstract base — complete.
registerJavaTraitnow admitsACC_ABSTRACTclasses, retains declared abstract members on the generated interface, and omits them from the static helper and concrete method metadata until a later mixin supplies an implementation. A no-argument mixin constructor may call an argument-taking base constructor; that direct call is discarded because the generated composite invokes the real base constructor before trait initialization. @SideOnlystripping on the Java path — complete.registerJavaTraitchecks both visible and invisible annotations before collecting fields, signatures or methods. Opposite-side members are omitted; an excluded no-argument constructor produces an empty$init$so generated composites remain valid without executing its body, including when Forge stripped it before registration. Current-side members retain their generated behavior.LightMicroblock.renderDynamiccan therefore remain@SideOnly(Side.CLIENT)when ProjectRed rewrites the trait to Java, even though itsaddTraitsignores theclientargument.
Both compiler prerequisites for the single-parent ProjectRed rewrite are complete. FMP's own microblock traits also use multiple Scala-trait inheritance: their implementation can move to Java helpers, while removing their Scala declarations needs a separate inheritance strategy. Removing external Scala-signature support still waits for a released ProjectRed Java rewrite.
UtilitiesInExcess is the cheapest case in the table because it is not yet in the pack. Anything fixed before it ships never becomes a compatibility obligation at all.
Its NBT key mismatch is real but currently mild, and the detail matters for whoever fixes it. loadPart is always
followed by part.load(tag) on every audited path, including BuildCraftCompat's schematic restore, so the factory's
wrong material is corrected immediately afterwards. The visible symptom is therefore one logged
Missing mapping for part with ID: per part per load, from materialID("") falling through to the missing-material
placeholder, rather than lost material data. Extra Utilities writes mat as well, in PartMicroBlock,
PartConnecting, and PartPipeJacket, so correcting the key also matches the legacy tags reached through
UtilitiesInExcess's extrautils:* aliases; there is no legacy-conversion risk in the fix.
- Land the two Phase 7 Java-mixin prerequisites with generated Java-trait fixtures.
- Provide the Java illuminated microblock example, including real Forge registration, all shape families, material/persistence/light contracts, server stripping and a headless retained client-body probe. The three example sources compile without Scala. Physical-client rendering remains a release gate.
- Land the ProjectRed
LightMicroblockJava rewrite with a fixture proving equivalent generated microblocks on both sides. - Migrate Galacticraft to direct registration in gated compatibility code; exact-signature reflection is an interim option.
- Fix the UtilitiesInExcess
mat/materialkey mismatch and itsgetIdMap()use before it enters the pack. - Add supported button-orientation mapping for Et Futurum as additive API, preserving the legacy arrays; guide and example. Consumer migration/release/adoption remain pending.
- Add the supported harvest-level setter needed by Iguana, preserving the legacy field name/type; guide and example. Iguana must finish changes before ForgeMicroblock post-init so its cached maximum sees the final saw tiers. Consumer migration/release/adoption remain pending.
- Provide Schematica's registered-factory lookup without exposing the mutable map:
getPartFactory(String), with identity/missing-name checks, exact public reflection, a compiling example and the retained private field. This does not complete Schematica's consumer migration. - Provide static Java client/server composite generation for Schematica and GuideNH, preserving exact reuse, staged loading, input traversal/failure behavior and the old companion reflection path; guide. Both consumers' release/adoption remain pending.
- Document and validate the existing Java microblock creation entry for GuideNH, including exact companion/static reflection, material trait callbacks, fresh-part/shape ownership and a compiling example. Keep physical-client validation and public-setter versus private-field override behavior explicit; guide.
- Document and validate GuideNH's direct typed material query through existing
block()/meta()accessors; guide. Consumer adoption and physical-client validation of the Java extension example remain open. - Patch those four consumers and record the released versions that no longer need the private shapes.
- Track all legacy FMP dependencies in the adoption ledger, including trait helper/companion calls and reflection outside the cleanup table; verify the released jars actually selected for the target pack.
- Only after a consumer's released version is in the pack, move its retained private shape onto the Phase 8 removal list.
Consumers can migrate and release independently while bridges keep mixed versions working. The final Scala-removal release requires coordinated pack-version selection and a closed adoption ledger; it does not require every consumer to make its source changes at once.
Exit condition: every supported consumer in the target pack uses the supported Java surface for the contracts being retired, with source, released-jar and runtime evidence. Remaining dependencies are explicit blockers for Phase 8.
Risk order
| Risk | Work |
|---|---|
| Low | Leaf utilities, value holders, straightforward packets, isolated Java API facades |
| Medium | Registries, recipes, placement logic, render helpers, schedulers, hot-loop rewrites |
| High | TileMultipart, networking/NBT compatibility, tile trait conversion, external API bridges |
| Very high | Runtime composite generator, Scala trait compatibility, removal of Scala runtime |
The work should move from low to high risk while keeping a buildable mixed-language repository. A one-shot conversion would combine source, ABI, lifecycle, data-format, and bytecode-generation failures and make regressions difficult to localize.
Characterization and compatibility test strategy
Extensive tests are both possible and recommended here. Their purpose is not to prove that the current Scala behavior is ideal; it is to make its observable behavior explicit before translation, so every later difference is intentional.
The repository uses Java-8-compatible JUnit Jupiter and the existing Forge integration runner; the current counts are in the handoff. Minecraft 1.7.10 has no modern GameTest framework, so initialization-dependent tests use this Forge harness. The dedicated server is headless and does not establish client rendering correctness.
Layer 1 — Plain JVM characterization tests
Run fast tests through the normal Gradle test task, using real implementation classes and small fakes only at world, network, or rendering boundaries. Good targets include:
- Slot masks, part lookup, occlusion and collision decisions.
- Placement grids, side transforms, microblock shape calculations, and recipe matching.
- Registry ordering, duplicate handling, missing-entry behavior, and callback order.
- Scheduler ordering and cancellation with a deterministic clock or tick source.
- Redstone connection and power matrices for all six sides and relevant masks.
- Packet codec logic that does not require a live channel.
- NBT read/write logic that can use real
NBTTagCompoundvalues without starting Forge.
Finite input spaces such as sides, slot masks, and small recipe grids should be covered exhaustively. Larger combinations should use named regression cases plus deterministic property/fuzz tests with a recorded seed. Avoid tests that merely restate the implementation or assert interactions with a large graph of mocks.
Layer 2 — Golden compatibility fixtures
Store compact fixtures under test resources when behavior has a stable serialized or structural representation:
- Canonicalized NBT trees including names, types, values, missing fields, and legacy data; do not depend on unspecified compound-key iteration order.
- Exact packet bytes for representative updates and malformed/truncated input behavior.
- Part identifiers, registration order, and conversion results.
- Public/protected JVM descriptors and required Scala bridge entry points.
- Generated tile superclass, interfaces, fields, methods, access flags, and cache identity. Compare structure and behavior rather than raw class bytes or constant-pool order.
- Small precompiled Java and Scala consumer fixtures built once against the reference dev jar, then loaded against the migrated jar to catch binary linkage failures that source recompilation would hide.
Golden fixtures must be reviewed rather than regenerated automatically when they fail. Otherwise an accidental change can silently redefine the baseline.
Layer 3 — Real Forge integration tests
Use a dedicated test mod/source set launched in an actual Forge environment for behavior coupled to FML, the classloader, worlds, or networking. The harness should fail the Gradle task or process when an assertion fails. It should cover:
- Client and dedicated-server startup, mod lifecycle order, mixin selection, and side-only class safety.
- Registration and creation of representative dynamic tile combinations.
- Add/remove part sequences, ticking, scheduled ticks, redstone neighbor updates, and chunk unload/reload.
- Save with the reference implementation and load with the migrated implementation.
- Server-to-client part description and incremental update packets.
- Loading representative downstream binaries compiled against the reference API.
Keep this harness narrow: a tiny test mod and deterministic scenarios are preferable to a general automation framework. Run server cases routinely; reserve the slower client cases for changes that touch rendering, client packets, or side-specific initialization.
Layer 4 — Irreducible manual and performance checks
Some behavior remains poorly captured by assertions in a legacy client. Retain focused manual checks for visual correctness, selection boxes, particles, input interaction, resource reload, and third-party rendering compatibility. Screenshot comparisons can help, but GPU and renderer differences make them unsuitable as the only oracle.
Performance fixes require the same CPU/allocation scenario before and after the change. Functional tests can prove equivalent results, but they cannot prove reduced allocations or better tick time.
Coverage rule
Coverage percentage is not the goal. The required evidence for an area is:
- Normal behavior, edge cases, invalid input, ordering, and state transitions are represented.
- Compatibility-sensitive bytes, NBT, identifiers, descriptors, and generated interfaces are frozen where applicable.
- At least one real Forge scenario covers code that depends on lifecycle or world state.
- The tests demonstrably pass against the old Scala area before any Java replacement begins.
- The same tests pass unchanged against the Java implementation, except for separately documented bug fixes.
Decisions
Resolved from the consumer audits:
- All 27 binaries in the target pack must keep working without recompilation; UtilitiesInExcess is an additional forward-compatibility target.
- Third-party Scala trait registration is public in practice: ProjectRed registers
LightMicroblock. - The first-release milestone is a complete, documented Java API with supported legacy binaries still working. Removing FMP's Scala implementation and dependencies is the final milestone, after consumer releases are adopted in the target pack and FMP's internal Scala users are retired.
Still open, and only to be decided when they become necessary:
- How long deprecated Scala bridges remain supported after the initial Java release. Phase 9 fixes the policy for adding deprecations (only alongside a replacement); the removal window is still open and is gated by Phase 10.
- Which known bugs are intentionally fixed during the port rather than after compatibility is established.
- Which GTNHLib release is the minimum supported version once code actually needs it.
- Whether runtime tile composition merits redesign after the Java port is stable.
Effort tracking
Track remaining work by completed compatibility gates, not the retired pre-audit calendar estimates:
- Complete the supported Java entry points, extension contracts, documentation and buildable examples.
- Prove the consumer migration paths and record released-version adoption in the target pack.
- Retain tested Scala shells until their removal gates pass; extract further behavior only for a concrete benefit.
- Retire remaining internal Scala users and dependencies after adoption; complete the applicable pre-merge, client, packaged-pack and performance validation with recorded results.
Reflection/member guards, core ordering/data fixtures, the first profiles and built-in tile-trait ports are complete. Scala removal is gated by the compatibility evidence above, rather than by a remaining-source-line target.
Findings log
The dated evidence log is archived in docs/migration/HISTORY.md, including reference revisions, test results, ABI comparisons and resolved regressions. Add future per-target findings there. The working handoff carries the current baseline and next target.