From 003e8f94ca22dc99248b68bb2b4ec538f499eb5f Mon Sep 17 00:00:00 2001 From: Algent Date: Fri, 28 Aug 2026 18:35:22 +0300 Subject: [PATCH] test: characterize tile change trait Seven behavioral tests and a generated-interface freeze, both against the untouched Scala trait. Nothing in the frozen consumer baseline references TTileChangeTile or a $class helper for it, so there is no binary fixture to take: the load-bearing surface is INeighborTileChange, which is already Java and unchanged. The behavior pinned is the flag lifecycle and the coordinate filter. bindPart only ever ors the flag in, copyFrom takes it from another change tile but leaves it alone for a plain source, clearParts resets it, and partRemoved recomputes from the remaining list rather than from the part handed to it. onNeighborTileChange dispatches only for an axial offset of one or two blocks, and marks the two-block case weak. The runtime interface is the exact thirteen methods: six overrides, the field getter and setter, and five super accessors. --- .../test/ForgeEnvironmentSmokeTest.java | 47 +++++ .../test/TTileChangeTileFunctionalTest.java | 174 ++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/TTileChangeTileFunctionalTest.java diff --git a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java index b86c64b..afda930 100644 --- a/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java +++ b/src/functionalTest/java/codechicken/multipart/test/ForgeEnvironmentSmokeTest.java @@ -39,6 +39,7 @@ import codechicken.multipart.TMultiPart; import codechicken.multipart.TileMultipart; import codechicken.multipart.asm.MultipartMixinFactory; import codechicken.multipart.scalatraits.TSlottedTile; +import codechicken.multipart.scalatraits.TTileChangeTile; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.Loader; import scala.collection.JavaConversions; @@ -214,6 +215,52 @@ class ForgeEnvironmentSmokeTest { assertSame(replacement, trait.getMethod("v_partMap").invoke(first)); } + @Test + void generatesAndCachesTileChangeTileClass() throws Exception { + int traitId = MultipartMixinFactory.getId(TTileChangeTile.class.getName().replace('.', '/')); + assertFalse(traitId < 0); + + BitSet traits = new BitSet(); + traits.set(traitId); + Object first = MultipartMixinFactory.construct(traits, Nil$.MODULE$); + Object second = MultipartMixinFactory.construct(traits, Nil$.MODULE$); + Class trait = Class.forName("codechicken.multipart.scalatraits.TTileChangeTile"); + + assertTrue(first instanceof TileMultipart); + assertTrue(first instanceof TTileChangeTile); + assertTrue(trait.isInterface()); + assertNotSame(first, second); + assertSame(first.getClass(), second.getClass()); + assertEquals(0, trait.getDeclaredFields().length); + Set signatures = new TreeSet<>(); + for (java.lang.reflect.Method method : trait.getDeclaredMethods()) { + signatures.add(method.getName() + org.objectweb.asm.Type.getMethodDescriptor(method)); + } + String prefix = "codechicken$multipart$scalatraits$TTileChangeTile$$super$"; + assertEquals( + new TreeSet<>( + Arrays.asList( + "bindPart(Lcodechicken/multipart/TMultiPart;)V", + "clearParts()V", + "copyFrom(Lcodechicken/multipart/TileMultipart;)V", + "getWeakChanges()Z", + "onNeighborTileChange(III)V", + "partRemoved(Lcodechicken/multipart/TMultiPart;I)V", + "weakTileChanges()Z", + "weakTileChanges_$eq(Z)V", + prefix + "bindPart(Lcodechicken/multipart/TMultiPart;)V", + prefix + "clearParts()V", + prefix + "copyFrom(Lcodechicken/multipart/TileMultipart;)V", + prefix + "onNeighborTileChange(III)V", + prefix + "partRemoved(Lcodechicken/multipart/TMultiPart;I)V")), + signatures); + + assertEquals(boolean.class, first.getClass().getDeclaredField("weakTileChanges").getType()); + trait.getMethod("weakTileChanges_$eq", boolean.class).invoke(first, true); + assertEquals(true, trait.getMethod("weakTileChanges").invoke(first)); + assertEquals(false, trait.getMethod("weakTileChanges").invoke(second), "State is per tile"); + } + @Test void generatedJavaTraitOverridesPartialOcclusion() throws Exception { PartialPart existing = new PartialPart(voxel(0)); diff --git a/src/functionalTest/java/codechicken/multipart/test/TTileChangeTileFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/TTileChangeTileFunctionalTest.java new file mode 100644 index 0000000..d608e76 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/TTileChangeTileFunctionalTest.java @@ -0,0 +1,174 @@ +package codechicken.multipart.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import codechicken.multipart.INeighborTileChange; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import codechicken.multipart.asm.MultipartMixinFactory; +import codechicken.multipart.scalatraits.TTileChangeTile; +import scala.collection.immutable.Nil$; + +/** Consumer-visible state and behavior of the generated neighbor-tile-change trait. */ +class TTileChangeTileFunctionalTest { + + @Test + void theFlagStartsFalseAndIsPublishedThroughGetWeakChanges() { + TileMultipart tile = newTileChangeTile(); + + assertFalse(weakTileChanges(tile)); + assertFalse(tile.getWeakChanges()); + } + + @Test + void bindPartOnlyEverSetsTheFlag() { + TileMultipart tile = newTileChangeTile(); + + tile.bindPart(new PlainPart()); + assertFalse(tile.getWeakChanges(), "A part that is not an INeighborTileChange contributes nothing"); + + tile.bindPart(new ChangePart("quiet", false)); + assertFalse(tile.getWeakChanges()); + + tile.bindPart(new ChangePart("weak", true)); + assertTrue(tile.getWeakChanges()); + + // The flag is or-ed in, so a later quiet part cannot clear it. + tile.bindPart(new ChangePart("quiet2", false)); + assertTrue(tile.getWeakChanges()); + } + + @Test + void copyFromTakesTheFlagOnlyFromAnotherChangeTile() { + TileMultipart source = newTileChangeTile(); + source.bindPart(new ChangePart("weak", true)); + TileMultipart target = newTileChangeTile(); + + target.copyFrom(source); + assertTrue(target.getWeakChanges()); + + // A plain source leaves the existing value alone rather than clearing it. + target.copyFrom(new TileMultipart()); + assertTrue(target.getWeakChanges()); + } + + @Test + void clearPartsResetsTheFlagAndTheSuperclassList() { + TileMultipart tile = newTileChangeTile(); + tile.addPart_do(new ChangePart("weak", true)); + assertTrue(tile.getWeakChanges()); + + tile.clearParts(); + + assertFalse(tile.getWeakChanges()); + assertTrue(tile.jPartList().isEmpty()); + } + + @Test + void partRemovedRecomputesTheFlagFromTheRemainingParts() { + TileMultipart tile = newTileChangeTile(); + ChangePart weak = new ChangePart("weak", true); + ChangePart quiet = new ChangePart("quiet", false); + tile.addPart_do(weak); + tile.addPart_do(quiet); + assertTrue(tile.getWeakChanges()); + + // partRemoved recomputes from the list, so the part must already be gone from it. + tile.partList_$eq(seq(quiet)); + tile.partRemoved(weak, 0); + assertFalse(tile.getWeakChanges()); + + tile.partList_$eq(seq(quiet, weak)); + tile.partRemoved(quiet, 0); + assertTrue(tile.getWeakChanges(), "A remaining weak part keeps the flag set"); + } + + @Test + void neighborChangesDispatchOnlyWithinTwoBlocksAlongOneAxis() { + TileMultipart tile = newTileChangeTile(); + ChangePart part = new ChangePart("weak", true); + tile.addPart_do(part); + + // The tile sits at the origin, so the neighbor coordinates are the offset. + tile.onNeighborTileChange(1, 0, 0); + tile.onNeighborTileChange(0, -2, 0); + assertEquals(Arrays.asList("5:false", "0:true"), part.calls); + + part.calls.clear(); + tile.onNeighborTileChange(0, 0, 0); // no distance + tile.onNeighborTileChange(3, 0, 0); // too far + tile.onNeighborTileChange(1, 1, 0); // not axial, so no side + assertTrue(part.calls.isEmpty()); + } + + @Test + void onlyPartsWantingNeighborChangesAreCalled() { + TileMultipart tile = newTileChangeTile(); + ChangePart listening = new ChangePart("weak", true); + tile.addPart_do(new PlainPart()); + tile.addPart_do(listening); + + tile.onNeighborTileChange(0, 1, 0); + + assertEquals(Arrays.asList("1:false"), listening.calls); + } + + private static TileMultipart newTileChangeTile() { + int traitId = MultipartMixinFactory.getId(TTileChangeTile.class.getName().replace('.', '/')); + BitSet traits = new BitSet(); + traits.set(traitId); + return (TileMultipart) MultipartMixinFactory.construct(traits, Nil$.MODULE$); + } + + private static boolean weakTileChanges(TileMultipart tile) { + return ((TTileChangeTile) tile).weakTileChanges(); + } + + private static scala.collection.Seq seq(TMultiPart... parts) { + return scala.collection.JavaConversions.asScalaBuffer(new ArrayList<>(Arrays.asList(parts))).toList(); + } + + private static class PlainPart extends TMultiPart { + + @Override + public String getType() { + return "tilechange_test:plain"; + } + } + + private static final class ChangePart extends PlainPart implements INeighborTileChange { + + private final String type; + private final boolean weak; + private final List calls = new ArrayList<>(); + + private ChangePart(String type, boolean weak) { + this.type = type; + this.weak = weak; + } + + @Override + public String getType() { + return "tilechange_test:" + type; + } + + @Override + public boolean weakTileChanges() { + return weak; + } + + @Override + public void onNeighborTileChanged(int side, boolean weak) { + calls.add(side + ":" + weak); + } + } +}