diff --git a/Fabric/src/main/resources/fabric.mod.json b/Fabric/src/main/resources/fabric.mod.json index 6dc160a60d..75b5bd7fa6 100644 --- a/Fabric/src/main/resources/fabric.mod.json +++ b/Fabric/src/main/resources/fabric.mod.json @@ -51,6 +51,7 @@ "vazkii.botania.test.item.FlowerPouchTest", "vazkii.botania.test.item.LifeAggregatorTest", "vazkii.botania.test.item.GrassSeedDispensingTest", + "vazkii.botania.test.item.SpectatorScanTest", "vazkii.botania.test.item.lens.BoreLensTest", "vazkii.botania.test.item.lens.EntropicWarpLensTest", "vazkii.botania.test.item.lens.PaintslingerLensTest" diff --git a/Xplat/src/main/java/vazkii/botania/common/helper/ItemNBTHelper.java b/Xplat/src/main/java/vazkii/botania/common/helper/ItemNBTHelper.java index fdef8df826..9687eba939 100644 --- a/Xplat/src/main/java/vazkii/botania/common/helper/ItemNBTHelper.java +++ b/Xplat/src/main/java/vazkii/botania/common/helper/ItemNBTHelper.java @@ -27,6 +27,7 @@ public final class ItemNBTHelper { private static final int[] EMPTY_INT_ARRAY = new int[0]; + private static final long[] EMPTY_LONG_ARRAY = new long[0]; // SETTERS /////////////////////////////////////////////////////////////////// @@ -58,6 +59,10 @@ public static void setLong(ItemStack stack, String tag, long l) { stack.getOrCreateTag().putLong(tag, l); } + public static void setLongArray(ItemStack stack, String tag, long[] val) { + stack.getOrCreateTag().putLongArray(tag, val); + } + public static void setFloat(ItemStack stack, String tag, float f) { stack.getOrCreateTag().putFloat(tag, f); } @@ -91,6 +96,10 @@ public static boolean verifyExistance(ItemStack stack, String tag) { return !stack.isEmpty() && stack.hasTag() && stack.getOrCreateTag().contains(tag); } + public static boolean verifyType(ItemStack stack, String tag, Class tagClass) { + return !stack.isEmpty() && stack.hasTag() && tagClass.isInstance(stack.getOrCreateTag().get(tag)); + } + @Nullable public static Tag get(ItemStack stack, String tag) { return verifyExistance(stack, tag) ? stack.getOrCreateTag().get(tag) : null; @@ -120,6 +129,10 @@ public static long getLong(ItemStack stack, String tag, long defaultExpected) { return verifyExistance(stack, tag) ? stack.getOrCreateTag().getLong(tag) : defaultExpected; } + public static long[] getLongArray(ItemStack stack, String tag) { + return verifyExistance(stack, tag) ? stack.getOrCreateTag().getLongArray(tag) : EMPTY_LONG_ARRAY; + } + public static float getFloat(ItemStack stack, String tag, float defaultExpected) { return verifyExistance(stack, tag) ? stack.getOrCreateTag().getFloat(tag) : defaultExpected; } diff --git a/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/SpectatorItem.java b/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/SpectatorItem.java index d150d77bfb..a5510e071b 100644 --- a/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/SpectatorItem.java +++ b/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/SpectatorItem.java @@ -12,6 +12,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer; import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.longs.LongArrayList; import net.minecraft.client.Minecraft; import net.minecraft.client.model.HumanoidModel; @@ -20,19 +21,21 @@ import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.client.resources.model.BakedModel; import net.minecraft.core.BlockPos; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.LongTag; -import net.minecraft.nbt.Tag; +import net.minecraft.nbt.LongArrayTag; import net.minecraft.world.Container; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.animal.allay.Allay; +import net.minecraft.world.entity.animal.horse.AbstractChestedHorse; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.vehicle.AbstractMinecartContainer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.trading.Merchant; import net.minecraft.world.item.trading.MerchantOffer; import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; import net.minecraft.world.phys.AABB; import vazkii.botania.api.BotaniaAPI; @@ -42,13 +45,19 @@ import vazkii.botania.client.render.AccessoryRenderer; import vazkii.botania.common.helper.ItemNBTHelper; import vazkii.botania.common.proxy.Proxy; +import vazkii.botania.mixin.AbstractHorseAccessor; +import vazkii.botania.mixin.RandomizableContainerBlockEntityAccessor; import java.util.List; public class SpectatorItem extends BaubleItem { - - private static final String TAG_ENTITY_POSITIONS = "highlightPositionsEnt"; - private static final String TAG_BLOCK_POSITIONS = "highlightPositionsBlock"; + private static final int[] EMPTY_ENTITIES_ARRAY = new int[0]; + private static final long[] EMPTY_BLOCKPOS_ARRAY = new long[0]; + public static final String TAG_ENTITY_POSITIONS = "highlightPositionsEnt"; + public static final String TAG_BLOCK_POSITIONS = "highlightPositionsBlock"; + public static final int RANGE_ENTITIES = 24; + public static final int RANGE_BLOCKS = 12; + public static final int SCAN_INTERVAL_TICKS = 4; public SpectatorItem(Properties props) { super(props); @@ -62,12 +71,18 @@ public void onWornTick(ItemStack stack, LivingEntity living) { } if (living.level().isClientSide) { - this.tickClient(stack, player); - } else { - this.tickServer(stack, player); + this.showScanResults(stack, player); + } else if (living.tickCount % SCAN_INTERVAL_TICKS == 0) { + this.scanForItems(stack, player); } } + @Override + public void onUnequipped(ItemStack stack, LivingEntity entity) { + ItemNBTHelper.removeEntry(stack, TAG_BLOCK_POSITIONS); + ItemNBTHelper.removeEntry(stack, TAG_ENTITY_POSITIONS); + } + public static class Renderer implements AccessoryRenderer { @Override public void doRender(HumanoidModel bipedModel, ItemStack stack, LivingEntity living, PoseStack ms, MultiBufferSource buffers, int light, float limbSwing, float limbSwingAmount, float partialTicks, float ageInTicks, float netHeadYaw, float headPitch) { @@ -83,15 +98,18 @@ public void doRender(HumanoidModel bipedModel, ItemStack stack, LivingEntity } } - protected void tickClient(ItemStack stack, Player player) { + protected void showScanResults(ItemStack stack, Player player) { if (player != Proxy.INSTANCE.getClientPlayer()) { return; } - ListTag blocks = ItemNBTHelper.getList(stack, TAG_BLOCK_POSITIONS, Tag.TAG_LONG, false); + // backward compatibility: this was a list tag before + var blockPosLongs = ItemNBTHelper.verifyType(stack, TAG_BLOCK_POSITIONS, LongArrayTag.class) + ? ItemNBTHelper.getLongArray(stack, TAG_BLOCK_POSITIONS) + : EMPTY_BLOCKPOS_ARRAY; - for (var block : blocks) { - BlockPos pos = BlockPos.of(((LongTag) block).getAsLong()); + for (var blockPosLong : blockPosLongs) { + BlockPos pos = BlockPos.of(blockPosLong); float m = 0.02F; WispParticleData data = WispParticleData.wisp(0.15F + 0.05F * (float) Math.random(), (float) Math.random(), (float) Math.random(), (float) Math.random(), false); player.level().addParticle(data, pos.getX() + (float) Math.random(), pos.getY() + (float) Math.random(), pos.getZ() + (float) Math.random(), m * (float) (Math.random() - 0.5), m * (float) (Math.random() - 0.5), m * (float) (Math.random() - 0.5)); @@ -100,91 +118,108 @@ protected void tickClient(ItemStack stack, Player player) { int[] entities = ItemNBTHelper.getIntArray(stack, TAG_ENTITY_POSITIONS); for (int i : entities) { Entity e = player.level().getEntity(i); - if (e != null && Math.random() < 0.6) { + if (e != null && e.isAlive() && Math.random() < 0.6) { WispParticleData data = WispParticleData.wisp(0.15F + 0.05F * (float) Math.random(), (float) Math.random(), (float) Math.random(), (float) Math.random(), Math.random() < 0.6); player.level().addParticle(data, e.getX() + (float) (Math.random() * 0.5 - 0.25) * 0.45F, e.getY() + e.getBbHeight(), e.getZ() + (float) (Math.random() * 0.5 - 0.25) * 0.45F, 0, 0.05F + 0.03F * (float) Math.random(), 0); } } } - protected void tickServer(ItemStack stack, Player player) { - IntArrayList entPosBuilder = new IntArrayList(); - ListTag blockPosBuilder = new ListTag(); - - scanForStack(player.getMainHandItem(), player, entPosBuilder, blockPosBuilder); - scanForStack(player.getOffhandItem(), player, entPosBuilder, blockPosBuilder); + public void scanForItems(ItemStack stack, Player player) { + ItemStack mainHandStack = player.getMainHandItem(); + ItemStack offHandStack = player.getOffhandItem(); - int[] currentEnts = entPosBuilder.elements(); + int[] entityIds = scanEntities(player, mainHandStack, offHandStack); + ItemNBTHelper.setIntArray(stack, TAG_ENTITY_POSITIONS, entityIds); - ItemNBTHelper.setIntArray(stack, TAG_ENTITY_POSITIONS, currentEnts); - ItemNBTHelper.setList(stack, TAG_BLOCK_POSITIONS, blockPosBuilder); + long[] blockPositionLongs = scanBlockContainers(player, mainHandStack, offHandStack); + ItemNBTHelper.setLongArray(stack, TAG_BLOCK_POSITIONS, blockPositionLongs); } - private void scanForStack(ItemStack pstack, Player player, IntArrayList entIdBuilder, ListTag blockPosBuilder) { - if (!pstack.isEmpty() || player.isShiftKeyDown()) { - int range = 24; - - List entities = player.level().getEntitiesOfClass(Entity.class, new AABB(player.getX() - range, player.getY() - range, player.getZ() - range, player.getX() + range, player.getY() + range, player.getZ() + range)); - for (Entity e : entities) { - if (e == player) { - continue; + private int[] scanEntities(Player player, ItemStack mainHandStack, ItemStack offHandStack) { + boolean emptyHands = mainHandStack.isEmpty() && offHandStack.isEmpty(); + if (emptyHands && !player.isShiftKeyDown()) { + return EMPTY_ENTITIES_ARRAY; + } + var entityIds = new IntArrayList(); + List entities = player.level().getEntitiesOfClass(Entity.class, new AABB(player.blockPosition()).inflate(RANGE_ENTITIES)); + for (Entity e : entities) { + if (e == player) { + continue; + } + if (e instanceof ItemEntity item) { + ItemStack entityStack = item.getItem(); + if (player.isShiftKeyDown() || equalStacks(entityStack, mainHandStack, offHandStack)) { + entityIds.add(item.getId()); } - if (e instanceof ItemEntity item) { - ItemStack istack = item.getItem(); - if (player.isShiftKeyDown() || ItemStack.isSameItemSameTags(istack, pstack)) { - entIdBuilder.add(item.getId()); - } - } else if (e instanceof Player targetPlayer) { - Container binv = BotaniaAPI.instance().getAccessoriesInventory(targetPlayer); - if (scanInventory(targetPlayer.getInventory(), pstack) || scanInventory(binv, pstack)) { - entIdBuilder.add(targetPlayer.getId()); - } - - } else if (e instanceof Merchant villager) { - for (MerchantOffer offer : villager.getOffers()) { - if (equalStacks(pstack, offer.getBaseCostA()) - || equalStacks(pstack, offer.getCostB()) - || equalStacks(pstack, offer.getResult())) { - entIdBuilder.add(e.getId()); - } - } - } else if (e instanceof Container inv) { - if (scanInventory(inv, pstack)) { - entIdBuilder.add(e.getId()); + } else if (emptyHands) { + continue; + } + if (e instanceof Player targetPlayer) { + if (scanInventory(targetPlayer.getInventory(), mainHandStack, offHandStack)) { + entityIds.add(targetPlayer.getId()); + } else { + Container baubleInventory = BotaniaAPI.instance().getAccessoriesInventory(targetPlayer); + if (scanInventory(baubleInventory, mainHandStack, offHandStack)) { + entityIds.add(targetPlayer.getId()); } } - } - - if (!pstack.isEmpty()) { - range = 12; - BlockPos pos = player.blockPosition(); - for (BlockPos pos_ : BlockPos.betweenClosed(pos.offset(-range, -range, -range), pos.offset(range + 1, range + 1, range + 1))) { - BlockEntity tile = player.level().getBlockEntity(pos_); - if (tile != null) { - if (tile instanceof Container inv) { - if (scanInventory(inv, pstack)) { - blockPosBuilder.add(LongTag.valueOf(pos_.asLong())); - } - } + } else if (e instanceof AbstractChestedHorse horse && horse.hasChest()) { + if (scanInventory(((AbstractHorseAccessor) horse).getInventory(), mainHandStack, offHandStack)) { + entityIds.add(horse.getId()); + } + } else if (e instanceof Allay allay && allay.hasItemInHand()) { + if (equalStacks(allay.getMainHandItem(), mainHandStack, offHandStack)) { + entityIds.add(allay.getId()); + } + } else if (e instanceof Merchant villager) { + for (MerchantOffer offer : villager.getOffers()) { + if (equalStacks(offer.getBaseCostA(), mainHandStack, offHandStack) + || equalStacks(offer.getCostB(), mainHandStack, offHandStack) + || equalStacks(offer.getResult(), mainHandStack, offHandStack)) { + entityIds.add(e.getId()); } } + } else if (e instanceof Container inv && (!(inv instanceof AbstractMinecartContainer minecart) + || minecart.getLootTable() == null)) { + if (scanInventory(inv, mainHandStack, offHandStack)) { + entityIds.add(e.getId()); + } } } + entityIds.trim(); + return entityIds.elements(); } - private boolean equalStacks(ItemStack stack1, ItemStack stack2) { - return ItemStack.isSameItemSameTags(stack1, stack2); + private long[] scanBlockContainers(Player player, ItemStack mainHandStack, ItemStack offHandStack) { + if (mainHandStack.isEmpty() && offHandStack.isEmpty()) { + return EMPTY_BLOCKPOS_ARRAY; + } + var blockPositions = new LongArrayList(); + BlockPos.betweenClosedStream(new AABB(player.blockPosition()).inflate(RANGE_BLOCKS)) + .filter(pos -> scanBlock(player, pos, mainHandStack, offHandStack)) + .forEach(pos -> blockPositions.add(pos.asLong())); + blockPositions.trim(); + return blockPositions.elements(); } - private boolean scanInventory(Container inv, ItemStack pstack) { - if (pstack.isEmpty()) { - return false; - } + private boolean scanBlock(Player player, BlockPos pos, ItemStack mainHandStack, ItemStack offHandStack) { + BlockEntity blockEntity = player.level().getBlockEntity(pos); + return blockEntity instanceof Container inv && (!(blockEntity instanceof RandomizableContainerBlockEntity lootInv) + || ((RandomizableContainerBlockEntityAccessor) lootInv).getLootTable() == null) + && scanInventory(inv, mainHandStack, offHandStack); + } + + private boolean equalStacks(ItemStack testStack, ItemStack referenceStack1, ItemStack referenceStack2) { + return !testStack.isEmpty() && (ItemStack.isSameItemSameTags(testStack, referenceStack1) + || ItemStack.isSameItemSameTags(testStack, referenceStack2)); + } + private boolean scanInventory(Container inv, ItemStack mainHandStack, ItemStack offHandStack) { for (int l = 0; l < inv.getContainerSize(); l++) { - ItemStack istack = inv.getItem(l); + ItemStack inventoryStack = inv.getItem(l); // Some mods still set stuff to null apparently... - if (istack != null && !istack.isEmpty() && equalStacks(istack, pstack)) { + if (inventoryStack != null && equalStacks(inventoryStack, mainHandStack, offHandStack)) { return true; } } diff --git a/Xplat/src/main/java/vazkii/botania/mixin/AbstractHorseAccessor.java b/Xplat/src/main/java/vazkii/botania/mixin/AbstractHorseAccessor.java index 1d6da9dfac..d3d5f27b92 100644 --- a/Xplat/src/main/java/vazkii/botania/mixin/AbstractHorseAccessor.java +++ b/Xplat/src/main/java/vazkii/botania/mixin/AbstractHorseAccessor.java @@ -13,9 +13,13 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(AbstractHorse.class) public interface AbstractHorseAccessor { @Accessor SimpleContainer getInventory(); + + @Invoker("createInventory") + void botania_createInventory(); } diff --git a/Xplat/src/main/java/vazkii/botania/mixin/RandomizableContainerBlockEntityAccessor.java b/Xplat/src/main/java/vazkii/botania/mixin/RandomizableContainerBlockEntityAccessor.java new file mode 100644 index 0000000000..8348c3dfa5 --- /dev/null +++ b/Xplat/src/main/java/vazkii/botania/mixin/RandomizableContainerBlockEntityAccessor.java @@ -0,0 +1,13 @@ +package vazkii.botania.mixin; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(RandomizableContainerBlockEntity.class) +public interface RandomizableContainerBlockEntityAccessor { + @Accessor + ResourceLocation getLootTable(); +} diff --git a/Xplat/src/main/java/vazkii/botania/test/block/ApothecaryRecipeTest.java b/Xplat/src/main/java/vazkii/botania/test/block/ApothecaryRecipeTest.java index d27a7b527b..50463eb296 100644 --- a/Xplat/src/main/java/vazkii/botania/test/block/ApothecaryRecipeTest.java +++ b/Xplat/src/main/java/vazkii/botania/test/block/ApothecaryRecipeTest.java @@ -28,7 +28,7 @@ private void spawnItem(GameTestHelper helper, Item item) { helper.spawnItem(item, pos.getX() + 0.5F, pos.getY() + 0.5F, pos.getZ() + 0.5F); } - @GameTest(template = TEMPLATE) + @GameTest(template = TEMPLATE, batch = "apothecary") public void testItemEnterPrevention(GameTestHelper helper) { helper.startSequence().thenExecute(() -> { spawnItem(helper, BotaniaItems.whitePetal); diff --git a/Xplat/src/main/java/vazkii/botania/test/block/HopperhockTest.java b/Xplat/src/main/java/vazkii/botania/test/block/HopperhockTest.java index d18ec84865..d1da2cd0f1 100644 --- a/Xplat/src/main/java/vazkii/botania/test/block/HopperhockTest.java +++ b/Xplat/src/main/java/vazkii/botania/test/block/HopperhockTest.java @@ -177,7 +177,7 @@ public void testComposterFull(GameTestHelper helper) { helper.startSequence().thenExecuteAfter(61, () -> { helper.assertBlockProperty(composterPos, ComposterBlock.LEVEL, 8); helper.assertItemEntityPresent(Items.PUMPKIN_PIE, SPAWN_ITEM_POS, 0); - }).thenExecute(helper::killAllEntities).thenSucceed(); + }).thenSucceed(); } private void spawnItemAndExpectNoPickUp(GameTestHelper helper, BlockPos composterPos, Item item) { diff --git a/Xplat/src/main/java/vazkii/botania/test/block/RannuncarpusTest.java b/Xplat/src/main/java/vazkii/botania/test/block/RannuncarpusTest.java index 7fc800a2dc..fb22bf74e1 100644 --- a/Xplat/src/main/java/vazkii/botania/test/block/RannuncarpusTest.java +++ b/Xplat/src/main/java/vazkii/botania/test/block/RannuncarpusTest.java @@ -30,7 +30,7 @@ public class RannuncarpusTest { private static final BlockPos SEA_PICKLE_POS = new BlockPos(1, 2, 1); private static final BlockPos FLOATING_YELLOW_CANDLE_POS = new BlockPos(1, 5, 2); - @GameTest(template = TEMPLATE) + @GameTest(template = TEMPLATE, batch = "rannuncarpus1") public void testDestinationFilterPositive(GameTestHelper helper) { helper.startSequence().thenExecute(() -> { helper.killAllEntities(); @@ -40,7 +40,7 @@ public void testDestinationFilterPositive(GameTestHelper helper) { }).thenSucceed(); } - @GameTest(template = TEMPLATE) + @GameTest(template = TEMPLATE, batch = "rannuncarpus2") public void testDestinationFilterNegative(GameTestHelper helper) { helper.startSequence().thenExecute(() -> { helper.killAllEntities(); @@ -79,7 +79,7 @@ public void testPickupFilterNegative(GameTestHelper helper) { // new items are supplied twice during the test + 2 * DelayHelper.FUNCTIONAL_INHERENT_DELAY; - @GameTest(template = TEMPLATE_CANDLES, timeoutTicks = MULTI_PLACEMENT_TIMEOUT_TICKS) + @GameTest(template = TEMPLATE_CANDLES, timeoutTicks = MULTI_PLACEMENT_TIMEOUT_TICKS, batch = "rannuncarpus3") public void testMultiplePlacements(GameTestHelper helper) { final var yellowCandles = helper.spawnItem(Blocks.YELLOW_CANDLE.asItem(), FLOWER_POS.getX(), FLOWER_POS.getY() + 1, FLOWER_POS.getZ()); helper.startSequence().thenExecute(() -> { diff --git a/Xplat/src/main/java/vazkii/botania/test/item/SpectatorScanTest.java b/Xplat/src/main/java/vazkii/botania/test/item/SpectatorScanTest.java new file mode 100644 index 0000000000..de4360ff65 --- /dev/null +++ b/Xplat/src/main/java/vazkii/botania/test/item/SpectatorScanTest.java @@ -0,0 +1,131 @@ +package vazkii.botania.test.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.npc.VillagerData; +import net.minecraft.world.entity.npc.VillagerProfession; +import net.minecraft.world.entity.npc.VillagerType; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.phys.Vec3; + +import vazkii.botania.common.helper.ItemNBTHelper; +import vazkii.botania.common.item.BotaniaItems; +import vazkii.botania.common.item.equipment.bauble.SpectatorItem; +import vazkii.botania.mixin.AbstractHorseAccessor; +import vazkii.botania.mixin.RandomizableContainerBlockEntityAccessor; +import vazkii.botania.test.TestingUtil; + +import java.util.Arrays; +import java.util.function.BiConsumer; + +public class SpectatorScanTest { + private static final BlockPos POSITION_CHEST_NORMAL = new BlockPos(17, 16, 16); + private static final BlockPos POSITION_CHEST_LOOT = new BlockPos(15, 16, 16); + private static final BlockPos POSITION_CART_NORMAL = new BlockPos(11, 2, 6); + private static final BlockPos POSITION_CART_LOOT = new BlockPos(9, 2, 6); + private static final BlockPos POSITION_ITEM = new BlockPos(7, 3, 3); + private static final BlockPos POSITION_VILLAGER = new BlockPos(4, 2, 3); + private static final BlockPos POSITION_DONKEY = new BlockPos(19, 2, 6); + private static final BlockPos POSITION_ALLAY = new BlockPos(19, 2, 16); + + private static final String LOOT_TABLE_CHEST = "minecraft:chests/simple_dungeon"; + private static final String LOOT_TABLE_CART = "minecraft:chests/abandoned_mineshaft"; + + @GameTest(template = "botania:item/spectator_scan", batch = "spectator1") + public void testSpectatorScanMainHand(GameTestHelper helper) { + performTest(helper, (h, player) -> { + player.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_INGOT)); + }); + } + + @GameTest(template = "botania:item/spectator_scan", batch = "spectator2") + public void testSpectatorScanOffHand(GameTestHelper helper) { + performTest(helper, (h, player) -> { + player.setItemSlot(EquipmentSlot.OFFHAND, new ItemStack(Items.IRON_INGOT)); + }); + } + + @GameTest(template = "botania:item/spectator_scan", batch = "spectator3") + public void testSpectatorScanBothHands(GameTestHelper helper) { + performTest(helper, (h, player) -> { + player.setItemSlot(EquipmentSlot.MAINHAND, new ItemStack(Items.IRON_INGOT)); + player.setItemSlot(EquipmentSlot.OFFHAND, new ItemStack(Items.IRON_INGOT)); + }); + } + + private static void performTest(GameTestHelper helper, BiConsumer additionalSetup) { + helper.killAllEntities(); + + // set up test entities + var itemEntity = helper.spawnItem(Items.IRON_INGOT, POSITION_ITEM); + var villager = helper.spawn(EntityType.VILLAGER, POSITION_VILLAGER); + villager.setVillagerData(new VillagerData(VillagerType.PLAINS, VillagerProfession.TOOLSMITH, 2)); + + // set inventory content + var regularChestCart = helper.spawn(EntityType.CHEST_MINECART, POSITION_CART_NORMAL); + regularChestCart.setItem(2, new ItemStack(Items.COAL)); + regularChestCart.setItem(5, new ItemStack(Items.IRON_INGOT)); + + var regularChest = TestingUtil.assertBlockEntity(helper, POSITION_CHEST_NORMAL, BlockEntityType.CHEST); + regularChest.setItem(3, new ItemStack(Items.FLINT)); + regularChest.setItem(7, new ItemStack(Items.IRON_INGOT)); + + var donkey = helper.spawnWithNoFreeWill(EntityType.DONKEY, POSITION_DONKEY); + donkey.setTamed(true); + donkey.setChest(true); + ((AbstractHorseAccessor) donkey).botania_createInventory(); + var donkeyInventory = ((AbstractHorseAccessor) donkey).getInventory(); + donkeyInventory.setItem(3, new ItemStack(Items.COAL)); + donkeyInventory.setItem(4, new ItemStack(Items.IRON_INGOT)); + + var allay = helper.spawnWithNoFreeWill(EntityType.ALLAY, POSITION_ALLAY); + allay.setItemInHand(InteractionHand.MAIN_HAND, new ItemStack(Items.IRON_INGOT)); + + // set loot tables + var lootChestCart = helper.spawn(EntityType.CHEST_MINECART, POSITION_CART_LOOT); + lootChestCart.setLootTable(new ResourceLocation(LOOT_TABLE_CART), 1L); + + var lootChest = TestingUtil.assertBlockEntity(helper, POSITION_CHEST_LOOT, BlockEntityType.CHEST); + lootChest.setLootTable(new ResourceLocation(LOOT_TABLE_CHEST), 1L); + + // set up player + var player = helper.makeMockPlayer(); + player.moveTo(helper.absoluteVec(new Vec3(24, 24, 24))); + additionalSetup.accept(helper, player); + var spectatorStack = new ItemStack(BotaniaItems.itemFinder); + + // execute test + ((SpectatorItem) BotaniaItems.itemFinder).scanForItems(spectatorStack, player); + + // ensure loot content has not been rolled + TestingUtil.assertThat(lootChestCart.getLootTable() != null, () -> "Chest loot was rolled"); + TestingUtil.assertThat(((RandomizableContainerBlockEntityAccessor) lootChest).getLootTable() != null, + () -> "Chest loot was rolled"); + + // check that exactly the relevant positions have been found + long[] blocks = ItemNBTHelper.getLongArray(spectatorStack, SpectatorItem.TAG_BLOCK_POSITIONS); + TestingUtil.assertEquals(blocks.length, 1, () -> "Expected 1 block hit, was " + blocks.length); + BlockPos chestPos = BlockPos.of(blocks[0]); + TestingUtil.assertEquals(helper.absolutePos(POSITION_CHEST_NORMAL), chestPos, + () -> "Chest position " + helper.absolutePos(POSITION_CHEST_NORMAL) + " not in result, but found " + chestPos); + + int[] entities = ItemNBTHelper.getIntArray(spectatorStack, SpectatorItem.TAG_ENTITY_POSITIONS); + TestingUtil.assertEquals(entities.length, 5, () -> "Expected 5 entity hits, but got " + entities.length); + TestingUtil.assertThat(Arrays.stream(entities).anyMatch(id -> villager.getId() == id), () -> "Villager not in result"); + TestingUtil.assertThat(Arrays.stream(entities).anyMatch(id -> itemEntity.getId() == id), () -> "Item entity not in result"); + TestingUtil.assertThat(Arrays.stream(entities).anyMatch(id -> regularChestCart.getId() == id), () -> "Minecart not in result"); + TestingUtil.assertThat(Arrays.stream(entities).anyMatch(id -> donkey.getId() == id), () -> "Donkey not in result"); + TestingUtil.assertThat(Arrays.stream(entities).anyMatch(id -> allay.getId() == id), () -> "Allay not in result"); + + helper.killAllEntities(); + helper.succeed(); + } +} diff --git a/Xplat/src/main/resources/botania_xplat.mixins.json b/Xplat/src/main/resources/botania_xplat.mixins.json index 875113571e..32c7e27573 100644 --- a/Xplat/src/main/resources/botania_xplat.mixins.json +++ b/Xplat/src/main/resources/botania_xplat.mixins.json @@ -43,6 +43,7 @@ "PistonStructureResolverMixin", "PlayerMixin", "PollinateGoalMixin", + "RandomizableContainerBlockEntityAccessor", "RecipeManagerAccessor", "RecipeProviderAccessor", "ServerLevelMixin", diff --git a/Xplat/src/main/resources/data/botania/gametest/structures/item/spectator_scan.snbt b/Xplat/src/main/resources/data/botania/gametest/structures/item/spectator_scan.snbt new file mode 100644 index 0000000000..b8c673c153 --- /dev/null +++ b/Xplat/src/main/resources/data/botania/gametest/structures/item/spectator_scan.snbt @@ -0,0 +1,28 @@ +{ + DataVersion: 3465, + size: [48, 48, 48], + data: [ + {pos: [4, 0, 3], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [9, 0, 6], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [11, 0, 6], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [19, 0, 6], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [3, 1, 3], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [4, 1, 2], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [4, 1, 4], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [5, 1, 3], state: "minecraft:smithing_table"}, + {pos: [7, 1, 3], state: "minecraft:polished_andesite_slab{type:top,waterlogged:false}"}, + {pos: [9, 1, 6], state: "minecraft:rail{shape:east_west,waterlogged:false}"}, + {pos: [11, 1, 6], state: "minecraft:rail{shape:east_west,waterlogged:false}"}, + {pos: [4, 3, 3], state: "minecraft:polished_andesite_slab{type:bottom,waterlogged:false}"}, + {pos: [17, 15, 16], state: "minecraft:chest{facing:south,type:single,waterlogged:false}"}, + {pos: [15, 15, 16], state: "minecraft:chest{facing:south,type:single,waterlogged:false}"} + ], + entities: [], + palette: [ + "minecraft:smithing_table", + "minecraft:polished_andesite_slab{type:top,waterlogged:false}", + "minecraft:rail{shape:east_west,waterlogged:false}", + "minecraft:polished_andesite_slab{type:bottom,waterlogged:false}", + "minecraft:chest{facing:south,type:single,waterlogged:false}" + ] +}