refactor: port TItemMultiPart to Java

Replaces ItemMultiPart.scala with Java implementations of JItemMultiPart,
TItemMultiPart and the TItemMultiPart$class bridge. The interface is
descriptor-identical to the reference, JItemMultiPart keeps its
constructor and both instance methods and gains only additive statics, and
the bridge keeps all three public statics for ProjRed, its only consumer.
The emitted class-file set for this area is unchanged.

This is the first converted trait that extends a Minecraft class rather
than TMultiPart, so the linearization check ran against Item. The same
split applied: getHitDepth became a real default because Item does not
declare it, while onItemUse stayed abstract because Item does and a
default would silently lose to the vanilla implementation. The rule
generalises to the implementor's actual superclass.

The bridge's getHitDepth ignores its part argument and calls the static
directly, matching the reference whose trait body never used this. The
onItemUse path still dispatches getHitDepth through the interface, so an
implementor's override is honoured.

Validated with 57 plain-JVM tests, a clean build, and the Java 8 Forge
dedicated-server suite.
This commit is contained in:
Algent committed 2026-08-14 21:25:57 +02:00
1 parent 6813144d84
commit f2f1870931
6 files changed
+171 -68

No files matched your search

+3
View File
@@ -455,4 +455,7 @@ These ranges were formed before designing the characterization suite and should
- Removed `IconHitEffects$` and its `MODULE$` field. No jar in the pack references it, in bytecode or by name, so it is dead under the inventory rule. This is the first public class the migration has dropped rather than a method, and it contrasts with `NormalOcclusionTest$`, which is referenced and was kept.
- Preserved evaluation order inside `addDestroyEffects` rather than hoisting the tile read, because the characterization pins which work happens before a null tile fails.
- Established that particle appearance cannot be asserted headless. Icon selection, side order and bounds scaling are covered by tests; the visual result stays on the manual checklist.
- Ported `ItemMultiPart.scala` into `JItemMultiPart`, `TItemMultiPart` and `TItemMultiPart$class`. The interface is descriptor-identical to the reference, `JItemMultiPart` gains only additive statics, and the bridge keeps all three public statics for ProjRed.
- Generalised the default-versus-abstract rule. `TItemMultiPart` extends `Item` rather than `TMultiPart`, and the same split applied: `getHitDepth` became a default because `Item` does not declare it, while `onItemUse` stayed abstract because `Item` does. What matters is the implementor's actual superclass, not `TMultiPart` specifically.
- Confirmed that placement logic is testable headless. `onItemUse` short-circuits as soon as `newPart` returns null, before any world, player or stack access, so the depth branch, the neighbour offset and the shared mutated `BlockCoord` can all be asserted with null arguments.
- 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.
+53
View File
@@ -358,3 +358,56 @@ loop produces no replacement class.
- Clean `spotlessApply checkstyleTest build`: passing.
- Java 8 Forge dedicated-server suite: 3 tests, 0 failures, 0 errors.
- Particle appearance itself needs a client and stays on the manual checklist.
## 2026-08-14 — TItemMultiPart Java port
Covers `JItemMultiPart`, `TItemMultiPart` and `TItemMultiPart$class`, which shared `ItemMultiPart.scala`.
### Observable behavior
No known divergence. `getHitDepth` still projects a copy of the hit vector onto the axis for that side and adds 1 on
even sides, leaving the caller's vector untouched. `onItemUse` still tries the clicked block only when the depth is
below 1, then always tries the neighbour on that side, reusing and mutating a single `BlockCoord` across both attempts
and passing the same `Vector3` to every `newPart` call. Placement still short-circuits on a null part before touching
the world, still skips `addPart` on the client, and still decrements the stack only outside creative mode.
### First trait extending a Minecraft class rather than TMultiPart
`TItemMultiPart extends Item`, so the linearization check runs against `Item` instead of `TMultiPart`. The outcome is
the same split seen in the `TIconHitEffects` port:
- `getHitDepth` becomes a real `default`. `Item` does not declare it, so nothing in an implementor's superclass chain
can beat it.
- `onItemUse` stays abstract. `Item` declares it, so a default would silently lose to the vanilla implementation and
placement would stop working.
This confirms the rule generalises: what matters is the implementor's actual superclass, not `TMultiPart` specifically.
There were no in-repo implementors beyond `JItemMultiPart` itself, so nothing else needed changing.
### Supported JVM API
- `TItemMultiPart` is descriptor-identical to the reference.
- `JItemMultiPart` keeps its constructor, both instance methods and their descriptors; WR-CBE links against the
constructor and the inherited `func_77658_a`. It gains two public statics, `hitDepth` and the `onItemUse` overload
taking the part, plus a private `place`. All additive.
- `TItemMultiPart$class` keeps all three public statics for ProjRed, its only consumer. It adds a private constructor
and drops the reference's private `place$1` helper, which was never visible to consumers.
- `getHitDepth` on the bridge ignores its part argument and calls the static directly, matching the reference, whose
trait body never used `this`. The `onItemUse` path still dispatches `getHitDepth` through the interface, so an
implementor's override is honoured exactly as before.
### Compiler artifacts
None removed or added. The emitted class-file set for this area is unchanged; the local `place` function compiled to a
private static method rather than a closure class in both versions.
### Validation
- `ItemMultiPartCharacterizationTest`: 7 tests, 0 failures, 0 errors, unchanged from the Scala baseline.
- `TItemMultiPartBinaryCompatibilityTest`: 1 test, 0 failures, 0 errors, driving a reference-compiled Scala consumer
through both attempt positions.
- Complete plain-JVM suite: 57 tests, 0 failures, 0 errors.
- Clean `spotlessApply checkstyleTest build`: passing.
- Java 8 Forge dedicated-server suite: 3 tests, 0 failures, 0 errors.
- Real placement into a world needs a client or a server scenario and stays on the functional and manual layers.
@@ -1,68 +0,0 @@
package codechicken.multipart
import net.minecraft.item.Item
import codechicken.lib.vec.Vector3
import codechicken.lib.vec.Rotation
import net.minecraft.item.ItemStack
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.world.World
import codechicken.lib.vec.BlockCoord
/** Java class implementation
*/
abstract class JItemMultiPart extends Item with TItemMultiPart
/** Simple multipart item class for easy placement. Simply override the newPart
* function and it the part will be added to the block space if it passes the
* occlusion tests.
*/
trait TItemMultiPart extends Item {
def getHitDepth(vhit: Vector3, side: Int): Double =
vhit.copy.scalarProject(Rotation.axes(side)) + (side % 2 ^ 1)
override def onItemUse(
item: ItemStack,
player: EntityPlayer,
world: World,
x: Int,
y: Int,
z: Int,
side: Int,
hitX: Float,
hitY: Float,
hitZ: Float
): Boolean = {
val pos = new BlockCoord(x, y, z)
val vhit = new Vector3(hitX, hitY, hitZ)
val d = getHitDepth(vhit, side)
def place(): Boolean = {
val part = newPart(item, player, world, pos, side, vhit)
if (part == null || !TileMultipart.canPlacePart(world, pos, part))
return false
if (!world.isRemote)
TileMultipart.addPart(world, pos, part)
if (!player.capabilities.isCreativeMode)
item.stackSize -= 1
return true
}
if (d < 1 && place())
return true
pos.offset(side)
return place()
}
/** Create a new part based on the placement information parameters.
*/
def newPart(
item: ItemStack,
player: EntityPlayer,
world: World,
pos: BlockCoord,
side: Int,
vhit: Vector3
): TMultiPart
}
@@ -0,0 +1,58 @@
package codechicken.multipart;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import codechicken.lib.vec.BlockCoord;
import codechicken.lib.vec.Rotation;
import codechicken.lib.vec.Vector3;
/** Java class implementation of {@link TItemMultiPart}, and the canonical home of its behavior. */
public abstract class JItemMultiPart extends Item implements TItemMultiPart {
@Override
public double getHitDepth(Vector3 vhit, int side) {
return hitDepth(vhit, side);
}
@Override
public boolean onItemUse(ItemStack item, EntityPlayer player, World world, int x, int y, int z, int side,
float hitX, float hitY, float hitZ) {
return onItemUse(this, item, player, world, x, y, z, side, hitX, hitY, hitZ);
}
public static double hitDepth(Vector3 vhit, int side) {
return vhit.copy().scalarProject(Rotation.axes[side]) + (side % 2 ^ 1);
}
public static boolean onItemUse(TItemMultiPart part, ItemStack item, EntityPlayer player, World world, int x, int y,
int z, int side, float hitX, float hitY, float hitZ) {
BlockCoord pos = new BlockCoord(x, y, z);
Vector3 vhit = new Vector3(hitX, hitY, hitZ);
if (part.getHitDepth(vhit, side) < 1 && place(part, item, player, world, pos, side, vhit)) {
return true;
}
pos.offset(side);
return place(part, item, player, world, pos, side, vhit);
}
private static boolean place(TItemMultiPart part, ItemStack item, EntityPlayer player, World world, BlockCoord pos,
int side, Vector3 vhit) {
TMultiPart newPart = part.newPart(item, player, world, pos, side, vhit);
if (newPart == null || !TileMultipart.canPlacePart(world, pos, newPart)) {
return false;
}
if (!world.isRemote) {
TileMultipart.addPart(world, pos, newPart);
}
if (!player.capabilities.isCreativeMode) {
item.stackSize -= 1;
}
return true;
}
}
@@ -0,0 +1,28 @@
package codechicken.multipart;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import codechicken.lib.vec.Vector3;
/**
* 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 TItemMultiPart$class {
private TItemMultiPart$class() {}
public static double getHitDepth(TItemMultiPart part, Vector3 vhit, int side) {
return JItemMultiPart.hitDepth(vhit, side);
}
public static boolean onItemUse(TItemMultiPart part, ItemStack item, EntityPlayer player, World world, int x, int y,
int z, int side, float hitX, float hitY, float hitZ) {
return JItemMultiPart.onItemUse(part, item, player, world, x, y, z, side, hitX, hitY, hitZ);
}
public static void $init$(TItemMultiPart part) {}
}
@@ -0,0 +1,29 @@
package codechicken.multipart;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import codechicken.lib.vec.BlockCoord;
import codechicken.lib.vec.Vector3;
/**
* Simple multipart item interface for easy placement. Override newPart and the part will be added to the block space if
* it passes the occlusion tests.
* <p>
* Implementors must extend {@link net.minecraft.item.Item} and override onItemUse themselves, delegating to
* {@link JItemMultiPart#onItemUse}, as {@link JItemMultiPart} does. Item declares onItemUse and a superclass method
* beats an interface default on the JVM, so declaring it default here would silently lose.
*/
public interface TItemMultiPart {
default double getHitDepth(Vector3 vhit, int side) {
return JItemMultiPart.hitDepth(vhit, side);
}
boolean onItemUse(ItemStack item, EntityPlayer player, World world, int x, int y, int z, int side, float hitX,
float hitY, float hitZ);
/** Create a new part based on the placement information parameters. */
TMultiPart newPart(ItemStack item, EntityPlayer player, World world, BlockCoord pos, int side, Vector3 vhit);
}