From 970e888b7fa896801f1792cd79f1122f19acb832 Mon Sep 17 00:00:00 2001 From: Algent Date: Sun, 30 Aug 2026 15:24:58 +0200 Subject: [PATCH] refactor: port client tile traits to java Preserve the exact generated client interfaces, render-cache behavior, and ordered display ticks. Teach Java mixin generation to linearize a registered parent and exclude transient runtime caches from copying. --- JAVA_MIGRATION.md | 34 +++- JAVA_MIGRATION_DIVERGENCES.md | 55 ++++++ JAVA_MIGRATION_HANDOFF.md | 48 +++-- JAVA_MIGRATION_MANUAL_CHECKS.md | 2 + .../TileMultipartClientFunctionalTest.java | 66 +++++-- .../codechicken/multipart/BlockMultipart.java | 8 +- .../multipart/MultipartGenerator.scala | 4 +- .../multipart/MultipartRenderer$.java | 4 +- .../codechicken/multipart/TileMultipart.java | 14 ++ .../multipart/TileMultipartClient.java | 183 ++++++++++++++++++ .../multipart/TileMultipartClient.scala | 154 --------------- .../multipart/asm/ASMMixinCompiler.scala | 17 +- .../multipart/asm/ASMMixinFactory.scala | 15 +- .../multipart/asm/MultipartMixinFactory.scala | 5 +- .../multipart/handler/proxies.scala | 18 +- .../scalatraits/TRandomDisplayTickTile.java | 34 ++++ .../scalatraits/TRandomDisplayTickTile.scala | 14 -- 17 files changed, 448 insertions(+), 227 deletions(-) create mode 100644 src/main/scala/codechicken/multipart/TileMultipartClient.java delete mode 100644 src/main/scala/codechicken/multipart/TileMultipartClient.scala create mode 100644 src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.java delete mode 100644 src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.scala diff --git a/JAVA_MIGRATION.md b/JAVA_MIGRATION.md index f2e972a..03a269b 100644 --- a/JAVA_MIGRATION.md +++ b/JAVA_MIGRATION.md @@ -25,11 +25,12 @@ No trustworthy tool will produce a maintainable Java port automatically. A decom The binary and source-level consumer audits, low-coupling Phase 3 queue, audit-derived immediate compatibility gate, focused profile, first traversal optimization, complete redstone interaction helper unit, `MicroRecipe` port, and the -first six Java-trait ports are complete. `TPartialOcclusionTile` proved the no-field path, `TSlottedTile` proved state +first eight Java-trait ports are complete. `TPartialOcclusionTile` proved the no-field path, `TSlottedTile` proved state and lifecycle rewriting, `TRedstoneTile` removed the measured redstone allocation, `TTileChangeTile` exercised mutable state plus inherited access, `TFluidHandlerTile` preserved ordered Forge fluid distribution, and -`TIInventoryTile`/`JInventoryTile` preserved AE2's direct rebuild linkage plus flattened sided inventory routing. All -retain their exact runtime interfaces. See `JAVA_MIGRATION_PROFILE.md`. +`TIInventoryTile`/`JInventoryTile` preserved AE2's direct rebuild linkage plus flattened sided inventory routing. +`TileMultipartClient` and `TRandomDisplayTickTile` complete the built-in trait queue with their render-cache and random +display-tick behavior intact. All retain their exact runtime interfaces. See `JAVA_MIGRATION_PROFILE.md`. That audit found and this branch has now repaired one existing regression: Schematica 1.12.6 reflects the original private `MultiPartRegistry$` field `codechicken$multipart$MultiPartRegistry$$typeMap` and casts it to a Scala mutable map. The @@ -356,15 +357,20 @@ Exit condition: identified steady-state Scala allocation sites are removed with - [x] Verify generated field accessors, initialization, copying, lifecycle callbacks, super bridges, and class caching on the stateful `TSlottedTile` checkpoint. - [x] Characterize ProjectRed/Extra Utilities redstone calls and port `TRedstoneTile` with an immediate paired profile. -- [ ] Convert remaining traits in small related groups. -- [ ] Document or deliberately relax the Java trait restrictions only when a real trait requires it. -- [ ] Preserve pass-through interface behavior. +- [x] Convert remaining traits in small related groups. +- [x] Document or deliberately relax the Java trait restrictions only when a real trait requires it. +- [x] Preserve pass-through interface behavior. - [x] Keep the completed OpenComputers `TSlottedTile.v_partMap` mutation/rebinding case green and cover ProjectRed/ Extra Utilities behavior before `TRedstoneTile`. - [x] Explicitly cover AE2's `TIInventoryTile.rebuildSlotMap` before the inventory trait. Exit condition: built-in behavior is implemented in Java while the established runtime composition mechanism remains stable. +Status: complete. All built-in tile traits are Java. The client pair required narrowly extending `registerJavaTrait` +for a registered Java trait extending another registered Java trait, explicit Java field accessors, and transient +runtime-only fields that must not participate in generated `copyFrom`. The generated runtime interfaces and class +cache remain reference-identical; pass-through-interface coverage remains green. + ### Phase 6 — Convert multipart core and microblocks - [ ] Add characterization coverage for each subsystem immediately before its conversion. @@ -714,3 +720,19 @@ generated-trait compatibility work. Scala-runtime removal remains a later projec snapshots before publishing a replacement immutable `Seq`. - In the paired run, `getLightValue` fell from 183.9 B to 0.0 B per call with 11.42x throughput, and `getTile` fell from 24.0 B to 0.0 B per call with 2.89x throughput. All 147 plain-JVM and 46 Forge tests pass. + +### 2026-08-30 + +- Characterized `TileMultipartClient` and `TRandomDisplayTickTile` together against untouched Scala. Six behavior + cases freeze render-cache partitioning and bounds, lazy initialization, dynamic short-circuiting, base no-op display + ticks, and ordered `IRandomDisplayTick` dispatch; a Forge smoke case freezes both exact runtime interfaces, + inheritance, generated fields, and generated-class caching. +- Ported both traits to Java with package-private access shims. `registerJavaTrait` now linearizes an already registered + Java parent trait, recognizes explicit field accessors, and skips transient runtime caches when auto-generating + `copyFrom`. Trait registration now precedes `BlockMultipart` construction so Forge defines the runtime interfaces + before ordinary Java bytecode can preload the mixin inputs. +- Added three no-op client dispatch hooks to `TileMultipart`, allowing in-repo Java callers to invoke generated + overrides through the stable superclass rather than emit class opcodes against types Forge rewrites to interfaces. + The runtime trait surfaces remain exact; the raw dev-jar source-build limitation remains documented. +- All 153 plain-JVM tests and all 77 Java 8 Forge dedicated-server tests pass. Actual static/dynamic rendering and + particle appearance remain client-only manual checks. diff --git a/JAVA_MIGRATION_DIVERGENCES.md b/JAVA_MIGRATION_DIVERGENCES.md index c0c26ca..aa203f1 100644 --- a/JAVA_MIGRATION_DIVERGENCES.md +++ b/JAVA_MIGRATION_DIVERGENCES.md @@ -1693,3 +1693,58 @@ supported. The helper adds no public surface. - Complete plain-JVM suite: 153 tests, 0 failures, 0 errors. - Java 8 Forge dedicated-server suite: 70 tests, 0 failures, 0 errors. - Raw `javap` descriptors for both public types match the reference, including `scala.Tuple2[]` state accessors. + +## 2026-08-30 — TileMultipartClient/TRandomDisplayTickTile Java-trait port + +GuideNH is the only audited shipping consumer naming `TileMultipartClient`; it loads the class reflectively and uses +it only for `isInstanceOf`. No frozen binary or audited source consumer references either trait's `$class` helper. + +### Observable behavior + +No known runtime divergence. `updateRenderCache` still partitions each part once in source order, using the same +`doesTick || shouldRenderDynamic` short-circuit, then builds static and dynamic arrays in their respective order. Its +bounding box remains the union of all render bounds translated by tile coordinates, or the full block for an empty or +null part list. Static rendering still visits both caches, dynamic rendering still returns immediately when its flag +is false, queries still initialize lazily, and `markRender` still calls super before rebuilding the cache. + +The base display-tick method remains a no-op. `TRandomDisplayTickTile` still visits only `IRandomDisplayTick` parts in +part-list order and passes the same `Random` instance to every callback. + +### Generated ABI and raw input divergence + +At Forge runtime `TileMultipartClient` remains a public field-free interface with no parent interface and the exact +same 16 methods, including six private-cache accessors and the `markRender` super bridge. +`TRandomDisplayTickTile` remains a field-free child interface extending exactly `TileMultipartClient` and declaring +only `randomDisplayTick(Random)`. Generated composites retain the three private prefixed cache fields plus the plain +private `hasDynamicParts` field, and continue to use the same class cache. + +The raw dev artifact replaces the two Scala interfaces, two `$class` helpers, and three closure classes with concrete +Java mixin inputs plus package-private `TileMultipartClientAccess` and `TRandomDisplayTickTileAccess`. As with earlier +Java-trait ports, a consumer compiling directly against the untransformed dev jar must not emit direct class/field +opcodes for these runtime interfaces; use a transformed compile stub for source-compatible downstream rebuilds. + +`TileMultipart` gains additive no-op `renderStatic`, `renderDynamic`, and `randomDisplayTick` hooks. In-repo Java code +invokes generated overrides through that stable superclass because javac otherwise emits `invokevirtual` against the +concrete mixin input that Forge later rewrites to an interface. Existing binary consumers are unaffected. + +### Transformer constraints + +This is the first registered Java mixin extending another registered Java mixin. `registerJavaTrait` now carries the +parent runtime interface into the generated hierarchy and linearization, while parent validation resolves through the +registered mixin to `TileMultipart`. It also recognizes explicit source field accessors so it does not emit duplicate +runtime declarations. + +Render caches must not be copied by `TileMultipart.copyFrom`, matching the Scala trait. They are marked transient on +the Java mixin input, and automatic Java-trait field copying now ignores transient fields. The generated runtime fields +remain ordinary private fields. Trait registration moved before `BlockMultipart` construction so its +`instanceof TileMultipartClient` bytecode cannot preload the raw class before Forge defines the runtime interface. + +### Validation + +- `TileMultipartClientFunctionalTest`: 6 Forge tests covering cache partition/order, short-circuiting, translated and + fallback bounds, lazy initialization, dynamic early return, base no-op behavior, and ordered display-tick dispatch. +- `ForgeEnvironmentSmokeTest.generatesClientAndRandomDisplayTickTraitsWithTheirExactHierarchy`: exact interfaces, + inheritance, methods, generated fields, distinct combinations, and class-cache behavior. +- All seven tests passed against the untouched Scala implementation before the port; all 77 Forge tests pass after it. +- Complete plain-JVM suite: 153 tests, 0 failures, 0 errors. +- Actual block/TESR rendering and particle appearance require a client and remain on the manual checklist. diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index 7122248..cec0670 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -12,8 +12,8 @@ what breaks, and what is left. The other documents hold the reasoning. | `JAVA_MIGRATION_MANUAL_CHECKS.md` | What no automated test can cover, and must be checked by hand in a client | | `JAVA_MIGRATION_PROFILE.md` | The focused baseline, first measured result, findings, and exact rerun command | -Branch: `algent/java`. Base: `master`. 83 commits including separate characterization and port commits for -`TIInventoryTile`/`JInventoryTile`. +Branch: `algent/java`. Base: `master`. 85 commits including separate characterization and port commits for +`TileMultipartClient`/`TRandomDisplayTickTile`. ## The one rule that matters @@ -67,7 +67,7 @@ awk -F'"' '/[] { int.class }, 17)); + AxisAlignedBB bounds = (AxisAlignedBB) invokeClient(client, "getRenderBoundingBox", AxisAlignedBB.class); assertEquals(0, staticCache(client).length); assertEquals(0, dynamicCache(client).length); @@ -95,24 +98,32 @@ class TileMultipartClientFunctionalTest { @Test void dynamicRenderingReturnsBeforeTouchingTheCacheWhenTheFlagIsFalse() throws Exception { - TileMultipartClient client = (TileMultipartClient) newClientTile(); + TileMultipart tile = newClientTile(); + TileMultipartClient client = (TileMultipartClient) tile; RenderPart dynamic = new RenderPart("dynamic", true, false, Cuboid6.full); setDynamicCache(client, new TMultiPart[] { dynamic }); - client.hasDynamicParts_$eq(false); + invokeClient(client, "hasDynamicParts_$eq", void.class, new Class[] { boolean.class }, false); - client.renderDynamic(null, 0.5F, 1); + invokeClient( + client, + "renderDynamic", + void.class, + new Class[] { codechicken.lib.vec.Vector3.class, float.class, int.class }, + null, + 0.5F, + 1); assertEquals(0, dynamic.renderDynamicCalls); } @Test - void baseRandomDisplayTickIsANoOp() { + void baseRandomDisplayTickIsANoOp() throws Exception { TileMultipartClient client = (TileMultipartClient) newClientTile(); - client.randomDisplayTick(new Random(1L)); + invokeClient(client, "randomDisplayTick", void.class, new Class[] { Random.class }, new Random(1L)); } @Test - void randomDisplayTickVisitsOnlyMatchingPartsInOrderWithTheSameRandom() { + void randomDisplayTickVisitsOnlyMatchingPartsInOrderWithTheSameRandom() throws Exception { TileMultipart tile = newRandomDisplayTickTile(); List calls = new ArrayList<>(); DisplayPart first = new DisplayPart("first", calls); @@ -121,7 +132,12 @@ class TileMultipartClientFunctionalTest { setParts(tile, first, skipped, second); Random random = new Random(2L); - ((TRandomDisplayTickTile) tile).randomDisplayTick(random); + invokeClient( + (TileMultipartClient) tile, + "randomDisplayTick", + void.class, + new Class[] { Random.class }, + random); assertEquals(Arrays.asList("first", "second"), calls); assertSame(random, first.lastRandom); @@ -146,6 +162,30 @@ class TileMultipartClientFunctionalTest { tile.partList_$eq(JavaConversions.asScalaBuffer(Arrays.asList(parts)).toList()); } + private static void invokeUpdateRenderCache(TileMultipartClient client) throws Exception { + invokeClient(client, "updateRenderCache", void.class); + } + + private static Object invokeClient(TileMultipartClient client, String name, Class returnType, Object... args) + throws Exception { + return invokeClient(client, name, returnType, new Class[0], args); + } + + private static Object invokeClient(TileMultipartClient client, String name, Class returnType, + Class[] parameterTypes, Object... args) throws Exception { + try { + return MethodHandles.publicLookup() + .findVirtual(TileMultipartClient.class, name, MethodType.methodType(returnType, parameterTypes)) + .bindTo(client).invokeWithArguments(args); + } catch (Exception exception) { + throw exception; + } catch (Error error) { + throw error; + } catch (Throwable throwable) { + throw new AssertionError(throwable); + } + } + private static TMultiPart[] staticCache(TileMultipartClient client) throws Exception { return (TMultiPart[]) generatedField(client, "staticCache").get(client); } diff --git a/src/main/scala/codechicken/multipart/BlockMultipart.java b/src/main/scala/codechicken/multipart/BlockMultipart.java index 162e1be..2930fed 100644 --- a/src/main/scala/codechicken/multipart/BlockMultipart.java +++ b/src/main/scala/codechicken/multipart/BlockMultipart.java @@ -194,9 +194,9 @@ public class BlockMultipart extends Block { public boolean addHitEffects(World world, MovingObjectPosition hit, EffectRenderer effectRenderer) { TileMultipartClient tile = getClientTile(world, hit.blockX, hit.blockY, hit.blockZ); if (tile != null) { - // TileMultipartClient is a Scala trait, so it is a bare interface here and carries no TileMultipart - // members. - TileMultipart parts = (TileMultipart) tile; + // The mixin transformer turns TileMultipartClient into an interface at runtime, so retain the cast in + // bytecode even though the Java source model extends TileMultipart. + TileMultipart parts = TileMultipart.class.cast(tile); Tuple2 reduced = reduceMOP(hit); int index = (Integer) reduced._1(); if (index < parts.partList().size()) { @@ -291,7 +291,7 @@ public class BlockMultipart extends Block { public void randomDisplayTick(World world, int x, int y, int z, Random random) { TileMultipartClient tile = getClientTile(world, x, y, z); if (tile != null) { - tile.randomDisplayTick(random); + TileMultipart.class.cast(tile).randomDisplayTick(random); } } diff --git a/src/main/scala/codechicken/multipart/MultipartGenerator.scala b/src/main/scala/codechicken/multipart/MultipartGenerator.scala index 2220f95..2e1e242 100644 --- a/src/main/scala/codechicken/multipart/MultipartGenerator.scala +++ b/src/main/scala/codechicken/multipart/MultipartGenerator.scala @@ -35,7 +35,9 @@ object MultipartGenerator extends ScratchBitSet { private val partTraitMap_c = Map[Class[_], BitSet]() private val partTraitMap_s = Map[Class[_], BitSet]() private val clientTraitId = - MultipartMixinFactory.registerTrait(classOf[TileMultipartClient]) + MultipartMixinFactory.registerTrait( + "codechicken.multipart.TileMultipartClient" + ) private def partTraitMap(client: Boolean) = if (client) partTraitMap_c else partTraitMap_s diff --git a/src/main/scala/codechicken/multipart/MultipartRenderer$.java b/src/main/scala/codechicken/multipart/MultipartRenderer$.java index 1a4b15f..48c77df 100644 --- a/src/main/scala/codechicken/multipart/MultipartRenderer$.java +++ b/src/main/scala/codechicken/multipart/MultipartRenderer$.java @@ -50,7 +50,7 @@ public final class MultipartRenderer$ extends TileEntitySpecialRenderer implemen state.pullLightmapInstance(); state.useNormals = true; - client.renderDynamic(new Vector3(x, y, z), f, MinecraftForgeClient.getRenderPass()); + tile.renderDynamic(new Vector3(x, y, z), f, MinecraftForgeClient.getRenderPass()); } @Override @@ -80,7 +80,7 @@ public final class MultipartRenderer$ extends TileEntitySpecialRenderer implemen CCRenderState state = CCRenderState.instance(); state.resetInstance(); state.lightMatrix.locate(world, x, y, z); - boolean b = client.renderStatic(world, new Vector3(x, y, z), renderer); + boolean b = tile.renderStatic(world, new Vector3(x, y, z), renderer); state.lightMatrix.access = null; return b; } diff --git a/src/main/scala/codechicken/multipart/TileMultipart.java b/src/main/scala/codechicken/multipart/TileMultipart.java index d814600..38ab95d 100644 --- a/src/main/scala/codechicken/multipart/TileMultipart.java +++ b/src/main/scala/codechicken/multipart/TileMultipart.java @@ -4,8 +4,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Random; import java.util.function.Consumer; +import net.minecraft.client.renderer.RenderBlocks; import net.minecraft.entity.Entity; import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; @@ -14,6 +16,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.Vec3; +import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; import codechicken.lib.data.MCDataOutput; @@ -259,6 +262,17 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { worldObj.func_147479_m(xCoord, yCoord, zCoord); } + /** Client render hook overridden by generated tiles carrying TileMultipartClient. */ + public boolean renderStatic(IBlockAccess world, Vector3 position, RenderBlocks renderer) { + return false; + } + + /** Client render hook overridden by generated tiles carrying TileMultipartClient. */ + public void renderDynamic(Vector3 position, float frame, int pass) {} + + /** Client particle hook overridden by generated tiles carrying TRandomDisplayTickTile. */ + public void randomDisplayTick(Random random) {} + /** Helper function for calling a second level notify on a side (eg indirect power from a lever). */ public void notifyNeighborChange(int side) { BlockCoord pos = new BlockCoord(this).offset(side); diff --git a/src/main/scala/codechicken/multipart/TileMultipartClient.java b/src/main/scala/codechicken/multipart/TileMultipartClient.java new file mode 100644 index 0000000..12459e7 --- /dev/null +++ b/src/main/scala/codechicken/multipart/TileMultipartClient.java @@ -0,0 +1,183 @@ +package codechicken.multipart; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.world.IBlockAccess; +import net.minecraftforge.client.ForgeHooksClient; + +import codechicken.lib.vec.Cuboid6; +import codechicken.lib.vec.Vector3; +import scala.collection.Iterator; +import scala.collection.Seq; + +/** Client-side multipart render-cache mixin. */ +public class TileMultipartClient extends TileMultipart { + + private transient AxisAlignedBB cachedRenderBounds; + private transient TMultiPart[] staticCache; + private transient TMultiPart[] dynamicCache; + + public transient boolean hasDynamicParts; + + public boolean hasDynamicParts() { + return hasDynamicParts; + } + + public void hasDynamicParts_$eq(boolean value) { + hasDynamicParts = value; + } + + public void updateRenderCache() { + Seq parts = TileMultipartClientAccess.partList(this); + if (parts != null) { + List dynamicParts = new ArrayList<>(); + List staticParts = new ArrayList<>(); + Iterator iterator = parts.iterator(); + while (iterator.hasNext()) { + TMultiPart part = iterator.next(); + if (part.doesTick() || part.shouldRenderDynamic()) { + dynamicParts.add(part); + } else { + staticParts.add(part); + } + } + + TMultiPart[] statics = staticParts.toArray(new TMultiPart[staticParts.size()]); + TMultiPart[] dynamics = dynamicParts.toArray(new TMultiPart[dynamicParts.size()]); + Cuboid6 bounds = null; + for (TMultiPart part : statics) { + Cuboid6 partBounds = part.getRenderBounds(); + if (bounds == null) { + bounds = partBounds.copy(); + } else { + bounds.enclose(partBounds); + } + } + for (TMultiPart part : dynamics) { + Cuboid6 partBounds = part.getRenderBounds(); + if (bounds == null) { + bounds = partBounds.copy(); + } else { + bounds.enclose(partBounds); + } + } + if (bounds == null) { + bounds = Cuboid6.full; + } + + bounds.add(TileMultipartClientAccess.position(this)); + cachedRenderBounds = bounds.toAABB(); + staticCache = statics; + dynamicCache = dynamics; + hasDynamicParts = dynamics.length > 0; + } else { + staticCache = new TMultiPart[0]; + dynamicCache = new TMultiPart[0]; + hasDynamicParts = false; + cachedRenderBounds = TileMultipartClientAccess.fullBlockBounds(this); + } + } + + @Override + public boolean renderStatic(IBlockAccess world, Vector3 vector, RenderBlocks renderer) { + if (staticCache == null) { + updateRenderCache(); + } + + boolean rendered = false; + TMultiPart[] statics = staticCache; + if (statics != null) { + for (TMultiPart part : statics) { + rendered |= TileMultipartClientAccess.renderPart(part, world, vector, renderer); + } + } + + TMultiPart[] dynamics = dynamicCache; + if (dynamics != null) { + for (TMultiPart part : dynamics) { + rendered |= TileMultipartClientAccess.renderPart(part, world, vector, renderer); + } + } + return rendered; + } + + @Override + public void renderDynamic(Vector3 position, float frame, int pass) { + if (!hasDynamicParts) { + return; + } + + TMultiPart[] dynamics = dynamicCache; + if (dynamics != null) { + for (TMultiPart part : dynamics) { + if (part != null) { + part.renderDynamic(position, frame, pass); + } + } + } + } + + @Override + public void randomDisplayTick(Random random) {} + + @Override + public boolean shouldRenderInPass(int pass) { + if (staticCache == null) { + updateRenderCache(); + } + return hasDynamicParts; + } + + @Override + public AxisAlignedBB getRenderBoundingBox() { + if (cachedRenderBounds == null) { + updateRenderCache(); + } + return cachedRenderBounds; + } + + @Override + public void markRender() { + super.markRender(); + updateRenderCache(); + } +} + +/** Keeps inherited tile access and client rendering calls outside the generated trait transformer. */ +final class TileMultipartClientAccess { + + private TileMultipartClientAccess() {} + + static Seq partList(Object tile) { + return ((TileMultipart) tile).partList(); + } + + static Vector3 position(Object tile) { + return Vector3.fromTileEntity((TileMultipart) tile); + } + + static AxisAlignedBB fullBlockBounds(Object tile) { + TileMultipart multipart = (TileMultipart) tile; + return AxisAlignedBB.getBoundingBox( + multipart.xCoord, + multipart.yCoord, + multipart.zCoord, + multipart.xCoord + 1, + multipart.yCoord + 1, + multipart.zCoord + 1); + } + + static boolean renderPart(TMultiPart part, IBlockAccess world, Vector3 vector, RenderBlocks renderer) { + if (part == null) { + return false; + } + if (part instanceof ISBRHPart) { + return ((ISBRHPart) part).renderWorldBlock(world, (int) vector.x, (int) vector.y, (int) vector.z, renderer); + } + return part.renderStatic(vector, ForgeHooksClient.getWorldRenderPass()); + } +} diff --git a/src/main/scala/codechicken/multipart/TileMultipartClient.scala b/src/main/scala/codechicken/multipart/TileMultipartClient.scala deleted file mode 100644 index c228a86..0000000 --- a/src/main/scala/codechicken/multipart/TileMultipartClient.scala +++ /dev/null @@ -1,154 +0,0 @@ -package codechicken.multipart - -import java.util.Random - -import codechicken.lib.vec.{Cuboid6, Vector3} -import net.minecraft.client.renderer.RenderBlocks -import net.minecraft.util.AxisAlignedBB -import net.minecraft.world.IBlockAccess -import net.minecraftforge.client.ForgeHooksClient - -import scala.collection.JavaConversions._ - -trait TileMultipartClient extends TileMultipart { - private var cachedRenderBounds: AxisAlignedBB = null - private var staticCache: Array[TMultiPart] = null - private var dynamicCache: Array[TMultiPart] = null - - var hasDynamicParts: Boolean = false - - def updateRenderCache() { - if (partList != null) { - val (dynamic, static) = - partList.partition(p => p.doesTick || p.shouldRenderDynamic) - val sArr = static.toArray - val dArr = dynamic.toArray - - var c: Cuboid6 = null - var i = 0 - val allParts = sArr ++ dArr - while (i < allParts.length) { - val b = allParts(i).getRenderBounds - if (c == null) c = b.copy - else c.enclose(b) - i += 1 - } - - if (c == null) c = Cuboid6.full - - c.add(Vector3.fromTileEntity(this)) - cachedRenderBounds = c.toAABB - staticCache = sArr - dynamicCache = dArr - hasDynamicParts = dArr.length > 0 - } else { - staticCache = Array.empty - dynamicCache = Array.empty - hasDynamicParts = false - cachedRenderBounds = AxisAlignedBB.getBoundingBox( - xCoord, - yCoord, - zCoord, - xCoord + 1, - yCoord + 1, - zCoord + 1 - ) - } - } - - def renderStatic( - world: IBlockAccess, - vec: Vector3, - renderer: RenderBlocks - ) = { - if (staticCache == null) - updateRenderCache() - - var rendered = false - - def renderPart(part: TMultiPart): Unit = { - if (part == null) - return - - part match { - case isbrh: ISBRHPart => - if ( - isbrh.renderWorldBlock( - world, - vec.x.toInt, - vec.y.toInt, - vec.z.toInt, - renderer - ) - ) { - rendered = true - } - - case _ => - if ( - part.renderStatic( - vec, - ForgeHooksClient.getWorldRenderPass - ) - ) { - rendered = true - } - } - } - - val statics = staticCache - if (statics != null) { - var i = 0 - val len = statics.length - - while (i < len) { - renderPart(statics(i)) - i += 1 - } - } - - val dynamics = dynamicCache - if (dynamics != null) { - var i = 0 - val len = dynamics.length - - while (i < len) { - renderPart(dynamics(i)) - i += 1 - } - } - - rendered - } - - def renderDynamic(pos: Vector3, frame: Float, pass: Int) { - if (!hasDynamicParts) return - - val dynamics = dynamicCache - if (dynamics != null) { - var i = 0 - val len = dynamics.length - while (i < len) { - if (dynamics(i) != null) dynamics(i).renderDynamic(pos, frame, pass) - i += 1 - } - } - } - - def randomDisplayTick(random: Random) {} - - override def shouldRenderInPass(pass: Int) = { - if (staticCache == null) updateRenderCache() - hasDynamicParts - } - - override def getRenderBoundingBox = { - if (cachedRenderBounds == null) updateRenderCache() - cachedRenderBounds - } - - override def markRender(): Unit = { - super.markRender() - updateRenderCache() - } -} diff --git a/src/main/scala/codechicken/multipart/asm/ASMMixinCompiler.scala b/src/main/scala/codechicken/multipart/asm/ASMMixinCompiler.scala index ca2bfed..ff1c0c4 100644 --- a/src/main/scala/codechicken/multipart/asm/ASMMixinCompiler.scala +++ b/src/main/scala/codechicken/multipart/asm/ASMMixinCompiler.scala @@ -556,7 +556,7 @@ object ASMMixinCompiler { "Cannot register abstract class " + cnode.name + " as a java mixin trait. Use scala" ) - // val parentTraits = getAndRegisterParentTraits(cnode) + val parentTrait = getMixinInfo(cnode.superName) val fields = cnode.fields .map(f => (f.name, FieldMixin(f.name, f.desc, f.access))) .toMap @@ -591,7 +591,7 @@ object ASMMixinCompiler { cnode.name, null, "java/lang/Object", - Array(cnode.interfaces: _*) + (cnode.interfaces ++ parentTrait.map(_.name)).distinct.toArray ) def fname(name: String) = fields(name).accessName(cnode.name) @@ -787,7 +787,14 @@ object ASMMixinCompiler { staticTransform(mv, mnode) } - cnode.methods.foreach(convertMethod) + def isGeneratedFieldAccessor(mnode: MethodNode) = fields.values.exists { + field => + val name = field.accessName(cnode.name) + mnode.name == name && mnode.desc == "()" + field.desc || + mnode.name == name + "_$eq" && mnode.desc == "(" + field.desc + ")V" + } + + cnode.methods.filterNot(isGeneratedFieldAccessor).foreach(convertMethod) define(inode.name, createBytes(inode, 0)) define(tnode.name, createBytes(tnode, 0)) @@ -796,8 +803,8 @@ object ASMMixinCompiler { tnode.name, MixinInfo( tnode.name, - cnode.superName, - Seq(), + parentTrait.map(_.parent).getOrElse(cnode.superName), + parentTrait.toSeq, fields.values.toSeq, methods, supers diff --git a/src/main/scala/codechicken/multipart/asm/ASMMixinFactory.scala b/src/main/scala/codechicken/multipart/asm/ASMMixinFactory.scala index 63cb2e3..598c83c 100644 --- a/src/main/scala/codechicken/multipart/asm/ASMMixinFactory.scala +++ b/src/main/scala/codechicken/multipart/asm/ASMMixinFactory.scala @@ -67,10 +67,17 @@ class ASMMixinFactory[T]( } val info = getClassInfo(cnode) - def concreteParent(info: ClassInfo): ClassInfo = info.superClass.map { - case i if i.isTrait => concreteParent(i) - case i => i - }.get + def concreteParent(info: ClassInfo): ClassInfo = getMixinInfo( + info.name + ) match { + case Some(mixin) => getClassInfo(mixin.parent) + case None => + info.superClass.map { + case i if i.isTrait || getMixinInfo(i.name).isDefined => + concreteParent(i) + case i => i + }.get + } val parentName = concreteParent(info).name def checkParent(info: ClassInfo): Boolean = diff --git a/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala b/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala index 650a0d7..f911096 100644 --- a/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala +++ b/src/main/scala/codechicken/multipart/asm/MultipartMixinFactory.scala @@ -14,8 +14,9 @@ import scala.collection.JavaConversions._ object MultipartMixinFactory extends ASMMixinFactory(classOf[TileMultipart]) { override protected def autoCompleteJavaTrait(cnode: ClassNode) { + val copyFields = cnode.fields.filter(f => (f.access & ACC_TRANSIENT) == 0) if ( - !cnode.fields.isEmpty && findMethod( + !copyFields.isEmpty && findMethod( new ObfMapping( cnode.name, "copyFrom", @@ -45,7 +46,7 @@ object MultipartMixinFactory extends ASMMixinFactory(classOf[TileMultipart]) { val end = new Label() mv.visitJumpInsn(IFEQ, end) - cnode.fields.foreach { f => + copyFields.foreach { f => mv.visitVarInsn(ALOAD, 0) mv.visitVarInsn(ALOAD, 1) mv.visitFieldInsn(GETFIELD, cnode.name, f.name, f.desc) diff --git a/src/main/scala/codechicken/multipart/handler/proxies.scala b/src/main/scala/codechicken/multipart/handler/proxies.scala index d626492..118a0ff 100644 --- a/src/main/scala/codechicken/multipart/handler/proxies.scala +++ b/src/main/scala/codechicken/multipart/handler/proxies.scala @@ -28,15 +28,6 @@ class MultipartProxy_serverImpl { config = new ConfigFile(new File(cfgdir, "multipart.cfg")) .setComment("Multipart API config file") - GameRegistry.registerBlock( - new BlockMultipart().setBlockName("multipart"), - null, - "block" - ) - block = Block.blockRegistry - .getObject("ForgeMultipart:block") - .asInstanceOf[BlockMultipart] - MultipartGenerator.registerTrait( "codechicken.multipart.TSlottedPart", "codechicken.multipart.scalatraits.TSlottedTile" @@ -72,6 +63,15 @@ class MultipartProxy_serverImpl { "codechicken.multipart.scalatraits.TTileChangeTile" ) + GameRegistry.registerBlock( + new BlockMultipart().setBlockName("multipart"), + null, + "block" + ) + block = Block.blockRegistry + .getObject("ForgeMultipart:block") + .asInstanceOf[BlockMultipart] + MultipartSaveLoad.hookLoader() } diff --git a/src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.java b/src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.java new file mode 100644 index 0000000..2a4a249 --- /dev/null +++ b/src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.java @@ -0,0 +1,34 @@ +package codechicken.multipart.scalatraits; + +import java.util.Random; + +import codechicken.multipart.IRandomDisplayTick; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import codechicken.multipart.TileMultipartClient; +import scala.collection.Iterator; + +/** Client mixin that dispatches random display ticks only to interested parts. */ +public class TRandomDisplayTickTile extends TileMultipartClient { + + @Override + public void randomDisplayTick(Random random) { + TRandomDisplayTickTileAccess.randomDisplayTick(this, random); + } +} + +/** Keeps inherited part-list access outside the generated trait transformer. */ +final class TRandomDisplayTickTileAccess { + + private TRandomDisplayTickTileAccess() {} + + static void randomDisplayTick(Object tile, Random random) { + Iterator parts = ((TileMultipart) tile).partList().iterator(); + while (parts.hasNext()) { + TMultiPart part = parts.next(); + if (part instanceof IRandomDisplayTick) { + ((IRandomDisplayTick) part).randomDisplayTick(random); + } + } + } +} diff --git a/src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.scala b/src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.scala deleted file mode 100644 index eca0050..0000000 --- a/src/main/scala/codechicken/multipart/scalatraits/TRandomDisplayTickTile.scala +++ /dev/null @@ -1,14 +0,0 @@ -package codechicken.multipart.scalatraits - -import codechicken.multipart.TileMultipartClient -import codechicken.multipart.IRandomDisplayTick -import java.util.Random - -/** Saves processor time looping on tiles that don't need it - */ -trait TRandomDisplayTickTile extends TileMultipartClient { - override def randomDisplayTick(random: Random) { - for (p @ (_p: IRandomDisplayTick) <- partList.iterator) - p.randomDisplayTick(random) - } -}