From e5e1a041e46e7234ba34eb8c4e3c21fb53a0480f Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 4 Sep 2026 22:43:19 +0200 Subject: [PATCH] fix: preserve tile part-list dispatch --- JAVA_MIGRATION_HANDOFF.md | 2 +- docs/migration/HISTORY.md | 13 +++ .../codechicken/multipart/TileMultipart.java | 56 +++++----- .../TileMultipartCharacterizationTest.java | 101 ++++++++++++++++++ 4 files changed, 145 insertions(+), 27 deletions(-) diff --git a/JAVA_MIGRATION_HANDOFF.md b/JAVA_MIGRATION_HANDOFF.md index 920d354..fe45d49 100644 --- a/JAVA_MIGRATION_HANDOFF.md +++ b/JAVA_MIGRATION_HANDOFF.md @@ -16,7 +16,7 @@ migration checkout; `codex/tile-compatibility-fixes` was deleted after its fixes ## Current state and next target -**519 plain-JVM tests and 237 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java +**523 plain-JVM tests and 237 Java 8 Forge tests pass, with zero failures/errors/skips.** Sources total **224 Java files and 9 Scala files / 747 nonblank Scala lines**. The packaged inventory has 443 classes. Review follow-up: restored packet-scheduler callback mutation behavior with the original Scala hash-map traversal, diff --git a/docs/migration/HISTORY.md b/docs/migration/HISTORY.md index 18c1b24..3d0ffbd 100644 --- a/docs/migration/HISTORY.md +++ b/docs/migration/HISTORY.md @@ -1684,3 +1684,16 @@ differences belong in the [divergence ledger](../../JAVA_MIGRATION_DIVERGENCES.m frozen tests/resources, reports, logs and comparison tools. Frozen runs use its `version.txt`; final builds use the committed version. Sources remain 224 Java files and nine Scala files / 747 nonblank lines. Next: `ClassSymbolRef.info`, preserving outer/info-id/evaluation order, casts, trait bridges and failures. + +### 2026-09-04 — TileMultipart part-list accessor compatibility + +- Restored virtual `partList()` / `partList_$eq()` dispatch throughout the Java tile implementation, including + traversal, mutation, copying, serialization and queries. Retained the optimized list traversal, captured-sequence + behavior and detached-part guard; light and explosion queries retain the original accessor lookup counts. +- Four new regression cases compiled against the original `ForgeMultipart-1.7.12-dev.jar` pass on Scala, fail on + the unfixed port and pass unchanged after the fix. Build/style checks, 523 JVM tests and 237 Java 8 Forge tests + pass. The tile's declared JVM signatures and private fields are unchanged. +- Audited the supplied GTNH consumer checkouts: no `TileMultipart` subclass overriding these accessors was found. + This fixes an extension-contract regression, without claiming a demonstrated current-pack gameplay failure. + Evidence is under ignored `run/conversion-review/`; migration-plan changes remain deferred until the review fixes + are complete. diff --git a/src/main/scala/codechicken/multipart/TileMultipart.java b/src/main/scala/codechicken/multipart/TileMultipart.java index a54f413..56d0702 100644 --- a/src/main/scala/codechicken/multipart/TileMultipart.java +++ b/src/main/scala/codechicken/multipart/TileMultipart.java @@ -62,12 +62,12 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { * automatically generated on java tile traits with fields if it is not overridden. */ public void copyFrom(TileMultipart that) { - partList = that.partList; + partList_$eq(that.partList()); doesTick = that.doesTick; } public void loadFrom(TileMultipart that) { - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { iterator.next().bind(this); } @@ -84,7 +84,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Implicit java conversion of part list. */ public List jPartList() { - return JavaConversions.seqAsJavaList(partList); + return JavaConversions.seqAsJavaList(partList()); } @Override @@ -95,7 +95,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { // Direct list traversal avoids measured iterator/wrapper allocations; the setter also accepts other Seq types. @SuppressWarnings("unchecked") public void operate(Function1 f) { - Seq current = partList; + Seq current = partList(); if (!(current instanceof scala.collection.immutable.List)) { scala.collection.Iterator iterator = current.iterator(); while (iterator.hasNext()) { @@ -150,7 +150,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { if (!isInvalid()) { super.invalidate(); if (worldObj != null) { - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { iterator.next().onWorldSeparate(); } @@ -221,8 +221,11 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { @SuppressWarnings("unchecked") public int getLightValue() { + if (partList().isEmpty()) { + return 0; + } int max = 0; - Seq current = partList; + Seq current = partList(); if (!(current instanceof scala.collection.immutable.List)) { Iterator iterator = current.iterator(); while (iterator.hasNext()) { @@ -240,12 +243,13 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { } public float getExplosionResistance(Entity entity) { - if (partList.isEmpty()) { + Seq current = partList(); + if (current.isEmpty()) { // Matches the reference, where max on an empty view throws. throw new UnsupportedOperationException("empty.max"); } float max = Float.NEGATIVE_INFINITY; - Iterator iterator = partList.iterator(); + Iterator iterator = current.iterator(); while (iterator.hasNext()) { max = Math.max(max, iterator.next().explosionResistance(entity)); } @@ -291,7 +295,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { } public boolean canPlaceTorchOnTop() { - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { if (iterator.next().canPlaceTorchOnTop()) { return true; @@ -317,8 +321,8 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Returns true if part can be added to this space. */ public boolean canAddPart(TMultiPart part) { - return compatibilityAllows(worldObj, xCoord, yCoord, zCoord) && !partList.contains(part) - && occlusionTest(partList, part); + return compatibilityAllows(worldObj, xCoord, yCoord, zCoord) && !partList().contains(part) + && occlusionTest(partList(), part); } /** @@ -331,7 +335,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { */ public boolean canReplacePart(TMultiPart opart, TMultiPart npart) { List olist = new ArrayList<>(); - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { TMultiPart part = iterator.next(); if (part == null ? opart != null : !part.equals(opart)) { @@ -358,7 +362,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Get the write stream for updates to part. */ public MCDataOutput getWriteStream(TMultiPart part) { - return writeStream().writeByte(partList.indexOf(part)); + return writeStream().writeByte(partList().indexOf(part)); } private MCDataOutput writeStream() { @@ -386,14 +390,14 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { } public void addPart_do(TMultiPart part) { - if (partList.size() >= 250) { + if (partList().size() >= 250) { throw new AssertionError( "assertion failed: Tried to add more than 250 parts to the one tile. You're doing it wrong"); } List next = mutablePartsSnapshot(); next.add(part); - partList = toSeq(next); + partList_$eq(toSeq(next)); bindPart(part); part.bind(this); @@ -434,7 +438,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { } private int remPart_do(TMultiPart part, boolean sendPacket) { - int r = partList.indexOf(part); + int r = partList().indexOf(part); if (r < 0) { throw new IllegalArgumentException("Tried to remove a non-existant part"); } @@ -444,7 +448,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { part.preRemove(); List current = mutablePartsSnapshot(); current.removeIf(p -> p == null ? part == null : p.equals(part)); - partList = toSeq(current); + partList_$eq(toSeq(current)); if (sendPacket) { writeStream().writeByte(254).writeByte(r); @@ -454,11 +458,11 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { part.onRemoved(); part.tile_$eq(null); - if (partList.isEmpty()) { + if (partList().isEmpty()) { worldObj.setBlockToAir(xCoord, yCoord, zCoord); } else if (part.doesTick() && doesTick) { boolean ntick = false; - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { ntick |= iterator.next().doesTick(); } @@ -488,13 +492,13 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Remove all parts from internal cache. Provided for trait overrides, do not call externally. */ public void clearParts() { - partList = emptyParts(); + partList_$eq(emptyParts()); } /** Writes the description of this tile, and all parts composing it, to packet. */ public void writeDesc(MCDataOutput packet) { - packet.writeByte(partList.size()); - Iterator iterator = partList.iterator(); + packet.writeByte(partList().size()); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { TMultiPart part = iterator.next(); MultiPartRegistry.writePartID(packet, part); @@ -505,7 +509,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Perform a raytrace returning all intersecting parts sorted nearest to farthest. */ public Iterable rayTraceAll(Vec3 start, Vec3 end) { List list = new ArrayList<>(); - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); int i = 0; while (iterator.hasNext()) { Object mop = iterator.next().collisionRayTrace(start, end); @@ -531,7 +535,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Drop and remove part at index (internal mining callback). */ public void harvestPart(int index, ExtendedMOP hit, EntityPlayer player) { - TMultiPart part = partList.apply(index); + TMultiPart part = partList().apply(index); if (part != null) { part.harvest(hit, player); } @@ -549,7 +553,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { public final void writeToNBT(NBTTagCompound tag) { super.writeToNBT(tag); NBTTagList taglist = new NBTTagList(); - Iterator iterator = partList.iterator(); + Iterator iterator = partList().iterator(); while (iterator.hasNext()) { TMultiPart part = iterator.next(); NBTTagCompound parttag = new NBTTagCompound(); @@ -582,7 +586,7 @@ public class TileMultipart extends TileEntity implements IChunkLoadTile { /** Mutable snapshot used only while publishing a replacement Seq. */ private List mutablePartsSnapshot() { - return new ArrayList<>(JavaConversions.seqAsJavaList(partList)); + return new ArrayList<>(JavaConversions.seqAsJavaList(partList())); } private static Seq toSeq(List parts) { diff --git a/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java b/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java index 14437a8..ba679cf 100644 --- a/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java +++ b/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java @@ -115,6 +115,90 @@ class TileMultipartCharacterizationTest { assertTrue(tile.jPartList().isEmpty()); } + @Test + void overriddenPartListDrivesQueriesAndCallbacks() { + AccessorTile tile = new AccessorTile(); + CountingPart first = new CountingPart("first"); + RayTracingPart hit = new RayTracingPart("hit", 1d); + tile.parts = seq(first, new LightPart(13), new TorchSupportingPart(), hit); + tile.loadFrom(new TileMultipart()); + + assertEquals(4, tile.jPartList().size()); + assertSame(tile, first.tile()); + assertEquals(13, tile.getLightValue()); + assertTrue(tile.canPlaceTorchOnTop()); + assertFalse(tile.canReplacePart(first, hit)); + tile.onChunkLoad(); + assertEquals(1, first.chunkLoads); + tile.parts = seq(new RayTracingPart("missing", null), hit); + assertEquals(1, index(tile.collisionRayTrace(ORIGIN, ORIGIN))); + tile.harvestPart(1, null, null); + assertEquals(1, hit.harvests); + } + + @Test + void overriddenPartListReceivesMutationsAndCopies() { + AccessorTile source = new AccessorTile(); + CountingPart first = new CountingPart("first"); + Seq before = source.parts; + source.addPart_do(first); + assertEquals(1, source.writes); + assertEquals(seq(first), source.parts); + assertTrue(before.isEmpty()); + + AccessorTile target = new AccessorTile(); + target.copyFrom(source); + assertEquals(1, target.writes); + assertSame(source.parts, target.parts); + target.loadFrom(source); + assertSame(target, first.tile()); + target.clearParts(); + assertEquals(2, target.writes); + assertTrue(target.parts.isEmpty()); + assertEquals(seq(first), source.parts); + } + + @Test + void overriddenPartListKeepsTheCapturedTraversalAndDetachedPartGuard() { + AccessorTile tile = new AccessorTile(); + CountingPart first = new CountingPart("first"); + CountingPart removed = new CountingPart("removed"); + CountingPart last = new CountingPart("last"); + CountingPart added = new CountingPart("added"); + tile.parts = seq(first, removed, last); + tile.loadFrom(new TileMultipart()); + List visited = new ArrayList<>(); + + tile.operate(action(part -> { + visited.add(part.getType()); + if (part == first) { + tile.partList_$eq(seq(first, last, added)); + removed.tile_$eq(null); + added.bind(tile); + } + })); + + assertEquals(Arrays.asList("first", "last"), visited); + assertEquals(seq(first, last, added), tile.parts); + } + + @Test + void overriddenPartListKeepsLightAndResistanceLookupOrder() { + int[] reads = { 0 }; + TileMultipart tile = new TileMultipart() { + + @Override + public Seq partList() { + return ++reads[0] == 1 ? seq(new ResistancePart(9f)) : seq(new LightPart(13)); + } + }; + assertEquals(13, tile.getLightValue()); + assertEquals(2, reads[0]); + reads[0] = 0; + assertEquals(9f, tile.getExplosionResistance(null)); + assertEquals(1, reads[0]); + } + @Test void operateSkipsPartsWhoseTileHasBeenCleared() { TileMultipart tile = new TileMultipart(); @@ -374,6 +458,23 @@ class TileMultipartCharacterizationTest { }; } + private static final class AccessorTile extends TileMultipart { + + private Seq parts = seq(); + private int writes; + + @Override + public Seq partList() { + return parts; + } + + @Override + public void partList_$eq(Seq value) { + writes++; + parts = value; + } + } + private static class CountingPart extends TMultiPart { private final String type;