diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index a0c90b4..c038bc0 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -450,4 +450,9 @@ These ranges were formed before designing the characterization suite and should - 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. +- Ported the whole `TIconHitEffects.scala` unit: `IconHitEffects`, `JIconHitEffects`, `JIconHitEffects$class`, `TIconHitEffects` and `TIconHitEffects$class`. Both interfaces are descriptor-identical to the reference and the other three add only a private constructor. +- Confirmed the default-versus-abstract rule discriminates correctly within a single file. `JIconHitEffects.getBreakingIcon` became a default because `TMultiPart` does not declare it, while `TIconHitEffects.addHitEffects` and `addDestroyEffects` stayed abstract because it does. `MicroblockClient` needed explicit forwarders only for the latter pair. +- 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. - 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. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index 25cb2df..6c18312 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -292,3 +292,69 @@ overrides are unaffected. - 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`. + +## 2026-08-14 — TIconHitEffects Java port + +Covers `IconHitEffects`, `JIconHitEffects`, `JIconHitEffects$class`, `TIconHitEffects` and `TIconHitEffects$class`, +which shared one Scala file. + +### Observable behavior + +No known divergence. `getBreakingIcon` still ignores its `subPart` argument and returns `getBrokenIcon(side)`, destroy +effects still request sides 0 through 5 in order, and the `scaleDensity` overload still chooses between the part's own +bounds and `Cuboid6.full`. + +Evaluation order in `addDestroyEffects` is deliberately preserved: icons first, then bounds, then the tile. Hoisting +the tile read to the top of the method would have been tidier but changes which work happens before a null tile fails, +and the characterization pins the reference order. + +### Removed API: IconHitEffects$ + +Accepted divergence: the `IconHitEffects$` companion singleton and its `MODULE$` field are removed. No jar in the pack +references the class, by bytecode or by name; the four consumers of this area use the static forwarders on +`IconHitEffects` (WR-CBE, extrautilities) or the two `$class` bridges (ForgeRelocationFMP, OpenComputers, ProjRed). + +This is the same reasoning applied to the dead `IDWriter` and `JPartialOcclusion` bridges, but it removes a public +class rather than a method, so it is worth calling out separately. Restoring it is a thin delegating class if any +consumer outside the pack turns out to need it. Note the contrast with `NormalOcclusionTest$`, which is referenced by +ForgeRelocationFMP and OpenComputers and was therefore kept. + +### Trait linearization, split decision + +The two interfaces needed opposite treatment, which is the rule from the `TFacePart` port working as intended: + +- `JIconHitEffects.getBreakingIcon` becomes a real `default`. `TMultiPart` does not declare it, so nothing in an + implementor's superclass chain can beat it. Java implementors can now inherit it; `minecraft/McBlockPart` already + declared its own and stays descriptor-identical. +- `TIconHitEffects.addHitEffects` and `addDestroyEffects` stay abstract. `TMultiPart` declares both, so a default would + silently lose to its empty versions. + +`MicroblockClient` was the one in-repo case and now declares both explicitly, delegating to the `IconHitEffects` +statics. + +### Supported JVM API + +- `JIconHitEffects` and `TIconHitEffects` are descriptor-identical to the reference. +- `IconHitEffects`, `JIconHitEffects$class` and `TIconHitEffects$class` add only a private constructor. All three + `IconHitEffects` static overloads keep their reference descriptors, including the three-argument `scaleDensity` form + that extrautilities links against. +- `@SideOnly(Side.CLIENT)` placement matches the reference exactly: two on `JIconHitEffects`, one on + `JIconHitEffects$class`, two on `TIconHitEffects`, two on `TIconHitEffects$class`, and none on `IconHitEffects`. +- `JIconHitEffects` does not extend `TMultiPart` at bytecode level in the reference either, so the statics cast to + `TMultiPart` internally exactly as the reference does. Passing a non-`TMultiPart` still fails with + `ClassCastException`. + +### Compiler artifacts + +Accepted divergence: `IconHitEffects$$anonfun$addDestroyEffects$1` is removed along with `IconHitEffects$`. The Java +loop produces no replacement class. + +### Validation + +- `IconHitEffectsCharacterizationTest`: 5 tests, 0 failures, 0 errors, unchanged from the Scala baseline. +- `TIconHitEffectsBinaryCompatibilityTest`: 1 test, 0 failures, 0 errors, exercising all five statics across both + bridges. +- Complete plain-JVM suite: 49 tests, 0 failures, 0 errors. +- 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. diff --git a/src/main/scala/codechicken/microblock/Microblock.scala b/src/main/scala/codechicken/microblock/Microblock.scala index 2c4daf9..2067d72 100644 --- a/src/main/scala/codechicken/microblock/Microblock.scala +++ b/src/main/scala/codechicken/microblock/Microblock.scala @@ -11,6 +11,8 @@ import net.minecraft.world.World import codechicken.lib.vec.Cuboid6 import codechicken.lib.vec.Vector3 import codechicken.multipart.TIconHitEffects +import codechicken.multipart.IconHitEffects +import net.minecraft.client.particle.EffectRenderer import net.minecraft.util.IIcon import net.minecraft.block.Block import codechicken.multipart.TCuboidPart @@ -144,6 +146,15 @@ trait MicroblockClient case mat => mat.getBreakingIcon(side) } + // TIconHitEffects is a Java interface, so these must be declared explicitly or TMultiPart's empty versions win. + override def addHitEffects( + hit: MovingObjectPosition, + effectRenderer: EffectRenderer + ) = IconHitEffects.addHitEffects(this, hit, effectRenderer) + + override def addDestroyEffects(effectRenderer: EffectRenderer) = + IconHitEffects.addDestroyEffects(this, effectRenderer) + override def renderStatic(pos: Vector3, pass: Int) = { if (getIMaterial.canRenderInPass(pass)) { render(pos, pass) diff --git a/src/main/scala/codechicken/multipart/IconHitEffects.java b/src/main/scala/codechicken/multipart/IconHitEffects.java new file mode 100644 index 0000000..b93c43f --- /dev/null +++ b/src/main/scala/codechicken/multipart/IconHitEffects.java @@ -0,0 +1,52 @@ +package codechicken.multipart; + +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.util.IIcon; +import net.minecraft.util.MovingObjectPosition; + +import codechicken.lib.raytracer.ExtendedMOP; +import codechicken.lib.render.EntityDigIconFX; +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; + +/** + * Static implementations of the functions a part would override on {@link TMultiPart} for standard Minecraft style hit + * and break particles. + *
+ * Java composition setup. A Java class implements {@link JIconHitEffects}, overrides the functions on + * {@link TMultiPart}, and calls these statics with 'this' as the first parameter. {@link TIconHitEffects} is the + * equivalent for Scala implementors. + */ +public final class IconHitEffects { + + private IconHitEffects() {} + + public static void addHitEffects(JIconHitEffects part, MovingObjectPosition hit, EffectRenderer effectRenderer) { + TileMultipart tile = ((TMultiPart) part).tile(); + EntityDigIconFX.addBlockHitEffects( + tile.getWorldObj(), + part.getBounds().copy().add(Vector3.fromTileEntity(tile)), + hit.sideHit, + part.getBreakingIcon(ExtendedMOP.getData(hit), hit.sideHit), + effectRenderer); + } + + public static void addDestroyEffects(JIconHitEffects part, EffectRenderer effectRenderer) { + addDestroyEffects(part, effectRenderer, true); + } + + public static void addDestroyEffects(JIconHitEffects part, EffectRenderer effectRenderer, boolean scaleDensity) { + IIcon[] icons = new IIcon[6]; + for (int i = 0; i < 6; i++) { + icons[i] = part.getBrokenIcon(i); + } + Cuboid6 bounds = scaleDensity ? part.getBounds().copy() : Cuboid6.full.copy(); + // The tile is read only here, after the icons and bounds, matching the reference evaluation order. + TileMultipart tile = ((TMultiPart) part).tile(); + EntityDigIconFX.addBlockDestroyEffects( + tile.getWorldObj(), + bounds.add(Vector3.fromTileEntity(tile)), + icons, + effectRenderer); + } +} diff --git a/src/main/scala/codechicken/multipart/JIconHitEffects$class.java b/src/main/scala/codechicken/multipart/JIconHitEffects$class.java new file mode 100644 index 0000000..821bb41 --- /dev/null +++ b/src/main/scala/codechicken/multipart/JIconHitEffects$class.java @@ -0,0 +1,20 @@ +package codechicken.multipart; + +import net.minecraft.util.IIcon; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** Binary bridge for Scala implementations compiled against the original trait. */ +@Deprecated +public abstract class JIconHitEffects$class { + + private JIconHitEffects$class() {} + + @SideOnly(Side.CLIENT) + public static IIcon getBreakingIcon(JIconHitEffects part, Object subPart, int side) { + return part.getBrokenIcon(side); + } + + public static void $init$(JIconHitEffects part) {} +} diff --git a/src/main/scala/codechicken/multipart/JIconHitEffects.java b/src/main/scala/codechicken/multipart/JIconHitEffects.java new file mode 100644 index 0000000..360848e --- /dev/null +++ b/src/main/scala/codechicken/multipart/JIconHitEffects.java @@ -0,0 +1,24 @@ +package codechicken.multipart; + +import net.minecraft.util.IIcon; + +import codechicken.lib.vec.Cuboid6; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Java interface containing callbacks for particle rendering. Make sure to override addHitEffects and addDestroyEffects + * on {@link TMultiPart} as {@link TIconHitEffects} does, calling the {@link IconHitEffects} statics. + */ +public interface JIconHitEffects { + + Cuboid6 getBounds(); + + @SideOnly(Side.CLIENT) + default IIcon getBreakingIcon(Object subPart, int side) { + return getBrokenIcon(side); + } + + @SideOnly(Side.CLIENT) + IIcon getBrokenIcon(int side); +} diff --git a/src/main/scala/codechicken/multipart/TIconHitEffects$class.java b/src/main/scala/codechicken/multipart/TIconHitEffects$class.java new file mode 100644 index 0000000..9764df3 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TIconHitEffects$class.java @@ -0,0 +1,26 @@ +package codechicken.multipart; + +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.util.MovingObjectPosition; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** Binary bridge for Scala implementations compiled against the original trait. */ +@Deprecated +public abstract class TIconHitEffects$class { + + private TIconHitEffects$class() {} + + @SideOnly(Side.CLIENT) + public static void addHitEffects(TIconHitEffects part, MovingObjectPosition hit, EffectRenderer effectRenderer) { + IconHitEffects.addHitEffects(part, hit, effectRenderer); + } + + @SideOnly(Side.CLIENT) + public static void addDestroyEffects(TIconHitEffects part, EffectRenderer effectRenderer) { + IconHitEffects.addDestroyEffects(part, effectRenderer); + } + + public static void $init$(TIconHitEffects part) {} +} diff --git a/src/main/scala/codechicken/multipart/TIconHitEffects.java b/src/main/scala/codechicken/multipart/TIconHitEffects.java new file mode 100644 index 0000000..bb12c1b --- /dev/null +++ b/src/main/scala/codechicken/multipart/TIconHitEffects.java @@ -0,0 +1,23 @@ +package codechicken.multipart; + +import net.minecraft.client.particle.EffectRenderer; +import net.minecraft.util.MovingObjectPosition; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Particle callbacks for parts that also extend {@link TMultiPart}. + *
+ * TMultiPart declares both members, and a superclass method beats an interface default on the JVM, so implementors must + * override them themselves and call the {@link IconHitEffects} statics. Declaring them default here would silently lose + * to TMultiPart's empty versions. + */ +public interface TIconHitEffects extends JIconHitEffects { + + @SideOnly(Side.CLIENT) + void addHitEffects(MovingObjectPosition hit, EffectRenderer effectRenderer); + + @SideOnly(Side.CLIENT) + void addDestroyEffects(EffectRenderer effectRenderer); +} diff --git a/src/main/scala/codechicken/multipart/TIconHitEffects.scala b/src/main/scala/codechicken/multipart/TIconHitEffects.scala deleted file mode 100644 index 2ff57ea..0000000 --- a/src/main/scala/codechicken/multipart/TIconHitEffects.scala +++ /dev/null @@ -1,92 +0,0 @@ -package codechicken.multipart - -import codechicken.lib.vec.Cuboid6 -import cpw.mods.fml.relauncher.SideOnly -import cpw.mods.fml.relauncher.Side -import net.minecraft.util.IIcon -import net.minecraft.util.MovingObjectPosition -import net.minecraft.client.particle.EffectRenderer -import codechicken.lib.vec.Vector3 -import codechicken.lib.raytracer.ExtendedMOP -import codechicken.lib.render.EntityDigIconFX - -/** This suite of 3 classes provides simple functions for standard minecraft - * style hit and break particles. - * - * Scala|Java composition setup. Due to the lack of mixin inheritance in Java, - * the classes are structured to suit both languages as follows. IconHitEffects - * contains static implementations of the functions that would be overriden in - * TMultiPart JIconHitEffects is the interface that should be implemented by a - * Java class, which can then override the functions in TMultipart and call the - * static methods in IconHitEffects with 'this' as the first parameter - * TIconHitEffects is a trait for scala implementors that does includes the - * overrides/static calls that Java programmers need to include themselves. - */ -object IconHitEffects { - def addHitEffects( - part: JIconHitEffects, - hit: MovingObjectPosition, - effectRenderer: EffectRenderer - ) { - EntityDigIconFX.addBlockHitEffects( - part.tile.getWorldObj, - part.getBounds.copy.add(Vector3.fromTileEntity(part.tile)), - hit.sideHit, - part.getBreakingIcon(ExtendedMOP.getData(hit), hit.sideHit), - effectRenderer - ) - } - - def addDestroyEffects(part: JIconHitEffects, effectRenderer: EffectRenderer) { - addDestroyEffects(part, effectRenderer, true) - } - - def addDestroyEffects( - part: JIconHitEffects, - effectRenderer: EffectRenderer, - scaleDensity: Boolean - ) { - val icons = new Array[IIcon](6) - for (i <- 0 until 6) - icons(i) = part.getBrokenIcon(i) - val bounds = - if (scaleDensity) part.getBounds.copy - else Cuboid6.full.copy - EntityDigIconFX.addBlockDestroyEffects( - part.tile.getWorldObj, - bounds.add(Vector3.fromTileEntity(part.tile)), - icons, - effectRenderer - ) - } -} - -/** Java interface containing callbacks for particle rendering. Make sure to - * override addHitEffects and addDestroyEffects as in TIconHitEffects - */ -trait JIconHitEffects extends TMultiPart { - def getBounds: Cuboid6 - - @SideOnly(Side.CLIENT) - def getBreakingIcon(subPart: Any, side: Int): IIcon = getBrokenIcon(side) - - @SideOnly(Side.CLIENT) - def getBrokenIcon(side: Int): IIcon -} - -/** Trait for scala programmers - */ -trait TIconHitEffects extends JIconHitEffects { - @SideOnly(Side.CLIENT) - override def addHitEffects( - hit: MovingObjectPosition, - effectRenderer: EffectRenderer - ) { - IconHitEffects.addHitEffects(this, hit, effectRenderer) - } - - @SideOnly(Side.CLIENT) - override def addDestroyEffects(effectRenderer: EffectRenderer) { - IconHitEffects.addDestroyEffects(this, effectRenderer) - } -}