refactor: port TFacePart to Java

Replaces TFacePart.scala with a Java interface and retains the
TFacePart$class bridge for OpenComputers, ProjRed and ProjectBlue. The
interface is descriptor-identical to the reference; the bridge adds only a
private constructor.

This is the first converted trait where Java default methods are safe.
TMultiPart declares neither solid nor redstoneConductionMap, and neither
does anything in the microblock superclass chain, so there is no
superclass method for a default to lose to. Both members therefore become
real defaults rather than staying abstract with explicit forwarders, and
no in-repo Scala needed changing. The rule for the remaining traits is to
check that condition before choosing.

A side effect is that Java classes can now inherit these members;
McSidedMetaPart previously had to declare both just to accept the
defaults, and stays descriptor-identical.

Adds the first functional coverage of runtime class generation against a
ported type. Microblock classes are built by ASM at run time, so the
server suite now checks that a generated FaceMicroblock resolves the
inherited default and that a generated HollowMicroblock still returns its
own overrides.

Validated with 43 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite at 3 tests.
This commit is contained in:
Algent committed 2026-08-14 21:00:18 +02:00
1 parent d71a4b893d
commit 57130a6955
6 files changed
+120 -17

No files matched your search

+4
View File
@@ -447,3 +447,7 @@ These ranges were formed before designing the characterization suite and should
- Recorded a second source-only divergence: `NormalOcclusionTest` was a Scala `object`, so `NormalOcclusionTest(a, b)` apply sugar no longer compiles and recompiled Scala callers must write `NormalOcclusionTest.apply(a, b)`. Both static forwarders and the `MODULE$` singleton keep their descriptors, so binaries are unaffected.
- Kept `scala.collection.Traversable` in two descriptors, as the ABI inventory predicted. They are drained with `JavaConversions.asJavaIterator(boxes.toIterator())`, which is the one interop route callable from Java.
- Removed two more closure artifacts, `NormalOcclusionTest$$anonfun$apply$1` and its nested `$$anonfun$apply$2`, and corrected the scaladoc that claimed the test "returns true if the test fails".
- Ported `TFacePart`, the first trait where Java default methods are actually safe. `TMultiPart` declares neither `solid` nor `redstoneConductionMap`, so there is no superclass method for a default to lose to. The interface is descriptor-identical to the reference and no in-repo Scala needed changing.
- Adopted the resulting rule for the remaining traits: emit default methods only when no class in an implementor's superclass chain declares the same member, otherwise leave the member abstract and add explicit forwarders as `TCuboidPart` and `TNormalOcclusion` required.
- Made `TFacePart` inheritable from Java as a side effect. Both members were abstract on the reference interface, so `minecraft/McSidedMetaPart` had to declare them even to accept the defaults. That class stays descriptor-identical, so the change is purely additive.
- Added the first functional coverage of runtime class generation interacting with a ported type. Microblock classes are built by ASM at run time, so a plain-JVM test cannot show that a generated class resolves a Java interface default; the Forge server suite now generates a `FaceMicroblock` and asserts it inherits `redstoneConductionMap`, and generates a `HollowMicroblock` and asserts its own `0x10` and `false` still win.
+49
View File
@@ -243,3 +243,52 @@ backwards. The Java doc comments now say the test returns true when the parts ma
- Complete plain-JVM suite: 38 tests, 0 failures, 0 errors.
- Clean `spotlessApply checkstyleTest build`: passing.
- Java 8 Forge dedicated-server suite: 2 tests, 0 failures, 0 errors.
## 2026-08-14 — TFacePart Java port
### Observable behavior
No known divergence. `solid` still returns true for every side, including negative and out-of-range values, and
`redstoneConductionMap` still returns 0. `TFacePart` still extends `TSlottedPart`.
### The first trait where default methods are safe
`TMultiPart` declares neither `solid` nor `redstoneConductionMap`, and neither does anything in the microblock
superclass chain. There is therefore no superclass method for an interface default to lose to, and both members are
emitted as real Java `default` methods rather than left abstract.
This is the check to apply to every remaining trait: a default is safe only when no class in an implementor's
superclass chain declares the same member. `TCuboidPart` and `TNormalOcclusion` both failed that check and had to stay
abstract with explicit forwarders.
As a result no in-repo Scala needed changing. `FaceMicroblock` and `HollowMicroblock` already override what they care
about, and `FaceMicroblock` picks up the `redstoneConductionMap` default exactly as it previously picked up the trait
implementation.
### Improvement for Java implementors
Both members were abstract on the reference interface, so a Java class implementing `TFacePart` had to supply them even
to accept the defaults; `minecraft/McSidedMetaPart` does exactly that. Java implementors can now inherit them. This is
additive: `McSidedMetaPart` is descriptor-identical to the reference, and existing implementors that declare their own
overrides are unaffected.
### Supported JVM API
- `TFacePart` is descriptor-identical to the reference, including both members remaining public and non-static on the
interface. Changing them from abstract to default does not affect method resolution for existing implementors, which
all declare their own.
- Retained `TFacePart$class` with all three statics for OpenComputers, ProjRed and ProjectBlue. It adds only a private
constructor. Its bodies hold the defaults directly rather than dispatching through the instance, matching the
reference and avoiding recursion from old forwarders.
- The emitted class-file set for this area is unchanged; the Scala implementation generated no closure classes here.
### Validation
- `FacePartCharacterizationTest`: 4 tests, 0 failures, 0 errors, unchanged from the Scala baseline.
- `TFacePartBinaryCompatibilityTest`: 1 test, 0 failures, 0 errors, loading a Scala 2.11.5 consumer whose forwarders
call all three statics.
- Complete plain-JVM suite: 43 tests, 0 failures, 0 errors.
- Clean `spotlessApply checkstyleTest build`: passing.
- Java 8 Forge dedicated-server suite: 3 tests, 0 failures, 0 errors. A new case covers what plain-JVM tests cannot:
microblock classes are generated by ASM at runtime, so it checks that a generated `FaceMicroblock` inherits the
`redstoneConductionMap` default and that a generated `HollowMicroblock` still returns its own `0x10` and `false`.
@@ -13,6 +13,11 @@ import net.minecraft.server.MinecraftServer;
import org.junit.jupiter.api.Test;
import codechicken.microblock.FaceMicroClass$;
import codechicken.microblock.HollowMicroClass$;
import codechicken.microblock.Microblock;
import codechicken.microblock.MicroblockGenerator;
import codechicken.multipart.TFacePart;
import codechicken.multipart.TileMultipart;
import codechicken.multipart.asm.MultipartMixinFactory;
import codechicken.multipart.scalatraits.TSlottedTile;
@@ -41,6 +46,25 @@ class ForgeEnvironmentSmokeTest {
assertNotNull(server.worldServers[0]);
}
/**
* Microblock classes are generated by ASM at runtime. TFacePart's members are now Java interface defaults rather
* than Scala trait forwarders, so a generated class inherits them instead of declaring its own; this checks the JVM
* resolves them for a class that never existed at compile time.
*/
@Test
void generatedMicroblockInheritsFacePartDefaults() {
Microblock face = MicroblockGenerator.create(FaceMicroClass$.MODULE$, 0, false);
Microblock hollow = MicroblockGenerator.create(HollowMicroClass$.MODULE$, 0, false);
assertTrue(face instanceof TFacePart);
assertEquals(0, ((TFacePart) face).redstoneConductionMap());
// HollowMicroblock overrides both members, so its generated class must not pick up the defaults.
assertTrue(hollow instanceof TFacePart);
assertEquals(0x10, ((TFacePart) hollow).redstoneConductionMap());
assertFalse(((TFacePart) hollow).solid(0));
}
@Test
void generatesAndCachesSlottedTileClass() {
int traitId = MultipartMixinFactory.getId(TSlottedTile.class.getName().replace('.', '/'));
@@ -0,0 +1,21 @@
package codechicken.multipart;
/**
* 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 TFacePart$class {
private TFacePart$class() {}
public static boolean solid(TFacePart part, int side) {
return true;
}
public static int redstoneConductionMap(TFacePart part) {
return 0;
}
public static void $init$(TFacePart part) {}
}
@@ -0,0 +1,22 @@
package codechicken.multipart;
/**
* Interface which must be implemented by parts that go in a face part.
* <p>
* TMultiPart declares neither member, so unlike the other converted traits these can safely be default methods.
*/
public interface TFacePart extends TSlottedPart {
/** Passed down from Block.isSideSolid. Return true if this part is solid and opaque on the specified side. */
default boolean solid(int side) {
return true;
}
/**
* Return the redstone conduction map for which signal can pass through this part on the face. Eg, hollow covers
* return 0x10 as signal can pass through the center hole.
*/
default int redstoneConductionMap() {
return 0;
}
}
@@ -1,17 +0,0 @@
package codechicken.multipart
/** Inteface which must be implemented by parts that go in a face part.
*/
trait TFacePart extends TSlottedPart {
/** Passed down from Block.isSideSolid. Return true if this part is solid and
* opaque on the specified side
*/
def solid(side: Int): Boolean = true
/** Return the redstone conduction map for which signal can pass through this
* part on the face. Eg, hollow covers return 0x10 as signal can pass through
* the center hole.
*/
def redstoneConductionMap = 0
}