/* * 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.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @UtilityClass public class PlaceItemUtils { // https://github.com/Articdive/ArticData/blob/1.20.1/1_20_1_tags/1_20_1_block_tags.json // #minecraft:replaceable private static final Set replaceables; static { replaceables = new HashSet<>(Arrays.asList( "minecraft:air", "minecraft:water", "minecraft:lava", "minecraft:grass", "minecraft:fern", "minecraft:dead_bush", "minecraft:seagrass", "minecraft:tall_seagrass", "minecraft:fire", "minecraft:soul_fire", "minecraft:snow", "minecraft:vine", "minecraft:glow_lichen", "minecraft:light", "minecraft:tall_grass", "minecraft:large_fern", "minecraft:structure_void", "minecraft:void_air", "minecraft:cave_air", "minecraft:bubble_column", "minecraft:warped_roots", "minecraft:nether_sprouts", "minecraft:crimson_roots", "minecraft:hanging_roots")) .stream() .map(s -> s.substring(10)) .map(String::toUpperCase) .collect(Collectors.toSet()); } 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 * @param playSound enables sound of placing */ public PlaceItemResult placeItem(Player player, ItemStack itemStack, Block against, BlockFace againstSide, EquipmentSlot hand, boolean force, boolean applyPhysics, boolean rotateAway, boolean playSound) { // 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 = PlaceItemWrapper.ITEM_MATERIAL_TO_BLOCK_MATERIAL.getOrDefault(itemStack.getType(), itemStack.getType()); BlockData blockData = null; AtomicBoolean usedForcePlace = new AtomicBoolean(); if (againstSide == BlockFace.NORTH || againstSide == BlockFace.SOUTH || againstSide == BlockFace.EAST || againstSide == BlockFace.WEST) { // Try Wall Placement first blockData = toBlockData(player, blockDataMeta, PlaceItemWrapper.BLOCK_MATERIAL_TO_WALL_BLOCK_MATERIAL.getOrDefault(typeToPlace, typeToPlace)); if (blockData != null && !canPlace(block, blockData, force, usedForcePlace)) { // Check if default Rotation from input could be valid BlockFace rotation = getRotation(blockData); setRotation(blockData, againstSide); if (!canPlace(block, blockData, force, usedForcePlace)) { setRotation(blockData, rotation); } } } // Try default Placement if (blockData == null || !canPlace(block, blockData, force, usedForcePlace)) { blockData = toBlockData(player, blockDataMeta, typeToPlace); } if (blockData != null && !canPlace(block, blockData, force, usedForcePlace)) { 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, usedForcePlace)) { faceAttachable.setAttachedFace(topFirst ? FaceAttachable.AttachedFace.FLOOR : FaceAttachable.AttachedFace.CEILING); } if (!canPlace(block, blockData, force, usedForcePlace)) { return PlaceItemResult.NO_VALID_PLACEMENT; } } } if (blockData == null) return PlaceItemResult.NO_BLOCK_ITEM_HELD; // Placing a Block inside of Water should set it to Waterlogged if (blockData instanceof Waterlogged && block.getType() == Material.WATER) { Levelled levelled = (Levelled) block.getBlockData(); ((Waterlogged) blockData).setWaterlogged(levelled.getLevel() == levelled.getMaximumLevel()); } if (blockData instanceof Slab) { // Slabs can be set at Top or Bottom ((Slab) blockData).setType(isHitHalfTop(player) ? Slab.Type.TOP : Slab.Type.BOTTOM); if (againstSide == BlockFace.DOWN) { ((Slab) blockData).setType(Slab.Type.TOP); } } else if (blockData instanceof Stairs) { // Stairs can be set at Top or Bottom ((Stairs) blockData).setHalf(isHitHalfTop(player) ? Bisected.Half.TOP : Bisected.Half.BOTTOM); if (againstSide == BlockFace.DOWN) { ((Stairs) blockData).setHalf(Bisected.Half.TOP); } } else if (blockData instanceof TrapDoor) { // TrapDoors can be set at Top or Bottom ((TrapDoor) blockData).setHalf(isHitHalfTop(player) ? Bisected.Half.TOP : Bisected.Half.BOTTOM); if (againstSide == BlockFace.DOWN) { ((TrapDoor) blockData).setHalf(Bisected.Half.TOP); } } 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 FaceAttachable) { // Forcing to Place a FaceAttachable against the Block you specified. Needs the force flag to be set FaceAttachable faceAttachable = (FaceAttachable) blockData; if (blockData instanceof Switch && againstSide == BlockFace.DOWN) { faceAttachable.setAttachedFace(FaceAttachable.AttachedFace.CEILING); } else if (againstSide == BlockFace.UP) { faceAttachable.setAttachedFace(FaceAttachable.AttachedFace.FLOOR); } else if (blockData instanceof Directional) { ((Directional) blockData).setFacing(againstSide); } 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 (force && blockData instanceof Directional && !(blockData instanceof FaceAttachable) && PlaceItemWrapper.BLOCK_MATERIAL_TO_WALL_BLOCK_MATERIAL.containsValue(blockData.getMaterial())) { Directional directional = (Directional) blockData; if (directional.getFaces().contains(againstSide)) { directional.setFacing(againstSide); } } if (force && blockData instanceof Rotatable && !(blockData instanceof FaceAttachable)) { Rotatable rotatable = (Rotatable) blockData; if (againstSide != BlockFace.UP && againstSide != BlockFace.DOWN) { rotatable.setRotation(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, usedForcePlace)); 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()); } if (blockData.getMaterial().isSolid()) { for (Player p : Bukkit.getOnlinePlayers()) { Location min = p.getLocation().add(-0.3, 0, -0.3); Location max = p.getLocation().add(0.3, p.isSneaking() ? 1.5 : 1.8, 0.3); Location blockmin = block.getLocation(); Location blockmax = block.getLocation().add(1.0, 1.0, 1.0); if ( !(max.getX() <= blockmin.getX() || min.getX() >= blockmax.getX() || max.getY() <= blockmin.getY() || min.getY() >= blockmax.getY() || max.getZ() <= blockmin.getZ() || min.getZ() >= blockmax.getZ()) ) { return PlaceItemResult.NO_PLACE_INSIDE_PLAYER; } } } // 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, usedForcePlace)) { 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()); if (skullMeta.getOwningPlayer() != null) { skull.setOwningPlayer(skullMeta.getOwningPlayer()); } skull.update(true, false); } if (playSound) { // 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 usedForcePlace.get() ? PlaceItemResult.SUCCESS_FORCE : 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, AtomicBoolean usedForcePlace) { if (!block.canPlace(blockData)) { if (force) usedForcePlace.set(true); return force; } return replaceables.contains(block.getType().name()); } @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), NO_PLACE_INSIDE_PLAYER(false), SUCCESS(true), SUCCESS_FORCE(true), ; private final boolean success; public boolean wasForced() { return this == SUCCESS_FORCE; } } }