diff --git a/docs/migration/HISTORY.md b/docs/migration/HISTORY.md index 1879e65..58d2e65 100644 --- a/docs/migration/HISTORY.md +++ b/docs/migration/HISTORY.md @@ -1277,3 +1277,12 @@ differences belong in the [divergence ledger](../../JAVA_MIGRATION_DIVERGENCES.m pending entry. The first failed with the reported exception before the fix. The new-entry hash placement was checked against the original Scala implementation under Java 8. - Formatting, test checkstyle, all four packet-interface JVM tests and all 237 Forge server tests pass. + +### 2026-09-04 — Multipart virtual tile accessor regression + +- Restored virtual `tile()` and `tile_$eq` dispatch throughout `TMultiPart`, matching the original Scala methods. + Binding, coordinates/world lookup, ray tracing, harvesting and packet operations now honor part subclasses that + override the tile accessors instead of reading or writing the base class's private storage directly. +- Added three JVM regression cases covering overridden binding/unbinding, convenience methods and description + reads that publish a replacement tile before the render update. All three failed before the fix. +- Formatting, test checkstyle, the three new cases and all seven existing tile equality cases pass. diff --git a/src/main/scala/codechicken/multipart/TMultiPart.java b/src/main/scala/codechicken/multipart/TMultiPart.java index bde1109..d88202c 100644 --- a/src/main/scala/codechicken/multipart/TMultiPart.java +++ b/src/main/scala/codechicken/multipart/TMultiPart.java @@ -46,27 +46,27 @@ public abstract class TMultiPart { */ @Deprecated public TileEntity getTile() { - return tile; + return tile(); } /** Getter for tile.worldObj. */ public World world() { - return tile == null ? null : tile.getWorldObj(); + return tile() == null ? null : tile().getWorldObj(); } /** Short getter for xCoord. */ public int x() { - return tile.xCoord; + return tile().xCoord; } /** Short getter for yCoord. */ public int y() { - return tile.yCoord; + return tile().yCoord; } /** Short getter for zCoord. */ public int z() { - return tile.zCoord; + return tile().zCoord; } /** The unique string identifier for this class of multipart. */ @@ -74,7 +74,7 @@ public abstract class TMultiPart { /** Called when the container tile instance is changed to update reference. */ public void bind(TileMultipart t) { - tile = t; + tile_$eq(t); } /** Perform an occlusion test to determine whether this and npart can 'fit' in this block space. */ @@ -107,7 +107,7 @@ public abstract class TMultiPart { new Vector3(end), boxes, new BlockCoord(x(), y(), z()), - tile.getBlockType()); + tile().getBlockType()); } /** @@ -140,9 +140,9 @@ public abstract class TMultiPart { */ public void harvest(MovingObjectPosition hit, EntityPlayer player) { if (!player.capabilities.isCreativeMode) { - tile.dropItems(getDrops()); + tile().dropItems(getDrops()); } - tile.remPart(this); + tile().remPart(this); } /** The light level emitted by this part. */ @@ -256,7 +256,7 @@ public abstract class TMultiPart { * as a buffer which is flushed in a compressed databurst packet at the end of the tick. */ public MCDataOutput getWriteStream() { - return tile.getWriteStream(this); + return tile().getWriteStream(this); } /** @@ -265,7 +265,7 @@ public abstract class TMultiPart { */ public void read(MCDataInput packet) { readDesc(packet); - tile.markRender(); + tile().markRender(); } /** @@ -274,7 +274,7 @@ public abstract class TMultiPart { * possible. */ public void sendDescUpdate() { - if (tile != null) { + if (tile() != null) { writeDesc(getWriteStream()); } } diff --git a/src/test/java/codechicken/multipart/TMultiPartTileAccessorRegressionTest.java b/src/test/java/codechicken/multipart/TMultiPartTileAccessorRegressionTest.java new file mode 100644 index 0000000..f5a1eae --- /dev/null +++ b/src/test/java/codechicken/multipart/TMultiPartTileAccessorRegressionTest.java @@ -0,0 +1,142 @@ +package codechicken.multipart; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; + +import net.minecraft.block.Block; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; + +import codechicken.lib.data.MCDataInput; +import codechicken.lib.data.MCDataOutput; +import codechicken.lib.data.MCDataOutputWrapper; + +class TMultiPartTileAccessorRegressionTest { + + @Test + void bindingUsesTheOverriddenSetterIncludingUnbinding() { + VirtualPart part = new VirtualPart(); + RecordingTile tile = new RecordingTile(); + + part.bind(tile); + assertSame(tile, part.current); + assertEquals(1, part.binds); + part.bind(null); + assertNull(part.current); + assertEquals(2, part.binds); + } + + @Test + void convenienceMethodsUseTheTileSuppliedByTheOverride() { + VirtualPart part = new VirtualPart(); + RecordingTile tile = new RecordingTile(); + part.current = tile; + tile.xCoord = 42; + tile.yCoord = 73; + tile.zCoord = -4; + + assertSame(tile, part.getTile()); + assertEquals(42, part.x()); + assertEquals(73, part.y()); + assertEquals(-4, part.z()); + assertNull(part.world()); + assertEquals(1, tile.worldReads); + assertSame(tile.stream, part.getWriteStream()); + assertSame(part, tile.streamPart); + assertNull(part.collisionRayTrace(Vec3.createVectorHelper(0, 0, 0), Vec3.createVectorHelper(1, 1, 1))); + assertEquals(1, tile.blockReads); + } + + @Test + void descriptionUpdatesFollowTheTilePublishedByReadDescAndSkipAnUnboundPart() { + VirtualPart part = new VirtualPart(); + RecordingTile original = new RecordingTile(); + RecordingTile replacement = new RecordingTile(); + part.current = original; + part.onRead = () -> part.current = replacement; + + part.read(null); + assertEquals(0, original.renderUpdates); + assertEquals(1, replacement.renderUpdates); + part.sendDescUpdate(); + assertEquals(0, original.bytes.size()); + assertArrayEquals(new byte[] { 7 }, replacement.bytes.toByteArray()); + + part.current = null; + assertNull(part.world()); + part.sendDescUpdate(); + assertEquals(1, part.descriptionsWritten); + } + + private static final class VirtualPart extends TMultiPart { + + TileMultipart current; + int binds; + int descriptionsWritten; + Runnable onRead; + + @Override + public String getType() { + return "test:virtual-tile"; + } + + @Override + public TileMultipart tile() { + return current; + } + + @Override + public void tile_$eq(TileMultipart value) { + binds++; + current = value; + } + + @Override + public void readDesc(MCDataInput packet) { + onRead.run(); + } + + @Override + public void writeDesc(MCDataOutput packet) { + descriptionsWritten++; + packet.writeByte(7); + } + } + + private static final class RecordingTile extends TileMultipart { + + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + final MCDataOutput stream = new MCDataOutputWrapper(new DataOutputStream(bytes)); + TMultiPart streamPart; + int worldReads; + int blockReads; + int renderUpdates; + + @Override + public World getWorldObj() { + worldReads++; + return null; + } + + @Override + public Block getBlockType() { + blockReads++; + return null; + } + + @Override + public MCDataOutput getWriteStream(TMultiPart part) { + streamPart = part; + return stream; + } + + @Override + public void markRender() { + renderUpdates++; + } + } +}