diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index 3a58e66..9189382 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -539,3 +539,9 @@ These ranges were formed before designing the characterization suite and should - Proved the substitution was an identity before making it. The functional test asserted that each alias returned the very instance its proxy holds, so replacing `logger` with `MultipartProxy.logger` / `MicroblockProxy.logger` cannot change behavior. `ConfigContent` and `MultipartGenerator` are descriptor-identical afterwards. - Noted the first case where the migration's answer is deletion. Zero downstream references plus an impossible target name means there is nothing to preserve, and inventing a `MultipartLogger` type to hold a one-line forwarder would add API rather than remove it. - Emptied the low-risk queue. Everything remaining is the medium group or later, so the next piece of work should be chosen deliberately rather than taken off the top of a list. +- Ported `MultipartRenderer` in the `TickScheduler$` shape, with the companion holding the implementation and the plain class holding static forwarders. Forge registers the singleton itself and guidenh resolves the static first, so both halves are load-bearing for different reasons and the emitted class list is unchanged. +- Applied the reflective-name rule for the second time, and this is the case it was written for: `MultipartRenderer` has no bytecode consumer at all, only guidenh string constants, and both the class and its companion had to survive. +- Found a Scala-to-Java trap that is not silent but will recur: uniform access makes `renderer.hasOverrideBlockTexture` read the same whether it is a field or a no-arg method. It is a method, and the port did not compile until it grew parentheses. Every remaining renderer conversion will meet this. +- Dropped the three static forwarders Scala generated for inherited `TileEntitySpecialRenderer` members, the same category as the twelve `KeyBinding` forwarders dropped from `ControlKeyHandler`. +- Dropped an unused `ThreadSafeISBRH` import. It annotated nothing and appears nowhere else; if the intent was to mark this ISBRH thread-safe for Angelica then the annotation was never applied in the reference either, which is worth checking independently of the migration. +- Recorded that this port adds nothing to the automated suites and everything to the manual checklist. Static and dynamic rendering, the breaking overlay and the render id all need a client. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index ef77a0e..c121e4c 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -1168,3 +1168,62 @@ no Scala consumer did. - Test churn, both expected for a removal rather than a port: `PackageObjectCharacterizationTest` was deleted with its subject, and `ProxyLoggerFunctionalTest` lost only its alias-identity assertion. What survives is the assertion worth keeping — that both proxies publish a non-null logger during preInit, which is what the inlined call sites now read. + +## 2026-08-15 — MultipartRenderer Java port + +The client rendering callbacks. No jar links against any member, but guidenh reflects on **both** `MultipartRenderer` +and `MultipartRenderer$` by name, resolving `renderWorldBlock` as a static first and falling back to the companion's +`MODULE$`. Both classes are therefore kept, in the `TickScheduler$` shape: the companion holds the implementation +because Forge registers that instance itself, and the plain class is static forwarders. + +### Observable behavior + +No known divergence. The render id is still claimed in the singleton's initialiser, which preserves the reference's +timing: a Scala object runs its body on first access, and the first access is the client proxy's `postInit`. +`renderWorldBlock` still returns false for a non-multipart tile, for an empty part list, and after drawing a breaking +overlay; `renderTileEntityAt` still returns early unless the tile has dynamic parts; `getRenderId` still reads +`TileMultipart.renderID`. + +The reference wrote the dynamic-part guard with a non-short-circuiting `|`. The port uses `||`. Both operands are +side-effect-free getters, so this cannot differ. + +The breaking-overlay branch moved into a private helper. The reference nested a pattern match inside an `if`; the hit +data is a `scala.Tuple2` that arrives erased, so the port checks the tuple and then the boxed index before unboxing, +which is what the match compiled to. `BlockMultipart` already reads the same data the same way. + +### Supported JVM API + +The emitted class list is **unchanged** — nothing added, nothing removed. All five rendering callbacks keep their +exact signatures on `MultipartRenderer`, `MODULE$` keeps its type and flags on `MultipartRenderer$`, and both classes +keep class-level `@SideOnly(Side.CLIENT)`, so both are still stripped on a dedicated server. + +`MultipartRenderer$` still extends `TileEntitySpecialRenderer` and implements `ISimpleBlockRenderingHandler`. Forge +rejects the registration outright if either is missing, and the characterization asserts both. + +### Differences that are not API + +- `MultipartRenderer` loses `func_147498_b`, `func_147496_a` and `func_147497_a`, the forwarders Scala generates for + inherited `TileEntitySpecialRenderer` members. Same category as the twelve `KeyBinding` forwarders dropped from + `ControlKeyHandler`: artifacts of the object-forwarder mechanism, not API, and unreferenced. +- `MultipartRenderer$`'s class initialiser loses `ACC_PUBLIC`, as every converted companion has. +- The unused `com.gtnewhorizons.angelica.api.ThreadSafeISBRH` import is dropped. It appears nowhere else in the + codebase and annotated nothing; no behavior depended on it. Worth a second look independently — if the intent was to + mark this ISBRH thread-safe for Angelica, that annotation was never actually applied in the reference either. + +### Scala's uniform access hid a field-versus-method difference + +`renderer.hasOverrideBlockTexture` reads identically in Scala whether it is a field or a no-arg method. It is a method +on `RenderBlocks`, and the port did not compile until it became `hasOverrideBlockTexture()`. This is a compile-time +failure rather than a silent one, but it will recur in every remaining renderer conversion. + +### Validation + +- Complete plain-JVM suite: 125 tests, 0 failures, 0 errors. +- Clean `spotlessApply checkstyleTest build`: passing. +- Java 8 Forge dedicated-server suite: 22 tests, 0 failures, 0 errors. It does not exercise this class and cannot: + the server never loads it. +- Class list and members diffed against a reference jar built from the pre-port `src/main`. +- No characterization assertion changed with the port. +- What stays manual, and it is everything this class actually does: static block rendering, dynamic part rendering, + the breaking overlay on the struck part, inventory rendering, and the render id being claimed exactly once. None of + it can run without a client, so all of it is on the checklist. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index c7d3e77..7f95e0a 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -10,7 +10,7 @@ what breaks, and what is left. The other documents hold the reasoning. | `JAVA_MIGRATION_DIVERGENCES.md` | Every intentional difference from the reference, one entry per port | | `JAVA_MIGRATION_MANUAL_CHECKS.md` | What no automated test can cover, and must be checked by hand in a client | -Branch: `algent/java`. Base: `master`. 52 commits so far. +Branch: `algent/java`. Base: `master`. 54 commits so far. ## The one rule that matters @@ -56,7 +56,7 @@ awk -F'"' '/ + * This is the Scala companion singleton, and it is not a mere forwarder: Forge registers this instance itself, through + * both {@code RenderingRegistry.registerBlockHandler} and {@code ClientRegistry.bindTileEntitySpecialRenderer}, so the + * implementation has to live here. guidenh also reads {@code MODULE$} as its fallback path. + *

+ * Claiming the render id in the constructor preserves the reference's timing: a Scala object runs its body on first + * access, and this class is first touched from the client proxy's postInit. + */ +@SideOnly(Side.CLIENT) +public final class MultipartRenderer$ extends TileEntitySpecialRenderer implements ISimpleBlockRenderingHandler { + + public static final MultipartRenderer$ MODULE$ = new MultipartRenderer$(); + + private MultipartRenderer$() { + TileMultipart.renderID_$eq(RenderingRegistry.getNextAvailableRenderId()); + } + + @Override + public void renderTileEntityAt(TileEntity t, double x, double y, double z, float f) { + TileMultipartClient client = (TileMultipartClient) t; + TileMultipart tile = (TileMultipart) t; + if (tile.jPartList().isEmpty() || !client.hasDynamicParts()) { + return; + } + + CCRenderState state = CCRenderState.instance(); + state.resetInstance(); + state.pullLightmapInstance(); + state.useNormals = true; + + client.renderDynamic(new Vector3(x, y, z), f, MinecraftForgeClient.getRenderPass()); + } + + @Override + public int getRenderId() { + return TileMultipart.renderID(); + } + + @Override + public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, + RenderBlocks renderer) { + TileEntity t = world.getTileEntity(x, y, z); + if (!(t instanceof TileMultipartClient)) { + return false; + } + + TileMultipartClient client = (TileMultipartClient) t; + TileMultipart tile = (TileMultipart) t; + if (tile.jPartList().isEmpty()) { + return false; + } + + if (renderer.hasOverrideBlockTexture()) { + drawBreakingPart(tile, x, y, z, renderer); + return false; + } + + CCRenderState state = CCRenderState.instance(); + state.resetInstance(); + state.lightMatrix.locate(world, x, y, z); + boolean b = client.renderStatic(world, new Vector3(x, y, z), renderer); + state.lightMatrix.access = null; + return b; + } + + /** + * The hit data is a Scala Tuple2 whose first member is the struck part's index. It arrives erased, so both the + * tuple and the index are checked before use, exactly as the reference's pattern match did. + */ + private static void drawBreakingPart(TileMultipart tile, int x, int y, int z, RenderBlocks renderer) { + MovingObjectPosition hit = Minecraft.getMinecraft().objectMouseOver; + if (hit == null || hit.blockX != x || hit.blockY != y || hit.blockZ != z) { + return; + } + + Object data = ExtendedMOP.getData(hit); + if (!(data instanceof Tuple2)) { + return; + } + + Object index = ((Tuple2) data)._1(); + if (!(index instanceof Integer)) { + return; + } + + int i = (Integer) index; + if (i >= 0 && i < tile.jPartList().size()) { + tile.jPartList().get(i).drawBreaking(renderer); + } + } + + @Override + public void renderInventoryBlock(Block block, int meta, int modelId, RenderBlocks renderer) {} + + @Override + public boolean shouldRender3DInInventory(int modelId) { + return false; + } +} diff --git a/src/main/scala/codechicken/multipart/MultipartRenderer.java b/src/main/scala/codechicken/multipart/MultipartRenderer.java new file mode 100644 index 0000000..ebd1597 --- /dev/null +++ b/src/main/scala/codechicken/multipart/MultipartRenderer.java @@ -0,0 +1,40 @@ +package codechicken.multipart; + +import net.minecraft.block.Block; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.IBlockAccess; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Static forwarders for {@link MultipartRenderer$}, which holds the implementation. guidenh resolves renderWorldBlock + * here first and only falls back to the companion's MODULE$, so these must keep their exact signatures. + */ +@SideOnly(Side.CLIENT) +public final class MultipartRenderer { + + private MultipartRenderer() {} + + public static void renderTileEntityAt(TileEntity t, double x, double y, double z, float f) { + MultipartRenderer$.MODULE$.renderTileEntityAt(t, x, y, z, f); + } + + public static int getRenderId() { + return MultipartRenderer$.MODULE$.getRenderId(); + } + + public static boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, + RenderBlocks renderer) { + return MultipartRenderer$.MODULE$.renderWorldBlock(world, x, y, z, block, modelId, renderer); + } + + public static void renderInventoryBlock(Block block, int meta, int modelId, RenderBlocks renderer) { + MultipartRenderer$.MODULE$.renderInventoryBlock(block, meta, modelId, renderer); + } + + public static boolean shouldRender3DInInventory(int modelId) { + return MultipartRenderer$.MODULE$.shouldRender3DInInventory(modelId); + } +} diff --git a/src/main/scala/codechicken/multipart/MultipartRenderer.scala b/src/main/scala/codechicken/multipart/MultipartRenderer.scala deleted file mode 100644 index f52e0c7..0000000 --- a/src/main/scala/codechicken/multipart/MultipartRenderer.scala +++ /dev/null @@ -1,103 +0,0 @@ -package codechicken.multipart - -import codechicken.lib.render.CCRenderState -import codechicken.lib.vec.Vector3 -import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler -import cpw.mods.fml.client.registry.RenderingRegistry -import net.minecraft.block.Block -import net.minecraft.client.renderer.{Tessellator, RenderBlocks} -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraft.world.IBlockAccess -import net.minecraft.client.Minecraft -import net.minecraftforge.client.ForgeHooksClient -import net.minecraftforge.client.MinecraftForgeClient -import cpw.mods.fml.relauncher.SideOnly -import cpw.mods.fml.relauncher.Side -import codechicken.lib.raytracer.ExtendedMOP -import codechicken.lib.lighting.LightMatrix -import com.gtnewhorizons.angelica.api.ThreadSafeISBRH - -/** Internal class for rendering callbacks. Should be moved to the handler - * package - */ -@SideOnly(Side.CLIENT) -object MultipartRenderer - extends TileEntitySpecialRenderer - with ISimpleBlockRenderingHandler { - TileMultipart.renderID_$eq(RenderingRegistry.getNextAvailableRenderId) - - override def renderTileEntityAt( - t: TileEntity, - x: Double, - y: Double, - z: Double, - f: Float - ) { - val tmpart = t.asInstanceOf[TileMultipartClient] - if (tmpart.partList.isEmpty | !tmpart.hasDynamicParts) - return - - val state = CCRenderState.instance - state.resetInstance() - state.pullLightmapInstance() - state.useNormals = true - - val pos = new Vector3(x, y, z) - tmpart.renderDynamic(pos, f, MinecraftForgeClient.getRenderPass) - } - - override def getRenderId = TileMultipart.renderID - - override def renderWorldBlock( - world: IBlockAccess, - x: Int, - y: Int, - z: Int, - block: Block, - modelId: Int, - renderer: RenderBlocks - ): Boolean = { - val t = world.getTileEntity(x, y, z) - if (!t.isInstanceOf[TileMultipartClient]) - return false - - val tmpart = t.asInstanceOf[TileMultipartClient] - if (tmpart.partList.isEmpty) - return false - - if (renderer.hasOverrideBlockTexture) { - val hit = Minecraft.getMinecraft.objectMouseOver - if ( - hit != null && hit.blockX == x && hit.blockY == y && hit.blockZ == z && ExtendedMOP - .getData(hit) - .isInstanceOf[(_, _)] - ) { - val hitInfo: (Int, _) = ExtendedMOP.getData(hit) - if ( - hitInfo._1.isInstanceOf[ - Int - ] && hitInfo._1 >= 0 && hitInfo._1 < tmpart.partList.size - ) - tmpart.partList.apply(hitInfo._1).drawBreaking(renderer) - } - return false - } - - val state = CCRenderState.instance - state.resetInstance() - state.lightMatrix.locate(world, x, y, z) - val b = tmpart.renderStatic(world, new Vector3(x, y, z), renderer) - state.lightMatrix.access = null - b - } - - override def renderInventoryBlock( - block: Block, - meta: Int, - modelId: Int, - renderer: RenderBlocks - ) {} - - def shouldRender3DInInventory(modelId: Int) = false -} diff --git a/src/main/scala/codechicken/multipart/handler/proxies.scala b/src/main/scala/codechicken/multipart/handler/proxies.scala index c68ed57..d626492 100644 --- a/src/main/scala/codechicken/multipart/handler/proxies.scala +++ b/src/main/scala/codechicken/multipart/handler/proxies.scala @@ -101,7 +101,7 @@ class MultipartProxy_clientImpl extends MultipartProxy_serverImpl { @SideOnly(Side.CLIENT) override def postInit() { super.postInit() - RenderingRegistry.registerBlockHandler(MultipartRenderer) + RenderingRegistry.registerBlockHandler(MultipartRenderer$.MODULE$) PacketCustom.assignHandler(MultipartCPH.channel, MultipartCPH) PacketCustom.assignHandler(MultipartCPH.registryChannel, MultipartCPH) @@ -112,7 +112,7 @@ class MultipartProxy_clientImpl extends MultipartProxy_serverImpl { @SideOnly(Side.CLIENT) override def onTileClassBuilt(t: Class[_ <: TileEntity]) { super.onTileClassBuilt(t) - ClientRegistry.bindTileEntitySpecialRenderer(t, MultipartRenderer) + ClientRegistry.bindTileEntitySpecialRenderer(t, MultipartRenderer$.MODULE$) } }