From 85979168834e6deb7c0db28164ef519e5cd621df Mon Sep 17 00:00:00 2001 From: Cooble Date: Mon, 10 Aug 2026 14:18:27 +0200 Subject: [PATCH 1/7] builderwand - generalizing block filter + block picking - MovingObjectPosition.hitVec in onItemUse made consistent - more reliable entity collision avoidance on block placement - added exact copy/extrude mode - added Scribe offhand item - to change the wand mode to copy - placing blocks now using tryPlaceItemIntoWorld that correctly hooks to Forge - decrement blocks AFTER successful placing into the world --- .../utilitiesinexcess/ModItems.java | 2 + .../utilitiesinexcess/client/IMCForNEI.java | 4 + .../common/items/ItemBuildersWand.java | 171 +++++++----------- .../common/items/ItemScribe.java | 25 +++ .../config/items/BuildersWandsConfig.java | 5 + .../utils/BuildersBlockPicker.java | 146 +++++++++++++++ ...java => BuildersBlockSelectionFilter.java} | 90 +++++---- .../utils/BuildersMaterialBudget.java | 67 +++++++ .../utils/BuildersWandUtils.java | 166 ++++++++--------- .../utils/MovingObjectPositionUtil.java | 14 +- .../assets/utilitiesinexcess/lang/en_US.lang | 3 + .../textures/items/scribe.png | Bin 0 -> 433 bytes 12 files changed, 440 insertions(+), 253 deletions(-) create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemScribe.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java rename src/main/java/com/fouristhenumber/utilitiesinexcess/utils/{BuildersSelection.java => BuildersBlockSelectionFilter.java} (61%) create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java create mode 100644 src/main/resources/assets/utilitiesinexcess/textures/items/scribe.png diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/ModItems.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/ModItems.java index a82b963c1..3eeedae6c 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/ModItems.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/ModItems.java @@ -20,6 +20,7 @@ import com.fouristhenumber.utilitiesinexcess.common.items.ItemMobJar; import com.fouristhenumber.utilitiesinexcess.common.items.ItemPaintRoller; import com.fouristhenumber.utilitiesinexcess.common.items.ItemPseudoReversionSigil; +import com.fouristhenumber.utilitiesinexcess.common.items.ItemScribe; import com.fouristhenumber.utilitiesinexcess.common.items.ItemWateringCan; import com.fouristhenumber.utilitiesinexcess.common.items.ItemXRayGlasses; import com.fouristhenumber.utilitiesinexcess.common.items.tools.ItemAntiGravityShovel; @@ -83,6 +84,7 @@ public enum ModItems { CHUNCHUNMARU(ChunchunmaruConfig.INSTANCE.enable, new ItemChunchunmaru(), "chunchunmaru"), CAPACITY_UPGRADE(BlockConfig.filingCabinets.enableFilingCabinets, new ItemCapacityUpgrade(), "capacity_upgrade"), PAINT_ROLLER(ColoredBlocksConfig.INSTANCE.enablePaintRoller, new ItemPaintRoller(), "paint_roller"), + SCRIBE(BuildersWandsConfig.INSTANCE.enableScribe, new ItemScribe(), "scribe"), ; // leave trailing semicolon // spotless:on diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/client/IMCForNEI.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/client/IMCForNEI.java index abe065ffb..04b529dc5 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/client/IMCForNEI.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/client/IMCForNEI.java @@ -6,6 +6,7 @@ import com.fouristhenumber.utilitiesinexcess.common.blocks.BlockColored; import com.fouristhenumber.utilitiesinexcess.compat.Mods; import com.fouristhenumber.utilitiesinexcess.config.blocks.ColoredBlocksConfig; +import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; import com.fouristhenumber.utilitiesinexcess.config.items.InversionConfig; import com.fouristhenumber.utilitiesinexcess.mixins.early.minecraft.accessors.AccessorBlock; @@ -77,6 +78,9 @@ public static void IMCSender() { if (Mods.GregTech.isLoaded()) sendInfoPage( ",", "uie.nei.infopage.builders_wand.3"); + if (BuildersWandsConfig.INSTANCE.enableScribe) sendInfoPage( + ",", + "uie.nei.infopage.builders_wand.4"); } sendInfoPage("", "uie.nei.infopage.mob_jar.1"); diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java index fe16f275d..deddd93c4 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java @@ -4,9 +4,7 @@ import java.util.List; import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; -import net.minecraft.block.Block; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -19,14 +17,15 @@ import net.minecraft.world.World; import net.minecraftforge.common.util.ForgeDirection; -import org.jetbrains.annotations.NotNull; - import com.fouristhenumber.utilitiesinexcess.UtilitiesInExcess; import com.fouristhenumber.utilitiesinexcess.common.renderers.WireframeRenderer; import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; -import com.fouristhenumber.utilitiesinexcess.utils.BuildersSelection; +import com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockPicker; +import com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter; +import com.fouristhenumber.utilitiesinexcess.utils.BuildersMaterialBudget; import com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils; import com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils.WandAxisMode; +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; import com.gtnewhorizon.gtnhlib.api.ITranslucentItem; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; @@ -56,13 +55,9 @@ public void addInformation(ItemStack stack, EntityPlayer player, List to @SideOnly(Side.CLIENT) @Override public void onUpdate(ItemStack stack, World world, Entity entity, int slot, boolean isSelected) { - if (!world.isRemote || !(entity instanceof EntityPlayer player)) { - return; - } + if (!world.isRemote || !(entity instanceof EntityPlayer player)) return; - if (!isSelected) { - return; - } + if (!isSelected) return; // I'm pretty sure this will never determine whether we render or not but I'm not certain MovingObjectPosition movingObjectPosition = Minecraft.getMinecraft().objectMouseOver; @@ -74,20 +69,7 @@ public void onUpdate(ItemStack stack, World world, Entity entity, int slot, bool return; } - // 1. Target block location - BlockPos target = new BlockPos( - movingObjectPosition.blockX, - movingObjectPosition.blockY, - movingObjectPosition.blockZ); - - // 2. Side - int side = movingObjectPosition.sideHit; - ForgeDirection forgeSide = ForgeDirection.getOrientation(side); - if (forgeSide == ForgeDirection.UNKNOWN) { - UtilitiesInExcess.LOG - .warn("Builder's wand onUpdate was called with invalid facing direction: {}", forgeSide); - return; - } + ForgeDirection forgeSide = ForgeDirection.getOrientation(movingObjectPosition.sideHit); WandAxisMode axisMode; if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_H.isKeyDown(player)) { @@ -98,61 +80,21 @@ public void onUpdate(ItemStack stack, World world, Entity entity, int slot, bool axisMode = WandAxisMode.FREE; } - // 4. Target block to place - BuildersSelection selection = new BuildersSelection(player, world, movingObjectPosition); - List itemStackToPlace = selection.blockToPlace(player); - - // 3. Total amount to place - int placeCount = selection.maxPlaceCount(player, buildLimit); - - Set blocksToPlace = BuildersWandUtils.findAdjacentBlocks( - world, - itemStackToPlace, - placeCount, - forgeSide, - target, - movingObjectPosition, - player, - selection, - axisMode); - WireframeRenderer.clearCandidatePositions(); - for (BlockPos pos : blocksToPlace) { - WireframeRenderer.addCandidatePosition(pos.offset(forgeSide.offsetX, forgeSide.offsetY, forgeSide.offsetZ)); - } - } + // selection filter + var filter = new BuildersBlockSelectionFilter(player, world, movingObjectPosition); - private void placeBlock(World world, EntityPlayer player, @NotNull ItemStack itemStack, BlockPos pos, int side, - float hitX, float hitY, float hitZ, ForgeDirection forgeSide) { + // keep track of potentially used blocks in inventory + var itemBudget = new BuildersMaterialBudget(player.inventory, player.capabilities.isCreativeMode); - // This block is here because some mods want to use TEs to - ItemStack itemCopy = itemStack.copy(); - itemCopy.stackSize = 1; - Block comparisonBlock = world.getBlock(pos.x, pos.y, pos.z); - int comparisonMeta = world.getBlockMetadata(pos.x, pos.y, pos.z); - ItemStack comparisonItemStack = new ItemStack(comparisonBlock, 1, comparisonMeta); + // block picker + var blockPicker = BuildersBlockPicker.create(world, player, filter, itemBudget); - boolean useCompatPlacement = !ItemStack.areItemStacksEqual(itemCopy, comparisonItemStack); - if (useCompatPlacement) { - itemStack.getItem() - .onItemUse(itemCopy, player, world, pos.x, pos.y, pos.z, side, hitX, hitY, hitZ); - } else { - Block block = Block.getBlockFromItem(itemCopy.getItem()); - world.setBlock( - pos.x + forgeSide.offsetX, - pos.y + forgeSide.offsetY, - pos.z + forgeSide.offsetZ, - block, - comparisonMeta, - 3); - - world.playSoundEffect( - pos.x + forgeSide.offsetX, - pos.y + forgeSide.offsetY, - pos.z + forgeSide.offsetZ, - block.stepSound.func_150496_b(), - (block.stepSound.getVolume() + 1.0F) / 2.0F, - block.stepSound.getPitch() * 0.8F); - } + Set blocksToPlace = BuildersWandUtils + .findAdjacentBlocks(world, buildLimit, movingObjectPosition, player, filter, blockPicker, axisMode); + + WireframeRenderer.clearCandidatePositions(); + for (BlockPos pos : blocksToPlace) + WireframeRenderer.addCandidatePosition(pos.offset(forgeSide.offsetX, forgeSide.offsetY, forgeSide.offsetZ)); } @Override @@ -160,20 +102,20 @@ public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, float hitX, float hitY, float hitZ) { if (world.isRemote) return true; - // TODO: Prevent player from placing blocks into themself / other entities? + MovingObjectPosition mop = new MovingObjectPosition( + x, + y, + z, + side, + Vec3.createVectorHelper(x + hitX, y + hitY, z + hitZ)); + + // Sanity check ForgeDirection forgeSide = ForgeDirection.getOrientation(side); if (forgeSide == ForgeDirection.UNKNOWN) { - UtilitiesInExcess.LOG - .warn("Builder's wand onItemUse was called with invalid facing direction: {}", forgeSide); - return false; + UtilitiesInExcess.LOG.warn("Builder's wand onItemUse was called with invalid facing direction: {}", side); + return true; } - BlockPos target = new BlockPos(x, y, z); - MovingObjectPosition mop = new MovingObjectPosition(x, y, z, side, Vec3.createVectorHelper(hitX, hitY, hitZ)); - BuildersSelection selection = new BuildersSelection(player, world, mop); - - List itemStackToPlace = selection.blockToPlace(player); - WandAxisMode axisMode; if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_H.isKeyDown(player)) { axisMode = WandAxisMode.HORIZONTAL; @@ -183,33 +125,42 @@ public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, axisMode = WandAxisMode.FREE; } - int placeCount = selection.maxPlaceCount(player, buildLimit); + // selection filter + var filter = new BuildersBlockSelectionFilter(player, world, mop); + + // keep track of potentially used blocks in inventory + var itemBudget = new BuildersMaterialBudget(player.inventory, player.capabilities.isCreativeMode); - Set blocksToPlace = BuildersWandUtils.findAdjacentBlocks( - world, - itemStackToPlace, - placeCount, - forgeSide, - target, - mop, - player, - selection, - axisMode); + // block picker + var blockPicker = BuildersBlockPicker.create(world, player, filter, itemBudget); + + // potential block positions + Set blocksToPlace = BuildersWandUtils + .findAdjacentBlocks(world, buildLimit, mop, player, filter, blockPicker, axisMode); + + // reset the blockPicker with new budget + itemBudget = new BuildersMaterialBudget(player.inventory, player.capabilities.isCreativeMode); + blockPicker = BuildersBlockPicker.create(world, player, filter, itemBudget); - ItemStack nowPlacing; for (BlockPos pos : blocksToPlace) { - List candidates = selection.blockToPlace(player); - if (candidates.size() == 1) { - nowPlacing = candidates.get(0); - } else { - nowPlacing = candidates.get( - ThreadLocalRandom.current() - .nextInt(candidates.size())); - } + MovingObjectPositionUtil.TranslateMovingObjectPositionToLocation(mop, pos); + ItemStack toPlace = blockPicker + .pickBlock(mop, BuildersBlockSelectionFilter.getBlockByLocation(world, mop, player)); + + if (toPlace == null) continue; + + if (damageBackhand(BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand, player)) { + + ItemStack itemCopy = toPlace.copy(); + itemCopy.stackSize = 1; + + // uses ItemBlock to place the block with all the checks + // sets stackSize to 0 on success + itemCopy.tryPlaceItemIntoWorld(player, world, pos.x, pos.y, pos.z, side, hitX, hitY, hitZ); - if (player.capabilities.isCreativeMode || (BuildersWandUtils.decreaseFromInventory(player, nowPlacing) - && damageBackhand(BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand, player))) { - placeBlock(world, player, nowPlacing, pos, side, hitX, hitY, hitZ, forgeSide); + // Don't forget to take the spent item from the inventory + if (itemCopy.stackSize == 0 && !player.capabilities.isCreativeMode) + BuildersWandUtils.decreaseFromInventory(player, toPlace); } } player.inventoryContainer.detectAndSendChanges(); diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemScribe.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemScribe.java new file mode 100644 index 000000000..26a8ca5e5 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemScribe.java @@ -0,0 +1,25 @@ +package com.fouristhenumber.utilitiesinexcess.common.items; + +import java.util.List; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; + +public class ItemScribe extends Item { + + public ItemScribe() { + setMaxStackSize(1); + setMaxDamage(0); + setUnlocalizedName("scribe"); + setTextureName("utilitiesinexcess:scribe"); + } + + @Override + public void addInformation(ItemStack stack, EntityPlayer player, List tooltip, boolean advanced) { + tooltip.add(EnumChatFormatting.AQUA + StatCollector.translateToLocal("uie.desc.item.scribe.1")); + super.addInformation(stack, player, tooltip, advanced); + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/config/items/BuildersWandsConfig.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/config/items/BuildersWandsConfig.java index 2654b8eb4..629f9ada3 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/config/items/BuildersWandsConfig.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/config/items/BuildersWandsConfig.java @@ -39,4 +39,9 @@ public class BuildersWandsConfig { @Config.DefaultInt(100) @Config.Comment("[GT5U] Durability damage dealt to Trowels per block placed by the Builder's Wand. (Set to 0 to disable)") public int damageTrowelWithBuildersWand; + + @Config.Order(600) + @Config.DefaultBoolean(true) + @Config.RequiresMcRestart + public boolean enableScribe; } diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java new file mode 100644 index 000000000..8dd3dd11b --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java @@ -0,0 +1,146 @@ +package com.fouristhenumber.utilitiesinexcess.utils; + +import static com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils.canPlaceBlock; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; + +import com.gtnewhorizon.gtnhlib.util.ItemUtil; + +public abstract class BuildersBlockPicker { + + /** + * pick a suitable block to be placed, including whether its already in the inventory + * + * @param mop the source block on which the new one will be placed + * @param sourceBlock the stack the source block would drop when picked, already resolved by the caller + * @return block to be placed or null + */ + public abstract ItemStack pickBlock(MovingObjectPosition mop, ItemStack sourceBlock); + + /** + * pick a suitable block to be placed, including whether its already in the inventory + * + * @param mop the source block on which the new one will be placed + * @param sourceBlock the stack the source block would drop when picked, already resolved by the caller + * @return true if suitable block was picked + */ + public abstract boolean pickSomeBlock(MovingObjectPosition mop, ItemStack sourceBlock); + + public static BuildersBlockPicker create(World world, EntityPlayer player, BuildersBlockSelectionFilter filter, + BuildersMaterialBudget budget) { + if (filter.ignoresVariants()) { + return new CopyBlockPicker(world, budget); + } + return new PaletteBlockPicker( + world, + player.inventory, + filter.generatePalette(player), + budget, + ThreadLocalRandom.current()); + } + + public static class PaletteBlockPicker extends BuildersBlockPicker { + + private final BuildersMaterialBudget budget; + private final Random random; + private final List palette; + private final World world; + + private int remaining; + + private final int[] scratchTemplate; + private final int[] scratch; + + public PaletteBlockPicker(World world, InventoryPlayer inventory, List palette, + BuildersMaterialBudget budget, Random random) { + this.world = world; + this.palette = palette; + this.budget = budget; + this.random = random; + this.remaining = 0; + this.scratch = new int[palette.size()]; + this.scratchTemplate = new int[palette.size()]; + for (int i = 0; i < palette.size(); i++) this.scratchTemplate[i] = i; + + for (ItemStack stack : inventory.mainInventory) { + for (ItemStack entry : palette) { + if (ItemUtil.areStacksEqual(entry, stack)) { + remaining += stack.stackSize; + break; + } + } + } + } + + @Override + public ItemStack pickBlock(MovingObjectPosition mop, ItemStack ignored) { + if (remaining <= 0) return null; + + // prepare shuffling + System.arraycopy(scratchTemplate, 0, scratch, 0, scratch.length); + + // try every block in the palette in a random order + for (int i = 0; i < palette.size(); i++) { + + // shuffle one step + int j = i + random.nextInt(palette.size() - i); + int tmp = scratch[i]; + scratch[i] = scratch[j]; + scratch[j] = tmp; + + // pick one block from shuffled palette + ItemStack stack = palette.get(scratch[i]); + + if (canPlaceBlock(world, stack, mop) && budget.tryReserve(stack)) { + remaining--; + return stack; + } + } + return null; + } + + @Override + public boolean pickSomeBlock(MovingObjectPosition mop, ItemStack sourceBlock) { + if (remaining <= 0) return false; + + for (ItemStack stack : palette) { + if (canPlaceBlock(world, stack, mop)) { + remaining--; + return true; + } + } + return false; + } + } + + public static class CopyBlockPicker extends BuildersBlockPicker { + + private final BuildersMaterialBudget budget; + private final World world; + + public CopyBlockPicker(World world, BuildersMaterialBudget budget) { + this.budget = budget; + this.world = world; + } + + @Override + public ItemStack pickBlock(MovingObjectPosition mop, ItemStack sourceBlock) { + return sourceBlock != null && canPlaceBlock(world, sourceBlock, mop) && budget.tryReserve(sourceBlock) + ? sourceBlock + : null; + } + + @Override + public boolean pickSomeBlock(MovingObjectPosition mop, ItemStack sourceBlock) { + return pickBlock(mop, sourceBlock) != null; + } + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersSelection.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java similarity index 61% rename from src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersSelection.java rename to src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java index 976f2c929..954e67117 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersSelection.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java @@ -2,9 +2,7 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; @@ -15,92 +13,86 @@ import org.jetbrains.annotations.Nullable; +import com.fouristhenumber.utilitiesinexcess.common.items.ItemScribe; import com.fouristhenumber.utilitiesinexcess.compat.Mods; -import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; +import com.gtnewhorizon.gtnhlib.util.ItemUtil; import gregtech.api.items.MetaGeneratedTool; import gregtech.common.tools.ToolTrowel; import xonin.backhand.api.core.BackhandUtils; -public class BuildersSelection { +public class BuildersBlockSelectionFilter { - private final Set validBlocks; + private final List validBlocks; private final ItemStack backhand; private final ItemStack lookAtBlock; - public BuildersSelection(EntityPlayer player, World world, MovingObjectPosition movingObjectPosition) { - this.validBlocks = new HashSet<>(); + private final boolean ignoreVariants; + + public BuildersBlockSelectionFilter(EntityPlayer player, World world, MovingObjectPosition movingObjectPosition) { + this.validBlocks = new ArrayList<>(); backhand = Mods.Backhand.isLoaded() ? BackhandUtils.getOffhandItem(player) : null; lookAtBlock = getBlockByLocation(world, movingObjectPosition, player); + ignoreVariants = lookAtBlock != null && isScribe(backhand); // No logic is executed if we don't look at any block, no need to bother checking other cases - if (lookAtBlock == null) { - return; - } + if (lookAtBlock == null) return; this.validBlocks.add(lookAtBlock); // Clicked block is always valid - if (backhand == null) { - return; - } if (isValidBlock(backhand)) { this.validBlocks.add(backhand.copy()); - return; - } - if (isTrowel(backhand)) { + } else if (isTrowel(backhand)) { this.validBlocks.addAll(hotbarBlocks(player)); } } /** - * - * @param player - * @return Always a valid block list or null + * Generates a list of blocks to build with */ - public List blockToPlace(EntityPlayer player) { + public List generatePalette(EntityPlayer player) { + if (lookAtBlock == null) return Collections.emptyList(); + if (isValidBlock(backhand)) return Collections.singletonList(backhand); + if (isTrowel(backhand)) return hotbarBlocks(player); - if (backhand == null) { - return Collections.singletonList((lookAtBlock)); - } - if (isValidBlock(backhand)) { - return Collections.singletonList(backhand); - } else if (isTrowel(backhand)) { - return hotbarBlocks(player); - } else { - return Collections.singletonList(lookAtBlock); - } - } - - public int maxPlaceCount(EntityPlayer player, int wandLimit) { - if (player.capabilities.isCreativeMode) return BuildersWandsConfig.INSTANCE.buildersWandCreativeBuildLimit; - - int count = 0; - for (ItemStack block : blockToPlace(player)) { - count += BuildersWandUtils.countItemInInventory(player, block); - } - return Math.min(count, wandLimit); + return Collections.singletonList(lookAtBlock); } + /** + * Checks if the given ItemStack matches any of the valid blocks in the filter + */ public boolean matches(ItemStack other) { if (other == null) return false; return this.validBlocks.stream() .anyMatch( - validBlock -> validBlock.getItem() == other.getItem() - && ItemStack.areItemStackTagsEqual(validBlock, other) - && validBlock.getItemDamage() == other.getItemDamage()); + validBlock -> ignoreVariants ? validBlock.getItem() == other.getItem() + : ItemUtil.areStacksEqual(validBlock, other)); + } + + /** + * Whether the fill spans every variant of a block, matching on block identity alone and copying + * each position's own material rather than drawing from a palette. + */ + public boolean ignoresVariants() { + return ignoreVariants; } public static boolean isTrowel(@Nullable ItemStack stack) { - if (stack == null) { - return false; - } - if (Mods.GregTech.isLoaded() && stack.getItem() instanceof MetaGeneratedTool metaGeneratedTool) { + if (stack == null) return false; + + if (Mods.GregTech.isLoaded() && stack.getItem() instanceof MetaGeneratedTool metaGeneratedTool) return metaGeneratedTool.getToolStats(stack) instanceof ToolTrowel; - } + return false; } + public static boolean isScribe(@Nullable ItemStack stack) { + if (stack == null) return false; + + return stack.getItem() instanceof ItemScribe; + } + private static boolean isValidBlock(@Nullable ItemStack stack) { return (stack != null && stack.getItem() instanceof ItemBlock); } @@ -116,7 +108,7 @@ private static List hotbarBlocks(EntityPlayer player) { if (!isValidBlock(item)) { continue; } - candidates.add(item); + candidates.add(item.copy()); } return candidates; } diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java new file mode 100644 index 000000000..2d4e0e40c --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java @@ -0,0 +1,67 @@ +package com.fouristhenumber.utilitiesinexcess.utils; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.item.ItemStack; + +import com.gtnewhorizon.gtnhlib.util.ItemUtil; + +/** + * Tracks how much of each material a fill has committed to, so it never promises more than the player owns. + */ +public final class BuildersMaterialBudget { + + private static final class Reservation { + + ItemStack stack; + int remaining; + } + + private final List reservations = new ArrayList<>(); + private final InventoryPlayer inventory; + private final boolean creative; + + public BuildersMaterialBudget(InventoryPlayer inventory, boolean creative) { + this.inventory = inventory; + this.creative = creative; + } + + /** + * Claims one of the given material, returning false if none is left to spend. + */ + public boolean tryReserve(ItemStack material) { + for (Reservation reservation : reservations) { + if (ItemUtil.areStacksEqual(reservation.stack, material)) { + if (reservation.remaining <= 0) { + return false; + } + reservation.remaining--; + return true; + } + } + + int owned = 0; + for (int slot = 0; slot < inventory.getSizeInventory(); slot++) { + ItemStack inSlot = inventory.getStackInSlot(slot); + if (inSlot != null && ItemUtil.areStacksEqual(inSlot, material)) { + owned += inSlot.stackSize; + } + } + + // Creative still requires owning one, matching the stock wand. + if (owned == 0) { + return false; + } + if (creative) { + owned = Integer.MAX_VALUE; + } + + Reservation reservation = new Reservation(); + reservation.stack = material; + reservation.remaining = owned - 1; + reservations.add(reservation); + return true; + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java index b431bb76c..c8a1863ca 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java @@ -1,12 +1,12 @@ package com.fouristhenumber.utilitiesinexcess.utils; -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersSelection.getBlockByLocation; -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersSelection.isTrowel; -import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.TranslateMovingObjectPoistionToLocation; +import static com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter.getBlockByLocation; +import static com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter.isTrowel; +import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.TranslateMovingObjectPositionToLocation; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.LinkedList; -import java.util.List; import java.util.Objects; import java.util.Queue; import java.util.Set; @@ -14,6 +14,7 @@ import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; +import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.world.World; import net.minecraftforge.common.util.ForgeDirection; @@ -27,32 +28,13 @@ public class BuildersWandUtils { - public BuildersWandUtils() {} - - /** - * Counts the items of a certain type in a player's inventory - * - * @param player The player whose inventory to coutnt - * @param itemStack The itemstack incl metadata to count - * @return The item count - */ - public static int countItemInInventory(EntityPlayer player, ItemStack itemStack) { - int count = 0; - - for (ItemStack stack : player.inventory.mainInventory) { - if (ItemUtil.areStacksEqual(stack, itemStack)) { - count += stack.stackSize; - } - } - - return count; - } + private BuildersWandUtils() {} /** * Decreases an ItemStack containing the item in the player's inventory by 1 * * @param player The player in question - * @param itemStack The itemstack with metadata to compare + * @param itemStack The itemstack to compare against, including metadata and NBT * @return True if the ItemStack has been decremented, otherwise false */ public static boolean decreaseFromInventory(EntityPlayer player, ItemStack itemStack) { @@ -73,25 +55,28 @@ public static boolean decreaseFromInventory(EntityPlayer player, ItemStack itemS * Finds the blocks adjacent to the start position that are connected cardinally, or diagonally * and have a placeable block (e.g. air, fluid) in front of them relative to the side clicked on. * - * @param world The world in which to place - * @param findCount The maximum amount of blocks it should search - * @param clickedSide The side of the block that was clicked - * @param startPos The position to start - * @param buildersSelection The pattern used to search adjacent blocks - * @return The set of 1<=x<=findCount adjacent blocks with air on their face + * @param world The world in which to place + * @param maxCount The maximum amount of blocks it should search + * @param filter The pattern used to search adjacent blocks + * @param mop The position of the block that was clicked (with its clicked side) + * @param blockPicker decides which block to place + * @return The set of 1<=x<=maxCount adjacent blocks with air on their face */ - public static Set findAdjacentBlocks(World world, List possiblePlacements, int findCount, - ForgeDirection clickedSide, BlockPos startPos, MovingObjectPosition mop, EntityPlayer player, - BuildersSelection buildersSelection, WandAxisMode axisMode) { - Set region = new HashSet<>(); - if (findCount <= 0) { + public static Set findAdjacentBlocks(World world, int maxCount, MovingObjectPosition mop, + EntityPlayer player, BuildersBlockSelectionFilter filter, BuildersBlockPicker blockPicker, + BuildersWandUtils.WandAxisMode axisMode) { + Set region = LinkedHashSet.newLinkedHashSet(maxCount); + if (maxCount <= 0) { return region; } - Set visited = new HashSet<>(); + Set visited = HashSet.newHashSet(maxCount); Queue queue = new LinkedList<>(); + // copy the mop so we can translate it without affecting the original + mop = MovingObjectPositionUtil.copy(mop); + // Determine allowed offsets depending on the face that was clicked. - int[][] allowedOffsets = switch (clickedSide) { + int[][] allowedOffsets = switch (ForgeDirection.getOrientation(mop.sideHit)) { case UP, DOWN -> switch (axisMode) { case FREE -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 0, 1 }, { 0, 0, -1 }, { 1, 0, 1 }, { 1, 0, -1 }, { -1, 0, 1 }, { -1, 0, -1 } }; @@ -113,73 +98,44 @@ public static Set findAdjacentBlocks(World world, List poss default -> throw new RuntimeException("UE's BuilderWand's findAdjacentBlocks called with invalid side"); }; - // translate the mop - TranslateMovingObjectPoistionToLocation(mop, startPos); - - // Base case - if (IsValidForWireFrame(world, possiblePlacements, startPos, mop, player, clickedSide, buildersSelection)) { - region.add(startPos); - queue.add(startPos); - visited.add(startPos); - } else { - return region; - } + // start block + BlockPos startPos = new BlockPos(mop.blockX, mop.blockY, mop.blockZ); + queue.add(startPos); + visited.add(startPos); // Flood-fill the contiguous region in the allowed plane. - while (!queue.isEmpty() && region.size() < findCount) { + while (!queue.isEmpty() && region.size() < maxCount) { BlockPos current = queue.poll(); - for (int[] off : allowedOffsets) { - if (region.size() >= findCount) { - break; - } + // translate the mop + TranslateMovingObjectPositionToLocation(mop, current); + if (!IsValidForWireFrame(world, mop, player, filter, blockPicker)) continue; + region.add(current); + + for (int[] off : allowedOffsets) { + if (region.size() >= maxCount) break; BlockPos key = current.offset(off[0], off[1], off[2]); - if (visited.contains(key)) { - continue; - } + if (visited.contains(key)) continue; visited.add(key); - - // translate the mop - TranslateMovingObjectPoistionToLocation(mop, key); - if (IsValidForWireFrame(world, possiblePlacements, key, mop, player, clickedSide, buildersSelection)) { - region.add(key); - queue.add(key); - } + queue.add(key); } } - return region; } - private static boolean IsValidForWireFrame(World world, List possibleBlocks, BlockPos targetLocation, - MovingObjectPosition mop, EntityPlayer player, ForgeDirection clickedSide, BuildersSelection selection) { - ItemStack currentBlock = getBlockByLocation(world, mop, player); - - if (currentBlock == null) return false; - - return possibleBlocks.stream() - .allMatch(itemStackToPlace -> { - if (itemStackToPlace == null) return false; - - Block block = Block.getBlockFromItem(itemStackToPlace.getItem()); - return selection.matches(currentBlock) - && block.canPlaceBlockOnSide( - world, - targetLocation.x + clickedSide.offsetX, - targetLocation.y + clickedSide.offsetY, - targetLocation.z + clickedSide.offsetZ, - clickedSide.ordinal()) - && world.canPlaceEntityOnSide( - block, - targetLocation.x + clickedSide.offsetX, - targetLocation.y + clickedSide.offsetY, - targetLocation.z + clickedSide.offsetZ, - false, - clickedSide.ordinal(), - null, - itemStackToPlace); - }); + private static boolean IsValidForWireFrame(World world, MovingObjectPosition mop, EntityPlayer player, + BuildersBlockSelectionFilter filter, BuildersBlockPicker blockPicker) { + + // 0. cannot place on air + ItemStack sourceBlock = getBlockByLocation(world, mop, player); + if (sourceBlock == null) return false; + + // 1. matches the filter? + if (!filter.matches(sourceBlock)) return false; + + // 2. pick a suitable block + return blockPicker.pickSomeBlock(mop, sourceBlock); } public static boolean damageBackhand(int damage, EntityPlayer player) { @@ -195,6 +151,30 @@ && isTrowel(BackhandUtils.getOffhandItem(player))) { return true; } + public static boolean canPlaceBlock(World world, ItemStack targetBlock, MovingObjectPosition mop) { + Block block = Block.getBlockFromItem(targetBlock.getItem()); + ForgeDirection direction = ForgeDirection.getOrientation(mop.sideHit); + BlockPos targetPos = new BlockPos( + mop.blockX + direction.offsetX, + mop.blockY + direction.offsetY, + mop.blockZ + direction.offsetZ); + + // Blocks with no collision box (torches, rails, plants) are placeable through entities, + // For the rest use a whole cube, otherwise weird things happen, better safe than sorry. + boolean solid = block.getCollisionBoundingBoxFromPool(world, targetPos.x, targetPos.y, targetPos.z) != null; + + return block.canPlaceBlockOnSide(world, targetPos.x, targetPos.y, targetPos.z, mop.sideHit) && world + .canPlaceEntityOnSide(block, targetPos.x, targetPos.y, targetPos.z, true, mop.sideHit, null, targetBlock) + && (!solid || world.checkNoEntityCollision( + AxisAlignedBB.getBoundingBox( + targetPos.x, + targetPos.y, + targetPos.z, + targetPos.x + 1, + targetPos.y + 1, + targetPos.z + 1))); + } + public enum WandAxisMode { FREE, HORIZONTAL, diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java index 2ab8a5fae..2138d5a9c 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java @@ -7,7 +7,7 @@ public class MovingObjectPositionUtil { - public static void TranslateMovingObjectPoistionToLocation(MovingObjectPosition movingObjectPosition, + public static void TranslateMovingObjectPositionToLocation(MovingObjectPosition movingObjectPosition, BlockPos location) { double offsetXn = movingObjectPosition.hitVec.xCoord - movingObjectPosition.blockX; double offsetYn = movingObjectPosition.hitVec.yCoord - movingObjectPosition.blockY; @@ -20,4 +20,16 @@ public static void TranslateMovingObjectPoistionToLocation(MovingObjectPosition movingObjectPosition.hitVec = Vec3 .createVectorHelper(location.x + offsetXn, location.y + offsetYn, location.z + offsetZn); } + + public static MovingObjectPosition copy(MovingObjectPosition movingObjectPosition) { + return new MovingObjectPosition( + movingObjectPosition.blockX, + movingObjectPosition.blockY, + movingObjectPosition.blockZ, + movingObjectPosition.sideHit, + Vec3.createVectorHelper( + movingObjectPosition.hitVec.xCoord, + movingObjectPosition.hitVec.yCoord, + movingObjectPosition.hitVec.zCoord)); + } } diff --git a/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang b/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang index 575abcdfc..f4fa9a678 100644 --- a/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang +++ b/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang @@ -47,6 +47,7 @@ uie.nei.infopage.pseudo_reversion_sigil.3=Finally, activate the ritual by sacrif uie.nei.infopage.builders_wand.1=This wand is designed to aid the bearer in construction by placing many blocks at the same time. While looking at a block, a wireframe indicates where the wand will place blocks. Holding Sprint or Sneak locks the orientation of the wand. uie.nei.infopage.builders_wand.2=When holding a building block in your off-hand, the wand tries to place it instead. uie.nei.infopage.builders_wand.3=If, instead of holding a block, you hold a trowel, random blocks from your hotbar are placed. +uie.nei.infopage.builders_wand.4=If you hold a scribe, each block placed is a copy of the one it extends. uie.nei.infopage.sating_axe.1=This axe contains potent restorative magic that restores the bearer's hunger while it is held. Attacking with the axe heals whatever is hit at the cost of hunger - but this healing is deadly to the undead. uie.nei.infopage.erasure_pickaxe.1=This pickaxe is imbued with a destructive rune that allows it to mine stone 5x faster than a diamond pickaxe. However, it is extremely slow at mining anything else and blocks mined are destroyed instead of dropping. uie.nei.infopage.anti_gravity_shovel.1=This shovel deals with the annoyance of falling blocks by destroying all sand or gravel in a column above the block mined. Blocks mined with this are destroyed instead of dropping. @@ -532,6 +533,7 @@ item.chunchunmaru.name=Chunchunmaru item.capacity_upgrade.name=Filing Cabinet Capacity Upgrade item.paint_roller.name=Paint Roller +item.scribe.name=Scribe ### Item Description uie.desc.item.inverted_ingot.1=This ingot is deteriorating rapidly... @@ -574,6 +576,7 @@ uie.desc.item.paint_roller.0=[§a%s§7] Colorable blocks to dye them uie.desc.item.paint_roller.1=[§a%s§7] The air to select hex color uie.desc.item.paint_roller.2=[§aCrouch §7+ §a%s§7] Use line-mode uie.desc.item.paint_roller.3=[§a%s§7] Colorable blocks to copy color +uie.desc.item.scribe.1=Off-hand: the Builder's Wand copies each block it extends ### Item aliases item.heavenly_ring_magic.aliases=Angel Ring diff --git a/src/main/resources/assets/utilitiesinexcess/textures/items/scribe.png b/src/main/resources/assets/utilitiesinexcess/textures/items/scribe.png new file mode 100644 index 0000000000000000000000000000000000000000..8d62fb4b9808295f86ce613f6607ca93735a5550 GIT binary patch literal 433 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8X6a z5n0T@z%2~Ij105pNB|9&@^opc8=_srb%+X)4G@WrpoQQ;bus>W{5k+D|W0e>h;6Bmg1*2 zo%u0uqc%g1fB1#~S-Bf?^FQ2EXLAi?V33$}`*dB$nZm8tfP$O7Qx_ELF*wNSYxcd~ z+y1?-;1BO4L7=MAiiF}C>7;Y6^55H3xfC~8nspwQa`E~Zqa?_x$TDkLWXY4lSG(E_ zpK*35=H6B-?sE+Cl1NTidsgUx1?TQw<>QYy6Y}Ona3~0#XG%ITsbFh|#s_iztNhw6 WtM1yL{9FkPItEWyKbLh*2~7aI-KBE? literal 0 HcmV?d00001 From 50daa87c3f09b0b9f881672db9debca517edfe3e Mon Sep 17 00:00:00 2001 From: Cooble Date: Tue, 11 Aug 2026 19:09:51 +0200 Subject: [PATCH 2/7] changed spread mode for copyMode to anything in the inventory instead of must match blockID --- .../common/items/ItemBuildersWand.java | 13 ++++++++++-- .../utils/BuildersBlockPicker.java | 2 +- .../utils/BuildersBlockSelectionFilter.java | 20 ++++++++++--------- .../utils/BuildersWandUtils.java | 11 ++++++++++ 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java index deddd93c4..3b36b1341 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java @@ -159,8 +159,17 @@ public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, itemCopy.tryPlaceItemIntoWorld(player, world, pos.x, pos.y, pos.z, side, hitX, hitY, hitZ); // Don't forget to take the spent item from the inventory - if (itemCopy.stackSize == 0 && !player.capabilities.isCreativeMode) - BuildersWandUtils.decreaseFromInventory(player, toPlace); + if (itemCopy.stackSize == 0) { + if (!player.capabilities.isCreativeMode) BuildersWandUtils.decreaseFromInventory(player, toPlace); + + // copy the rotation or other metadata pieces that do not transfer through itemStack + if (filter.isCopyMode()) world.setBlockMetadataWithNotify( + pos.x + forgeSide.offsetX, + pos.y + forgeSide.offsetY, + pos.z + forgeSide.offsetZ, + world.getBlockMetadata(pos.x, pos.y, pos.z), + 3); + } } } player.inventoryContainer.detectAndSendChanges(); diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java index 8dd3dd11b..c2de89444 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java @@ -36,7 +36,7 @@ public abstract class BuildersBlockPicker { public static BuildersBlockPicker create(World world, EntityPlayer player, BuildersBlockSelectionFilter filter, BuildersMaterialBudget budget) { - if (filter.ignoresVariants()) { + if (filter.isCopyMode()) { return new CopyBlockPicker(world, budget); } return new PaletteBlockPicker( diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java index 954e67117..5f3b27a1b 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java @@ -6,6 +6,7 @@ import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemStack; import net.minecraft.util.MovingObjectPosition; @@ -28,13 +29,13 @@ public class BuildersBlockSelectionFilter { private final ItemStack backhand; private final ItemStack lookAtBlock; - private final boolean ignoreVariants; + private final boolean isCopyMode; public BuildersBlockSelectionFilter(EntityPlayer player, World world, MovingObjectPosition movingObjectPosition) { this.validBlocks = new ArrayList<>(); backhand = Mods.Backhand.isLoaded() ? BackhandUtils.getOffhandItem(player) : null; lookAtBlock = getBlockByLocation(world, movingObjectPosition, player); - ignoreVariants = lookAtBlock != null && isScribe(backhand); + isCopyMode = lookAtBlock != null && isScribe(backhand); // No logic is executed if we don't look at any block, no need to bother checking other cases if (lookAtBlock == null) return; @@ -64,18 +65,19 @@ public List generatePalette(EntityPlayer player) { */ public boolean matches(ItemStack other) { if (other == null) return false; + + if (isCopyMode) return true; + return this.validBlocks.stream() - .anyMatch( - validBlock -> ignoreVariants ? validBlock.getItem() == other.getItem() - : ItemUtil.areStacksEqual(validBlock, other)); + .anyMatch(validBlock -> ItemUtil.areStacksEqual(validBlock, other)); } /** - * Whether the fill spans every variant of a block, matching on block identity alone and copying - * each position's own material rather than drawing from a palette. + * Whether the fill spans the whole surface regardless of what it is made of, copying each position's + * own block rather than drawing from a palette. */ - public boolean ignoresVariants() { - return ignoreVariants; + public boolean isCopyMode() { + return isCopyMode; } public static boolean isTrowel(@Nullable ItemStack stack) { diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java index c8a1863ca..53fb771a6 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java @@ -13,6 +13,7 @@ import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.item.ItemStack; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; @@ -51,6 +52,16 @@ public static boolean decreaseFromInventory(EntityPlayer player, ItemStack itemS return false; } + public static boolean containsItemStack(InventoryPlayer player, ItemStack itemStack) { + for (int slotIndex = player.mainInventory.length - 1; slotIndex >= 0; slotIndex--) { + ItemStack stack = player.mainInventory[slotIndex]; + if (ItemUtil.areStacksEqual(stack, itemStack)) { + return true; + } + } + return false; + } + /** * Finds the blocks adjacent to the start position that are connected cardinally, or diagonally * and have a placeable block (e.g. air, fluid) in front of them relative to the side clicked on. From 83516386a20493e8a0491ed81071ff21d3a2a16a Mon Sep 17 00:00:00 2001 From: Cooble Date: Tue, 11 Aug 2026 22:27:24 +0200 Subject: [PATCH 3/7] spotlessApply --- .../utilitiesinexcess/utils/BuildersBlockSelectionFilter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java index 5f3b27a1b..a1e102cf1 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java @@ -6,7 +6,6 @@ import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemStack; import net.minecraft.util.MovingObjectPosition; From 6879e2c36bd6ac870901d849728822a684a5dca2 Mon Sep 17 00:00:00 2001 From: Cooble Date: Sun, 23 Aug 2026 23:13:43 +0200 Subject: [PATCH 4/7] BuildersWand - added soft-dep on ArchitectureCraft for copying TEs --- dependencies.gradle | 1 + .../common/items/ItemBuildersWand.java | 37 ++++++---- .../utilitiesinexcess/compat/Mods.java | 1 + .../ArchitectureCraftCompat.java | 68 +++++++++++++++++++ 4 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java diff --git a/dependencies.gradle b/dependencies.gradle index 397c6d121..6afb706cf 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -47,6 +47,7 @@ dependencies { compileOnly("com.github.GTNewHorizons:TinkersConstruct:1.14.93-GTNH:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:ForgeMultipart:1.7.10:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:Backhand:1.8.11:dev") {transitive = false} + compileOnly("com.github.GTNewHorizons:ArchitectureCraft:1.11.6:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:waila:1.19.30:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:Postea:1.2.5:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:SimpleSkinBackport:1.0.2-GTNH:dev") {transitive = false} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java index 3b36b1341..e68fb3013 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java @@ -19,6 +19,8 @@ import com.fouristhenumber.utilitiesinexcess.UtilitiesInExcess; import com.fouristhenumber.utilitiesinexcess.common.renderers.WireframeRenderer; +import com.fouristhenumber.utilitiesinexcess.compat.Mods; +import com.fouristhenumber.utilitiesinexcess.compat.architecturecraft.ArchitectureCraftCompat; import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; import com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockPicker; import com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter; @@ -149,26 +151,35 @@ public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, if (toPlace == null) continue; - if (damageBackhand(BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand, player)) { + if (!damageBackhand(BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand, player)) continue; - ItemStack itemCopy = toPlace.copy(); - itemCopy.stackSize = 1; + ItemStack itemCopy = toPlace.copy(); + itemCopy.stackSize = 1; - // uses ItemBlock to place the block with all the checks - // sets stackSize to 0 on success - itemCopy.tryPlaceItemIntoWorld(player, world, pos.x, pos.y, pos.z, side, hitX, hitY, hitZ); + // uses ItemBlock to place the block with all the checks + // sets stackSize to 0 on success + itemCopy.tryPlaceItemIntoWorld(player, world, pos.x, pos.y, pos.z, side, hitX, hitY, hitZ); - // Don't forget to take the spent item from the inventory - if (itemCopy.stackSize == 0) { - if (!player.capabilities.isCreativeMode) BuildersWandUtils.decreaseFromInventory(player, toPlace); + // Don't forget to take the spent item from the inventory + if (itemCopy.stackSize == 0) { + if (!player.capabilities.isCreativeMode) BuildersWandUtils.decreaseFromInventory(player, toPlace); + + if (filter.isCopyMode()) { + int destX = pos.x + forgeSide.offsetX; + int destY = pos.y + forgeSide.offsetY; + int destZ = pos.z + forgeSide.offsetZ; // copy the rotation or other metadata pieces that do not transfer through itemStack - if (filter.isCopyMode()) world.setBlockMetadataWithNotify( - pos.x + forgeSide.offsetX, - pos.y + forgeSide.offsetY, - pos.z + forgeSide.offsetZ, + world.setBlockMetadataWithNotify( + destX, + destY, + destZ, world.getBlockMetadata(pos.x, pos.y, pos.z), 3); + + // Special handling for ArchitectureCraft shape TEs + if (Mods.ArchitectureCraft.isLoaded()) ArchitectureCraftCompat + .tryCopyShapeState(world, pos.x, pos.y, pos.z, destX, destY, destZ, player); } } } diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/Mods.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/Mods.java index 7e887ed52..f6ddccee2 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/Mods.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/Mods.java @@ -18,6 +18,7 @@ public enum Mods { Postea("postea"), ForgeMicroBlock("ForgeMicroblock"), Backhand("backhand"), + ArchitectureCraft("ArchitectureCraft"), GregTech("gregtech_nh"), SimpleSkinBackport("simpleskinbackport"), EndlessIDs("endlessids"), diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java new file mode 100644 index 000000000..842bd32ee --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java @@ -0,0 +1,68 @@ +package com.fouristhenumber.utilitiesinexcess.compat.architecturecraft; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.world.World; + +import com.gtnewhorizon.gtnhlib.util.ItemUtil; + +import gcewing.architecture.ArchitectureCraft; +import gcewing.architecture.common.tile.TileShape; +import gcewing.architecture.compat.BlockPos; + +/** + * Allows exact ArchitectureCraft shape copying with BuildersWand. + * + * ArchitectureCraft does not copy rotation and cladding into the ItemStack. + * -> Needs to be copied from source TE to the target TE after placement. + */ +public final class ArchitectureCraftCompat { + + private ArchitectureCraftCompat() {} + + /** + * Tries to copy the parts of a shape that live in the TileEntity rather than the item. + * - Orientation is always copied + * - cladding only if the player still has the matching one to spend + * + * @return true if the destination was an ArchitectureCraft shape and was updated + */ + public static boolean tryCopyShapeState(World world, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, + EntityPlayer player) { + + TileShape source = TileShape.get(world, new BlockPos(fromX, fromY, fromZ)); + TileShape target = TileShape.get(world, new BlockPos(toX, toY, toZ)); + if (source == null || target == null) return false; + + target.setSide(source.side); + target.setTurn(source.turn); + target.setOffsetX(source.getOffsetX()); + target.disabledConnections = source.disabledConnections; + + applyCladding(source, target, player); + + target.markChanged(); + return true; + } + + /** + * Tries to copy the cladding if player has it in the inventory + */ + private static void applyCladding(TileShape source, TileShape target, EntityPlayer player) { + if (source.secondaryBlockState == null) return; + + ItemStack wanted = ArchitectureCraft.content.itemCladding.newStack(source.secondaryBlockState, 1); + if (wanted == null) return; + + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + ItemStack slot = player.inventory.mainInventory[i]; + if (!ItemUtil.areStacksEqual(slot, wanted)) continue; + + // decrements the itemStack size + target.applySecondaryMaterial(slot, player); + if (slot.stackSize <= 0) player.inventory.setInventorySlotContents(i, null); + + return; + } + } +} From 607790bbf57c8a3b3c1fcf6f8268957db5ec81db Mon Sep 17 00:00:00 2001 From: Cooble Date: Mon, 24 Aug 2026 10:56:33 +0200 Subject: [PATCH 5/7] added Scribe recipe --- .../utilitiesinexcess/common/recipe/RecipeLoader.java | 4 ++++ .../utilitiesinexcess/config/RecipeConfig.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/recipe/RecipeLoader.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/recipe/RecipeLoader.java index 09e1c2982..e23e9ef83 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/recipe/RecipeLoader.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/recipe/RecipeLoader.java @@ -1473,6 +1473,10 @@ private static void loadInversionRecipes() { 's', ModItems.BUILDERS_WAND); + // Scribe + if (RecipeConfig.enableScribeRecipe) + addShapedRecipe(ModItems.SCRIBE, "np", "np", "np", 'n', Items.gold_nugget, 'p', Items.paper); + // Inverted Ingot -> Block if (RecipeConfig.enableInvertedBlockRecipe) addShapedRecipe( ModBlocks.INVERTED_BLOCK, diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/config/RecipeConfig.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/config/RecipeConfig.java index efc6ef180..051939538 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/config/RecipeConfig.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/config/RecipeConfig.java @@ -104,6 +104,10 @@ public class RecipeConfig { @Config.DefaultBoolean(true) public static boolean enableSuperBuildersWandRecipe; + @Config.RequiresMcRestart + @Config.DefaultBoolean(true) + public static boolean enableScribeRecipe; + @Config.RequiresMcRestart @Config.DefaultBoolean(true) public static boolean enableBedrockiumIngotRecipe; From be9b6abe2d2d72fc7718a8d829be90fe1afd66e6 Mon Sep 17 00:00:00 2001 From: Cooble Date: Sat, 19 Sep 2026 21:33:50 +0200 Subject: [PATCH 6/7] BW Refactoring + FMB Refactored BW to be easily extensible for other mods. Added ForgeMicroBlocks BuildersWand support. Added javadoc for BWCellHandler Renamed copy mode -> extrude mode --- .../utilitiesinexcess/CommonProxy.java | 8 + .../common/items/ItemBuildersWand.java | 163 ++++++------- .../MultipartBWCellHandler.java | 216 ++++++++++++++++++ .../ArchitectureCraftBWCellHandler.java | 86 +++++++ .../ArchitectureCraftCompat.java | 68 ------ .../utils/BuildersBlockPicker.java | 146 ------------ .../utils/BuildersBlockSelectionFilter.java | 131 ----------- .../utils/BuildersWandUtils.java | 194 ---------------- .../utils/ChickenLibRayTracer.java | 57 +++++ ...terialBudget.java => InventoryBudget.java} | 44 +++- .../utils/MovingObjectPositionUtil.java | 27 +++ .../utils/bw/BWBlockPicker.java | 175 ++++++++++++++ .../utils/bw/BWCellHandlers.java | 56 +++++ .../utilitiesinexcess/utils/bw/BWContext.java | 79 +++++++ .../utilitiesinexcess/utils/bw/BWMode.java | 73 ++++++ .../utilitiesinexcess/utils/bw/BWRegion.java | 95 ++++++++ .../utils/bw/DefaultBWCellHandler.java | 120 ++++++++++ .../utils/bw/IBWCellHandler.java | 96 ++++++++ 18 files changed, 1189 insertions(+), 645 deletions(-) create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/compat/ForgeMultipart/MultipartBWCellHandler.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java delete mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java delete mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java delete mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java delete mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/ChickenLibRayTracer.java rename src/main/java/com/fouristhenumber/utilitiesinexcess/utils/{BuildersMaterialBudget.java => InventoryBudget.java} (56%) create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWMode.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWRegion.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java create mode 100644 src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/CommonProxy.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/CommonProxy.java index b77da5781..e68b32eac 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/CommonProxy.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/CommonProxy.java @@ -19,8 +19,10 @@ import com.fouristhenumber.utilitiesinexcess.common.worldgen.WorldGenEnderLotus; import com.fouristhenumber.utilitiesinexcess.compat.ForgeMultipart.FMPCompat; import com.fouristhenumber.utilitiesinexcess.compat.ForgeMultipart.FMPItems; +import com.fouristhenumber.utilitiesinexcess.compat.ForgeMultipart.MultipartBWCellHandler; import com.fouristhenumber.utilitiesinexcess.compat.ForgeMultipart.multipart.Content; import com.fouristhenumber.utilitiesinexcess.compat.Mods; +import com.fouristhenumber.utilitiesinexcess.compat.architecturecraft.ArchitectureCraftBWCellHandler; import com.fouristhenumber.utilitiesinexcess.compat.crafttweaker.EnderLocusCraftTweakerSupport; import com.fouristhenumber.utilitiesinexcess.compat.exu.ExuWorldConversionWarning; import com.fouristhenumber.utilitiesinexcess.compat.exu.PosteaTransforms; @@ -31,6 +33,7 @@ import com.fouristhenumber.utilitiesinexcess.utils.PinkFuelHelper; import com.fouristhenumber.utilitiesinexcess.utils.SoundVolumeChecks; import com.fouristhenumber.utilitiesinexcess.utils.TEChunkLoadingCallback; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWCellHandlers; import com.gtnewhorizon.gtnhlib.api.gui.WorldConversionWarningManager; import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry; import com.gtnewhorizon.gtnhlib.datastructs.space.ArrayProximityCheck4D; @@ -99,6 +102,11 @@ public void preInit(FMLPreInitializationEvent event) { FMPItems.init(); new Content().init(); FMPCompat.init(); + BWCellHandlers.register(new MultipartBWCellHandler()); + } + + if (Mods.ArchitectureCraft.isLoaded()) { + BWCellHandlers.register(new ArchitectureCraftBWCellHandler()); } if (ColoredBlocksConfig.INSTANCE.enableColoredBlocks) { diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java index e68fb3013..ebee32952 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java @@ -1,7 +1,5 @@ package com.fouristhenumber.utilitiesinexcess.common.items; -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils.damageBackhand; - import java.util.List; import java.util.Set; @@ -19,15 +17,15 @@ import com.fouristhenumber.utilitiesinexcess.UtilitiesInExcess; import com.fouristhenumber.utilitiesinexcess.common.renderers.WireframeRenderer; -import com.fouristhenumber.utilitiesinexcess.compat.Mods; -import com.fouristhenumber.utilitiesinexcess.compat.architecturecraft.ArchitectureCraftCompat; -import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; -import com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockPicker; -import com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter; -import com.fouristhenumber.utilitiesinexcess.utils.BuildersMaterialBudget; -import com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils; -import com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils.WandAxisMode; +import com.fouristhenumber.utilitiesinexcess.utils.ChickenLibRayTracer; +import com.fouristhenumber.utilitiesinexcess.utils.InventoryBudget; import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWBlockPicker; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWCellHandlers; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWContext; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWMode; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWRegion; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWRegion.WandAxisLock; import com.gtnewhorizon.gtnhlib.api.ITranslucentItem; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; @@ -36,7 +34,7 @@ public class ItemBuildersWand extends Item implements ITranslucentItem { - public int buildLimit; + private final int buildLimit; public ItemBuildersWand(int buildLimit) { super(); @@ -61,7 +59,12 @@ public void onUpdate(ItemStack stack, World world, Entity entity, int slot, bool if (!isSelected) return; - // I'm pretty sure this will never determine whether we render or not but I'm not certain + // adventure mode + if (!player.capabilities.allowEdit) { + WireframeRenderer.clearCandidatePositions(); + return; + } + MovingObjectPosition movingObjectPosition = Minecraft.getMinecraft().objectMouseOver; // Check if player is looking at a block. @@ -73,26 +76,12 @@ public void onUpdate(ItemStack stack, World world, Entity entity, int slot, bool ForgeDirection forgeSide = ForgeDirection.getOrientation(movingObjectPosition.sideHit); - WandAxisMode axisMode; - if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_H.isKeyDown(player)) { - axisMode = WandAxisMode.HORIZONTAL; - } else if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_V.isKeyDown(player)) { - axisMode = WandAxisMode.VERTICAL; - } else { - axisMode = WandAxisMode.FREE; - } - - // selection filter - var filter = new BuildersBlockSelectionFilter(player, world, movingObjectPosition); - - // keep track of potentially used blocks in inventory - var itemBudget = new BuildersMaterialBudget(player.inventory, player.capabilities.isCreativeMode); - - // block picker - var blockPicker = BuildersBlockPicker.create(world, player, filter, itemBudget); - - Set blocksToPlace = BuildersWandUtils - .findAdjacentBlocks(world, buildLimit, movingObjectPosition, player, filter, blockPicker, axisMode); + WandAxisLock axisLock = axisLock(player); + Set blocksToPlace = BWRegion.findAdjacentBlocksToBuildOn( + newContext(world, player, movingObjectPosition), + buildLimit, + movingObjectPosition, + axisLock); WireframeRenderer.clearCandidatePositions(); for (BlockPos pos : blocksToPlace) @@ -103,13 +92,14 @@ public void onUpdate(ItemStack stack, World world, Entity entity, int slot, bool public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, int x, int y, int z, int side, float hitX, float hitY, float hitZ) { if (world.isRemote) return true; + if (!player.capabilities.allowEdit) return false; MovingObjectPosition mop = new MovingObjectPosition( x, y, z, side, - Vec3.createVectorHelper(x + hitX, y + hitY, z + hitZ)); + Vec3.createVectorHelper((double) x + hitX, (double) y + hitY, (double) z + hitZ)); // Sanity check ForgeDirection forgeSide = ForgeDirection.getOrientation(side); @@ -118,75 +108,60 @@ public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, return true; } - WandAxisMode axisMode; - if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_H.isKeyDown(player)) { - axisMode = WandAxisMode.HORIZONTAL; - } else if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_V.isKeyDown(player)) { - axisMode = WandAxisMode.VERTICAL; - } else { - axisMode = WandAxisMode.FREE; - } - - // selection filter - var filter = new BuildersBlockSelectionFilter(player, world, mop); - - // keep track of potentially used blocks in inventory - var itemBudget = new BuildersMaterialBudget(player.inventory, player.capabilities.isCreativeMode); - - // block picker - var blockPicker = BuildersBlockPicker.create(world, player, filter, itemBudget); + WandAxisLock axisLock = axisLock(player); - // potential block positions - Set blocksToPlace = BuildersWandUtils - .findAdjacentBlocks(world, buildLimit, mop, player, filter, blockPicker, axisMode); + // the scan pass spends the budget working out what fits, so placement starts from a fresh one + BWContext scanCtx = newContext(world, player, mop); + Set blocksToPlace = BWRegion.findAdjacentBlocksToBuildOn(scanCtx, buildLimit, mop, axisLock); - // reset the blockPicker with new budget - itemBudget = new BuildersMaterialBudget(player.inventory, player.capabilities.isCreativeMode); - blockPicker = BuildersBlockPicker.create(world, player, filter, itemBudget); - - for (BlockPos pos : blocksToPlace) { - MovingObjectPositionUtil.TranslateMovingObjectPositionToLocation(mop, pos); - ItemStack toPlace = blockPicker - .pickBlock(mop, BuildersBlockSelectionFilter.getBlockByLocation(world, mop, player)); - - if (toPlace == null) continue; - - if (!damageBackhand(BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand, player)) continue; - - ItemStack itemCopy = toPlace.copy(); - itemCopy.stackSize = 1; - - // uses ItemBlock to place the block with all the checks - // sets stackSize to 0 on success - itemCopy.tryPlaceItemIntoWorld(player, world, pos.x, pos.y, pos.z, side, hitX, hitY, hitZ); - - // Don't forget to take the spent item from the inventory - if (itemCopy.stackSize == 0) { - if (!player.capabilities.isCreativeMode) BuildersWandUtils.decreaseFromInventory(player, toPlace); - - if (filter.isCopyMode()) { - int destX = pos.x + forgeSide.offsetX; - int destY = pos.y + forgeSide.offsetY; - int destZ = pos.z + forgeSide.offsetZ; - - // copy the rotation or other metadata pieces that do not transfer through itemStack - world.setBlockMetadataWithNotify( - destX, - destY, - destZ, - world.getBlockMetadata(pos.x, pos.y, pos.z), - 3); - - // Special handling for ArchitectureCraft shape TEs - if (Mods.ArchitectureCraft.isLoaded()) ArchitectureCraftCompat - .tryCopyShapeState(world, pos.x, pos.y, pos.z, destX, destY, destZ, player); - } + // every cell is protection-checked on its own, Forge recording the whole use at once would muddle that + boolean capturing = world.captureBlockSnapshots; + world.captureBlockSnapshots = false; + try { + BWContext buildCtx = newContext(world, player, mop); + for (BlockPos srcPos : blocksToPlace) { + MovingObjectPositionUtil.TranslateMovingObjectPositionToLocation(mop, srcPos); + BWCellHandlers.build(buildCtx, mop, true); } + } finally { + world.captureBlockSnapshots = capturing; } player.inventoryContainer.detectAndSendChanges(); return true; } + /** Mode, budget and picker for one pass over the fill, seeded from the clicked cell. */ + private static BWContext newContext(World world, EntityPlayer player, MovingObjectPosition mop) { + var budget = new InventoryBudget(player.inventory, player.capabilities.isCreativeMode); + + // plain copy: the client ray can be an ExtendedMOP naming a part broken this tick + MovingObjectPosition plainMop = MovingObjectPositionUtil.copy(mop); + ItemStack lookedAtBlock = world.getBlock(plainMop.blockX, plainMop.blockY, plainMop.blockZ) + .getPickBlock(plainMop, world, plainMop.blockX, plainMop.blockY, plainMop.blockZ, player); + + BWMode mode = BWMode.of(player); + var picker = BWBlockPicker.create(world, player, mode, lookedAtBlock, budget); + + // null when the ray misses the clicked block, then handlers just get the plain position + MovingObjectPosition tracedMop = ChickenLibRayTracer + .retraceBlock(world, player, mop.blockX, mop.blockY, mop.blockZ); + return new BWContext( + world, + tracedMop != null ? tracedMop : MovingObjectPositionUtil.copy(mop), + lookedAtBlock, + player, + mode, + picker, + budget); + } + + private static WandAxisLock axisLock(EntityPlayer player) { + if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_H.isKeyDown(player)) return WandAxisLock.HORIZONTAL; + if (UtilitiesInExcess.proxy.BUILDERS_KEYBIND_V.isKeyDown(player)) return WandAxisLock.VERTICAL; + + return WandAxisLock.FREE; + } + @Override public boolean isDamageable() { return false; diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/ForgeMultipart/MultipartBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/ForgeMultipart/MultipartBWCellHandler.java new file mode 100644 index 000000000..fbf7017fe --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/ForgeMultipart/MultipartBWCellHandler.java @@ -0,0 +1,216 @@ +package com.fouristhenumber.utilitiesinexcess.compat.ForgeMultipart; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; +import net.minecraftforge.common.util.BlockSnapshot; +import net.minecraftforge.common.util.ForgeDirection; +import net.minecraftforge.event.ForgeEventFactory; +import net.minecraftforge.event.world.BlockEvent.PlaceEvent; + +import org.jetbrains.annotations.Nullable; + +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWContext; +import com.fouristhenumber.utilitiesinexcess.utils.bw.DefaultBWCellHandler; +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +import codechicken.lib.raytracer.ExtendedMOP; +import codechicken.lib.vec.BlockCoord; +import codechicken.multipart.BlockMultipart; +import codechicken.multipart.MultiPartRegistry; +import codechicken.multipart.TMultiPart; +import codechicken.multipart.TileMultipart; +import scala.Tuple2; + +/** + * A ForgeMultipart cell is a bag of parts rather than one block with one item. + *
    + *
  • extrude mode: the whole cell, all the parts or none + *
  • trowel: the multipart is just a surface to paint over + *
  • offhand block: placed in front of every cell holding the part the player pointed at + *
  • clicked block: that part itself spreads over every cell holding it + *
+ */ +public final class MultipartBWCellHandler extends DefaultBWCellHandler { + + @Override + public boolean handles(BWContext ctx, int x, int y, int z) { + TileMultipart tile = BlockMultipart.getTile(ctx.world, x, y, z); + return tile != null && !tile.jPartList() + .isEmpty(); + } + + @Override + public boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) { + return switch (ctx.mode) { + case EXTRUDE -> copyWholeCell(ctx, mop, place); + case CLICKED_BLOCK -> extendPointedPart(ctx, mop, place); + case TROWEL, OFFHAND_BLOCK -> super.build(ctx, mop, place); + }; + } + + /** A multipart cell is the same kind if it holds the part the player pointed at. */ + @Override + protected boolean sameKindAsClicked(BWContext ctx, MovingObjectPosition mop, @Nullable ItemStack source) { + TMultiPart pointed = pointedPart(ctx); + return pointed != null && holdsSame(ctx.world, mop, pointed); + } + + /** All the parts or none. */ + private static boolean copyWholeCell(BWContext ctx, MovingObjectPosition mop, boolean place) { + World world = ctx.world; + BlockPos from = MovingObjectPositionUtil.blockPos(mop); + BlockPos to = MovingObjectPositionUtil.targetPos(mop); + + List costs = partCosts(world, from); + if (costs.isEmpty()) return false; + + List clones = fittingClones(world, from, to); + if (clones == null || !ctx.budget.tryReserve(costs)) return false; + if (!place) return true; + + if (!tryPlaceParts(ctx, mop, clones)) return false; + costs.forEach(ctx::spend); + return true; + } + + private static boolean extendPointedPart(BWContext ctx, MovingObjectPosition mop, boolean place) { + TMultiPart pointed = pointedPart(ctx); + if (pointed == null) return false; + + // only spread onto blocks containing the looked at part + if (!holdsSame(ctx.world, mop, pointed)) return false; + + // canPlacePart also allows fitting the part into a cell that already holds other parts + BlockPos to = MovingObjectPositionUtil.targetPos(mop); + BlockCoord target = new BlockCoord(to.x, to.y, to.z); + TMultiPart clone = clone(pointed); + if (clone == null || !TileMultipart.canPlacePart(ctx.world, target, clone)) return false; + + List costs = drops(pointed); + if (costs.isEmpty() || !ctx.budget.tryReserve(costs)) return false; + if (!place) return true; + + if (!tryPlaceParts(ctx, mop, List.of(clone))) return false; + costs.forEach(ctx::spend); + return true; + } + + /** The part the player pointed at, e.g. a pane */ + private static TMultiPart pointedPart(BWContext ctx) { + MovingObjectPosition clicked = ctx.originalMop; + if (!(clicked instanceof ExtendedMOP extended) || !(extended.data instanceof Tuple2data)) return null; + if (!(data._1() instanceof Integer index)) return null; + + TileMultipart tile = BlockMultipart.getTile(ctx.world, clicked.blockX, clicked.blockY, clicked.blockZ); + if (tile == null || index < 0 + || index >= tile.jPartList() + .size()) + return null; + return tile.jPartList() + .get(index); + } + + /** Whether this cell holds a part matching the pointed one: same kind, same slot and size, same material. */ + private static boolean holdsSame(World world, MovingObjectPosition mop, TMultiPart pointed) { + TileMultipart tile = BlockMultipart.getTile(world, mop.blockX, mop.blockY, mop.blockZ); + if (tile == null) return false; + + NBTTagCompound wanted = saved(pointed); + for (TMultiPart part : tile.jPartList()) { + if (part.getType() + .equals(pointed.getType()) && saved(part).equals(wanted)) return true; + } + return false; + } + + /** Every item the parts at this position would cost to rebuild. */ + private static List partCosts(World world, BlockPos pos) { + TileMultipart tile = BlockMultipart.getTile(world, pos.x, pos.y, pos.z); + if (tile == null) return new ArrayList<>(); + + List costs = new ArrayList<>(); + for (TMultiPart part : tile.jPartList()) { + costs.addAll(drops(part)); + } + return costs; + } + + private static List drops(TMultiPart part) { + List costs = new ArrayList<>(); + for (ItemStack drop : part.getDrops()) { + if (drop != null) costs.add(drop); + } + return costs; + } + + /** @return clones of every part at from, or null when they do not all fit into the empty cell at to */ + private static List fittingClones(World world, BlockPos from, BlockPos to) { + // only into empty space, canPlacePart alone would merge into an existing multipart + if (!world.getBlock(to.x, to.y, to.z) + .isReplaceable(world, to.x, to.y, to.z)) return null; + + TileMultipart source = BlockMultipart.getTile(world, from.x, from.y, from.z); + if (source == null || source.jPartList() + .isEmpty()) return null; + + BlockCoord target = new BlockCoord(to.x, to.y, to.z); + List clones = new ArrayList<>(); + for (TMultiPart part : source.jPartList()) { + TMultiPart clone = clone(part); + if (clone == null || !TileMultipart.canPlacePart(world, target, clone)) return null; + clones.add(clone); + } + return clones; + } + + /** + * Adds the parts to the cell, checks if the player is allowed to place them, and rolls back if not. + * (So we don't allow placing into forbidden territories) + * (Necessary, since FMP does not issue onPlaceEvent when a cell is just modified) + * + * @return true on success + */ + private static boolean tryPlaceParts(BWContext ctx, MovingObjectPosition mop, List parts) { + BlockPos to = MovingObjectPositionUtil.targetPos(mop); + BlockSnapshot before = BlockSnapshot.getBlockSnapshot(ctx.world, to.x, to.y, to.z); + BlockCoord target = new BlockCoord(to.x, to.y, to.z); + + // place + for (TMultiPart part : parts) TileMultipart.addPart(ctx.world, target, part); + + // check if we are allowed to place it + PlaceEvent event = ForgeEventFactory + .onPlayerBlockPlace(ctx.player, before, ForgeDirection.getOrientation(mop.sideHit)); + if (!event.isCanceled()) return true; + + // rollback if necessary + // (cannot use silently snapshot.restore(), since the TileMultipart.addPart already queued a message to clients) + for (TMultiPart part : parts) { + TileMultipart tile = BlockMultipart.getTile(ctx.world, to.x, to.y, to.z); + if (tile != null) tile.remPart(part); + } + return false; + } + + private static NBTTagCompound saved(TMultiPart part) { + NBTTagCompound nbt = new NBTTagCompound(); + part.save(nbt); + return nbt; + } + + /** + * Round trip through NBT; loadPart only constructs, hence the load after it. + */ + private static TMultiPart clone(TMultiPart part) { + NBTTagCompound nbt = saved(part); + TMultiPart clone = MultiPartRegistry.loadPart(part.getType(), nbt); + if (clone != null) clone.load(nbt); + return clone; + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java new file mode 100644 index 000000000..f55b13f00 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java @@ -0,0 +1,86 @@ +package com.fouristhenumber.utilitiesinexcess.compat.architecturecraft; + +import java.util.Collections; +import java.util.List; + +import net.minecraft.item.ItemStack; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; + +import org.jetbrains.annotations.Nullable; + +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +import com.fouristhenumber.utilitiesinexcess.utils.bw.BWContext; +import com.fouristhenumber.utilitiesinexcess.utils.bw.DefaultBWCellHandler; +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +import gcewing.architecture.ArchitectureCraft; +import gcewing.architecture.common.tile.TileShape; + +/** + * Special Copy handler for ArchitectureCraft shapes. + */ +public final class ArchitectureCraftBWCellHandler extends DefaultBWCellHandler { + + @Override + public boolean handles(BWContext ctx, int x, int y, int z) { + return ctx.isExtrudeMode() && shapeAt(ctx.world, x, y, z) != null; + } + + @Override + public boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) { + if (!super.build(ctx, mop, place)) return false; + if (!place) return true; + + BlockPos from = MovingObjectPositionUtil.blockPos(mop); + copyShapeState( + ctx, + from, + MovingObjectPositionUtil.targetPos(mop), + claddingOf(shapeAt(ctx.world, from.x, from.y, from.z))); + return true; + } + + /** A clad shape costs its cladding too, so a copy nobody can pay for is never started. */ + @Override + protected List extraCosts(BWContext ctx, MovingObjectPosition mop) { + BlockPos from = MovingObjectPositionUtil.blockPos(mop); + ItemStack cladding = claddingOf(shapeAt(ctx.world, from.x, from.y, from.z)); + + return cladding == null ? Collections.emptyList() : Collections.singletonList(cladding); + } + + /** Copies the TileShape state the item cannot carry: side, turn, offset, connections and cladding. */ + private static void copyShapeState(BWContext ctx, BlockPos from, BlockPos to, @Nullable ItemStack cladding) { + TileShape source = shapeAt(ctx.world, from.x, from.y, from.z); + TileShape target = shapeAt(ctx.world, to.x, to.y, to.z); + if (source == null || target == null) return; + + target.setSide(source.side); + target.setTurn(source.turn); + target.setOffsetX(source.getOffsetX()); + target.disabledConnections = source.disabledConnections; + + if (cladding != null) { + // applySecondaryMaterial decrements what it is given, so it gets a copy and the budget pays + target.applySecondaryMaterial(cladding.copy(), ctx.player); + ctx.spend(cladding); + } + + // markDirty plus a block update, otherwise the change is server side only until a relog + target.markChanged(); + } + + /** The cladding stack this shape wears, null when it has none. */ + @Nullable + private static ItemStack claddingOf(@Nullable TileShape shape) { + if (shape == null || shape.secondaryBlockState == null) return null; + + // built by AC itself, so it matches whatever the source is clad with, NBT included + return ArchitectureCraft.content.itemCladding.newStack(shape.secondaryBlockState, 1); + } + + private static TileShape shapeAt(World world, int x, int y, int z) { + return TileShape.get(world, new gcewing.architecture.compat.BlockPos(x, y, z)); + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java deleted file mode 100644 index 842bd32ee..000000000 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftCompat.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.fouristhenumber.utilitiesinexcess.compat.architecturecraft; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.world.World; - -import com.gtnewhorizon.gtnhlib.util.ItemUtil; - -import gcewing.architecture.ArchitectureCraft; -import gcewing.architecture.common.tile.TileShape; -import gcewing.architecture.compat.BlockPos; - -/** - * Allows exact ArchitectureCraft shape copying with BuildersWand. - * - * ArchitectureCraft does not copy rotation and cladding into the ItemStack. - * -> Needs to be copied from source TE to the target TE after placement. - */ -public final class ArchitectureCraftCompat { - - private ArchitectureCraftCompat() {} - - /** - * Tries to copy the parts of a shape that live in the TileEntity rather than the item. - * - Orientation is always copied - * - cladding only if the player still has the matching one to spend - * - * @return true if the destination was an ArchitectureCraft shape and was updated - */ - public static boolean tryCopyShapeState(World world, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, - EntityPlayer player) { - - TileShape source = TileShape.get(world, new BlockPos(fromX, fromY, fromZ)); - TileShape target = TileShape.get(world, new BlockPos(toX, toY, toZ)); - if (source == null || target == null) return false; - - target.setSide(source.side); - target.setTurn(source.turn); - target.setOffsetX(source.getOffsetX()); - target.disabledConnections = source.disabledConnections; - - applyCladding(source, target, player); - - target.markChanged(); - return true; - } - - /** - * Tries to copy the cladding if player has it in the inventory - */ - private static void applyCladding(TileShape source, TileShape target, EntityPlayer player) { - if (source.secondaryBlockState == null) return; - - ItemStack wanted = ArchitectureCraft.content.itemCladding.newStack(source.secondaryBlockState, 1); - if (wanted == null) return; - - for (int i = 0; i < player.inventory.mainInventory.length; i++) { - ItemStack slot = player.inventory.mainInventory[i]; - if (!ItemUtil.areStacksEqual(slot, wanted)) continue; - - // decrements the itemStack size - target.applySecondaryMaterial(slot, player); - if (slot.stackSize <= 0) player.inventory.setInventorySlotContents(i, null); - - return; - } - } -} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java deleted file mode 100644 index c2de89444..000000000 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockPicker.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.fouristhenumber.utilitiesinexcess.utils; - -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils.canPlaceBlock; - -import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; - -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.InventoryPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.util.MovingObjectPosition; -import net.minecraft.world.World; - -import com.gtnewhorizon.gtnhlib.util.ItemUtil; - -public abstract class BuildersBlockPicker { - - /** - * pick a suitable block to be placed, including whether its already in the inventory - * - * @param mop the source block on which the new one will be placed - * @param sourceBlock the stack the source block would drop when picked, already resolved by the caller - * @return block to be placed or null - */ - public abstract ItemStack pickBlock(MovingObjectPosition mop, ItemStack sourceBlock); - - /** - * pick a suitable block to be placed, including whether its already in the inventory - * - * @param mop the source block on which the new one will be placed - * @param sourceBlock the stack the source block would drop when picked, already resolved by the caller - * @return true if suitable block was picked - */ - public abstract boolean pickSomeBlock(MovingObjectPosition mop, ItemStack sourceBlock); - - public static BuildersBlockPicker create(World world, EntityPlayer player, BuildersBlockSelectionFilter filter, - BuildersMaterialBudget budget) { - if (filter.isCopyMode()) { - return new CopyBlockPicker(world, budget); - } - return new PaletteBlockPicker( - world, - player.inventory, - filter.generatePalette(player), - budget, - ThreadLocalRandom.current()); - } - - public static class PaletteBlockPicker extends BuildersBlockPicker { - - private final BuildersMaterialBudget budget; - private final Random random; - private final List palette; - private final World world; - - private int remaining; - - private final int[] scratchTemplate; - private final int[] scratch; - - public PaletteBlockPicker(World world, InventoryPlayer inventory, List palette, - BuildersMaterialBudget budget, Random random) { - this.world = world; - this.palette = palette; - this.budget = budget; - this.random = random; - this.remaining = 0; - this.scratch = new int[palette.size()]; - this.scratchTemplate = new int[palette.size()]; - for (int i = 0; i < palette.size(); i++) this.scratchTemplate[i] = i; - - for (ItemStack stack : inventory.mainInventory) { - for (ItemStack entry : palette) { - if (ItemUtil.areStacksEqual(entry, stack)) { - remaining += stack.stackSize; - break; - } - } - } - } - - @Override - public ItemStack pickBlock(MovingObjectPosition mop, ItemStack ignored) { - if (remaining <= 0) return null; - - // prepare shuffling - System.arraycopy(scratchTemplate, 0, scratch, 0, scratch.length); - - // try every block in the palette in a random order - for (int i = 0; i < palette.size(); i++) { - - // shuffle one step - int j = i + random.nextInt(palette.size() - i); - int tmp = scratch[i]; - scratch[i] = scratch[j]; - scratch[j] = tmp; - - // pick one block from shuffled palette - ItemStack stack = palette.get(scratch[i]); - - if (canPlaceBlock(world, stack, mop) && budget.tryReserve(stack)) { - remaining--; - return stack; - } - } - return null; - } - - @Override - public boolean pickSomeBlock(MovingObjectPosition mop, ItemStack sourceBlock) { - if (remaining <= 0) return false; - - for (ItemStack stack : palette) { - if (canPlaceBlock(world, stack, mop)) { - remaining--; - return true; - } - } - return false; - } - } - - public static class CopyBlockPicker extends BuildersBlockPicker { - - private final BuildersMaterialBudget budget; - private final World world; - - public CopyBlockPicker(World world, BuildersMaterialBudget budget) { - this.budget = budget; - this.world = world; - } - - @Override - public ItemStack pickBlock(MovingObjectPosition mop, ItemStack sourceBlock) { - return sourceBlock != null && canPlaceBlock(world, sourceBlock, mop) && budget.tryReserve(sourceBlock) - ? sourceBlock - : null; - } - - @Override - public boolean pickSomeBlock(MovingObjectPosition mop, ItemStack sourceBlock) { - return pickBlock(mop, sourceBlock) != null; - } - } -} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java deleted file mode 100644 index a1e102cf1..000000000 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersBlockSelectionFilter.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.fouristhenumber.utilitiesinexcess.utils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.minecraft.block.Block; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.item.ItemBlock; -import net.minecraft.item.ItemStack; -import net.minecraft.util.MovingObjectPosition; -import net.minecraft.world.World; - -import org.jetbrains.annotations.Nullable; - -import com.fouristhenumber.utilitiesinexcess.common.items.ItemScribe; -import com.fouristhenumber.utilitiesinexcess.compat.Mods; -import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; -import com.gtnewhorizon.gtnhlib.util.ItemUtil; - -import gregtech.api.items.MetaGeneratedTool; -import gregtech.common.tools.ToolTrowel; -import xonin.backhand.api.core.BackhandUtils; - -public class BuildersBlockSelectionFilter { - - private final List validBlocks; - private final ItemStack backhand; - private final ItemStack lookAtBlock; - - private final boolean isCopyMode; - - public BuildersBlockSelectionFilter(EntityPlayer player, World world, MovingObjectPosition movingObjectPosition) { - this.validBlocks = new ArrayList<>(); - backhand = Mods.Backhand.isLoaded() ? BackhandUtils.getOffhandItem(player) : null; - lookAtBlock = getBlockByLocation(world, movingObjectPosition, player); - isCopyMode = lookAtBlock != null && isScribe(backhand); - - // No logic is executed if we don't look at any block, no need to bother checking other cases - if (lookAtBlock == null) return; - - this.validBlocks.add(lookAtBlock); // Clicked block is always valid - - if (isValidBlock(backhand)) { - this.validBlocks.add(backhand.copy()); - } else if (isTrowel(backhand)) { - this.validBlocks.addAll(hotbarBlocks(player)); - } - } - - /** - * Generates a list of blocks to build with - */ - public List generatePalette(EntityPlayer player) { - if (lookAtBlock == null) return Collections.emptyList(); - if (isValidBlock(backhand)) return Collections.singletonList(backhand); - if (isTrowel(backhand)) return hotbarBlocks(player); - - return Collections.singletonList(lookAtBlock); - } - - /** - * Checks if the given ItemStack matches any of the valid blocks in the filter - */ - public boolean matches(ItemStack other) { - if (other == null) return false; - - if (isCopyMode) return true; - - return this.validBlocks.stream() - .anyMatch(validBlock -> ItemUtil.areStacksEqual(validBlock, other)); - } - - /** - * Whether the fill spans the whole surface regardless of what it is made of, copying each position's - * own block rather than drawing from a palette. - */ - public boolean isCopyMode() { - return isCopyMode; - } - - public static boolean isTrowel(@Nullable ItemStack stack) { - if (stack == null) return false; - - if (Mods.GregTech.isLoaded() && stack.getItem() instanceof MetaGeneratedTool metaGeneratedTool) - return metaGeneratedTool.getToolStats(stack) instanceof ToolTrowel; - - return false; - } - - public static boolean isScribe(@Nullable ItemStack stack) { - if (stack == null) return false; - - return stack.getItem() instanceof ItemScribe; - } - - private static boolean isValidBlock(@Nullable ItemStack stack) { - return (stack != null && stack.getItem() instanceof ItemBlock); - } - - private static List hotbarBlocks(EntityPlayer player) { - List candidates = new ArrayList<>(); - - for (int i = 0; i < 9; i++) { - if (i == player.inventory.currentItem) { - continue; - } - ItemStack item = player.inventory.mainInventory[i]; - if (!isValidBlock(item)) { - continue; - } - candidates.add(item.copy()); - } - return candidates; - } - - public static ItemStack getBlockByLocation(World world, MovingObjectPosition movingObjectPosition, - EntityPlayer player) { - - BlockPos blockPos = new BlockPos( - movingObjectPosition.blockX, - movingObjectPosition.blockY, - movingObjectPosition.blockZ); - - Block block = world.getBlock(blockPos.x, blockPos.y, blockPos.z); - if (block == null) { - return null; - } - return block.getPickBlock(movingObjectPosition, world, blockPos.x, blockPos.y, blockPos.z, player); - } -} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java deleted file mode 100644 index 53fb771a6..000000000 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.fouristhenumber.utilitiesinexcess.utils; - -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter.getBlockByLocation; -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersBlockSelectionFilter.isTrowel; -import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.TranslateMovingObjectPositionToLocation; - -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.Objects; -import java.util.Queue; -import java.util.Set; - -import net.minecraft.block.Block; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.InventoryPlayer; -import net.minecraft.item.ItemStack; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.MovingObjectPosition; -import net.minecraft.world.World; -import net.minecraftforge.common.util.ForgeDirection; - -import com.fouristhenumber.utilitiesinexcess.compat.Mods; -import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; -import com.gtnewhorizon.gtnhlib.util.ItemUtil; - -import gregtech.api.items.MetaGeneratedTool; -import xonin.backhand.api.core.BackhandUtils; - -public class BuildersWandUtils { - - private BuildersWandUtils() {} - - /** - * Decreases an ItemStack containing the item in the player's inventory by 1 - * - * @param player The player in question - * @param itemStack The itemstack to compare against, including metadata and NBT - * @return True if the ItemStack has been decremented, otherwise false - */ - public static boolean decreaseFromInventory(EntityPlayer player, ItemStack itemStack) { - for (int slotIndex = player.inventory.mainInventory.length - 1; slotIndex >= 0; slotIndex--) { - ItemStack stack = player.inventory.mainInventory[slotIndex]; - if (ItemUtil.areStacksEqual(stack, itemStack)) { - stack.stackSize -= 1; - if (stack.stackSize <= 0) { - player.inventory.setInventorySlotContents(slotIndex, null); - } - return true; - } - } - return false; - } - - public static boolean containsItemStack(InventoryPlayer player, ItemStack itemStack) { - for (int slotIndex = player.mainInventory.length - 1; slotIndex >= 0; slotIndex--) { - ItemStack stack = player.mainInventory[slotIndex]; - if (ItemUtil.areStacksEqual(stack, itemStack)) { - return true; - } - } - return false; - } - - /** - * Finds the blocks adjacent to the start position that are connected cardinally, or diagonally - * and have a placeable block (e.g. air, fluid) in front of them relative to the side clicked on. - * - * @param world The world in which to place - * @param maxCount The maximum amount of blocks it should search - * @param filter The pattern used to search adjacent blocks - * @param mop The position of the block that was clicked (with its clicked side) - * @param blockPicker decides which block to place - * @return The set of 1<=x<=maxCount adjacent blocks with air on their face - */ - public static Set findAdjacentBlocks(World world, int maxCount, MovingObjectPosition mop, - EntityPlayer player, BuildersBlockSelectionFilter filter, BuildersBlockPicker blockPicker, - BuildersWandUtils.WandAxisMode axisMode) { - Set region = LinkedHashSet.newLinkedHashSet(maxCount); - if (maxCount <= 0) { - return region; - } - Set visited = HashSet.newHashSet(maxCount); - Queue queue = new LinkedList<>(); - - // copy the mop so we can translate it without affecting the original - mop = MovingObjectPositionUtil.copy(mop); - - // Determine allowed offsets depending on the face that was clicked. - int[][] allowedOffsets = switch (ForgeDirection.getOrientation(mop.sideHit)) { - case UP, DOWN -> switch (axisMode) { - case FREE -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 0, 1 }, { 0, 0, -1 }, { 1, 0, 1 }, - { 1, 0, -1 }, { -1, 0, 1 }, { -1, 0, -1 } }; - case HORIZONTAL -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 } }; - case VERTICAL -> new int[][] { { 0, 0, 1 }, { 0, 0, -1 } }; - }; - case NORTH, SOUTH -> switch (axisMode) { - case FREE -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 }, { 1, 1, 0 }, - { 1, -1, 0 }, { -1, 1, 0 }, { -1, -1, 0 } }; - case HORIZONTAL -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 } }; - case VERTICAL -> new int[][] { { 0, 1, 0 }, { 0, -1, 0 } }; - }; - case EAST, WEST -> switch (axisMode) { - case FREE -> new int[][] { { 0, 1, 0 }, { 0, -1, 0 }, { 0, 0, 1 }, { 0, 0, -1 }, { 0, 1, 1 }, - { 0, 1, -1 }, { 0, -1, 1 }, { 0, -1, -1 } }; - case HORIZONTAL -> new int[][] { { 0, 0, 1 }, { 0, 0, -1 } }; - case VERTICAL -> new int[][] { { 0, 1, 0 }, { 0, -1, 0 } }; - }; - default -> throw new RuntimeException("UE's BuilderWand's findAdjacentBlocks called with invalid side"); - }; - - // start block - BlockPos startPos = new BlockPos(mop.blockX, mop.blockY, mop.blockZ); - queue.add(startPos); - visited.add(startPos); - - // Flood-fill the contiguous region in the allowed plane. - while (!queue.isEmpty() && region.size() < maxCount) { - BlockPos current = queue.poll(); - - // translate the mop - TranslateMovingObjectPositionToLocation(mop, current); - if (!IsValidForWireFrame(world, mop, player, filter, blockPicker)) continue; - - region.add(current); - - for (int[] off : allowedOffsets) { - if (region.size() >= maxCount) break; - BlockPos key = current.offset(off[0], off[1], off[2]); - if (visited.contains(key)) continue; - visited.add(key); - queue.add(key); - } - } - return region; - } - - private static boolean IsValidForWireFrame(World world, MovingObjectPosition mop, EntityPlayer player, - BuildersBlockSelectionFilter filter, BuildersBlockPicker blockPicker) { - - // 0. cannot place on air - ItemStack sourceBlock = getBlockByLocation(world, mop, player); - if (sourceBlock == null) return false; - - // 1. matches the filter? - if (!filter.matches(sourceBlock)) return false; - - // 2. pick a suitable block - return blockPicker.pickSomeBlock(mop, sourceBlock); - } - - public static boolean damageBackhand(int damage, EntityPlayer player) { - if (!player.capabilities.isCreativeMode && Mods.Backhand.isLoaded() - && isTrowel(BackhandUtils.getOffhandItem(player))) { - MetaGeneratedTool trowel = (MetaGeneratedTool) Objects.requireNonNull(BackhandUtils.getOffhandItem(player)) - .getItem(); - if (trowel == null) { - return true; - } - return trowel.doDamage(BackhandUtils.getOffhandItem(player), damage); - } - return true; - } - - public static boolean canPlaceBlock(World world, ItemStack targetBlock, MovingObjectPosition mop) { - Block block = Block.getBlockFromItem(targetBlock.getItem()); - ForgeDirection direction = ForgeDirection.getOrientation(mop.sideHit); - BlockPos targetPos = new BlockPos( - mop.blockX + direction.offsetX, - mop.blockY + direction.offsetY, - mop.blockZ + direction.offsetZ); - - // Blocks with no collision box (torches, rails, plants) are placeable through entities, - // For the rest use a whole cube, otherwise weird things happen, better safe than sorry. - boolean solid = block.getCollisionBoundingBoxFromPool(world, targetPos.x, targetPos.y, targetPos.z) != null; - - return block.canPlaceBlockOnSide(world, targetPos.x, targetPos.y, targetPos.z, mop.sideHit) && world - .canPlaceEntityOnSide(block, targetPos.x, targetPos.y, targetPos.z, true, mop.sideHit, null, targetBlock) - && (!solid || world.checkNoEntityCollision( - AxisAlignedBB.getBoundingBox( - targetPos.x, - targetPos.y, - targetPos.z, - targetPos.x + 1, - targetPos.y + 1, - targetPos.z + 1))); - } - - public enum WandAxisMode { - FREE, - HORIZONTAL, - VERTICAL - } -} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/ChickenLibRayTracer.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/ChickenLibRayTracer.java new file mode 100644 index 000000000..4b80fc75b --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/ChickenLibRayTracer.java @@ -0,0 +1,57 @@ +package com.fouristhenumber.utilitiesinexcess.utils; + +import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +/** + * Gracefully ~~stolen~~ borrowed from CodeChickenLib: codechicken.lib.raytracer.RayTracer, only the part needed to + * trace one block again. Copied rather than called, so nothing here depends on CodeChickenLib being loaded. + */ +public final class ChickenLibRayTracer { + + private ChickenLibRayTracer() {} + + public static MovingObjectPosition retraceBlock(World world, EntityPlayer player, int x, int y, int z) { + Block block = world.getBlock(x, y, z); + + Vec3 headVec = getCorrectedHeadVec(player); + Vec3 lookVec = player.getLook(1.0F); + double reach = getBlockReachDistance(player); + Vec3 endVec = headVec.addVector(lookVec.xCoord * reach, lookVec.yCoord * reach, lookVec.zCoord * reach); + return block.collisionRayTrace(world, x, y, z, headVec, endVec); + } + + public static Vec3 getCorrectedHeadVec(EntityPlayer player) { + Vec3 v = Vec3.createVectorHelper(player.posX, player.posY, player.posZ); + if (player.worldObj.isRemote) { + v.yCoord += player.getEyeHeight() - player.getDefaultEyeHeight(); // compatibility with eye height changing + // mods + } else { + v.yCoord += player.getEyeHeight(); + if (player instanceof EntityPlayerMP && player.isSneaking()) v.yCoord -= 0.08; + } + return v; + } + + public static double getBlockReachDistance(EntityPlayer player) { + return player.worldObj.isRemote ? getBlockReachDistance_client() + : player instanceof EntityPlayerMP ? getBlockReachDistance_server((EntityPlayerMP) player) : 5D; + } + + private static double getBlockReachDistance_server(EntityPlayerMP player) { + return player.theItemInWorldManager.getBlockReachDistance(); + } + + @SideOnly(Side.CLIENT) + private static double getBlockReachDistance_client() { + return Minecraft.getMinecraft().playerController.getBlockReachDistance(); + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/InventoryBudget.java similarity index 56% rename from src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java rename to src/main/java/com/fouristhenumber/utilitiesinexcess/utils/InventoryBudget.java index 2d4e0e40c..2010f8e50 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersMaterialBudget.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/InventoryBudget.java @@ -9,9 +9,9 @@ import com.gtnewhorizon.gtnhlib.util.ItemUtil; /** - * Tracks how much of each material a fill has committed to, so it never promises more than the player owns. + * Tracks what an inventory can still afford, without touching it. */ -public final class BuildersMaterialBudget { +public final class InventoryBudget { private static final class Reservation { @@ -23,13 +23,36 @@ private static final class Reservation { private final InventoryPlayer inventory; private final boolean creative; - public BuildersMaterialBudget(InventoryPlayer inventory, boolean creative) { + public InventoryBudget(InventoryPlayer inventory, boolean creative) { this.inventory = inventory; this.creative = creative; } /** - * Claims one of the given material, returning false if none is left to spend. + * Claims one of every listed material, or none of them. + * + * @return true on success + */ + public boolean tryReserve(List materials) { + int mark = reservations.size(); + int[] spent = new int[mark]; + for (int i = 0; i < mark; i++) spent[i] = reservations.get(i).remaining; + + for (ItemStack material : materials) { + if (tryReserve(material)) continue; + + // Rollback + for (int i = 0; i < mark; i++) reservations.get(i).remaining = spent[i]; + while (reservations.size() > mark) reservations.removeLast(); + return false; + } + return true; + } + + /** + * Claims one of the given material, or not. + * + * @return true on success */ public boolean tryReserve(ItemStack material) { for (Reservation reservation : reservations) { @@ -43,20 +66,17 @@ public boolean tryReserve(ItemStack material) { } int owned = 0; - for (int slot = 0; slot < inventory.getSizeInventory(); slot++) { - ItemStack inSlot = inventory.getStackInSlot(slot); + for (int slot = 0; slot < inventory.mainInventory.length; slot++) { + ItemStack inSlot = inventory.mainInventory[slot]; if (inSlot != null && ItemUtil.areStacksEqual(inSlot, material)) { owned += inSlot.stackSize; } } // Creative still requires owning one, matching the stock wand. - if (owned == 0) { - return false; - } - if (creative) { - owned = Integer.MAX_VALUE; - } + if (owned == 0) return false; + + if (creative) owned = Integer.MAX_VALUE; Reservation reservation = new Reservation(); reservation.stack = material; diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java index 2138d5a9c..f42e2dd59 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java @@ -2,6 +2,7 @@ import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; +import net.minecraftforge.common.util.ForgeDirection; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; @@ -32,4 +33,30 @@ public static MovingObjectPosition copy(MovingObjectPosition movingObjectPositio movingObjectPosition.hitVec.yCoord, movingObjectPosition.hitVec.zCoord)); } + + /** The block on the other side of the hit face. */ + public static BlockPos targetPos(MovingObjectPosition movingObjectPosition) { + ForgeDirection side = ForgeDirection.getOrientation(movingObjectPosition.sideHit); + return new BlockPos( + movingObjectPosition.blockX + side.offsetX, + movingObjectPosition.blockY + side.offsetY, + movingObjectPosition.blockZ + side.offsetZ); + } + + public static BlockPos blockPos(MovingObjectPosition movingObjectPosition) { + return new BlockPos(movingObjectPosition.blockX, movingObjectPosition.blockY, movingObjectPosition.blockZ); + } + + /** Where on the block the ray landed, 0..1, as onItemUse and ItemBlock take it. */ + public static float hitX(MovingObjectPosition movingObjectPosition) { + return (float) (movingObjectPosition.hitVec.xCoord - movingObjectPosition.blockX); + } + + public static float hitY(MovingObjectPosition movingObjectPosition) { + return (float) (movingObjectPosition.hitVec.yCoord - movingObjectPosition.blockY); + } + + public static float hitZ(MovingObjectPosition movingObjectPosition) { + return (float) (movingObjectPosition.hitVec.zCoord - movingObjectPosition.blockZ); + } } diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java new file mode 100644 index 000000000..30ee240b4 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java @@ -0,0 +1,175 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +import net.minecraft.block.Block; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; + +import org.jetbrains.annotations.Nullable; + +import com.fouristhenumber.utilitiesinexcess.utils.InventoryBudget; +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +/** + * Chooses which block the Builder's Wand puts down + * - extrude mode reproduces whatever the source cell is + * - otherwise it draws from a palette: the offhand block, hotbar, or the clicked block + */ +public abstract class BWBlockPicker { + + protected final World world; + protected final InventoryBudget budget; + + protected BWBlockPicker(World world, InventoryBudget budget) { + this.world = world; + this.budget = budget; + } + + /** + * pick a suitable block to be placed + * -> has to be placeable, use canPlaceBlock() + * + * @param mop the source block on which the new one will be placed + * @param sourceBlock the stack the source block hands over when picked + * @param extraCosts what this cell costs besides the block itself, reserved together with it + * @return block to be placed, already reserved against the budget, or null + */ + public abstract ItemStack pickBlock(MovingObjectPosition mop, @Nullable ItemStack sourceBlock, + List extraCosts); + + /** Reserves the block and everything else the cell costs, all of them or none. */ + protected boolean tryReserve(ItemStack stack, List extraCosts) { + if (extraCosts.isEmpty()) return budget.tryReserve(stack); + + List costs = new ArrayList<>(extraCosts.size() + 1); + costs.add(stack); + costs.addAll(extraCosts); + return budget.tryReserve(costs); + } + + /** + * @return Copy or Palette BlockPicker based on the mode + */ + public static BWBlockPicker create(World world, EntityPlayer player, BWMode mode, @Nullable ItemStack lookedAtBlock, + InventoryBudget budget) { + if (mode == BWMode.EXTRUDE) { + return new ExtrudeBlockPicker(world, budget); + } + return new PaletteBlockPicker(world, budget, palette(mode, player, lookedAtBlock), ThreadLocalRandom.current()); + } + + private static List palette(BWMode mode, EntityPlayer player, @Nullable ItemStack lookedAtBlock) { + return switch (mode) { + case OFFHAND_BLOCK -> Collections.singletonList(BWMode.offhand(player)); + case TROWEL -> hotbarBlocks(player); + case CLICKED_BLOCK -> lookedAtBlock == null ? Collections.emptyList() + : Collections.singletonList(lookedAtBlock); + case EXTRUDE -> Collections.emptyList(); + }; + } + + private static List hotbarBlocks(EntityPlayer player) { + List candidates = new ArrayList<>(); + + for (int i = 0; i < 9; i++) { + if (i == player.inventory.currentItem) { + continue; + } + ItemStack item = player.inventory.mainInventory[i]; + if (!BWMode.isItemBlock(item)) { + continue; + } + candidates.add(item.copy()); + } + return candidates; + } + + /** Whether the stack could be placed into the target cell, the one in front of the clicked face. */ + static boolean canPlaceBlock(World world, ItemStack toPlace, MovingObjectPosition mop) { + // items like doors and signs place by their own rules, which nothing here can predict + if (!BWMode.isItemBlock(toPlace)) return false; + + Block block = Block.getBlockFromItem(toPlace.getItem()); + BlockPos targetPos = MovingObjectPositionUtil.targetPos(mop); + + // Blocks with no collision box (torches, rails, plants) are placeable through entities, + // For the rest use a whole cube, otherwise weird things happen, better safe than sorry. + boolean solid = block.getCollisionBoundingBoxFromPool(world, targetPos.x, targetPos.y, targetPos.z) != null; + + return block.canPlaceBlockOnSide(world, targetPos.x, targetPos.y, targetPos.z, mop.sideHit) + && world + .canPlaceEntityOnSide(block, targetPos.x, targetPos.y, targetPos.z, true, mop.sideHit, null, toPlace) + && (!solid || world.checkNoEntityCollision( + AxisAlignedBB.getBoundingBox( + targetPos.x, + targetPos.y, + targetPos.z, + targetPos.x + 1, + targetPos.y + 1, + targetPos.z + 1))); + } + + static class PaletteBlockPicker extends BWBlockPicker { + + private final Random random; + private final List palette; + + private final int[] scratchTemplate; + private final int[] scratch; + + PaletteBlockPicker(World world, InventoryBudget budget, List palette, Random random) { + super(world, budget); + this.palette = palette; + this.random = random; + this.scratch = new int[palette.size()]; + this.scratchTemplate = new int[palette.size()]; + for (int i = 0; i < palette.size(); i++) this.scratchTemplate[i] = i; + } + + @Override + public ItemStack pickBlock(MovingObjectPosition mop, @Nullable ItemStack ignored, List extraCosts) { + System.arraycopy(scratchTemplate, 0, scratch, 0, scratch.length); + + // try every block in the palette in a random order + for (int i = 0; i < palette.size(); i++) { + + // shuffle one step + int j = i + random.nextInt(palette.size() - i); + int tmp = scratch[i]; + scratch[i] = scratch[j]; + scratch[j] = tmp; + + ItemStack stack = palette.get(scratch[i]); + + if (canPlaceBlock(world, stack, mop) && tryReserve(stack, extraCosts)) { + return stack; + } + } + return null; + } + } + + static class ExtrudeBlockPicker extends BWBlockPicker { + + ExtrudeBlockPicker(World world, InventoryBudget budget) { + super(world, budget); + } + + @Override + public ItemStack pickBlock(MovingObjectPosition mop, @Nullable ItemStack sourceBlock, + List extraCosts) { + return sourceBlock != null && canPlaceBlock(world, sourceBlock, mop) && tryReserve(sourceBlock, extraCosts) + ? sourceBlock + : null; + } + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java new file mode 100644 index 000000000..26ed9569b --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java @@ -0,0 +1,56 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.MovingObjectPosition; + +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +/** + * The Builder's Wand's extension point. + * Add your own custom place handler by implementing IBWCellHandler and registering it here. + * + * Useful particularly for extrude mode, when naively copying just the metadata of a block is not enough. + * Also useful for any blocks for which Block to ItemStack method "pickBlock" is tricky (like ForgeMicroblocks) + */ +public final class BWCellHandlers { + + private static final List handlers = new ArrayList<>(); + private static final IBWCellHandler defaultHandler = new DefaultBWCellHandler(); + + private BWCellHandlers() {} + + public static void register(IBWCellHandler handler) { + handlers.add(handler); + } + + /** + * Hands one cell of the fill to whichever handler claims it + * + * @param ctx The context of the wand + * @param mop looking at the block on which to build + * @param place false to only reserve and report, true to actually build and charge the player + * @return true if this cell is part of the fill + */ + public static boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) { + if (place && spawnProtected(ctx, mop)) return false; + return find(ctx, mop.blockX, mop.blockY, mop.blockZ).build(ctx, mop, place); + } + + /** Vanilla checks spawn protection only for the block the player clicked, not for the rest of the fill. */ + private static boolean spawnProtected(BWContext ctx, MovingObjectPosition mop) { + BlockPos to = MovingObjectPositionUtil.targetPos(mop); + return MinecraftServer.getServer() + .isBlockProtected(ctx.world, to.x, to.y, to.z, ctx.player); + } + + public static IBWCellHandler find(BWContext ctx, int x, int y, int z) { + for (IBWCellHandler handler : handlers) { + if (handler.handles(ctx, x, y, z)) return handler; + } + return defaultHandler; + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java new file mode 100644 index 000000000..ebdae2fe6 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java @@ -0,0 +1,79 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; + +import org.jetbrains.annotations.Nullable; + +import com.fouristhenumber.utilitiesinexcess.utils.InventoryBudget; +import com.gtnewhorizon.gtnhlib.util.ItemUtil; + +/** Everything a cell handler needs during one pass of a Builder's Wand use. */ +public final class BWContext { + + public final World world; + public final EntityPlayer player; + public final BWMode mode; + public final BWBlockPicker picker; + public final InventoryBudget budget; + /** The clicked cell, traced again; a multipart hands back an ExtendedMOP naming the part. */ + public final MovingObjectPosition originalMop; + /** What the clicked cell hands over when picked, null when it cannot be expressed as one stack. */ + @Nullable + public final ItemStack clickedBlockStack; + + public BWContext(World world, MovingObjectPosition originalMop, @Nullable ItemStack clickedBlockStack, + EntityPlayer player, BWMode mode, BWBlockPicker picker, InventoryBudget budget) { + this.world = world; + this.player = player; + this.mode = mode; + this.picker = picker; + this.budget = budget; + this.originalMop = originalMop; + this.clickedBlockStack = clickedBlockStack; + } + + public boolean isCreative() { + return player.capabilities.isCreativeMode; + } + + public boolean isExtrudeMode() { + return mode == BWMode.EXTRUDE; + } + + public boolean isTrowelMode() { + return mode == BWMode.TROWEL; + } + + /** Whether the current mode spreads over any surface, no matter what is underneath */ + public boolean spreadsOverAnything() { + return mode == BWMode.EXTRUDE || mode == BWMode.TROWEL; + } + + /** + * Takes one of the stack from the inventory, unless creative. Storage first, then the hotbar, the offhand last. + */ + public void spend(ItemStack stack) { + if (isCreative()) return; + + InventoryPlayer inventory = player.inventory; + int offhand = BWMode.offhandSlot(player); + for (int slot = inventory.mainInventory.length - 1; slot >= 0; slot--) { + if (slot != offhand && take(inventory, slot, stack)) return; + } + if (offhand >= 0) take(inventory, offhand, stack); + } + + private static boolean take(InventoryPlayer inventory, int slot, ItemStack stack) { + ItemStack inSlot = inventory.mainInventory[slot]; + if (!ItemUtil.areStacksEqual(inSlot, stack)) return false; + + inSlot.stackSize--; + if (inSlot.stackSize <= 0) inventory.setInventorySlotContents(slot, null); + return true; + } + +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWMode.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWMode.java new file mode 100644 index 000000000..4deaa7a84 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWMode.java @@ -0,0 +1,73 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemBlock; +import net.minecraft.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +import com.fouristhenumber.utilitiesinexcess.common.items.ItemScribe; +import com.fouristhenumber.utilitiesinexcess.compat.Mods; + +import gregtech.api.items.MetaGeneratedTool; +import gregtech.common.tools.ToolTrowel; +import xonin.backhand.api.core.BackhandUtils; + +/** + * What the offhand tells the Builder's Wand to do. + *
    + *
  • EXTRUDE: a Scribe, every cell is pushed out by one, reproduced exactly as it is + *
  • TROWEL: paints the hotbar blocks over any surface + *
  • OFFHAND_BLOCK: extends the clicked kind of surface with the offhand block + *
  • CLICKED_BLOCK: extends the clicked kind of surface with more of the same + *
+ */ +public enum BWMode { + + EXTRUDE, + TROWEL, + OFFHAND_BLOCK, + CLICKED_BLOCK; + + public static BWMode of(EntityPlayer player) { + ItemStack offhand = offhand(player); + if (isScribe(offhand)) return EXTRUDE; + if (isTrowel(offhand)) return TROWEL; + if (isItemBlock(offhand)) return OFFHAND_BLOCK; + return CLICKED_BLOCK; + } + + @Nullable + static ItemStack offhand(EntityPlayer player) { + return Mods.Backhand.isLoaded() ? BackhandUtils.getOffhandItem(player) : null; + } + + /** The Backhand offhand slot index in mainInventory, -1 without Backhand. */ + static int offhandSlot(EntityPlayer player) { + return Mods.Backhand.isLoaded() ? BackhandUtils.getOffhandSlot(player) : -1; + } + + /** Trowels wear out as they are used; false when the trowel cannot take the damage. */ + static boolean tryDamageTrowel(EntityPlayer player, int damage) { + if (player.capabilities.isCreativeMode) return true; + + ItemStack offhand = offhand(player); + if (!isTrowel(offhand)) return true; + + return ((MetaGeneratedTool) offhand.getItem()).doDamage(offhand, damage); + } + + static boolean isTrowel(@Nullable ItemStack stack) { + if (stack == null || !Mods.GregTech.isLoaded()) return false; + + return stack.getItem() instanceof MetaGeneratedTool tool && tool.getToolStats(stack) instanceof ToolTrowel; + } + + static boolean isItemBlock(@Nullable ItemStack stack) { + return stack != null && stack.getItem() instanceof ItemBlock; + } + + private static boolean isScribe(@Nullable ItemStack stack) { + return stack != null && stack.getItem() instanceof ItemScribe; + } +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWRegion.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWRegion.java new file mode 100644 index 000000000..02d6cfaef --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWRegion.java @@ -0,0 +1,95 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Queue; +import java.util.Set; + +import net.minecraft.util.MovingObjectPosition; +import net.minecraftforge.common.util.ForgeDirection; + +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; + +/** + * Works out which cells one Builder's Wand use covers. + *
    + *
  • flood fills outward from the clicked block, in the plane of the face that was clicked + *
  • a cell joins only if its handler says it can be built and paid for + *
+ */ +public final class BWRegion { + + private BWRegion() {} + + /** + * @param maxCount The maximum amount of blocks it should return + * @param mop The position of the block that was clicked (with its clicked side) + * @return source blocks on top of which new blocks can be placed, + * at most maxCount cells, possibly none + */ + public static Set findAdjacentBlocksToBuildOn(BWContext ctx, int maxCount, MovingObjectPosition mop, + WandAxisLock axisMode) { + Set region = LinkedHashSet.newLinkedHashSet(maxCount); + if (maxCount <= 0) { + return region; + } + Set visited = HashSet.newHashSet(maxCount); + Queue queue = new ArrayDeque<>(); + + // copy the mop so we can translate it without affecting the original + mop = MovingObjectPositionUtil.copy(mop); + + // Determine allowed offsets depending on the face that was clicked. + int[][] allowedOffsets = switch (ForgeDirection.getOrientation(mop.sideHit)) { + case UP, DOWN -> switch (axisMode) { + case FREE -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 0, 1 }, { 0, 0, -1 }, { 1, 0, 1 }, + { 1, 0, -1 }, { -1, 0, 1 }, { -1, 0, -1 } }; + case HORIZONTAL -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 } }; + case VERTICAL -> new int[][] { { 0, 0, 1 }, { 0, 0, -1 } }; + }; + case NORTH, SOUTH -> switch (axisMode) { + case FREE -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 }, { 1, 1, 0 }, + { 1, -1, 0 }, { -1, 1, 0 }, { -1, -1, 0 } }; + case HORIZONTAL -> new int[][] { { 1, 0, 0 }, { -1, 0, 0 } }; + case VERTICAL -> new int[][] { { 0, 1, 0 }, { 0, -1, 0 } }; + }; + case EAST, WEST -> switch (axisMode) { + case FREE -> new int[][] { { 0, 1, 0 }, { 0, -1, 0 }, { 0, 0, 1 }, { 0, 0, -1 }, { 0, 1, 1 }, + { 0, 1, -1 }, { 0, -1, 1 }, { 0, -1, -1 } }; + case HORIZONTAL -> new int[][] { { 0, 0, 1 }, { 0, 0, -1 } }; + case VERTICAL -> new int[][] { { 0, 1, 0 }, { 0, -1, 0 } }; + }; + default -> throw new RuntimeException("UE's BuilderWand's findAdjacentBlocks called with invalid side"); + }; + + // start block + BlockPos startPos = new BlockPos(mop.blockX, mop.blockY, mop.blockZ); + queue.add(startPos); + visited.add(startPos); + + // Flood-fill the contiguous region in the allowed plane. + while (!queue.isEmpty() && region.size() < maxCount) { + BlockPos current = queue.poll(); + MovingObjectPositionUtil.TranslateMovingObjectPositionToLocation(mop, current); + if (!BWCellHandlers.build(ctx, mop, false)) continue; + + region.add(current); + + for (int[] off : allowedOffsets) { + if (region.size() >= maxCount) break; + BlockPos key = current.offset(off[0], off[1], off[2]); + if (visited.add(key)) queue.add(key); + } + } + return region; + } + + public enum WandAxisLock { + FREE, + HORIZONTAL, + VERTICAL + } + +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java new file mode 100644 index 000000000..0a6f2da0c --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java @@ -0,0 +1,120 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.hitX; +import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.hitY; +import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.hitZ; +import static com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil.targetPos; + +import java.util.Collections; +import java.util.List; + +import net.minecraft.block.Block; +import net.minecraft.item.ItemStack; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.world.World; +import net.minecraftforge.common.util.ForgeDirection; + +import org.jetbrains.annotations.Nullable; + +import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; +import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; +import com.gtnewhorizon.gtnhlib.util.ItemUtil; + +/** + * An ordinary block: one position, one ItemStack, placed the way a player would place it. Also the fallback for + * every cell no other handler claims, so plain blocks need no handler at all. + *

+ * Extend it and override only the one that describes your block: + *

    + *
  • Which cells are even yours? {@link #handles} claims them, otherwise your handler won't be called at all. + * + *
  • Does your block hand back a useless {@code getPickBlock()}? + * This is bad since wand needs to decide whether two blocks are identical, so it can spread over them. + * Override {@link #sameKindAsClicked} and compare them your way. + * + *
  • Does a cell cost more than its block, a cladding, a dye, a filter inside? Declare it in {@link #extraCosts} + * and the wand reserves it together with the block, or skips the cell. + * + *
  • Does the new block come out blank, missing a rotation, a color, a cover? + * That state lives in the tile entity, and the wand hands the new block nothing but an ItemStack, which leaves it + * behind. Override {@link #build}, call {@code super.build} first, then copy the tile entity state from the source + * cell onto the new block, only when {@code place} is true. + *
+ *

+ * Is your cell not one block with one item at all, the way a bag of multiparts is not? Then implement + * {@link IBWCellHandler} directly instead. + */ +public class DefaultBWCellHandler implements IBWCellHandler { + + @Override + public boolean handles(BWContext ctx, int x, int y, int z) { + return true; + } + + @Override + public boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) { + World world = ctx.world; + + Block block = world.getBlock(mop.blockX, mop.blockY, mop.blockZ); + // ItemBlock places into a replaceable cell (air, grass, snow, fluid) instead of in front of it + if (block.isReplaceable(world, mop.blockX, mop.blockY, mop.blockZ)) return false; + + ItemStack sourceBlockStack = block.getPickBlock(mop, ctx.world, mop.blockX, mop.blockY, mop.blockZ, ctx.player); + if (!(ctx.spreadsOverAnything() || sameKindAsClicked(ctx, mop, sourceBlockStack))) return false; + + ItemStack toPlace = ctx.picker.pickBlock(mop, sourceBlockStack, extraCosts(ctx, mop)); + if (toPlace == null) return false; + if (!place) return true; + + if (!BWMode.tryDamageTrowel(ctx.player, BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand)) + return false; + + ItemStack itemCopy = toPlace.copy(); + itemCopy.stackSize = 1; + + BlockPos to = targetPos(mop); + ForgeDirection side = ForgeDirection.getOrientation(mop.sideHit); + // extrude mode aims at the empty cell, clicking the source would merge slabs into a double slab + boolean atTarget = ctx.isExtrudeMode(); + + // uses ItemBlock to place the block with all the checks, sets stackSize to 0 on success + itemCopy.tryPlaceItemIntoWorld( + ctx.player, + world, + atTarget ? to.x : mop.blockX, + atTarget ? to.y : mop.blockY, + atTarget ? to.z : mop.blockZ, + mop.sideHit, + atTarget ? hitX(mop) - side.offsetX : hitX(mop), + atTarget ? hitY(mop) - side.offsetY : hitY(mop), + atTarget ? hitZ(mop) - side.offsetZ : hitZ(mop)); + if (itemCopy.stackSize != 0) return false; + + ctx.spend(toPlace); + + // copy the rotation or other metadata pieces that do not transfer through the itemStack + if (ctx.isExtrudeMode()) { + world.setBlockMetadataWithNotify( + to.x, + to.y, + to.z, + world.getBlockMetadata(mop.blockX, mop.blockY, mop.blockZ), + 3); + } + return true; + } + + /** What this cell costs besides the block itself */ + protected List extraCosts(BWContext ctx, MovingObjectPosition mop) { + return Collections.emptyList(); + } + + /** + * Whether this cell is the same kind as the one the player clicked. Override when a pick stack cannot tell. + * Used to determine spreading to other blocks + */ + protected boolean sameKindAsClicked(BWContext ctx, MovingObjectPosition mop, @Nullable ItemStack source) { + return ItemUtil.areStacksEqual(ctx.clickedBlockStack, source); + } + +} diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java new file mode 100644 index 000000000..3ad0b2298 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java @@ -0,0 +1,96 @@ +package com.fouristhenumber.utilitiesinexcess.utils.bw; + +import net.minecraft.util.MovingObjectPosition; + +/** + * Teaches the Builder's Wand to extend one kind of cell. + * + *

What one use of the wand does

+ *
    + *
  1. flood fills across the blocks that are already there, in the plane of the clicked face: the source cells + *
  2. offers every source cell to the handlers in registration order, first claim wins, the rest go to + * {@link DefaultBWCellHandler} + *
  3. the handler identifies the source cell and, unless the mode paints over anything, compares it with the + * clicked cell + *
  4. the handler builds onto the clicked face of that source cell, into the empty cell in front of it + *
+ * + *

Which cell is which

+ *
    + *
  • {@code mop}: the source cell being handled, the block that gets built onto + *
  • {@code MovingObjectPositionUtil.targetPos(mop)}: the target cell in front of it, where the new block lands + *
  • {@code ctx.originalMop}: the cell the player clicked, the same one all use long + *
  • {@code ctx.clickedBlockStack}: what that original clicked cell hands over when picked + *
+ * + *

Registering

During init, behind your own mod check: + * + *
+ * if (Mods.YourMod.isLoaded()) BWCellHandlers.register(new YourCellHandler());
+ * 
+ * + *

Example

A block whose tile entity holds state the item does not carry: + * + *
+ * public final class MyCellHandler extends DefaultBWCellHandler {
+ *
+ *     @Override
+ *     public boolean handles(BWContext ctx, int x, int y, int z) {
+ *         return ctx.world.getTileEntity(x, y, z) instanceof MyTile;
+ *     }
+ *
+ *     @Override
+ *     public boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) {
+ *         if (!super.build(ctx, mop, place)) return false; // ordinary placement into the target cell
+ *         if (place) copyMyState(ctx.world, blockPos(mop), targetPos(mop));
+ *         return true;
+ *     }
+ * }
+ * 
+ * + * Real examples: + *
    + *
  • ArchitectureCraftBWCellHandler: the example above, plus the cladding it costs declared in {@code extraCosts} + *
  • MultipartBWCellHandler: implements this interface directly, because a cell holding a bag of parts is no + * single block with a single item + *
+ * + *

Rules

+ *
    + *
  • handlers are registered once and shared by the client and the server thread, so they must be stateless + *
  • everything belonging to one use lives on {@link BWContext}: world, player, mode, budget, clicked cell + *
  • reserve materials through {@code ctx.budget} and pay through {@code ctx.spend}, or the wireframe promises + * cells the player cannot afford + *
  • place through {@code tryPlaceItemIntoWorld()} so Forge can veto a cell in a forbidden area, + * rather than circumventing it with direct {@code setBlock()} + *
+ * + * @see DefaultBWCellHandler which lists what to override when extending it + */ +public interface IBWCellHandler { + + /** + * Whether this handler wants the cell at the given position. The mode may be part of the answer: declining + * hands the cell to the next handler, and finally to {@link DefaultBWCellHandler}. + */ + boolean handles(BWContext ctx, int x, int y, int z); + + /** + * Report what this cell costs and, when asked, build it. + *
    + *
  • runs once to work out the fill, every tick on the client for the wireframe, and once to place it + *
  • both passes must answer alike, so the player gets what the wireframe showed + *
  • while {@code place} is false nothing in the world may change + *
  • Reserve materials through {@code ctx.budget} + *
  • Pay for the materials through {@code ctx.spend} + *
+ * + * @param mop the source cell to build onto + * @param place false to only reserve and report, true to also build and pay for the materials + * @return true if + * this cell is part of the fill, + * block can be placed and materials were reserved, + * && (!place || build successfully) + */ + boolean build(BWContext ctx, MovingObjectPosition mop, boolean place); +} From f21726a564defdaef93fe9fb5cbdcedc2a3843c8 Mon Sep 17 00:00:00 2001 From: Cooble Date: Sun, 20 Sep 2026 14:33:32 +0200 Subject: [PATCH 7/7] BW, modified API, overcome AC bug, since getPickBlock returns garbage. --- .../common/items/ItemBuildersWand.java | 13 +------ .../ArchitectureCraftBWCellHandler.java | 35 ++++++++++++++----- .../utils/bw/BWBlockPicker.java | 8 +++-- .../utils/bw/BWCellHandlers.java | 3 +- .../utilitiesinexcess/utils/bw/BWContext.java | 14 +++++--- .../utils/bw/DefaultBWCellHandler.java | 28 ++++++++++++--- .../utils/bw/IBWCellHandler.java | 15 ++++++-- 7 files changed, 80 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java index ebee32952..ff7c3ef43 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java @@ -20,7 +20,6 @@ import com.fouristhenumber.utilitiesinexcess.utils.ChickenLibRayTracer; import com.fouristhenumber.utilitiesinexcess.utils.InventoryBudget; import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; -import com.fouristhenumber.utilitiesinexcess.utils.bw.BWBlockPicker; import com.fouristhenumber.utilitiesinexcess.utils.bw.BWCellHandlers; import com.fouristhenumber.utilitiesinexcess.utils.bw.BWContext; import com.fouristhenumber.utilitiesinexcess.utils.bw.BWMode; @@ -134,24 +133,14 @@ public boolean onItemUse(ItemStack itemstack, EntityPlayer player, World world, private static BWContext newContext(World world, EntityPlayer player, MovingObjectPosition mop) { var budget = new InventoryBudget(player.inventory, player.capabilities.isCreativeMode); - // plain copy: the client ray can be an ExtendedMOP naming a part broken this tick - MovingObjectPosition plainMop = MovingObjectPositionUtil.copy(mop); - ItemStack lookedAtBlock = world.getBlock(plainMop.blockX, plainMop.blockY, plainMop.blockZ) - .getPickBlock(plainMop, world, plainMop.blockX, plainMop.blockY, plainMop.blockZ, player); - - BWMode mode = BWMode.of(player); - var picker = BWBlockPicker.create(world, player, mode, lookedAtBlock, budget); - // null when the ray misses the clicked block, then handlers just get the plain position MovingObjectPosition tracedMop = ChickenLibRayTracer .retraceBlock(world, player, mop.blockX, mop.blockY, mop.blockZ); return new BWContext( world, tracedMop != null ? tracedMop : MovingObjectPositionUtil.copy(mop), - lookedAtBlock, player, - mode, - picker, + BWMode.of(player), budget); } diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java index f55b13f00..187ec5a70 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java @@ -24,32 +24,49 @@ public final class ArchitectureCraftBWCellHandler extends DefaultBWCellHandler { @Override public boolean handles(BWContext ctx, int x, int y, int z) { - return ctx.isExtrudeMode() && shapeAt(ctx.world, x, y, z) != null; + return shapeAt(ctx.world, x, y, z) != null; } @Override public boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) { if (!super.build(ctx, mop, place)) return false; - if (!place) return true; + // only the Scribe reproduces a cell as it is, the other modes just place the shape item + if (!place || !ctx.isExtrudeMode()) return true; BlockPos from = MovingObjectPositionUtil.blockPos(mop); - copyShapeState( - ctx, - from, - MovingObjectPositionUtil.targetPos(mop), - claddingOf(shapeAt(ctx.world, from.x, from.y, from.z))); + copyShapeState(ctx, from, MovingObjectPositionUtil.targetPos(mop), claddingOf(ctx, from)); return true; } + /** + * AC has badly implemented pickBlock. + * We use what The Sawbench and block drops use instead. + */ + @Override + @Nullable + public ItemStack pickBlockAt(BWContext ctx, MovingObjectPosition mop) { + BlockPos from = MovingObjectPositionUtil.blockPos(mop); + TileShape shape = shapeAt(ctx.world, from.x, from.y, from.z); + if (shape == null) return super.pickBlockAt(ctx, mop); + + boolean emissive = ctx.world.getBlock(from.x, from.y, from.z) == ArchitectureCraft.content.blockShapeSE; + return shape.shape.kind.newStack(shape.shape, shape.baseBlockState, 1, emissive); + } + /** A clad shape costs its cladding too, so a copy nobody can pay for is never started. */ @Override protected List extraCosts(BWContext ctx, MovingObjectPosition mop) { - BlockPos from = MovingObjectPositionUtil.blockPos(mop); - ItemStack cladding = claddingOf(shapeAt(ctx.world, from.x, from.y, from.z)); + if (!ctx.isExtrudeMode()) return Collections.emptyList(); + ItemStack cladding = claddingOf(ctx, MovingObjectPositionUtil.blockPos(mop)); return cladding == null ? Collections.emptyList() : Collections.singletonList(cladding); } + @Nullable + private static ItemStack claddingOf(BWContext ctx, BlockPos pos) { + return claddingOf(shapeAt(ctx.world, pos.x, pos.y, pos.z)); + } + /** Copies the TileShape state the item cannot carry: side, turn, offset, connections and cladding. */ private static void copyShapeState(BWContext ctx, BlockPos from, BlockPos to, @Nullable ItemStack cladding) { TileShape source = shapeAt(ctx.world, from.x, from.y, from.z); diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java index 30ee240b4..d545cd5c2 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java @@ -37,13 +37,14 @@ protected BWBlockPicker(World world, InventoryBudget budget) { /** * pick a suitable block to be placed * -> has to be placeable, use canPlaceBlock() + * -> materials must be available in the inventory, use tryReserve() * * @param mop the source block on which the new one will be placed * @param sourceBlock the stack the source block hands over when picked * @param extraCosts what this cell costs besides the block itself, reserved together with it * @return block to be placed, already reserved against the budget, or null */ - public abstract ItemStack pickBlock(MovingObjectPosition mop, @Nullable ItemStack sourceBlock, + public abstract ItemStack pickBlockFor(MovingObjectPosition mop, @Nullable ItemStack sourceBlock, List extraCosts); /** Reserves the block and everything else the cell costs, all of them or none. */ @@ -136,7 +137,8 @@ static class PaletteBlockPicker extends BWBlockPicker { } @Override - public ItemStack pickBlock(MovingObjectPosition mop, @Nullable ItemStack ignored, List extraCosts) { + public ItemStack pickBlockFor(MovingObjectPosition mop, @Nullable ItemStack ignored, + List extraCosts) { System.arraycopy(scratchTemplate, 0, scratch, 0, scratch.length); // try every block in the palette in a random order @@ -165,7 +167,7 @@ static class ExtrudeBlockPicker extends BWBlockPicker { } @Override - public ItemStack pickBlock(MovingObjectPosition mop, @Nullable ItemStack sourceBlock, + public ItemStack pickBlockFor(MovingObjectPosition mop, @Nullable ItemStack sourceBlock, List extraCosts) { return sourceBlock != null && canPlaceBlock(world, sourceBlock, mop) && tryReserve(sourceBlock, extraCosts) ? sourceBlock diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java index 26ed9569b..06f1340b2 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java @@ -14,7 +14,8 @@ * Add your own custom place handler by implementing IBWCellHandler and registering it here. * * Useful particularly for extrude mode, when naively copying just the metadata of a block is not enough. - * Also useful for any blocks for which Block to ItemStack method "pickBlock" is tricky (like ForgeMicroblocks) + * Also useful for any blocks for which the vanilla Block to ItemStack method getPickBlock() is tricky + * (like ForgeMicroblocks) */ public final class BWCellHandlers { diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java index ebdae2fe6..a176a14b2 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import com.fouristhenumber.utilitiesinexcess.utils.InventoryBudget; +import com.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; import com.gtnewhorizon.gtnhlib.util.ItemUtil; /** Everything a cell handler needs during one pass of a Builder's Wand use. */ @@ -25,15 +26,20 @@ public final class BWContext { @Nullable public final ItemStack clickedBlockStack; - public BWContext(World world, MovingObjectPosition originalMop, @Nullable ItemStack clickedBlockStack, - EntityPlayer player, BWMode mode, BWBlockPicker picker, InventoryBudget budget) { + public BWContext(World world, MovingObjectPosition originalMop, EntityPlayer player, BWMode mode, + InventoryBudget budget) { this.world = world; this.player = player; this.mode = mode; - this.picker = picker; this.budget = budget; this.originalMop = originalMop; - this.clickedBlockStack = clickedBlockStack; + + // the clicked cell is priced by its own handler, like every other cell, which needs this context to exist. + // everything a handler reads while claiming a cell is assigned above. + MovingObjectPosition clicked = MovingObjectPositionUtil.copy(originalMop); + this.clickedBlockStack = BWCellHandlers.find(this, clicked.blockX, clicked.blockY, clicked.blockZ) + .pickBlockAt(this, clicked); + this.picker = BWBlockPicker.create(world, player, mode, clickedBlockStack, budget); } public boolean isCreative() { diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java index 0a6f2da0c..a0ddc2a29 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java @@ -28,11 +28,18 @@ *
    *
  • Which cells are even yours? {@link #handles} claims them, otherwise your handler won't be called at all. * - *
  • Does your block hand back a useless {@code getPickBlock()}? - * This is bad since wand needs to decide whether two blocks are identical, so it can spread over them. + *
  • Do you need custom selection spreading rules? + * By default, spreading works by comparing itemStacks acquired by {@link #pickBlockAt} from clickedBlock and a + * current block. + * If block does not have one simple itemStack to return, e.g. for multipart cases, this will fail. * Override {@link #sameKindAsClicked} and compare them your way. * - *
  • Does a cell cost more than its block, a cladding, a dye, a filter inside? Declare it in {@link #extraCosts} + *
  • Does your block hand back a wrong {@code getPickBlock()}? + * One that the inventory never contains, a dropped damage value or a leftover NBT tag. + * Override {@link #pickBlockAt} and build the stack your block drops. + * + *
  • Does a cell cost more than its block, perhaps a cladding, a dye, a filter inside? Declare it in + * {@link #extraCosts} * and the wand reserves it together with the block, or skips the cell. * *
  • Does the new block come out blank, missing a rotation, a color, a cover? @@ -59,10 +66,10 @@ public boolean build(BWContext ctx, MovingObjectPosition mop, boolean place) { // ItemBlock places into a replaceable cell (air, grass, snow, fluid) instead of in front of it if (block.isReplaceable(world, mop.blockX, mop.blockY, mop.blockZ)) return false; - ItemStack sourceBlockStack = block.getPickBlock(mop, ctx.world, mop.blockX, mop.blockY, mop.blockZ, ctx.player); + ItemStack sourceBlockStack = pickBlockAt(ctx, mop); if (!(ctx.spreadsOverAnything() || sameKindAsClicked(ctx, mop, sourceBlockStack))) return false; - ItemStack toPlace = ctx.picker.pickBlock(mop, sourceBlockStack, extraCosts(ctx, mop)); + ItemStack toPlace = ctx.picker.pickBlockFor(mop, sourceBlockStack, extraCosts(ctx, mop)); if (toPlace == null) return false; if (!place) return true; @@ -109,6 +116,17 @@ protected List extraCosts(BWContext ctx, MovingObjectPosition mop) { return Collections.emptyList(); } + /** + * The stack that stands for this cell: what the wand looks for in the inventory and what it places. + * Override when the pick stack is not the one this block drops. + */ + @Override + @Nullable + public ItemStack pickBlockAt(BWContext ctx, MovingObjectPosition mop) { + return ctx.world.getBlock(mop.blockX, mop.blockY, mop.blockZ) + .getPickBlock(mop, ctx.world, mop.blockX, mop.blockY, mop.blockZ, ctx.player); + } + /** * Whether this cell is the same kind as the one the player clicked. Override when a pick stack cannot tell. * Used to determine spreading to other blocks diff --git a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java index 3ad0b2298..581b0ae6e 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java @@ -1,7 +1,10 @@ package com.fouristhenumber.utilitiesinexcess.utils.bw; +import net.minecraft.item.ItemStack; import net.minecraft.util.MovingObjectPosition; +import org.jetbrains.annotations.Nullable; + /** * Teaches the Builder's Wand to extend one kind of cell. * @@ -51,8 +54,8 @@ * Real examples: *
      *
    • ArchitectureCraftBWCellHandler: the example above, plus the cladding it costs declared in {@code extraCosts} - *
    • MultipartBWCellHandler: implements this interface directly, because a cell holding a bag of parts is no - * single block with a single item + *
    • MultipartBWCellHandler: replaces {@link #build} with one branch per mode, because a cell holding a bag of + * parts is no single block with a single item *
    * *

    Rules

    @@ -75,6 +78,14 @@ public interface IBWCellHandler { */ boolean handles(BWContext ctx, int x, int y, int z); + /** + * The stack that stands for this cell: what the wand looks for in the inventory, what it places, and what the + * clicked cell is compared against. Null when the cell cannot be expressed as one stack. + * Usually {@code block.getPickBlock()}. + */ + @Nullable + ItemStack pickBlockAt(BWContext ctx, MovingObjectPosition mop); + /** * Report what this cell costs and, when asked, build it. *