diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index ae50a8a..6727415 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -27,10 +27,10 @@ The binary and source-level consumer audits are complete. Phase 0 still lacks br pre-optimization profile. The low-coupling Phase 3 queue is complete; before entering measured hot-path or generated tile work, the immediate milestone is the audit-derived compatibility gate below. -That audit found one existing regression on this branch: Schematica 1.12.6 reflects the original private +That audit found and this branch has now repaired one existing regression: Schematica 1.12.6 reflects the original private `MultiPartRegistry$` field `codechicken$multipart$MultiPartRegistry$$typeMap` and casts it to a Scala mutable map. The -Java registry port moved the state to a private Java `HashMap` on `MultiPartRegistry`, so Schematica's integration -currently disables itself. Restoring and testing that compatibility view is the next task. +companion again exposes that exact private field as a live view of the canonical Java `HashMap`, and a test performs +Schematica's reflective lookup. The remaining source-only member guards are next. Phase 1 has not properly started: GTNHLib is present but was added at `api` scope to satisfy a test compile rather than because a migration change needed it, and no Forge mod dependency was declared. Revisit that entry only when a @@ -297,7 +297,7 @@ Exit condition: leaf code is Java and no longer generates avoidable Scala closur Do this before another source conversion: -- [ ] Restore Schematica's reflective `MultiPartRegistry$` type-map view with its exact field name and Scala mutable-map +- [x] 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/meta` mixin fields, Et Futurum's mutable `ButtonPart.metaSideMap/sideMetaMap`, Iguana's `ItemSaw.harvestLevel`, and Galacticraft's name-only @@ -393,7 +393,7 @@ The work should move from low to high risk while keeping a buildable mixed-langu 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 now has a Java-8-compatible JUnit Jupiter suite and a small Forge integration runner. The current -baseline is 125 plain-JVM tests and 22 Forge server tests. Minecraft 1.7.10 does not provide the modern GameTest +baseline is 126 plain-JVM tests and 22 Forge server tests. Minecraft 1.7.10 does not provide the modern GameTest framework, so game-dependent coverage continues through the existing narrow Forge harness rather than a second test framework. @@ -476,7 +476,7 @@ The old pre-audit calendar ranges are retired. They mixed the Java source port, work, and optional Scala-runtime removal into estimates that are no longer useful. Track the remaining work by completed compatibility gates instead: -1. Repair the Schematica registry view and finish the source-only reflection/member guards. +1. Finish the source-only reflection/member guards; the Schematica registry view is repaired. 2. Freeze tile ordering, lifecycle, NBT, packet, and generator behavior with compact representative fixtures. 3. Capture the first focused CPU/allocation baseline and port the complete redstone unit. 4. Convert a representative generated tile-trait group, then the remaining trait groups. @@ -594,3 +594,13 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec - Dropped the three static forwarders Scala generated for inherited `TileEntitySpecialRenderer` members, the same category as the twelve `KeyBinding` forwarders dropped from `ControlKeyHandler`. - Dropped an unused `ThreadSafeISBRH` import. It annotated nothing and appears nowhere else; if the intent was to mark this ISBRH thread-safe for Angelica then the annotation was never applied in the reference either, which is worth checking independently of the migration. - Recorded that this port adds nothing to the automated suites and everything to the manual checklist. Static and dynamic rendering, the breaking overlay and the render id all need a client. + +### 2026-08-27 + +- Reproduced Schematica 1.12.6's exact `ReflectionHelper` lookup of the private + `MultiPartRegistry$.codechicken$multipart$MultiPartRegistry$$typeMap` field. The test failed against the existing Java + port with `UnableToFindFieldException`, matching Schematica's silent integration-disable path. +- Restored the exact private field name and `scala.collection.mutable.Map` descriptor as Scala's live wrapper over the + canonical Java registry map. The wrapper adds no copied state or synchronization path. +- Proved through the reflected Scala view that Schematica can resolve a factory and that the Java registry immediately + sees the same entry. The focused registry suite and all 126 plain-JVM tests pass. diff --git a/JAVA_MIGRATION_CONSUMER_AUDIT.md b/JAVA_MIGRATION_CONSUMER_AUDIT.md index 708e785..d7afa9c 100644 --- a/JAVA_MIGRATION_CONSUMER_AUDIT.md +++ b/JAVA_MIGRATION_CONSUMER_AUDIT.md @@ -351,9 +351,9 @@ effective public API**. A Java `Map` replacement will silently disable Schematic is retained or Schematica is patched and deployed first. This is a higher-risk constraint than the constant-pool inventory alone shows. -**Current branch:** this is already broken by the Java `MultiPartRegistry` port. The canonical state is now a private -Java `HashMap` on `MultiPartRegistry`, while `MultiPartRegistry$` has no field with the reflected name. Restoring and -testing a compatibility view is the immediate migration blocker. +**Current branch:** the Java port initially broke this lookup. `MultiPartRegistry$` now retains the exact private field +as a Scala mutable-map wrapper backed by the canonical Java `HashMap`, and a regression test performs Schematica's +lookup and verifies that both views resolve the same factory. ### UtilitiesInExcess — future replacement, already a substantial consumer diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index c3777ec..6ceb3f7 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -573,16 +573,18 @@ winning, registration still refuses to run once the registry is closed and still duplicate part id still throws, and `writePartID` and `getModContainer` still throw rather than returning null for an unknown name. -### Known reflective regression, discovered 2026-08-27 +### Resolved reflective regression, discovered and fixed 2026-08-27 The full consumer-source audit found a constraint that the bytecode scan missed. Schematica 1.12.6 obtains the private field `codechicken$multipart$MultiPartRegistry$$typeMap` from `MultiPartRegistry$` and casts its value to `scala.collection.mutable.Map`. The Java port moved the canonical map to a private `java.util.HashMap` on -`MultiPartRegistry`, and `MultiPartRegistry$` exposes no compatibility field, so Schematica currently disables its FMP +`MultiPartRegistry`, and `MultiPartRegistry$` exposed no compatibility field, so Schematica disabled its FMP integration during reflective initialization. -This is not an accepted divergence. Restore an exact reflective compatibility view backed by the canonical Java -registry state and add a test reproducing Schematica's field lookup before continuing the migration. +`MultiPartRegistry$` now restores that exact private field and descriptor using Scala's existing live Java-map wrapper; +the Java map remains the single source of truth. `MultiPartRegistryCharacterizationTest` performs Schematica's actual +`ReflectionHelper` lookup, casts the result to `scala.collection.mutable.Map`, and proves mutations reach the Java +registry. No separate registry or synchronization path was added. ### Preserved Scala-typed descriptors @@ -627,6 +629,15 @@ overloads build. run, and the suite resolves a part registered through each. - Error paths that log remain uncovered headless, as before, because the multipart logger is null until preInit. +### 2026-08-27 compatibility repair validation + +- The new Schematica reflection case failed first with `UnableToFindFieldException`, then passed with the compatibility + field present. +- `javap -private -s` confirms the field is private/final with descriptor `Lscala/collection/mutable/Map;`. +- `MultiPartRegistryCharacterizationTest`: 6 tests, 0 failures, 0 errors. +- Complete plain-JVM suite: 126 tests, 0 failures, 0 errors. +- Clean `spotlessApply checkstyleTest build`: passing. + ## 2026-08-14 — TileMultipart Java port The central tile. Twelve jars reference it and about twenty-five of its members. `TileMultipartClient` was split into diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index 09aa186..393cdbd 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -57,7 +57,7 @@ awk -F'"' '/ codechicken$multipart$MultiPartRegistry$$typeMap = JavaConversions + .mapAsScalaMap(MultiPartRegistry.typeMapBacking()); + private MultiPartRegistry$() {} @Deprecated diff --git a/src/main/scala/codechicken/multipart/MultiPartRegistry.java b/src/main/scala/codechicken/multipart/MultiPartRegistry.java index 516a965..551181c 100644 --- a/src/main/scala/codechicken/multipart/MultiPartRegistry.java +++ b/src/main/scala/codechicken/multipart/MultiPartRegistry.java @@ -87,6 +87,10 @@ public final class MultiPartRegistry { private MultiPartRegistry() {} + static Map typeMapBacking() { + return typeMap; + } + /** * Register a part factory with an array of types it is capable of instantiating. Must be called before postInit. * diff --git a/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java b/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java index 89cea3b..4593df5 100644 --- a/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java +++ b/src/test/java/codechicken/multipart/MultiPartRegistryCharacterizationTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -13,17 +14,22 @@ import java.util.NoSuchElementException; import net.minecraft.block.Block; import net.minecraft.block.material.Material; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import org.junit.jupiter.api.Test; +import codechicken.lib.data.MCDataInput; import codechicken.lib.vec.BlockCoord; import codechicken.multipart.MultiPartRegistry.IPartConverter; +import codechicken.multipart.MultiPartRegistry.IPartFactory2; +import cpw.mods.fml.relauncher.ReflectionHelper; +import scala.Option; /** * Part registration needs FML's active mod container, so it cannot run headless; that half of the registry is - * characterized in the Forge server suite instead. Converter registration and dispatch touch neither FML nor the - * logger, so they are covered here. + * characterized in the Forge server suite instead. Converter dispatch and the Schematica reflection contract touch + * neither FML nor the logger, so they are covered here. */ class MultiPartRegistryCharacterizationTest { @@ -37,6 +43,30 @@ class MultiPartRegistryCharacterizationTest { assertThrows(NoSuchElementException.class, () -> MultiPartRegistry.getModContainer("test:nosuchpart")); } + @Test + @SuppressWarnings("unchecked") + void schematicaCanReadTheLiveScalaTypeMap() throws IllegalAccessException { + Field moduleField = ReflectionHelper.findField(MultiPartRegistry$.class, "MODULE$"); + Field typeMapField = ReflectionHelper + .findField(MultiPartRegistry$.class, "codechicken$multipart$MultiPartRegistry$$typeMap"); + Object module = moduleField.get(MultiPartRegistry$.class); + scala.collection.mutable.Map reflectedTypeMap = (scala.collection.mutable.Map) typeMapField + .get(module); + + String type = "test:schematica_reflection"; + TestPart part = new TestPart(); + TestFactory factory = new TestFactory(part); + reflectedTypeMap.put(type, factory); + try { + Option reflectedFactory = reflectedTypeMap.get(type); + assertFalse(reflectedFactory.isEmpty()); + assertSame(factory, reflectedFactory.get()); + assertSame(part, MultiPartRegistry.loadPart(type, new NBTTagCompound())); + } finally { + reflectedTypeMap.remove(type); + } + } + @Test void convertersAreDispatchedPerBlockAndFirstNonNullWins() { Block block = new Block(Material.rock) {}; @@ -110,4 +140,23 @@ class MultiPartRegistryCharacterizationTest { return "test:converted"; } } + + private static final class TestFactory implements IPartFactory2 { + + private final TMultiPart part; + + private TestFactory(TMultiPart part) { + this.part = part; + } + + @Override + public TMultiPart createPart(String name, NBTTagCompound nbt) { + return part; + } + + @Override + public TMultiPart createPart(String name, MCDataInput packet) { + return part; + } + } }