From bde34454889aeb59f99a4263959d69c1a2f65af7 Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 28 Aug 2026 18:31:33 +0300 Subject: [PATCH] test: characterize multipart raytrace index production rayTraceAll was the one method the read-path cleanup restructured, moving from an indexed loop over a copied list to an iterator with a manual counter, and nothing covered it. The index it writes into ExtendedMOP.data is what reduceMOP hands back to every click, activate, harvest and pick block, so an off-by-one there would misroute interactions silently. Six tests pin the index production: a part that misses still consumes its index, sorting by distance happens after the indices are assigned, the part's own data is nested underneath rather than replaced, subHit keeps its default because data is assigned directly, and an index taken from a hit still selects the part that produced it. harvestPart's indexing and collisionRayTrace's nearest-or-null are covered alongside. All six also pass against the pre-cleanup implementation at 370b353^, which is the evidence that the restructure preserved behavior. --- .../TileMultipartCharacterizationTest.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java b/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java index 14b6c78..14437a8 100644 --- a/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java +++ b/src/test/java/codechicken/multipart/TileMultipartCharacterizationTest.java @@ -14,9 +14,14 @@ import java.util.List; import java.util.function.Consumer; import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; import org.junit.jupiter.api.Test; +import codechicken.lib.raytracer.ExtendedMOP; +import scala.Tuple2; import scala.collection.JavaConversions; import scala.collection.Seq; import scala.runtime.AbstractFunction1; @@ -29,6 +34,8 @@ import scala.runtime.BoxedUnit; */ class TileMultipartCharacterizationTest { + private static final Vec3 ORIGIN = Vec3.createVectorHelper(0, 0, 0); + @Test void aFreshTileHoldsNoPartsAndDoesNotTick() { TileMultipart tile = new TileMultipart(); @@ -261,6 +268,96 @@ class TileMultipartCharacterizationTest { assertTrue(tile.canPlaceTorchOnTop()); } + @Test + void rayTraceAllTagsEachHitWithItsPartListIndex() { + TileMultipart tile = new TileMultipart(); + tile.addPart_do(new RayTracingPart("far", 9d)); + tile.addPart_do(new RayTracingPart("missing", null)); + tile.addPart_do(new RayTracingPart("near", 1d)); + + List hits = hits(tile); + + // Sorted nearest first, but the part that missed still consumed its index. + assertEquals(Arrays.asList(2, 0), Arrays.asList(index(hits.get(0)), index(hits.get(1)))); + } + + @Test + void rayTraceAllIndicesStillSelectTheProducingPart() { + TileMultipart tile = new TileMultipart(); + tile.addPart_do(new RayTracingPart("far", 9d)); + tile.addPart_do(new RayTracingPart("missing", null)); + tile.addPart_do(new RayTracingPart("near", 1d)); + + // The round trip every click, activate, harvest and pick block depends on. + for (ExtendedMOP hit : hits(tile)) { + TMultiPart part = tile.partList().apply(index(hit)); + assertSame(hit, ((RayTracingPart) part).produced); + } + } + + @Test + void rayTraceAllNestsThePartsOwnDataUnderTheIndex() { + TileMultipart tile = new TileMultipart(); + RayTracingPart part = new RayTracingPart("only", 1d); + part.data = "subhit"; + tile.addPart_do(part); + + ExtendedMOP hit = hits(tile).get(0); + + assertEquals("subhit", ExtendedMOP.>getData(hit)._2()); + assertEquals(-1, hit.subHit, "data is assigned directly, so subHit keeps its default"); + } + + @Test + void rayTraceAllIsEmptyWhenNothingIsHit() { + TileMultipart tile = new TileMultipart(); + assertTrue(hits(tile).isEmpty()); + + tile.addPart_do(new RayTracingPart("missing", null)); + assertTrue(hits(tile).isEmpty()); + } + + @Test + void collisionRayTraceReturnsTheNearestHitOrNull() { + TileMultipart tile = new TileMultipart(); + assertNull(tile.collisionRayTrace(ORIGIN, ORIGIN)); + + tile.addPart_do(new RayTracingPart("far", 9d)); + RayTracingPart near = new RayTracingPart("near", 1d); + tile.addPart_do(near); + + ExtendedMOP nearest = tile.collisionRayTrace(ORIGIN, ORIGIN); + + assertSame(near.produced, nearest); + } + + @Test + void harvestPartSelectsByPartListIndex() { + TileMultipart tile = new TileMultipart(); + RayTracingPart first = new RayTracingPart("first", null); + RayTracingPart second = new RayTracingPart("second", null); + tile.addPart_do(first); + tile.addPart_do(second); + + tile.harvestPart(1, null, null); + + assertEquals(0, first.harvests); + assertEquals(1, second.harvests); + assertThrows(IndexOutOfBoundsException.class, () -> tile.harvestPart(2, null, null)); + } + + private static List hits(TileMultipart tile) { + List list = new ArrayList<>(); + for (ExtendedMOP hit : tile.rayTraceAll(ORIGIN, ORIGIN)) { + list.add(hit); + } + return list; + } + + private static int index(ExtendedMOP hit) { + return (Integer) ExtendedMOP.>getData(hit)._1(); + } + private static Seq seq(TMultiPart... parts) { List list = new ArrayList<>(Arrays.asList(parts)); return JavaConversions.asScalaBuffer(list).toList(); @@ -362,4 +459,33 @@ class TileMultipartCharacterizationTest { return true; } } + + /** Returns a canned hit so raytrace bookkeeping can be checked without a world. */ + private static final class RayTracingPart extends CountingPart { + + private final Double dist; + private Object data; + private ExtendedMOP produced; + private int harvests; + + private RayTracingPart(String type, Double dist) { + super(type); + this.dist = dist; + } + + @Override + public ExtendedMOP collisionRayTrace(Vec3 start, Vec3 end) { + if (dist == null) { + return null; + } + produced = new ExtendedMOP(0, 0, 0, 0, ORIGIN, data); + produced.dist = dist; + return produced; + } + + @Override + public void harvest(MovingObjectPosition hit, EntityPlayer player) { + harvests++; + } + } }