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;
+ }
+}