diff --git a/src/main/java/nl/requios/effortlessbuilding/ClientConfig.java b/src/main/java/nl/requios/effortlessbuilding/ClientConfig.java index 8dd432e..ef0c8e4 100644 --- a/src/main/java/nl/requios/effortlessbuilding/ClientConfig.java +++ b/src/main/java/nl/requios/effortlessbuilding/ClientConfig.java @@ -31,7 +31,7 @@ public class ClientConfig { maxBlockPreviews = builder .comment("Don't show block previews when placing more than this many blocks. " + "The outline will always be rendered.") - .defineInRange("maxBlockPreviews", 500, 0, 5000); + .defineInRange("maxBlockPreviews", 400, 0, 5000); appearAnimationLength = builder .comment("How long it takes for a block to appear when placed in ticks.", diff --git a/src/main/java/nl/requios/effortlessbuilding/ServerConfig.java b/src/main/java/nl/requios/effortlessbuilding/ServerConfig.java index 5dcc2f1..b249933 100644 --- a/src/main/java/nl/requios/effortlessbuilding/ServerConfig.java +++ b/src/main/java/nl/requios/effortlessbuilding/ServerConfig.java @@ -17,6 +17,7 @@ public class ServerConfig { public final BooleanValue allowInSurvival; public final BooleanValue useWhitelist; public final ConfigValue> whitelist; + public final IntValue maxBlocksPlacedAtOnce; public Validation(Builder builder) { builder.push("Validation"); @@ -33,6 +34,10 @@ public class ServerConfig { .comment("List of player names that can use the mod.") .defineList("whitelist", Arrays.asList("Player1", "Player2"), o -> true); + maxBlocksPlacedAtOnce = builder + .comment("Maximum number of blocks that can be placed at once.") + .defineInRange("maxBlocksPlacedAtOnce", 1000, 1, 10000); + builder.pop(); } } diff --git a/src/main/java/nl/requios/effortlessbuilding/systems/BuilderChain.java b/src/main/java/nl/requios/effortlessbuilding/systems/BuilderChain.java index 47d5067..847bf50 100644 --- a/src/main/java/nl/requios/effortlessbuilding/systems/BuilderChain.java +++ b/src/main/java/nl/requios/effortlessbuilding/systems/BuilderChain.java @@ -262,6 +262,7 @@ public class BuilderChain { var clickedFace = lookingAt.getDirection(); Vec3 relativeHitVec = lookingAt.getLocation().subtract(Vec3.atLowerCornerOf(lookingAt.getBlockPos())); + //TODO keep track of count and find different itemstack if necessary if (itemStack.getItem() instanceof BlockItem) { for (BlockEntry blockEntry : blocks) { diff --git a/src/main/java/nl/requios/effortlessbuilding/systems/ServerBlockPlacer.java b/src/main/java/nl/requios/effortlessbuilding/systems/ServerBlockPlacer.java index 0888c1d..7fb8f3e 100644 --- a/src/main/java/nl/requios/effortlessbuilding/systems/ServerBlockPlacer.java +++ b/src/main/java/nl/requios/effortlessbuilding/systems/ServerBlockPlacer.java @@ -10,6 +10,7 @@ import nl.requios.effortlessbuilding.EffortlessBuilding; import nl.requios.effortlessbuilding.ServerConfig; import nl.requios.effortlessbuilding.create.foundation.utility.BlockHelper; import nl.requios.effortlessbuilding.utilities.BlockEntry; +import nl.requios.effortlessbuilding.utilities.BlockPlacerHelper; import nl.requios.effortlessbuilding.utilities.BlockSet; import nl.requios.effortlessbuilding.utilities.BlockUtilities; @@ -17,12 +18,15 @@ import java.util.*; // Receives block placement requests from the client and places them public class ServerBlockPlacer { + private boolean isPlacingOrBreakingBlocks = false; + +//region Delays private final Set delayedEntries = Collections.synchronizedSet(new HashSet<>()); private final Set delayedEntriesView = Collections.unmodifiableSet(delayedEntries); - private boolean isPlacingOrBreakingBlocks = false; public void placeBlocksDelayed(Player player, BlockSet blocks, long placeTime) { if (!checkAndNotifyAllowedToUseMod(player)) return; + if (!validateBlockSet(player, blocks)) return; delayedEntries.add(new DelayedEntry(player, blocks, placeTime)); } @@ -33,71 +37,103 @@ public class ServerBlockPlacer { DelayedEntry entry = iterator.next(); long gameTime = entry.player.level.getGameTime(); if (gameTime >= entry.placeTime) { - placeBlocks(entry.player, entry.blocks); + applyBlockSet(entry.player, entry.blocks); iterator.remove(); } } } - - public void placeBlocks(Player player, BlockSet blocks) { - if (!checkAndNotifyAllowedToUseMod(player)) return; -// EffortlessBuilding.log(player, "Placing " + blocks.size() + " blocks"); - var undoSet = new BlockSet(); - for (BlockEntry block : blocks) { - if (blocks.skipFirst && block.blockPos == blocks.firstPos) continue; - if (placeBlock(player, block)) { - undoSet.add(block); - } - } - EffortlessBuilding.UNDO_REDO.addUndo(player, undoSet); + public Set getDelayedEntries() { + return delayedEntriesView; } - - private boolean placeBlock(Player player, BlockEntry block) { - Level world = player.level; - if (!world.isLoaded(block.blockPos)) return false; - isPlacingOrBreakingBlocks = true; - boolean placedBlock = BlockUtilities.placeBlockEntry(player, block) == InteractionResult.SUCCESS; - isPlacingOrBreakingBlocks = false; - return placedBlock; - } - + public record DelayedEntry(Player player, BlockSet blocks, long placeTime) {} +//endregion + public void breakBlocks(Player player, BlockSet blocks) { + applyBlockSet(player, blocks); + } + + public void applyBlockSet(Player player, BlockSet blocks) { if (!checkAndNotifyAllowedToUseMod(player)) return; -// EffortlessBuilding.log(player, "Breaking " + blocks.size() + " blocks"); + if (!validateBlockSet(player, blocks)) return; var undoSet = new BlockSet(); for (BlockEntry block : blocks) { if (blocks.skipFirst && block.blockPos == blocks.firstPos) continue; - if (breakBlock(player, block)) { + + if (applyBlockEntry(player, block)) { undoSet.add(block); } } EffortlessBuilding.UNDO_REDO.addUndo(player, undoSet); } - - private boolean breakBlock(Player player, BlockEntry block) { - ServerLevel world = (ServerLevel) player.level; - if (!world.isLoaded(block.blockPos) || world.isEmptyBlock(block.blockPos)) return false; - isPlacingOrBreakingBlocks = true; - boolean brokeBlock = BlockHelper.destroyBlockAs(world, block.blockPos, player, player.getMainHandItem(), 0f, stack -> { - if (!player.isCreative()) { - ItemHandlerHelper.giveItemToPlayer(player, stack); + public void undoBlockSet(Player player, BlockSet blocks) { + if (!isAllowedToUndo(player)) return; + + var redoSet = new BlockSet(); + for (BlockEntry block : blocks) { + if (blocks.skipFirst && block.blockPos == blocks.firstPos) continue; + + if (undoBlockEntry(player, block)) { + redoSet.add(block); } - }); - isPlacingOrBreakingBlocks = false; - return brokeBlock; + } + EffortlessBuilding.UNDO_REDO.addRedo(player, redoSet); } - public boolean checkAndNotifyAllowedToUseMod(Player player) { - //TODO TEMP + private boolean applyBlockEntry(Player player, BlockEntry block) { + block.existingBlockState = player.level.getBlockState(block.blockPos); + boolean breaking = BlockUtilities.isNullOrAir(block.newBlockState); + if (!validateBlockEntry(player, block, breaking)) return false; + + boolean success; + isPlacingOrBreakingBlocks = true; + if (breaking) { + success = BlockPlacerHelper.breakBlock(player, block); + } else { + success = BlockPlacerHelper.placeBlock(player, block); + } + isPlacingOrBreakingBlocks = false; + return success; + } + + private boolean undoBlockEntry(Player player, BlockEntry block) { + //Update newBlockState for future redo's + block.newBlockState = player.level.getBlockState(block.blockPos); + boolean breaking = BlockUtilities.isNullOrAir(block.existingBlockState); + + var tempBlockEntry = new BlockEntry(block.blockPos); + var newBlockState = block.existingBlockState; + tempBlockEntry.existingBlockState = block.newBlockState; + tempBlockEntry.newBlockState = newBlockState; + + if (!validateBlockEntry(player, tempBlockEntry, breaking)) return false; + + boolean success; + isPlacingOrBreakingBlocks = true; + if (breaking) { + success = BlockPlacerHelper.placeBlock(player, tempBlockEntry); + } else { + success = BlockPlacerHelper.breakBlock(player, tempBlockEntry); + } + isPlacingOrBreakingBlocks = false; + return success; + } + + private boolean checkAndNotifyAllowedToUseMod(Player player) { + //TODO temp no survival allowed if (!player.isCreative()) { EffortlessBuilding.log(player, ChatFormatting.RED + "Effortless Building is not yet supported in survival mode."); return false; } + if (!player.getAbilities().mayBuild) { + EffortlessBuilding.log(player, ChatFormatting.RED + "You are not allowed to build."); + return false; + } + if (!isAllowedToUseMod(player)) { EffortlessBuilding.log(player, ChatFormatting.RED + "You are not allowed to use Effortless Building."); return false; @@ -105,7 +141,7 @@ public class ServerBlockPlacer { return true; } - public boolean isAllowedToUseMod(Player player) { + private boolean isAllowedToUseMod(Player player) { if (!ServerConfig.validation.allowInSurvival.get() && !player.isCreative()) return false; if (ServerConfig.validation.useWhitelist.get()) { @@ -114,15 +150,53 @@ public class ServerBlockPlacer { return true; } - - public Set getDelayedEntries() { - return delayedEntriesView; + + private boolean isAllowedToUndo(Player player) { + if (!player.isCreative()) { + EffortlessBuilding.log(player, ChatFormatting.RED + "Undo is not supported in survival mode."); + return false; + } + + return true; + } + + private boolean validateBlockSet(Player player, BlockSet blocks) { + if (blocks.size() > ServerConfig.validation.maxBlocksPlacedAtOnce.get()) { + EffortlessBuilding.log(player, ChatFormatting.RED + "Too many blocks to place. Max: " + ServerConfig.validation.maxBlocksPlacedAtOnce.get()); + return false; + } + + //Dont allow mixing breaking and placing blocks + //TODO fix if skipping first block + boolean breaking = blocks.getFirstBlockEntry().newBlockState == null || blocks.getFirstBlockEntry().newBlockState.isAir(); + for (var iterator = blocks.iterator(); iterator.hasNext(); ) { + var block = iterator.next(); + if (block.newBlockState == null || block.newBlockState.isAir()) { + if (!breaking) { + EffortlessBuilding.log(player, ChatFormatting.RED + "Cannot mix breaking and placing blocks."); + return false; + } + } else { + if (breaking) { + EffortlessBuilding.log(player, ChatFormatting.RED + "Cannot mix breaking and placing blocks."); + return false; + } + } + } + return true; + } + + private boolean validateBlockEntry(Player player, BlockEntry block, boolean breaking) { + if (!player.level.isLoaded(block.blockPos)) return false; + + if (breaking && BlockUtilities.isNullOrAir(block.existingBlockState)) return false; + + + + return true; } public boolean isPlacingOrBreakingBlocks() { return isPlacingOrBreakingBlocks; } - - public record DelayedEntry(Player player, BlockSet blocks, long placeTime) {} - } diff --git a/src/main/java/nl/requios/effortlessbuilding/systems/UndoRedo.java b/src/main/java/nl/requios/effortlessbuilding/systems/UndoRedo.java index e23c367..26abe73 100644 --- a/src/main/java/nl/requios/effortlessbuilding/systems/UndoRedo.java +++ b/src/main/java/nl/requios/effortlessbuilding/systems/UndoRedo.java @@ -31,7 +31,7 @@ public class UndoRedo { undoStacks.get(player.getUUID()).push(blockSet); } - private void addRedo(Player player, BlockSet blockSet) { + public void addRedo(Player player, BlockSet blockSet) { if (blockSet.isEmpty()) return; //If no stack exists, make one @@ -49,10 +49,7 @@ public class UndoRedo { if (undoStack.isEmpty()) return false; BlockSet blockSet = undoStack.pop(); -// blockSet.undo(player.level); - - BlockSet redoSet = new BlockSet(); - addRedo(player, redoSet); + EffortlessBuilding.SERVER_BLOCK_PLACER.undoBlockSet(player, blockSet); return true; } @@ -64,8 +61,7 @@ public class UndoRedo { if (redoStack.isEmpty()) return false; BlockSet blockSet = redoStack.pop(); -// blockSet.redo(player.level); - addUndo(player, blockSet); + EffortlessBuilding.SERVER_BLOCK_PLACER.applyBlockSet(player, blockSet); return true; } diff --git a/src/main/java/nl/requios/effortlessbuilding/utilities/BlockPlacerHelper.java b/src/main/java/nl/requios/effortlessbuilding/utilities/BlockPlacerHelper.java new file mode 100644 index 0000000..7e2c125 --- /dev/null +++ b/src/main/java/nl/requios/effortlessbuilding/utilities/BlockPlacerHelper.java @@ -0,0 +1,198 @@ +package nl.requios.effortlessbuilding.utilities; + +import com.google.common.collect.Lists; +import net.minecraft.core.Direction; +import net.minecraft.core.Registry; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.stats.Stats; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BucketItem; +import net.minecraft.world.item.DiggerItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraftforge.common.util.BlockSnapshot; +import net.minecraftforge.event.ForgeEventFactory; +import net.minecraftforge.items.ItemHandlerHelper; +import nl.requios.effortlessbuilding.create.foundation.utility.BlockHelper; + +import java.util.List; + +//Server only +public class BlockPlacerHelper { + + public static boolean breakBlock(Player player, BlockEntry blockEntry) { + ItemStack usedTool = player.getMainHandItem(); + if (usedTool.isEmpty() || !(usedTool.getItem() instanceof DiggerItem)) { + ItemStack offhand = player.getOffhandItem(); + if (!offhand.isEmpty() && offhand.getItem() instanceof DiggerItem) { + usedTool = offhand; + } + } + + boolean brokeBlock = BlockHelper.destroyBlockAs(player.level, blockEntry.blockPos, player, usedTool, 0f, stack -> { + if (!player.isCreative()) { + ItemHandlerHelper.giveItemToPlayer(player, stack); + } + }); + return brokeBlock; + } + + public static boolean placeBlock(Player player, BlockEntry blockEntry) { + if (blockEntry.itemStack == null) { + return placeBlockWithoutItem(player, blockEntry); + } else { + var interactionResult = placeItem(player, blockEntry); + interactionResult.shouldSwing() + return interactionResult == InteractionResult.SUCCESS; + } + } + + private static boolean placeBlockWithoutItem(Player player, BlockEntry blockEntry) { + Level level = player.level; + + level.captureBlockSnapshots = true; + BlockHelper.placeSchematicBlock(level, player, blockEntry.newBlockState, blockEntry.blockPos, blockEntry.itemStack, null); + level.captureBlockSnapshots = false; + + //Find out if we get to keep the placed block by sending a forge event + @SuppressWarnings("unchecked") + List blockSnapshots = (List)level.capturedBlockSnapshots.clone(); + level.capturedBlockSnapshots.clear(); + Direction side = Direction.UP; + + boolean eventResult = false; + if (blockSnapshots.size() > 1) + { + eventResult = ForgeEventFactory.onMultiBlockPlace(player, blockSnapshots, side); + } + else if (blockSnapshots.size() == 1) + { + eventResult = ForgeEventFactory.onBlockPlace(player, blockSnapshots.get(0), side); + } + + if (eventResult) + { + // revert back all captured blocks + for (BlockSnapshot blocksnapshot : Lists.reverse(blockSnapshots)) + { + level.restoringBlockSnapshots = true; + blocksnapshot.restore(true, false); + level.restoringBlockSnapshots = false; + } + } + else + { + for (BlockSnapshot snap : blockSnapshots) + { + int updateFlag = snap.getFlag(); + BlockState oldBlock = snap.getReplacedBlock(); + BlockState newBlock = level.getBlockState(snap.getPos()); + newBlock.onPlace(level, snap.getPos(), oldBlock, false); + + level.markAndNotifyBlock(snap.getPos(), level.getChunkAt(snap.getPos()), oldBlock, newBlock, updateFlag, 512); + } + } + level.capturedBlockSnapshots.clear(); + return !eventResult; + } + + //ForgeHooks::onPlaceItemIntoWorld + private static InteractionResult placeItem(Player player, BlockEntry block) { + ItemStack itemstack = block.itemStack; + Level level = player.level; + + if (player != null && !player.getAbilities().mayBuild) + return InteractionResult.PASS; + + if (itemstack != null && !itemstack.hasAdventureModePlaceTagForBlock(level.registryAccess().registryOrThrow(Registry.BLOCK_REGISTRY), new BlockInWorld(level, block.blockPos, false))) + return InteractionResult.PASS; + + // handle all placement events here + Item item = itemstack.getItem(); + int size = itemstack.getCount(); + CompoundTag nbt = null; + if (itemstack.getTag() != null) + nbt = itemstack.getTag().copy(); + + if (!(itemstack.getItem() instanceof BucketItem)) // if not bucket + level.captureBlockSnapshots = true; + + ItemStack copy = itemstack.copy(); + //// + BlockHelper.placeSchematicBlock(level, player, block.newBlockState, block.blockPos, block.itemStack, null); + //// + InteractionResult ret = InteractionResult.SUCCESS; + if (itemstack.isEmpty()) + ForgeEventFactory.onPlayerDestroyItem(player, copy, InteractionHand.MAIN_HAND); + + level.captureBlockSnapshots = false; + + if (ret.consumesAction()) + { + // save new item data + int newSize = itemstack.getCount(); + CompoundTag newNBT = null; + if (itemstack.getTag() != null) + { + newNBT = itemstack.getTag().copy(); + } + @SuppressWarnings("unchecked") + List blockSnapshots = (List)level.capturedBlockSnapshots.clone(); + level.capturedBlockSnapshots.clear(); + + // make sure to set pre-placement item data for event + itemstack.setCount(size); + itemstack.setTag(nbt); + + Direction side = Direction.UP; + + boolean eventResult = false; + if (blockSnapshots.size() > 1) + { + eventResult = ForgeEventFactory.onMultiBlockPlace(player, blockSnapshots, side); + } + else if (blockSnapshots.size() == 1) + { + eventResult = ForgeEventFactory.onBlockPlace(player, blockSnapshots.get(0), side); + } + + if (eventResult) + { + ret = InteractionResult.FAIL; // cancel placement + // revert back all captured blocks + for (BlockSnapshot blocksnapshot : Lists.reverse(blockSnapshots)) + { + level.restoringBlockSnapshots = true; + blocksnapshot.restore(true, false); + level.restoringBlockSnapshots = false; + } + } + else + { + // Change the stack to its new content + itemstack.setCount(newSize); + itemstack.setTag(newNBT); + + for (BlockSnapshot snap : blockSnapshots) + { + int updateFlag = snap.getFlag(); + BlockState oldBlock = snap.getReplacedBlock(); + BlockState newBlock = level.getBlockState(snap.getPos()); + newBlock.onPlace(level, snap.getPos(), oldBlock, false); + + level.markAndNotifyBlock(snap.getPos(), level.getChunkAt(snap.getPos()), oldBlock, newBlock, updateFlag, 512); + } + if (player != null) + player.awardStat(Stats.ITEM_USED.get(item)); + } + } + level.capturedBlockSnapshots.clear(); + + return ret; + } +} diff --git a/src/main/java/nl/requios/effortlessbuilding/utilities/BlockUtilities.java b/src/main/java/nl/requios/effortlessbuilding/utilities/BlockUtilities.java index 555c1c7..1827a7c 100644 --- a/src/main/java/nl/requios/effortlessbuilding/utilities/BlockUtilities.java +++ b/src/main/java/nl/requios/effortlessbuilding/utilities/BlockUtilities.java @@ -34,6 +34,10 @@ import java.util.List; //Common public class BlockUtilities { + public static boolean isNullOrAir(BlockState blockState) { + return blockState == null || blockState.isAir(); + } + @Deprecated //Use BlockEntry.setItemStackAndFindNewBlockState instead public static BlockState getBlockState(Player player, InteractionHand hand, ItemStack blockItemStack, BlockEntry blockEntry, Vec3 relativeHitVec, Direction sideHit) { Block block = Block.byItem(blockItemStack.getItem()); @@ -60,98 +64,6 @@ public class BlockUtilities { return result; } - //ForgeHooks::onPlaceItemIntoWorld - public static InteractionResult placeBlockEntry(Player player, BlockEntry block) { - ItemStack itemstack = block.itemStack; - Level level = player.level; - - if (player != null && !player.getAbilities().mayBuild && !itemstack.hasAdventureModePlaceTagForBlock(level.registryAccess().registryOrThrow(Registry.BLOCK_REGISTRY), new BlockInWorld(level, block.blockPos, false))) - return InteractionResult.PASS; - - // handle all placement events here - Item item = itemstack.getItem(); - int size = itemstack.getCount(); - CompoundTag nbt = null; - if (itemstack.getTag() != null) - nbt = itemstack.getTag().copy(); - - if (!(itemstack.getItem() instanceof BucketItem)) // if not bucket - level.captureBlockSnapshots = true; - - ItemStack copy = itemstack.copy(); - //// - BlockHelper.placeSchematicBlock(level, player, block.newBlockState, block.blockPos, block.itemStack, null); - //// - InteractionResult ret = InteractionResult.SUCCESS; - if (itemstack.isEmpty()) - ForgeEventFactory.onPlayerDestroyItem(player, copy, InteractionHand.MAIN_HAND); - - level.captureBlockSnapshots = false; - - if (ret.consumesAction()) - { - // save new item data - int newSize = itemstack.getCount(); - CompoundTag newNBT = null; - if (itemstack.getTag() != null) - { - newNBT = itemstack.getTag().copy(); - } - @SuppressWarnings("unchecked") - List blockSnapshots = (List)level.capturedBlockSnapshots.clone(); - level.capturedBlockSnapshots.clear(); - - // make sure to set pre-placement item data for event - itemstack.setCount(size); - itemstack.setTag(nbt); - - Direction side = Direction.UP; - - boolean eventResult = false; - if (blockSnapshots.size() > 1) - { - eventResult = ForgeEventFactory.onMultiBlockPlace(player, blockSnapshots, side); - } - else if (blockSnapshots.size() == 1) - { - eventResult = ForgeEventFactory.onBlockPlace(player, blockSnapshots.get(0), side); - } - - if (eventResult) - { - ret = InteractionResult.FAIL; // cancel placement - // revert back all captured blocks - for (BlockSnapshot blocksnapshot : Lists.reverse(blockSnapshots)) - { - level.restoringBlockSnapshots = true; - blocksnapshot.restore(true, false); - level.restoringBlockSnapshots = false; - } - } - else - { - // Change the stack to its new content - itemstack.setCount(newSize); - itemstack.setTag(newNBT); - - for (BlockSnapshot snap : blockSnapshots) - { - int updateFlag = snap.getFlag(); - BlockState oldBlock = snap.getReplacedBlock(); - BlockState newBlock = level.getBlockState(snap.getPos()); - newBlock.onPlace(level, snap.getPos(), oldBlock, false); - - level.markAndNotifyBlock(snap.getPos(), level.getChunkAt(snap.getPos()), oldBlock, newBlock, updateFlag, 512); - } - if (player != null) - player.awardStat(Stats.ITEM_USED.get(item)); - } - } - level.capturedBlockSnapshots.clear(); - - return ret; - } - public static void playSoundIfFurtherThanNormal(Player player, BlockEntry blockEntry, boolean breaking) { if (Minecraft.getInstance().hitResult != null && Minecraft.getInstance().hitResult.getType() == HitResult.Type.BLOCK)