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.
This commit is contained in:
Algent committed 2026-08-14 20:12:22 +02:00
1 parent 8294240659
commit cd6420f7f2
7 files changed
+204 -43

No files matched your search

+5
View File
@@ -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/`.
+71
View File
@@ -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.
@@ -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
@@ -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<IndexedCuboid6> getSubParts() {
return subParts(this);
}
@Override
public Iterable<Cuboid6> getCollisionBoxes() {
return collisionBoxes(this);
}
@SideOnly(Side.CLIENT)
@Override
public void drawBreaking(RenderBlocks renderBlocks) {
renderBreaking(this, renderBlocks);
}
public static Iterable<IndexedCuboid6> subParts(TCuboidPart part) {
return Collections.singletonList(new IndexedCuboid6(0, part.getBounds()));
}
public static Iterable<Cuboid6> 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);
}
}
@@ -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<IndexedCuboid6> getSubParts(TCuboidPart part) {
return JCuboidPart.subParts(part);
}
public static Iterable<Cuboid6> 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) {}
}
@@ -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.
* <p>
* 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<IndexedCuboid6> getSubParts();
Iterable<Cuboid6> getCollisionBoxes();
@SideOnly(Side.CLIENT)
void drawBreaking(RenderBlocks renderBlocks);
}
@@ -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)
}
}