diff --git a/BauSystem_Main/src/de/steamwar/bausystem/features/world/SignEditFrom20.java b/BauSystem_Main/src/de/steamwar/bausystem/features/world/SignEditFrom20.java index 668a9453..02375177 100644 --- a/BauSystem_Main/src/de/steamwar/bausystem/features/world/SignEditFrom20.java +++ b/BauSystem_Main/src/de/steamwar/bausystem/features/world/SignEditFrom20.java @@ -22,6 +22,7 @@ package de.steamwar.bausystem.features.world; import com.comphenix.tinyprotocol.Reflection; import com.comphenix.tinyprotocol.TinyProtocol; import de.steamwar.bausystem.BauSystem; +import de.steamwar.bausystem.utils.PlaceItemUtils; import de.steamwar.linkage.Linked; import de.steamwar.linkage.MinVersion; import org.bukkit.Bukkit; @@ -65,11 +66,14 @@ public class SignEditFrom20 implements Listener { @EventHandler public void editSign(PlayerInteractEvent event) { if (event.getClickedBlock() == null || !event.getClickedBlock().getType().name().contains("SIGN")) return; - if (event.getAction() == Action.RIGHT_CLICK_BLOCK) event.setCancelled(true); + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && !event.getPlayer().isSneaking()) { + PlaceItemUtils.placeItem(event.getPlayer(), event.getItem(), event.getClickedBlock(), event.getBlockFace(), event.getHand(), false, true, false); + event.setCancelled(true); + } if (!event.getPlayer().isSneaking()) return; - event.setCancelled(true); if (event.getAction() == Action.RIGHT_CLICK_BLOCK && (event.getItem() == null || event.getItem().getType() == Material.AIR) || event.getAction() == Action.LEFT_CLICK_BLOCK) { + event.setCancelled(true); Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { edit(event.getPlayer(), event.getClickedBlock()); }, 1); diff --git a/BauSystem_Main/src/de/steamwar/bausystem/utils/PlaceItemUtils.java b/BauSystem_Main/src/de/steamwar/bausystem/utils/PlaceItemUtils.java new file mode 100644 index 00000000..d9ca22c4 --- /dev/null +++ b/BauSystem_Main/src/de/steamwar/bausystem/utils/PlaceItemUtils.java @@ -0,0 +1,478 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.bausystem.utils; + +import com.comphenix.tinyprotocol.Reflection; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.experimental.UtilityClass; +import org.bukkit.*; +import org.bukkit.block.*; +import org.bukkit.block.data.*; +import org.bukkit.block.data.type.Hopper; +import org.bukkit.block.data.type.Observer; +import org.bukkit.block.data.type.*; +import org.bukkit.entity.Player; +import org.bukkit.event.block.BlockCanBuildEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.*; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; + +import java.util.*; + +@UtilityClass +public class PlaceItemUtils { + + private static final Map ITEM_MATERIAL_TO_BLOCK_MATERIAL = new HashMap<>(); + private static final Map BLOCK_MATERIAL_TO_WALL_BLOCK_MATERIAL = new HashMap<>(); + + static { + for (Material material : Material.values()) { + if (!material.isBlock()) continue; + if (material.isLegacy()) continue; + BlockData blockData = material.createBlockData(); + Material placementMaterial = blockData.getPlacementMaterial(); + if (material == placementMaterial) continue; + if (placementMaterial == Material.AIR) continue; + if (placementMaterial.isItem() && !placementMaterial.isBlock()) { + ITEM_MATERIAL_TO_BLOCK_MATERIAL.put(placementMaterial, material); + } + if (material.name().contains("WALL")) { + BLOCK_MATERIAL_TO_WALL_BLOCK_MATERIAL.put(placementMaterial, material); + } + } + } + + private static final Class blockPosition = Reflection.getClass("{nms.core}.BlockPosition"); + private static final Reflection.ConstructorInvoker blockPositionConstructor = Reflection.getConstructor(blockPosition, int.class, int.class, int.class); + private static final Class craftBlock = Reflection.getClass("{obc}.block.CraftBlockState"); + private static final Class craftWorld = Reflection.getClass("{obc}.CraftWorld"); + private static final Reflection.FieldAccessor positionAccessor = Reflection.getField(craftBlock, blockPosition, 0); + private static final Reflection.FieldAccessor worldAccessor = Reflection.getField(craftBlock, craftWorld, 0); + + /** + * Attempt to place an {@link ItemStack} the {@link Player} is holding against a {@link Block} inside the World. + * This can be easily used inside the {@link org.bukkit.event.player.PlayerInteractEvent} to mimik placing a + * block without any minecraft related GUI's etc. executing. + * + * @param player the Player placing the block + * @param itemStack the ItemStack to be placed + * @param against the Block at which the player aims (does not need to be in range of the player) + * @param againstSide the BlockFace the player aims + * @param hand the Hand the player is using + * @param force allow illegal states to be created by placing the block + * @param applyPhysics apply physics while placing the block + * @param rotateAway rotate everything in the opposite direction, so a block facing the Player will face away, and the other way round + */ + public PlaceItemResult placeItem(Player player, ItemStack itemStack, Block against, BlockFace againstSide, EquipmentSlot hand, boolean force, boolean applyPhysics, boolean rotateAway) { + // If the ItemStack is null or air we cannot place it + if (itemStack == null) return PlaceItemResult.NO_ITEM_HELD; + if (itemStack.getType().isAir()) return PlaceItemResult.NO_ITEM_HELD; + + // This Block should be replaced by the new Block + Block block = against.getRelative(againstSide); + + // We cannot place any Item that is not also a Block, this is checked by testing for the ItemMeta + ItemMeta itemMeta = itemStack.getItemMeta(); + if (!(itemMeta instanceof BlockDataMeta)) { + return PlaceItemResult.NO_BLOCK_ITEM_HELD; + } + + BlockDataMeta blockDataMeta = (BlockDataMeta) itemMeta; + + // Converting the Item Material to a Block Material + // e.g. Material.REDSTONE -> Material.REDSTONE_WIRE + Material typeToPlace = ITEM_MATERIAL_TO_BLOCK_MATERIAL.getOrDefault(itemStack.getType(), itemStack.getType()); + + BlockData blockData = null; + if (againstSide == BlockFace.NORTH || againstSide == BlockFace.SOUTH || againstSide == BlockFace.EAST || againstSide == BlockFace.WEST) { + // Try Wall Placement first + blockData = toBlockData(player, blockDataMeta, BLOCK_MATERIAL_TO_WALL_BLOCK_MATERIAL.getOrDefault(typeToPlace, typeToPlace)); + if (blockData != null && !canPlace(block, blockData, force)) { + // Check if default Rotation from input could be valid + BlockFace rotation = getRotation(blockData); + setRotation(blockData, againstSide); + if (!canPlace(block, blockData, force)) { + setRotation(blockData, rotation); + } + } + } + + // Try default Placement + if (blockData == null || !canPlace(block, blockData, force)) { + blockData = toBlockData(player, blockDataMeta, typeToPlace); + } + + if (blockData != null && !canPlace(block, blockData, force)) { + if (blockData instanceof FaceAttachable) { + // FaceAttachable Blocks should be placed on the Ceiling/Floor if possible + // This applies mainly to Lever and Buttons + FaceAttachable faceAttachable = (FaceAttachable) blockData; + boolean topFirst = isHitHalfTop(player); + faceAttachable.setAttachedFace(topFirst ? FaceAttachable.AttachedFace.CEILING : FaceAttachable.AttachedFace.FLOOR); + if (!canPlace(block, blockData, force)) { + faceAttachable.setAttachedFace(topFirst ? FaceAttachable.AttachedFace.FLOOR : FaceAttachable.AttachedFace.CEILING); + } + if (!canPlace(block, blockData, force)) { + return PlaceItemResult.NO_VALID_PLACEMENT; + } + } + if (blockData instanceof Switch) { + // Levers and Buttons are always Rotated the other way + Switch switchType = (Switch) blockData; + switch (switchType.getAttachedFace()) { + case FLOOR: + case CEILING: + switchType.setFacing(switchType.getFacing().getOppositeFace()); + break; + } + } + } + + if (blockData == null) return PlaceItemResult.NO_BLOCK_ITEM_HELD; + + // Placing a Block inside of Water should set it to Waterlogged + if (blockData instanceof Waterlogged) { + ((Waterlogged) blockData).setWaterlogged(block.getType() == Material.WATER); + } + + if (blockData instanceof Slab) { + // Slabs can be set at Top or Bottom + ((Slab) blockData).setType(isHitHalfTop(player) ? Slab.Type.TOP : Slab.Type.BOTTOM); + } else if (blockData instanceof Stairs) { + // Stairs can be set at Top or Bottom + ((Stairs) blockData).setHalf(isHitHalfTop(player) ? Bisected.Half.TOP : Bisected.Half.BOTTOM); + } else if (blockData instanceof TrapDoor) { + // TrapDoors can be set at Top or Bottom + ((TrapDoor) blockData).setHalf(isHitHalfTop(player) ? Bisected.Half.TOP : Bisected.Half.BOTTOM); + } else if (blockData instanceof Chain) { + // Chains are always rotated against the block you place against + Orientable orientable = (Orientable) blockData; + switch (againstSide) { + case EAST: + case WEST: + orientable.setAxis(Axis.X); + break; + case UP: + case DOWN: + orientable.setAxis(Axis.Y); + break; + case NORTH: + case SOUTH: + orientable.setAxis(Axis.Z); + break; + } + } else if (blockData instanceof Hopper && (againstSide == BlockFace.UP || againstSide == BlockFace.DOWN)) { + // Placing at the Top or Bottom of a Block result in a downwards facing Hopper + ((Hopper) blockData).setFacing(BlockFace.DOWN); + } else if (blockData instanceof Directional && (blockData.getMaterial().name().endsWith("_HEAD") || blockData.getMaterial().name().endsWith("_SKULL")) && againstSide != BlockFace.DOWN && againstSide != BlockFace.UP) { + // Skulls and Heads are always rotated towards you if not in Wall variant + ((Directional) blockData).setFacing(againstSide); + } else if (blockData instanceof LightningRod) { + // Lightning Rod is always rotated against the block you place against + ((Directional) blockData).setFacing(againstSide); + } + if (force && blockData instanceof Switch) { + // Forcing to Place a switch against the Block you specified. Needs the force flag to be set + Switch switchType = (Switch) blockData; + if (againstSide == BlockFace.DOWN) { + switchType.setAttachedFace(FaceAttachable.AttachedFace.CEILING); + } else if (againstSide == BlockFace.UP) { + switchType.setAttachedFace(FaceAttachable.AttachedFace.FLOOR); + } else { + switchType.setFacing(againstSide); + } + } + + if (blockData instanceof RedstoneWire) { + // Redstone Wire is connected to every Side by default + RedstoneWire redstoneWire = (RedstoneWire) blockData; + redstoneWire.setFace(BlockFace.NORTH, RedstoneWire.Connection.SIDE); + redstoneWire.setFace(BlockFace.SOUTH, RedstoneWire.Connection.SIDE); + redstoneWire.setFace(BlockFace.EAST, RedstoneWire.Connection.SIDE); + redstoneWire.setFace(BlockFace.WEST, RedstoneWire.Connection.SIDE); + } + + if (rotateAway) { + // Rotate the other way if rotateAway is set to true + BlockFace blockFace = getRotation(blockData); + if (blockFace != null) { + blockFace = blockFace.getOppositeFace(); + if (blockData instanceof Hopper && (blockFace == BlockFace.UP || blockFace == BlockFace.DOWN)) { + ((Hopper) blockData).setFacing(BlockFace.DOWN); + } else { + setRotation(blockData, blockFace); + } + } + } + + // Check if the Block can be build + BlockCanBuildEvent blockCanBuildEvent = new BlockCanBuildEvent(against, player, blockData, canPlace(block, blockData, force)); + Bukkit.getPluginManager().callEvent(blockCanBuildEvent); + if (!blockCanBuildEvent.isBuildable()) return PlaceItemResult.NO_BUILD; + + BlockState oldState = block.getState(); + + // Retrieve the BlockState of the ItemStack if present + BlockState blockState = null; + if (itemMeta instanceof BlockStateMeta) { + blockState = ((BlockStateMeta) itemMeta).getBlockState(); + } + + if (blockState == null) { + // If no BlockState is present use the BlockState of the Block you want to edit + blockState = block.getState(); + } else { + // If a BlockState is present set the Position and World to the Block you want to place + Location blockLocation = block.getLocation(); + positionAccessor.set(blockState, blockPositionConstructor.invoke(blockLocation.getBlockX(), blockLocation.getBlockY(), blockLocation.getBlockZ())); + worldAccessor.set(blockState, blockLocation.getWorld()); + } + + // Set the generated BlockData to the BlockState and update the world without physics + blockState.setBlockData(blockData); + blockState.update(true, false); + + // Check if the Block is allowed to be placed + BlockPlaceEvent blockPlaceEvent = new BlockPlaceEvent(block, oldState, against, itemStack, player, true, hand); + Bukkit.getPluginManager().callEvent(blockCanBuildEvent); + if (blockPlaceEvent.isCancelled() || !blockPlaceEvent.canBuild()) { + // Reset world + oldState.update(true, false); + return PlaceItemResult.NO_PLACE; + } + + if (hasSecondBlock(blockData)) { + // Place tht second block of a Door or Tallgrass. + Bisected bisected = (Bisected) blockData; + Block blockAbove = block.getRelative(0, 1, 0); + bisected.setHalf(Bisected.Half.TOP); + if (canPlace(blockAbove, blockData, force)) { + if (blockData instanceof Waterlogged) { + ((Waterlogged) blockData).setWaterlogged(blockAbove.getType() == Material.WATER); + } + blockAbove.setBlockData(blockData, applyPhysics); + } else { + // If the second Block couldn't be placed remove the first Block as well + oldState.update(true, false); + return PlaceItemResult.NO_DOUBLE_HIGH_BLOCK_SPACE; + } + } + if (applyPhysics) { + // Apply Physics by placing the old State without Physics and setting the new with physics + oldState.update(true, false); + blockState.update(true, true); + } + + if (itemMeta instanceof BannerMeta) { + // Apply Banner Patterns to now placed Block in World + BannerMeta bannerMeta = (BannerMeta) itemMeta; + Banner banner = (Banner) block.getState(); + banner.setPatterns(bannerMeta.getPatterns()); + banner.update(true, false); + } else if (itemMeta instanceof SkullMeta) { + // Apply Skull Data to now placed Block in World + SkullMeta skullMeta = (SkullMeta) itemMeta; + Skull skull = (Skull) block.getState(); + skull.setOwnerProfile(skullMeta.getOwnerProfile()); + skull.setOwningPlayer(skullMeta.getOwningPlayer()); + skull.update(true, false); + } + + // Play the corresponding sound of placing the now placed Block + SoundGroup soundGroup = blockData.getSoundGroup(); + block.getWorld().playSound(block.getLocation(), soundGroup.getPlaceSound(), soundGroup.getVolume() * 0.8F, soundGroup.getPitch() * 0.8F); + return PlaceItemResult.SUCCESS; + } + + public BlockFace[] axis = { BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST }; + public BlockFace[] radial = { BlockFace.NORTH, BlockFace.NORTH_EAST, BlockFace.EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH, BlockFace.SOUTH_WEST, BlockFace.WEST, BlockFace.NORTH_WEST }; + + public BlockFace yawToFace(float yaw) { + return radial[Math.round(yaw / 45f) & 0x7]; + } + + public BlockFace yawToFaceAxis(float yaw) { + return axis[Math.round(yaw / 90f) & 0x3]; + } + + public BlockFace toFace(Set faces, float pitch, float yaw) { + if (faces.contains(BlockFace.UP) && pitch >= 45.0) { + return BlockFace.UP; + } + if (faces.contains(BlockFace.DOWN) && pitch <= -45.0) { + return BlockFace.DOWN; + } + return yawToFaceAxis(yaw); + } + + public BlockData toBlockData(Player player, BlockDataMeta blockDataMeta, Material material) { + BlockData blockData; + try { + blockData = blockDataMeta.getBlockData(material); + } catch (NullPointerException e) { + // Some items have a BlockDataMeta but they cannot be converted to one like ItemFrame, those will be ignored + return null; + } + + if (blockData instanceof Stairs || blockData instanceof Observer || blockData instanceof Hopper || blockData instanceof Door) { + // Stairs, Observer, Hopper and Doors are placed the opposite way + Directional directional = (Directional) blockData; + BlockFace face = toFace(directional.getFaces(), player.getLocation().getPitch(), player.getLocation().getYaw()); + directional.setFacing(face.getOppositeFace()); + } else if (blockData instanceof Orientable) { + // Orientable only have 3 Axis: X, Y, Z + Orientable orientable = (Orientable) blockData; + Set faces = new HashSet<>(); + Set axisSet = orientable.getAxes(); + if (axisSet.contains(Axis.X)) { + faces.add(BlockFace.EAST); + faces.add(BlockFace.WEST); + } + if (axisSet.contains(Axis.Y)) { + faces.add(BlockFace.UP); + faces.add(BlockFace.DOWN); + } + if (axisSet.contains(Axis.Z)) { + faces.add(BlockFace.NORTH); + faces.add(BlockFace.SOUTH); + } + BlockFace face = toFace(faces, player.getLocation().getPitch(), player.getLocation().getYaw()); + switch (face) { + case EAST: + case WEST: + orientable.setAxis(Axis.X); + break; + case UP: + case DOWN: + orientable.setAxis(Axis.Y); + break; + case NORTH: + case SOUTH: + orientable.setAxis(Axis.Z); + break; + } + } else if (blockData instanceof Rotatable) { + Rotatable rotatable = (Rotatable) blockData; + BlockFace blockeFace = yawToFace(player.getLocation().getYaw()); + if (blockData.getMaterial().name().endsWith("_HEAD") || blockData.getMaterial().name().endsWith("_SKULL")) { + // Wall Heads and Wall Skulls are placed the opposite way + rotatable.setRotation(blockeFace.getOppositeFace()); + } else { + rotatable.setRotation(blockeFace); + } + } else if (blockData instanceof Directional) { + Directional directional = (Directional) blockData; + directional.setFacing(toFace(directional.getFaces(), player.getLocation().getPitch(), player.getLocation().getYaw())); + } else if (blockData instanceof Rail) { + Rail rail = (Rail) blockData; + BlockFace face = yawToFaceAxis(player.getLocation().getYaw()); + // Rails are only represented by 2 States, North_South or East_West the remaining States will be ignored here + rail.setShape(face == BlockFace.NORTH || face == BlockFace.SOUTH ? Rail.Shape.NORTH_SOUTH : Rail.Shape.EAST_WEST); + } + + return blockData; + } + + private BlockFace getRotation(BlockData blockData) { + if (blockData instanceof Rotatable) { + return ((Rotatable) blockData).getRotation(); + } else if (blockData instanceof Directional) { + return ((Directional) blockData).getFacing(); + } else if (blockData instanceof Rail) { + Rail rail = (Rail) blockData; + switch (rail.getShape()) { + case NORTH_SOUTH: + return BlockFace.NORTH; + case EAST_WEST: + return BlockFace.EAST; + } + } + return null; + } + + private void setRotation(BlockData blockData, BlockFace rotation) { + if (blockData instanceof Rotatable) { + ((Rotatable) blockData).setRotation(rotation); + } else if (blockData instanceof Directional) { + ((Directional) blockData).setFacing(rotation); + } else if (blockData instanceof Rail) { + Rail rail = (Rail) blockData; + switch (rotation) { + case NORTH: + case SOUTH: + rail.setShape(Rail.Shape.NORTH_SOUTH); + break; + case EAST: + case WEST: + rail.setShape(Rail.Shape.EAST_WEST); + break; + } + } + } + + private boolean isHitHalfTop(Player player) { + RayTraceResult rayTraceResult = player.rayTraceBlocks(6, FluidCollisionMode.NEVER); + if (rayTraceResult != null) { + Vector vector = rayTraceResult.getHitPosition(); + return (vector.getY() - vector.getBlockY()) > 0.5; + } + return false; + } + + private boolean hasSecondBlock(BlockData blockData) { + if (!(blockData instanceof Bisected)) { + return false; + } + if (blockData instanceof Door) { + return true; + } + return blockData.getClass().getName().contains("Tall"); + } + + private boolean canPlace(Block block, BlockData blockData, boolean force) { + if (!force && !block.canPlace(blockData)) return false; + if (block.getType().name().equals("LIGHT")) return true; + if (block.getType().isSolid()) return false; + if (block.getType() == Material.LADDER) return false; + if (block.getType() == Material.SCAFFOLDING) return false; + if (block.getType() == Material.LEVER) return false; + if (block.getType().name().endsWith("_BUTTON")) return false; + return block.getPistonMoveReaction() == PistonMoveReaction.BREAK || block.isEmpty(); + } + + @AllArgsConstructor + @Getter + public enum PlaceItemResult { + NO_ITEM_HELD(false), + NO_BLOCK_ITEM_HELD(false), + NO_VALID_PLACEMENT(false), + NO_BUILD(false), + NO_PLACE(false), + NO_DOUBLE_HIGH_BLOCK_SPACE(false), + SUCCESS(true), + ; + + private final boolean success; + } +}