diff --git a/dependencies.gradle b/dependencies.gradle index 397c6d12..6afb706c 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/CommonProxy.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/CommonProxy.java index b77da578..e68b32ea 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/ModItems.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/ModItems.java index a82b963c..3eeedae6 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 abe065ff..04b529dc 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 fe16f275..ff7c3ef4 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/items/ItemBuildersWand.java @@ -1,12 +1,8 @@ package com.fouristhenumber.utilitiesinexcess.common.items; -import static com.fouristhenumber.utilitiesinexcess.utils.BuildersWandUtils.damageBackhand; - 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 +15,16 @@ 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.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.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; @@ -35,7 +33,7 @@ public class ItemBuildersWand extends Item implements ITranslucentItem { - public int buildLimit; + private final int buildLimit; public ItemBuildersWand(int buildLimit) { super(); @@ -56,15 +54,16 @@ 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) { + // adventure mode + if (!player.capabilities.allowEdit) { + WireframeRenderer.clearCandidatePositions(); return; } - // I'm pretty sure this will never determine whether we render or not but I'm not certain MovingObjectPosition movingObjectPosition = Minecraft.getMinecraft().objectMouseOver; // Check if player is looking at a block. @@ -74,146 +73,82 @@ 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); + ForgeDirection forgeSide = ForgeDirection.getOrientation(movingObjectPosition.sideHit); - // 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; - } - - 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; - } - - // 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, + WandAxisLock axisLock = axisLock(player); + Set blocksToPlace = BWRegion.findAdjacentBlocksToBuildOn( + newContext(world, player, movingObjectPosition), + buildLimit, movingObjectPosition, - player, - selection, - axisMode); + axisLock); + WireframeRenderer.clearCandidatePositions(); - for (BlockPos pos : blocksToPlace) { + for (BlockPos pos : blocksToPlace) WireframeRenderer.addCandidatePosition(pos.offset(forgeSide.offsetX, forgeSide.offsetY, forgeSide.offsetZ)); - } - } - - private void placeBlock(World world, EntityPlayer player, @NotNull ItemStack itemStack, BlockPos pos, int side, - float hitX, float hitY, float hitZ, ForgeDirection forgeSide) { - - // 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); - - 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); - } } @Override 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; - // TODO: Prevent player from placing blocks into themself / other entities? + MovingObjectPosition mop = new MovingObjectPosition( + x, + y, + z, + side, + Vec3.createVectorHelper((double) x + hitX, (double) y + hitY, (double) 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); + WandAxisLock axisLock = axisLock(player); - List itemStackToPlace = selection.blockToPlace(player); + // 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); - 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; + // 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; + } - int placeCount = selection.maxPlaceCount(player, buildLimit); + /** 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); - Set blocksToPlace = BuildersWandUtils.findAdjacentBlocks( + // 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, - itemStackToPlace, - placeCount, - forgeSide, - target, - mop, + tracedMop != null ? tracedMop : MovingObjectPositionUtil.copy(mop), player, - selection, - axisMode); - - 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())); - } + BWMode.of(player), + budget); + } - if (player.capabilities.isCreativeMode || (BuildersWandUtils.decreaseFromInventory(player, nowPlacing) - && damageBackhand(BuildersWandsConfig.INSTANCE.damageTrowelWithBuildersWand, player))) { - placeBlock(world, player, nowPlacing, pos, side, hitX, hitY, hitZ, forgeSide); - } - } - player.inventoryContainer.detectAndSendChanges(); - return true; + 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 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 00000000..26a8ca5e --- /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/common/recipe/RecipeLoader.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/common/recipe/RecipeLoader.java index 09e1c298..e23e9ef8 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/compat/ForgeMultipart/MultipartBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/ForgeMultipart/MultipartBWCellHandler.java new file mode 100644 index 00000000..fbf7017f --- /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/Mods.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/Mods.java index 7e887ed5..f6ddccee 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/ArchitectureCraftBWCellHandler.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java new file mode 100644 index 00000000..187ec5a7 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/compat/architecturecraft/ArchitectureCraftBWCellHandler.java @@ -0,0 +1,103 @@ +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 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; + // 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(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) { + 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); + 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/config/RecipeConfig.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/config/RecipeConfig.java index efc6ef18..05193953 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; 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 2654b8eb..629f9ada 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/BuildersSelection.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersSelection.java deleted file mode 100644 index 976f2c92..00000000 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersSelection.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.fouristhenumber.utilitiesinexcess.utils; - -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; -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.compat.Mods; -import com.fouristhenumber.utilitiesinexcess.config.items.BuildersWandsConfig; -import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; - -import gregtech.api.items.MetaGeneratedTool; -import gregtech.common.tools.ToolTrowel; -import xonin.backhand.api.core.BackhandUtils; - -public class BuildersSelection { - - private final Set validBlocks; - private final ItemStack backhand; - private final ItemStack lookAtBlock; - - public BuildersSelection(EntityPlayer player, World world, MovingObjectPosition movingObjectPosition) { - this.validBlocks = new HashSet<>(); - backhand = Mods.Backhand.isLoaded() ? BackhandUtils.getOffhandItem(player) : null; - lookAtBlock = getBlockByLocation(world, movingObjectPosition, player); - - // 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 (backhand == null) { - return; - } - if (isValidBlock(backhand)) { - this.validBlocks.add(backhand.copy()); - return; - } - if (isTrowel(backhand)) { - this.validBlocks.addAll(hotbarBlocks(player)); - } - } - - /** - * - * @param player - * @return Always a valid block list or null - */ - public List blockToPlace(EntityPlayer 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); - } - - 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()); - } - - 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; - } - - 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); - } - 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 b431bb76..00000000 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/BuildersWandUtils.java +++ /dev/null @@ -1,203 +0,0 @@ -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 java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -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.item.ItemStack; -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 { - - 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; - } - - /** - * 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 - * @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; - } - - /** - * 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 - */ - 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) { - return region; - } - Set visited = new HashSet<>(); - Queue queue = new LinkedList<>(); - - // Determine allowed offsets depending on the face that was clicked. - int[][] allowedOffsets = switch (clickedSide) { - 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"); - }; - - // 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; - } - - // Flood-fill the contiguous region in the allowed plane. - while (!queue.isEmpty() && region.size() < findCount) { - BlockPos current = queue.poll(); - - for (int[] off : allowedOffsets) { - if (region.size() >= findCount) { - break; - } - - BlockPos key = current.offset(off[0], off[1], off[2]); - 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); - } - } - } - - 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); - }); - } - - 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 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 00000000..4b80fc75 --- /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/InventoryBudget.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/InventoryBudget.java new file mode 100644 index 00000000..2010f8e5 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/InventoryBudget.java @@ -0,0 +1,87 @@ +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 what an inventory can still afford, without touching it. + */ +public final class InventoryBudget { + + private static final class Reservation { + + ItemStack stack; + int remaining; + } + + private final List reservations = new ArrayList<>(); + private final InventoryPlayer inventory; + private final boolean creative; + + public InventoryBudget(InventoryPlayer inventory, boolean creative) { + this.inventory = inventory; + this.creative = creative; + } + + /** + * 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) { + 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.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; + + 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/MovingObjectPositionUtil.java b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java index 2ab8a5fa..f42e2dd5 100644 --- a/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/MovingObjectPositionUtil.java @@ -2,12 +2,13 @@ import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; +import net.minecraftforge.common.util.ForgeDirection; import com.gtnewhorizon.gtnhlib.blockpos.BlockPos; 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 +21,42 @@ 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)); + } + + /** 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 00000000..d545cd5c --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWBlockPicker.java @@ -0,0 +1,177 @@ +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() + * -> 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 pickBlockFor(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 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 + 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 pickBlockFor(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 00000000..06f1340b --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWCellHandlers.java @@ -0,0 +1,57 @@ +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 the vanilla Block to ItemStack method getPickBlock() 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 00000000..a176a14b --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/BWContext.java @@ -0,0 +1,85 @@ +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.fouristhenumber.utilitiesinexcess.utils.MovingObjectPositionUtil; +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, EntityPlayer player, BWMode mode, + InventoryBudget budget) { + this.world = world; + this.player = player; + this.mode = mode; + this.budget = budget; + this.originalMop = originalMop; + + // 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() { + 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 00000000..4deaa7a8 --- /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 00000000..02d6cfae --- /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 00000000..a0ddc2a2 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/DefaultBWCellHandler.java @@ -0,0 +1,138 @@ +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. + * + *
  • 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 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? + * 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 = pickBlockAt(ctx, mop); + if (!(ctx.spreadsOverAnything() || sameKindAsClicked(ctx, mop, sourceBlockStack))) return false; + + ItemStack toPlace = ctx.picker.pickBlockFor(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(); + } + + /** + * 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 + */ + 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 00000000..581b0ae6 --- /dev/null +++ b/src/main/java/com/fouristhenumber/utilitiesinexcess/utils/bw/IBWCellHandler.java @@ -0,0 +1,107 @@ +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. + * + *

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: 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

+ *
    + *
  • 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); + + /** + * 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. + *
    + *
  • 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); +} diff --git a/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang b/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang index 4ee93904..150dc209 100644 --- a/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang +++ b/src/main/resources/assets/utilitiesinexcess/lang/en_US.lang @@ -48,6 +48,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. @@ -533,6 +534,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... @@ -575,6 +577,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 00000000..8d62fb4b Binary files /dev/null and b/src/main/resources/assets/utilitiesinexcess/textures/items/scribe.png differ