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.
This commit is contained in:
Algent committed 2026-08-30 15:25:15 +02:00
1 parent 6417e9b731
commit 970e888b7f
17 files changed
+448 -227

No files matched your search

+28 -6
View File
@@ -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.
+55
View File
@@ -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.
+35 -13
View File
@@ -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'"' '/<testsuite /{t+=$4;f+=$8;e+=$10} END{print "tests="t" failures="f" e
grep -o 'tests="[0-9]*" skipped="[0-9]*" failures="[0-9]*" errors="[0-9]*"' run/server/junit-out/TEST-*.xml
```
Current baseline: **153 plain-JVM tests, 70 Forge server tests, all passing at their last completed runs.**
Current baseline: **153 plain-JVM tests, 77 Forge server tests, all passing at their last completed runs.**
### ABI diff against the reference
@@ -197,6 +197,21 @@ trait-to-superclass cast is involved.
`MatchError: 188` while registering the trait. `JInventoryTile` therefore converts its collected sided slots to
`int[]` in package-private `JInventoryTileAccess`. Reference arrays use `ANEWARRAY` and remain safe inside traits.
**Registered Java traits may now extend another registered Java trait.** Register the parent first. The transformer
adds the parent runtime interface and linearization entry, while parent validation resolves through it to the concrete
`TileMultipart` base. This path is characterized by `TRandomDisplayTickTile`; do not generalize it to arbitrary class
inheritance without another fixture.
**Mark runtime-only Java-trait fields transient when Scala did not copy them.** `autoCompleteJavaTrait` copies every
ordinary field through generated `copyFrom`, but now excludes transient fields. `TileMultipartClient` uses this for
render caches and its derived dynamic flag, preserving the Scala trait's absence of a `copyFrom` override.
**Do not invoke a registered mixin type directly from Java source.** The untransformed input is a class, so javac emits
class opcodes that become invalid when Forge rewrites it to an interface. In-repo client calls use additive no-op hooks
on `TileMultipart` so generated overrides dispatch through the stable superclass. Dedicated-server tests use targeted
method handles for trait-only methods; ordinary reflection enumerates the client-only `RenderBlocks` descriptor and
triggers the side transformer.
**Type the access shim's parameters `Object`.** `TTileChangeTileAccess` first typed them as the trait itself and cast
to `TileMultipart` inside. Because the untransformed trait extends `TileMultipart`, javac elided the cast, and once
Forge rewrote the trait to an interface the verifier rejected the `getfield`. `TRedstoneTileAccess` only escapes this
@@ -214,7 +229,8 @@ All eight load-bearing `$class` helpers from the inventory, both registries, and
`TFacePart`, the `TIconHitEffects` unit, `TItemMultiPart`/`JItemMultiPart`, `TEdgePart`, `Saw`, `MicroMaterialRegistry`,
`MultiPartRegistry`, `TileMultipart`, `TMultiPart`, `TickScheduler`, `BlockMultipart`, the complete
`IRedstonePart`/`RedstoneInteractions` unit, `MicroRecipe`, and the `TPartialOcclusionTile`, `TSlottedTile`, and
`TRedstoneTile`, `TTileChangeTile`, `TFluidHandlerTile`, and `TIInventoryTile`/`JInventoryTile` Java-trait ports.
`TRedstoneTile`, `TTileChangeTile`, `TFluidHandlerTile`, `TIInventoryTile`/`JInventoryTile`, `TileMultipartClient`, and
`TRandomDisplayTickTile` Java-trait ports.
Plus the six marker interfaces: `TSlottedPart`, `IRandomDisplayTick`, `INeighborTileChange`, `TRandomUpdateTick`,
`ISidedHollowConnect`, `IMicroMaterialRender`, plus `MultipartHelper`, `TileCache`, `PacketScheduler` and the `ControlKeyModifer` pair.
@@ -229,7 +245,7 @@ across the port. It is also where the two shim constraints in the gotchas list w
`rayTraceAll`'s index production is now characterized, closing the gap the read-path cleanup left: the index written
into `ExtendedMOP.data` is what `reduceMOP` hands back to every click, activate, harvest and pick block.
89 Java files, 40 Scala files, ~5,453 Scala lines left (non-blank; that is the metric this figure has always used).
91 Java files, 38 Scala files, ~5,328 Scala lines left (non-blank; that is the metric this figure has always used).
## What is left, and in what order
@@ -305,18 +321,23 @@ behavior tests freeze inventory-list sharing, flattened routing, sided offsets a
generated composite retains its two private prefixed fields and per-tile initialization. Primitive `int[]` creation is
isolated in `JInventoryTileAccess` because the transformer cannot analyze `NEWARRAY`.
`TileMultipartClient` and `TRandomDisplayTickTile` complete the built-in Java-trait queue. Six focused behavior tests
freeze render-cache order, bounds, lazy initialization, dynamic short-circuiting, and display-tick dispatch. The Forge
shape guard pins the exact 16-method base interface, one-method child interface, inheritance, generated private fields,
and class caching. GuideNH only loads `TileMultipartClient` by name for `isInstanceOf`; no audited consumer references
either removed `$class` helper. The true client rendering and particle paths remain on the manual checklist.
**Medium.** The `handler` packages on both sides, `ItemMicroPart`,
`MicroblockPlacement`, `PlacementGrids`, `BlockMicroMaterial`, `MissingMicroMaterial`, `ConfigContent`,
`DefaultContent`.
**Phase 5, needs the `registerJavaTrait` path.** `TileMultipartClient` and the remaining Scala files in
`multipart/scalatraits/`. The no-field, stateful, measured hot-query, and inherited-member cases are all complete
without a generator change.
**Phase 5 is complete.** There are no Scala files left in `multipart/scalatraits/`. The client pair required the first
narrow generator relaxation: Java-trait parent linearization, explicit field-accessor recognition, and exclusion of
transient runtime caches from generated copying.
Take the client tile pair next: `TileMultipartClient` and `TRandomDisplayTickTile`. The latter is the only Scala file
left in `multipart/scalatraits/`, is registered client-only, and reads inherited `partList`, so it needs both an access
shim and client-side characterization rather than relying on the dedicated-server harness. Audit the base trait's
consumer-visible shape before deciding whether to port them together.
Take `multipart/handler/MultipartCompatiblity.scala` next as the smallest medium-risk Phase 6 handler unit. Preserve
the misspelled public singleton name and its Scala `Function4` accessor, characterize the default allow path and the
reflective MCPC hook failure path first, and keep the characterization/port commits separate.
**Phase 6/7, last.** `Microblock` and the microblock shape hierarchy, `MicroblockGenerator`, `MultipartGenerator`, and
all of `multipart/asm/`. The ASM subsystem should be last; freeze generated-class fixtures before touching it.
@@ -330,7 +351,8 @@ all of `multipart/asm/`. The ASM subsystem should be last; freeze generated-clas
work and is required before release claims about real TPS.
- The server side of flag-sensitive pass-through registration is automated; client-side exclusion still belongs to
the packaged-client/manual compatibility run.
- Shipping consumers compiled against the old `TSlottedTile`, `TRedstoneTile`, and `TIInventoryTile` interfaces remain binary-compatible
- Shipping consumers compiled against the old `TSlottedTile`, `TRedstoneTile`, `TIInventoryTile`,
`TileMultipartClient`, and `TRandomDisplayTickTile` interfaces remain binary-compatible
because Forge still exposes those exact interfaces at runtime. A consumer recompiled against the untransformed dev
jar instead sees concrete Java mixin inputs and can emit class/field opcodes that are invalid after Forge rewrites
them to interfaces. Provide a transformed compile stub or downstream source guidance before claiming
+2
View File
@@ -17,6 +17,8 @@ release that includes those ports.
| Hover a microblock placement target | The placement highlight renders, and a mod supplying its own highlight renderer overrides it | `MicroMaterialRegistry.renderHighlight` |
| Look at a saw in inventory and in hand | The saw item renders with its custom transform | `ItemSaw` `IItemRenderer` |
| Place microblocks of several materials, including glass | Transparent materials render in the correct pass and are not opaque | `IMicroMaterial.canRenderInPass` |
| Change a mixed multipart tile containing static and dynamically rendered parts | Static parts remain in the block render, dynamic parts remain in the TESR pass, and neither disappears after a part update | `TileMultipartClient` render caches |
| Watch a multipart torch long enough to emit display particles | Only parts implementing `IRandomDisplayTick` emit particles, with no duplicate or missing callbacks | `TRandomDisplayTickTile.randomDisplayTick` |
## Placement and interaction
@@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
@@ -43,11 +45,11 @@ class TileMultipartClientFunctionalTest {
setParts(tile, dynamicOnly, fixed, ticking);
TileMultipartClient client = (TileMultipartClient) tile;
client.updateRenderCache();
invokeUpdateRenderCache(client);
assertArrayEquals(new TMultiPart[] { fixed }, staticCache(client));
assertArrayEquals(new TMultiPart[] { dynamicOnly, ticking }, dynamicCache(client));
assertTrue(client.hasDynamicParts());
assertTrue((Boolean) invokeClient(client, "hasDynamicParts", boolean.class));
assertEquals(1, dynamicOnly.doesTickCalls);
assertEquals(1, dynamicOnly.dynamicCalls);
assertEquals(1, fixed.doesTickCalls);
@@ -66,11 +68,11 @@ class TileMultipartClientFunctionalTest {
tile.partList_$eq(null);
TileMultipartClient client = (TileMultipartClient) tile;
client.updateRenderCache();
invokeUpdateRenderCache(client);
assertEquals(0, staticCache(client).length);
assertEquals(0, dynamicCache(client).length);
assertFalse(client.hasDynamicParts());
assertFalse((Boolean) invokeClient(client, "hasDynamicParts", boolean.class));
assertBounds(cachedBounds(client), -3.0, 7.0, 11.0, -2.0, 8.0, 12.0);
}
@@ -84,8 +86,9 @@ class TileMultipartClientFunctionalTest {
assertNull(staticCache(client));
assertNull(cachedBounds(client));
assertFalse(client.shouldRenderInPass(17));
AxisAlignedBB bounds = client.getRenderBoundingBox();
assertFalse(
(Boolean) invokeClient(client, "shouldRenderInPass", boolean.class, new Class<?>[] { 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<String> 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);
}
@@ -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<Object, ExtendedMOP> 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);
}
}
@@ -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
@@ -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;
}
@@ -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);
@@ -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<TMultiPart> parts = TileMultipartClientAccess.partList(this);
if (parts != null) {
List<TMultiPart> dynamicParts = new ArrayList<>();
List<TMultiPart> staticParts = new ArrayList<>();
Iterator<TMultiPart> 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<TMultiPart> 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());
}
}
@@ -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()
}
}
@@ -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
@@ -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 =
@@ -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)
@@ -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()
}
@@ -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<TMultiPart> parts = ((TileMultipart) tile).partList().iterator();
while (parts.hasNext()) {
TMultiPart part = parts.next();
if (part instanceof IRandomDisplayTick) {
((IRandomDisplayTick) part).randomDisplayTick(random);
}
}
}
}
@@ -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)
}
}