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) - } -}