From e286de3401bc786b15d0bf6e48ea92bc265887e0 Mon Sep 17 00:00:00 2001 From: Algent Date: Tue, 1 Sep 2026 09:53:09 +0200 Subject: [PATCH] test: characterize microblock placement --- .../MicroblockPlacementFunctionalTest.java | 241 ++++++++++++++++++ ...croblockPlacementCharacterizationTest.java | 212 +++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 src/functionalTest/java/codechicken/multipart/test/MicroblockPlacementFunctionalTest.java create mode 100644 src/test/java/codechicken/microblock/MicroblockPlacementCharacterizationTest.java diff --git a/src/functionalTest/java/codechicken/multipart/test/MicroblockPlacementFunctionalTest.java b/src/functionalTest/java/codechicken/multipart/test/MicroblockPlacementFunctionalTest.java new file mode 100644 index 0000000..7cc3ea5 --- /dev/null +++ b/src/functionalTest/java/codechicken/multipart/test/MicroblockPlacementFunctionalTest.java @@ -0,0 +1,241 @@ +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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.init.Blocks; +import net.minecraft.item.ItemStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.WorldServer; +import net.minecraftforge.common.util.FakePlayerFactory; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import codechicken.lib.raytracer.ExtendedMOP; +import codechicken.lib.vec.BlockCoord; +import codechicken.microblock.AdditionPlacement; +import codechicken.microblock.CommonMicroblock; +import codechicken.microblock.EdgePlacementGrid$; +import codechicken.microblock.ExecutablePlacement; +import codechicken.microblock.ExpandingPlacement; +import codechicken.microblock.FaceMicroClass$; +import codechicken.microblock.FacePlacement$; +import codechicken.microblock.MicroMaterialRegistry; +import codechicken.microblock.Microblock; +import codechicken.microblock.MicroblockClass; +import codechicken.microblock.MicroblockPlacement; +import codechicken.microblock.MicroblockPlacement$; +import codechicken.microblock.PlacementGrid; +import codechicken.microblock.PlacementProperties; +import codechicken.multipart.ControlKeyModifer; +import codechicken.multipart.TileMultipart; +import scala.Tuple2; + +class MicroblockPlacementFunctionalTest { + + private static final BlockCoord EXTERNAL_POS = new BlockCoord(160, 200, 160); + private static final BlockCoord INTERNAL_POS = new BlockCoord(164, 200, 160); + private static final BlockCoord EXPAND_POS = new BlockCoord(168, 200, 160); + private static final BlockCoord CUSTOM_POS = new BlockCoord(172, 200, 160); + + private WorldServer world; + private EntityPlayer player; + private int material; + + @BeforeEach + void setUp() { + world = MinecraftServer.getServer().worldServers[0]; + assertNotNull(world); + player = FakePlayerFactory.getMinecraft(world); + assertNotNull(player); + player.setSneaking(false); + ControlKeyModifer.map().clear(); + material = MicroMaterialRegistry.materialID("minecraft:stone"); + assertTrue(material >= 0); + clearPositions(); + } + + @AfterEach + void tearDown() { + player.setSneaking(false); + ControlKeyModifer.map().clear(); + clearPositions(); + } + + @Test + void selectsPlacesAndConsumesAnExternalFace() { + world.setBlock(EXTERNAL_POS.x, EXTERNAL_POS.y, EXTERNAL_POS.z, Blocks.stone); + MovingObjectPosition hit = hit(EXTERNAL_POS, 1.0); + + MicroblockPlacement decision = new MicroblockPlacement(player, hit, 1, material, true, FacePlacement$.MODULE$); + ExecutablePlacement placement = decision.apply(); + + AdditionPlacement addition = assertInstanceOf(AdditionPlacement.class, placement); + BlockCoord target = EXTERNAL_POS.copy().offset(1); + assertEquals(target, addition.pos()); + assertEquals(0, slot(addition.part())); + assertSame(player, decision.player()); + assertSame(hit, decision.hit()); + assertSame(world, decision.world()); + assertSame(FaceMicroClass$.MODULE$, decision.mcrClass()); + assertEquals(EXTERNAL_POS, decision.pos()); + assertEquals(0, decision.slot()); + assertEquals(1, decision.oslot()); + assertEquals(1.0, decision.d()); + assertFalse(decision.internal()); + assertFalse(decision.oppMod()); + + ItemStack stack = new ItemStack(net.minecraft.init.Items.stick, 3); + addition.place(world, player, stack); + addition.consume(world, player, stack); + + TileMultipart tile = TileMultipart.getTile(world, target); + assertNotNull(tile); + assertSame(addition.part(), tile.jPartList().get(0)); + assertEquals(2, stack.stackSize); + } + + @Test + void selectsTheInternalOrOppositeSlotFromTheControlModifier() { + player.setSneaking(true); + TileMultipart tile = TileMultipart.addPart(world, INTERNAL_POS, face(1, 2)); + + MicroblockPlacement normal = new MicroblockPlacement( + player, + hit(INTERNAL_POS, 0.25), + 1, + material, + true, + FacePlacement$.MODULE$); + AdditionPlacement normalPlacement = assertInstanceOf(AdditionPlacement.class, normal.apply()); + assertTrue(normal.internal()); + assertFalse(normal.doExpand()); + assertEquals(0, slot(normalPlacement.part())); + normalPlacement.place(world, player, new ItemStack(net.minecraft.init.Items.stick)); + assertSame(normalPlacement.part(), tile.partMap(0)); + + clear(INTERNAL_POS); + tile = TileMultipart.addPart(world, INTERNAL_POS, face(1, 2)); + ControlKeyModifer.map().put(player, true); + + MicroblockPlacement opposite = new MicroblockPlacement( + player, + hit(INTERNAL_POS, 0.25), + 1, + material, + true, + FacePlacement$.MODULE$); + AdditionPlacement oppositePlacement = assertInstanceOf(AdditionPlacement.class, opposite.apply()); + assertTrue(opposite.oppMod()); + assertEquals(1, slot(oppositePlacement.part())); + oppositePlacement.place(world, player, new ItemStack(net.minecraft.init.Items.stick)); + assertSame(oppositePlacement.part(), tile.partMap(1)); + } + + @Test + void expandsTheHitPartInPlaceAndConsumesOneItem() { + Microblock original = face(1, 0); + TileMultipart tile = TileMultipart.addPart(world, EXPAND_POS, original); + ExtendedMOP hit = indexedHit(EXPAND_POS, 0.25, 0); + + MicroblockPlacement decision = new MicroblockPlacement(player, hit, 1, material, true, FacePlacement$.MODULE$); + ExpandingPlacement placement = assertInstanceOf(ExpandingPlacement.class, decision.apply()); + assertTrue(decision.internal()); + assertTrue(decision.doExpand()); + assertEquals(2, placement.part().getSize()); + assertEquals(0, slot(placement.part())); + + ItemStack stack = new ItemStack(net.minecraft.init.Items.stick, 3); + placement.place(world, player, stack); + placement.consume(world, player, stack); + + assertSame(original, tile.jPartList().get(0)); + assertEquals(2, original.getSize()); + assertEquals(0, slot(original)); + assertEquals(2, stack.stackSize); + } + + @Test + void customPlacementWinsEvenWhenTheGridRejectsTheHit() { + world.setBlock(CUSTOM_POS.x, CUSTOM_POS.y, CUSTOM_POS.z, Blocks.stone); + ExecutablePlacement marker = new AdditionPlacement(CUSTOM_POS, null); + PlacementProperties properties = new PlacementProperties() { + + @Override + public int opposite(int slot, int side) { + return slot; + } + + @Override + public MicroblockClass microClass() { + return FaceMicroClass$.MODULE$; + } + + @Override + public PlacementGrid placementGrid() { + return EdgePlacementGrid$.MODULE$; + } + + @Override + public ExecutablePlacement customPlacement(MicroblockPlacement placement) { + return marker; + } + }; + + ExecutablePlacement result = MicroblockPlacement$.MODULE$ + .apply(player, hit(CUSTOM_POS, 1.0), 2, material, false, properties); + + assertSame(marker, result); + } + + private Microblock face(int size, int slot) { + Microblock part = FaceMicroClass$.MODULE$.create(false, material); + part.setShape(size, slot); + return part; + } + + private static int slot(Microblock part) { + return ((CommonMicroblock) part).getSlot(); + } + + private static MovingObjectPosition hit(BlockCoord pos, double relativeY) { + return new MovingObjectPosition( + pos.x, + pos.y, + pos.z, + 1, + Vec3.createVectorHelper(pos.x + 0.5, pos.y + relativeY, pos.z + 0.5)); + } + + private static ExtendedMOP indexedHit(BlockCoord pos, double relativeY, int index) { + return new ExtendedMOP( + pos.x, + pos.y, + pos.z, + 1, + Vec3.createVectorHelper(pos.x + 0.5, pos.y + relativeY, pos.z + 0.5), + new Tuple2(index, null)); + } + + private void clearPositions() { + clear(EXTERNAL_POS); + clear(EXTERNAL_POS.copy().offset(1)); + clear(INTERNAL_POS); + clear(EXPAND_POS); + clear(CUSTOM_POS); + } + + private void clear(BlockCoord pos) { + world.removeTileEntity(pos.x, pos.y, pos.z); + world.setBlockToAir(pos.x, pos.y, pos.z); + } +} diff --git a/src/test/java/codechicken/microblock/MicroblockPlacementCharacterizationTest.java b/src/test/java/codechicken/microblock/MicroblockPlacementCharacterizationTest.java new file mode 100644 index 0000000..d810152 --- /dev/null +++ b/src/test/java/codechicken/microblock/MicroblockPlacementCharacterizationTest.java @@ -0,0 +1,212 @@ +package codechicken.microblock; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +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.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.Type; + +import codechicken.lib.vec.BlockCoord; + +class MicroblockPlacementCharacterizationTest { + + private static final Set EXECUTABLE_METHODS = signatures( + "consume(Lnet/minecraft/world/World;Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)V", + "part()Lcodechicken/microblock/Microblock;", + "place(Lnet/minecraft/world/World;Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)V", + "pos()Lcodechicken/lib/vec/BlockCoord;"); + private static final Set CONCRETE_METHODS = signatures( + "consume(Lnet/minecraft/world/World;Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)V", + "place(Lnet/minecraft/world/World;Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/item/ItemStack;)V"); + private static final Set PROPERTY_METHODS = signatures( + "customPlacement(Lcodechicken/microblock/MicroblockPlacement;)Lcodechicken/microblock/ExecutablePlacement;", + "expand(II)Z", + "microClass()Lcodechicken/microblock/MicroblockClass;", + "opposite(II)I", + "placementGrid()Lcodechicken/microblock/PlacementGrid;", + "sneakOpposite(II)Z"); + private static final Set PLACEMENT_METHODS = signatures( + "apply()Lcodechicken/microblock/ExecutablePlacement;", + "checkMaterial()Z", + "create(III)Lcodechicken/microblock/Microblock;", + "d()D", + "doExpand()Z", + "expand(Lcodechicken/microblock/CommonMicroblock;)Lcodechicken/microblock/ExecutablePlacement;", + "expand(Lcodechicken/microblock/Microblock;Lcodechicken/microblock/Microblock;)Lcodechicken/microblock/ExecutablePlacement;", + "externalPlacement(I)Lcodechicken/microblock/ExecutablePlacement;", + "externalPlacement(Lcodechicken/microblock/Microblock;)Lcodechicken/microblock/ExecutablePlacement;", + "getHitDepth(Lcodechicken/lib/vec/Vector3;I)D", + "gtile()Lscala/Tuple2;", + "hit()Lnet/minecraft/util/MovingObjectPosition;", + "htile()Lcodechicken/multipart/TileMultipart;", + "internal()Z", + "internalPlacement(Lcodechicken/multipart/TileMultipart;I)Lcodechicken/microblock/ExecutablePlacement;", + "internalPlacement(Lcodechicken/multipart/TileMultipart;Lcodechicken/microblock/Microblock;)Lcodechicken/microblock/ExecutablePlacement;", + "material()I", + "mcrClass()Lcodechicken/microblock/MicroblockClass;", + "oppMod()Z", + "oslot()I", + "player()Lnet/minecraft/entity/player/EntityPlayer;", + "pos()Lcodechicken/lib/vec/BlockCoord;", + "pp()Lcodechicken/microblock/PlacementProperties;", + "side()I", + "size()I", + "slot()I", + "useOppMod()Z", + "vhit()Lcodechicken/lib/vec/Vector3;", + "world()Lnet/minecraft/world/World;"); + + @Test + void keepsTheSixClassHierarchyAndCallableSurface() throws Exception { + assertClass(ExecutablePlacement.class, Object.class, true, false, EXECUTABLE_METHODS); + assertClass(AdditionPlacement.class, ExecutablePlacement.class, false, false, CONCRETE_METHODS); + assertClass(ExpandingPlacement.class, ExecutablePlacement.class, false, false, CONCRETE_METHODS); + assertClass(PlacementProperties.class, Object.class, true, false, PROPERTY_METHODS); + assertClass(MicroblockPlacement.class, Object.class, false, false, PLACEMENT_METHODS); + assertClass( + MicroblockPlacement$.class, + Object.class, + false, + true, + signatures( + "apply(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IIZLcodechicken/microblock/PlacementProperties;)Lcodechicken/microblock/ExecutablePlacement;")); + + assertConstructor(ExecutablePlacement.class, BlockCoord.class, Microblock.class); + assertConstructor(AdditionPlacement.class, BlockCoord.class, Microblock.class); + assertConstructor(ExpandingPlacement.class, BlockCoord.class, Microblock.class, Microblock.class); + assertConstructor(PlacementProperties.class); + assertConstructor( + MicroblockPlacement.class, + EntityPlayer.class, + MovingObjectPosition.class, + int.class, + int.class, + boolean.class, + PlacementProperties.class); + + Field module = MicroblockPlacement$.class.getField("MODULE$"); + assertSame(MicroblockPlacement$.class, module.getType()); + assertTrue(Modifier.isStatic(module.getModifiers())); + assertTrue(Modifier.isFinal(module.getModifiers())); + assertSame(MicroblockPlacement$.MODULE$, module.get(null)); + } + + @Test + void keepsTheExactStoredStateShape() throws Exception { + assertPrivateFinalField(ExecutablePlacement.class, "pos", BlockCoord.class); + assertPrivateFinalField(ExecutablePlacement.class, "part", Microblock.class); + assertEquals(0, AdditionPlacement.class.getDeclaredFields().length); + assertPrivateFinalField(ExpandingPlacement.class, "opart", Microblock.class); + + assertPrivateFinalField(MicroblockPlacement.class, "player", EntityPlayer.class); + assertPrivateFinalField(MicroblockPlacement.class, "hit", MovingObjectPosition.class); + assertPrivateFinalField(MicroblockPlacement.class, "size", int.class); + assertPrivateFinalField(MicroblockPlacement.class, "material", int.class); + assertPrivateFinalField(MicroblockPlacement.class, "checkMaterial", boolean.class); + assertPrivateFinalField(MicroblockPlacement.class, "pp", PlacementProperties.class); + assertPrivateFinalField(MicroblockPlacement.class, "world", World.class); + assertPrivateFinalField(MicroblockPlacement.class, "mcrClass", MicroblockClass.class); + assertPrivateFinalField(MicroblockPlacement.class, "pos", BlockCoord.class); + assertPrivateFinalField(MicroblockPlacement.class, "vhit", codechicken.lib.vec.Vector3.class); + assertPrivateFinalField(MicroblockPlacement.class, "gtile", scala.Tuple2.class); + assertPrivateFinalField(MicroblockPlacement.class, "htile", codechicken.multipart.TileMultipart.class); + assertPrivateFinalField(MicroblockPlacement.class, "slot", int.class); + assertPrivateFinalField(MicroblockPlacement.class, "oslot", int.class); + assertPrivateFinalField(MicroblockPlacement.class, "d", double.class); + assertPrivateFinalField(MicroblockPlacement.class, "useOppMod", boolean.class); + assertPrivateFinalField(MicroblockPlacement.class, "oppMod", boolean.class); + assertPrivateFinalField(MicroblockPlacement.class, "internal", boolean.class); + assertPrivateFinalField(MicroblockPlacement.class, "doExpand", boolean.class); + assertPrivateFinalField(MicroblockPlacement.class, "side", int.class); + } + + @Test + void keepsDefaultPropertiesAndExecutableConsumption() { + PlacementProperties properties = new PlacementProperties() { + + @Override + public int opposite(int slot, int side) { + return slot + side; + } + + @Override + public MicroblockClass microClass() { + return null; + } + + @Override + public PlacementGrid placementGrid() { + return null; + } + }; + assertTrue(properties.sneakOpposite(2, 3)); + assertTrue(properties.expand(2, 3)); + assertNull(properties.customPlacement(null)); + + BlockCoord pos = new BlockCoord(1, 2, 3); + AdditionPlacement addition = new AdditionPlacement(pos, null); + ExpandingPlacement expansion = new ExpandingPlacement(pos, null, null); + assertSame(pos, addition.pos()); + assertNull(addition.part()); + assertSame(pos, expansion.pos()); + assertNull(expansion.part()); + + ItemStack stack = new ItemStack(net.minecraft.init.Items.stick, 4); + addition.consume(null, null, stack); + expansion.consume(null, null, stack); + assertEquals(2, stack.stackSize); + } + + private static Set signatures(String... signatures) { + return new TreeSet<>(Arrays.asList(signatures)); + } + + private static void assertClass(Class type, Class superclass, boolean isAbstract, boolean isFinal, + Set methods) { + assertTrue(Modifier.isPublic(type.getModifiers())); + assertEquals(isAbstract, Modifier.isAbstract(type.getModifiers())); + assertEquals(isFinal, Modifier.isFinal(type.getModifiers())); + assertSame(superclass, type.getSuperclass()); + assertArrayEquals(new Class[0], type.getInterfaces()); + assertEquals(methods, publicDeclaredMethods(type)); + } + + private static Set publicDeclaredMethods(Class type) { + Set signatures = new TreeSet<>(); + for (Method method : type.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + signatures.add(method.getName() + Type.getMethodDescriptor(method)); + } + } + return signatures; + } + + private static void assertConstructor(Class type, Class... parameterTypes) throws Exception { + Constructor constructor = type.getDeclaredConstructor(parameterTypes); + assertTrue(Modifier.isPublic(constructor.getModifiers())); + assertEquals(1, type.getDeclaredConstructors().length); + } + + private static void assertPrivateFinalField(Class owner, String name, Class type) throws Exception { + Field field = owner.getDeclaredField(name); + assertSame(type, field.getType()); + assertTrue(Modifier.isPrivate(field.getModifiers())); + assertTrue(Modifier.isFinal(field.getModifiers())); + } +}