From cd6420f7f280c1d7af3b6954fd362eb92f33a3e2 Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 14 Aug 2026 20:12:22 +0200 Subject: [PATCH] refactor: port TCuboidPart to Java Replaces TCuboidPart.scala with Java implementations of TCuboidPart, JCuboidPart and the TCuboidPart$class bridge. The canonical behavior now lives in three statics on JCuboidPart; the deprecated bridge delegates to them, since old Scala forwarders call the statics directly and delegating to the instance methods would recurse. Every reference descriptor is preserved, verified by diffing javap -s output against the reference dev jar. ForgeRelocationFMP, OpenComputers, ProjRed and ProjectBlue all call these four statics, and a frozen Scala 2.11.5 consumer compiled against the reference jar loads and runs against the port. Scala trait linearization cannot be reproduced by a Java interface: a superclass method beats an interface default on the JVM, so a recompiled Scala class mixing TCuboidPart into a TMultiPart subclass silently gets TMultiPart's empty implementations. Declaring the methods default would not help, so they stay abstract, matching the reference bytecode. Existing binaries are unaffected. Microblock, the one in-repo case, now extends TMultiPart with TCuboidPart and forwards explicitly. See JAVA_MIGRATION_DIVERGENCES.md. The three files live under src/main/scala because compileJava runs before compileScala and they still reference TMultiPart. Validated with 33 plain-JVM tests, a clean build, and the Java 8 Forge dedicated-server suite. --- JAVA_MIGRATION.md | 5 ++ JAVA_MIGRATION_DIVERGENCES.md | 71 +++++++++++++++++++ .../codechicken/microblock/Microblock.scala | 15 +++- .../codechicken/multipart/JCuboidPart.java | 53 ++++++++++++++ .../multipart/TCuboidPart$class.java | 33 +++++++++ .../codechicken/multipart/TCuboidPart.java | 28 ++++++++ .../codechicken/multipart/TCuboidPart.scala | 42 ----------- 7 files changed, 204 insertions(+), 43 deletions(-) create mode 100644 src/main/scala/codechicken/multipart/JCuboidPart.java create mode 100644 src/main/scala/codechicken/multipart/TCuboidPart$class.java create mode 100644 src/main/scala/codechicken/multipart/TCuboidPart.java delete mode 100644 src/main/scala/codechicken/multipart/TCuboidPart.scala diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 3945be6..9719887 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -436,3 +436,8 @@ These ranges were formed before designing the characterization suite and should - Removed both speculative bridges the inventory proved dead: the four `IDWriter` Scala function accessors and the whole `JPartialOcclusion$class` helper, along with the `ReferenceScalaPartialOcclusion` fixture that only existed to verify the helper. `JPartialOcclusion` itself and both of its method descriptors are unchanged. - `IDWriter` now selects a carrier width instead of storing Scala closures, removing the per-call `Integer` boxing and `Function1`/`Function2` allocation that the first port had preserved. The nine encoding cases still pass unchanged. - Established the working rule for the remaining phases: check `JAVA_MIGRATION_ABI_INVENTORY.md` before writing a bridge, rather than writing one reflexively for every converted file. +- Ported `TCuboidPart`, `JCuboidPart` and `TCuboidPart$class` to Java, the first conversion of a trait whose `$class` helper is genuinely load-bearing. All reference descriptors are preserved, verified by diffing `javap -s` against the reference dev jar, and a frozen Scala 2.11.5 consumer whose forwarders call all four statics loads and runs against the port. +- Found the first structural limit of the migration: Scala trait linearization cannot be reproduced by a Java interface, because a superclass method always beats an interface default on the JVM. Recompiled Scala consumers that mix `TCuboidPart` into a `TMultiPart` subclass now silently get `TMultiPart`'s empty implementations. Binary compatibility is unaffected, and `CuboidPartCharacterizationTest` carries the regression guard, but this applies to every remaining trait that overrides `TMultiPart` members and should be assumed for `TFacePart`, `TNormalOcclusion`, `TIconHitEffects` and `TItemMultiPart` as well. +- Recorded the consequence for consumers: Scala code that recompiles must extend `JCuboidPart` or declare the overrides itself. This belongs in the release notes for the first Java release, not only in the divergence log. +- Found a build-order constraint: `compileJava` runs before `compileScala`, so ported Java that still references Scala types must live under `src/main/scala` for joint compilation, as `minecraft/McBlockPart.java` already does. Files can move to `src/main/java` once their Scala dependencies are ported. +- Established the fixture recipe for the remaining load-bearing bridges: build a reference dev jar at the pre-port commit, compile the consumer with Scala 2.11.5 under Java 8 with `-target:jvm-1.8` against that jar plus the project compile classpath, then store the class base64-encoded under `src/test/resources/compat/`. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index 2dd86f4..0d436b3 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -85,3 +85,74 @@ Accepted divergence: calling `write` or `read` before `setMax` now throws `Illeg - Complete plain-JVM suite: 27 tests, 0 failures, 0 errors. - Clean `checkstyleTest build` with Spotless re-enabled: passing. - Java 8 Forge dedicated-server suite: 2 tests, 0 failures, 0 errors. + +## 2026-08-14 — TCuboidPart Java port + +### Observable behavior + +No known divergence for any part that reaches the cuboid implementation. `getSubParts` still yields one +`IndexedCuboid6` holding a boxed `Integer` 0 and a copy of the bounds, `getCollisionBoxes` still yields the bounds +instance itself, and `drawBreaking` performs the same pipeline setup and `BlockRenderer.renderCuboid` call. + +The concrete `java.lang.Iterable` implementation changes from a Scala `Seq` wrapped by `JavaConversions` to +`Collections.singletonList`. Both are single-element and immutable; only code inspecting the runtime class or +attempting mutation could observe the difference. + +### Accepted divergence: Scala trait linearization is not reproducible + +This is the significant one, and it affects source compatibility rather than binary compatibility. + +A Scala trait that overrides superclass methods wins over that superclass. A Java interface loses to it: on the JVM a +superclass method always beats an interface default. `TCuboidPart` is now a Java interface, so a **recompiled** Scala +class written as `class Foo extends TMultiPart with TCuboidPart` silently inherits `TMultiPart`'s empty `getSubParts` +and `getCollisionBoxes` instead of the cuboid ones. It compiles cleanly and fails at runtime with missing collision and +selection boxes. + +Declaring the interface methods as `default` would not help, because the superclass still wins. They are therefore left +abstract, which also matches the reference bytecode exactly. + +Consequences: + +- Already compiled binaries are unaffected. Their forwarders call `TCuboidPart$class`, which is retained. +- Java consumers are unaffected. They extend `JCuboidPart`, which carries the overrides. +- Scala consumers that recompile must extend `JCuboidPart`, or declare the three overrides themselves and delegate to + `JCuboidPart.subParts`, `JCuboidPart.collisionBoxes` and `JCuboidPart.renderBreaking`. + +`Microblock` was the one in-repo case and now does exactly that. It changed from `extends TCuboidPart` to +`extends TMultiPart with TCuboidPart` plus the three explicit forwarders, because a Java interface can no longer supply +`TMultiPart` as its superclass. + +`CuboidPartCharacterizationTest.cuboidBehaviorWinsOverTheEmptyTMultiPartDefaults` is the regression guard for this. + +### Supported JVM API + +- Preserved every reference descriptor on `TCuboidPart`, `JCuboidPart` and `TCuboidPart$class`, verified by diffing + `javap -s` output against the reference dev jar built at `246daff`. +- Retained `TCuboidPart$class` with all four statics, since ForgeRelocationFMP, OpenComputers, ProjRed and ProjectBlue + call them. Its bodies cannot delegate to the instance methods, because old forwarders call the statics and would + recurse; they delegate to the canonical statics on `JCuboidPart` instead. +- Kept `@SideOnly(Side.CLIENT)` on `TCuboidPart.drawBreaking` and `TCuboidPart$class.drawBreaking`, matching the + reference. As in the reference, `Microblock.drawBreaking` is deliberately not annotated, so on a dedicated server it + survives while its callee is stripped. That hazard is unchanged; the method is never invoked server-side. +- Added `JCuboidPart.subParts`, `JCuboidPart.collisionBoxes` and `JCuboidPart.renderBreaking` as public statics, plus a + private constructor on `TCuboidPart$class`. Both are additive. + +### Build layout + +`compileJava` runs before `compileScala`, so Java sources under `src/main/java` cannot reference types that are still +Scala. All three files live under `src/main/scala/codechicken/multipart/` for joint compilation, matching the existing +`minecraft/McBlockPart.java`. They can move to `src/main/java` once `TMultiPart` is ported. + +### Compiler artifacts + +None removed. The Scala implementation generated no closure classes for this trait. + +### Validation + +- `CuboidPartCharacterizationTest`: 5 tests, 0 failures, 0 errors, unchanged from the Scala baseline. +- `TCuboidPartBinaryCompatibilityTest`: 1 test, 0 failures, 0 errors, loading a Scala 2.11.5 class compiled against the + reference dev jar whose forwarders call all four `$class` statics. +- Complete plain-JVM suite: 33 tests, 0 failures, 0 errors. +- Clean `spotlessApply checkstyleTest build`: passing. +- Java 8 Forge dedicated-server suite: 2 tests, 0 failures, 0 errors. The `RenderBlocks` side warning and the `antlr4` + jar parse errors in that log are present on the pre-port baseline as well. diff --git a/src/main/scala/codechicken/microblock/Microblock.scala b/src/main/scala/codechicken/microblock/Microblock.scala index 34b611a..2c4daf9 100644 --- a/src/main/scala/codechicken/microblock/Microblock.scala +++ b/src/main/scala/codechicken/microblock/Microblock.scala @@ -1,5 +1,6 @@ package codechicken.microblock +import net.minecraft.client.renderer.RenderBlocks import net.minecraft.nbt.NBTTagCompound import codechicken.lib.lighting.LightMatrix import codechicken.microblock.MicroMaterialRegistry._ @@ -13,6 +14,8 @@ import codechicken.multipart.TIconHitEffects import net.minecraft.util.IIcon import net.minecraft.block.Block import codechicken.multipart.TCuboidPart +import codechicken.multipart.JCuboidPart +import codechicken.multipart.TMultiPart import net.minecraft.util.MovingObjectPosition import net.minecraft.entity.player.EntityPlayer import codechicken.multipart.JPartialOcclusion @@ -31,9 +34,19 @@ import net.minecraft.init.Blocks import cpw.mods.fml.relauncher.{Side, SideOnly} import codechicken.lib.render.BlockRenderer.BlockFace -abstract class Microblock(var material: Int = 0) extends TCuboidPart { +abstract class Microblock(var material: Int = 0) + extends TMultiPart + with TCuboidPart { var shape: Byte = 0 + // TCuboidPart is a Java interface, so these must be declared explicitly or TMultiPart's empty versions win. + override def getSubParts = JCuboidPart.subParts(this) + + override def getCollisionBoxes = JCuboidPart.collisionBoxes(this) + + override def drawBreaking(renderBlocks: RenderBlocks) = + JCuboidPart.renderBreaking(this, renderBlocks) + def microClass: MicroblockClass def getType = microClass.getName diff --git a/src/main/scala/codechicken/multipart/JCuboidPart.java b/src/main/scala/codechicken/multipart/JCuboidPart.java new file mode 100644 index 0000000..0f1cf34 --- /dev/null +++ b/src/main/scala/codechicken/multipart/JCuboidPart.java @@ -0,0 +1,53 @@ +package codechicken.multipart; + +import java.util.Collections; + +import net.minecraft.client.renderer.RenderBlocks; + +import codechicken.lib.raytracer.IndexedCuboid6; +import codechicken.lib.render.BlockRenderer; +import codechicken.lib.render.CCRenderState; +import codechicken.lib.render.uv.IconTransformation; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Translation; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** Java class implementation of {@link TCuboidPart}, and the canonical home of its behavior. */ +public abstract class JCuboidPart extends TMultiPart implements TCuboidPart { + + @Override + public Iterable getSubParts() { + return subParts(this); + } + + @Override + public Iterable getCollisionBoxes() { + return collisionBoxes(this); + } + + @SideOnly(Side.CLIENT) + @Override + public void drawBreaking(RenderBlocks renderBlocks) { + renderBreaking(this, renderBlocks); + } + + public static Iterable subParts(TCuboidPart part) { + return Collections.singletonList(new IndexedCuboid6(0, part.getBounds())); + } + + public static Iterable collisionBoxes(TCuboidPart part) { + return Collections.singletonList(part.getBounds()); + } + + @SideOnly(Side.CLIENT) + public static void renderBreaking(TCuboidPart part, RenderBlocks renderBlocks) { + TMultiPart multiPart = (TMultiPart) part; + CCRenderState state = CCRenderState.instance(); + state.resetInstance(); + state.setPipelineInstance( + new Translation(multiPart.x(), multiPart.y(), multiPart.z()), + new IconTransformation(renderBlocks.overrideBlockTexture)); + BlockRenderer.renderCuboid(part.getBounds(), 0); + } +} diff --git a/src/main/scala/codechicken/multipart/TCuboidPart$class.java b/src/main/scala/codechicken/multipart/TCuboidPart$class.java new file mode 100644 index 0000000..b19f07a --- /dev/null +++ b/src/main/scala/codechicken/multipart/TCuboidPart$class.java @@ -0,0 +1,33 @@ +package codechicken.multipart; + +import net.minecraft.client.renderer.RenderBlocks; + +import codechicken.lib.raytracer.IndexedCuboid6; +import codechicken.lib.vec.Cuboid6; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Binary bridge for Scala implementations compiled against the original trait. Their forwarders call these statics + * directly, so the bodies cannot delegate back to the instance methods without recursing. + */ +@Deprecated +public abstract class TCuboidPart$class { + + private TCuboidPart$class() {} + + public static Iterable getSubParts(TCuboidPart part) { + return JCuboidPart.subParts(part); + } + + public static Iterable getCollisionBoxes(TCuboidPart part) { + return JCuboidPart.collisionBoxes(part); + } + + @SideOnly(Side.CLIENT) + public static void drawBreaking(TCuboidPart part, RenderBlocks renderBlocks) { + JCuboidPart.renderBreaking(part, renderBlocks); + } + + public static void $init$(TCuboidPart part) {} +} diff --git a/src/main/scala/codechicken/multipart/TCuboidPart.java b/src/main/scala/codechicken/multipart/TCuboidPart.java new file mode 100644 index 0000000..44a7c69 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TCuboidPart.java @@ -0,0 +1,28 @@ +package codechicken.multipart; + +import net.minecraft.client.renderer.RenderBlocks; + +import codechicken.lib.raytracer.IndexedCuboid6; +import codechicken.lib.vec.Cuboid6; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Interface for parts that are simply a cuboid, having one bounding box. + *

+ * Implementors must extend {@link TMultiPart} and route the three overridden methods to the implementations in + * {@link JCuboidPart}, which {@link JCuboidPart} itself already does. A superclass method beats an interface default on + * the JVM, so declaring these as default methods here would silently lose to {@link TMultiPart}'s empty versions. + */ +public interface TCuboidPart { + + /** Return the bounding Cuboid6 for this part. */ + Cuboid6 getBounds(); + + Iterable getSubParts(); + + Iterable getCollisionBoxes(); + + @SideOnly(Side.CLIENT) + void drawBreaking(RenderBlocks renderBlocks); +} diff --git a/src/main/scala/codechicken/multipart/TCuboidPart.scala b/src/main/scala/codechicken/multipart/TCuboidPart.scala deleted file mode 100644 index 4b2fafa..0000000 --- a/src/main/scala/codechicken/multipart/TCuboidPart.scala +++ /dev/null @@ -1,42 +0,0 @@ -package codechicken.multipart - -import codechicken.lib.vec.Cuboid6 -import codechicken.lib.raytracer.IndexedCuboid6 -import net.minecraft.client.renderer.RenderBlocks -import codechicken.lib.render.{BlockRenderer, CCRenderState, RenderUtils} -import scala.collection.JavaConversions._ -import java.lang.Iterable -import codechicken.lib.vec.Translation -import codechicken.lib.render.uv.IconTransformation -import cpw.mods.fml.relauncher.{Side, SideOnly} - -/** Java class implementation - */ -abstract class JCuboidPart extends TCuboidPart - -/** Trait for parts that are simply a cuboid, having one bounding box. Overrides - * multipart functions to this effect. - */ -trait TCuboidPart extends TMultiPart { - - /** Return the bounding Cuboid6 for this part. - */ - def getBounds: Cuboid6 - - override def getSubParts: Iterable[IndexedCuboid6] = Seq( - new IndexedCuboid6(0, getBounds) - ) - - override def getCollisionBoxes: Iterable[Cuboid6] = Seq(getBounds) - - @SideOnly(Side.CLIENT) - override def drawBreaking(renderBlocks: RenderBlocks) { - val state = CCRenderState.instance - state.resetInstance() - state.setPipelineInstance( - new Translation(x, y, z), - new IconTransformation(renderBlocks.overrideBlockTexture) - ) - BlockRenderer.renderCuboid(getBounds, 0) - } -}